From 0023d5a525fc045c15c73fe8612988b1071c2742 Mon Sep 17 00:00:00 2001 From: bnaecker Date: Mon, 19 Sep 2022 15:16:37 -0700 Subject: [PATCH] Adds basic support for IPv6 (#255) * Adds basic support for IPv6 - Adds IPv6 support to the OPTE API and main engine types. This includes fleshing out some missing edges for IPv6 addresses and CIDRs, and adding support for specifying IPv6 addresses in router entries, etc. The main type expanded here is the `VpcCfg`, which now supports an `IpCfg` that specifies all L3 information. That supports exactly one IPv4 or IPv6, or one of each, for private addresses. An optional SNAT and external address for each are also supported. - Updates the `opte-ioctl` and `opteadm` crates to support IPv6, and to use a `VpcCfg` as the argument, rather than a bunch of disparate arguments. Fleshes out handling for IPv6 in router entries, port info and printing, and layer / rule printing. - Adds a few niceties to the D scripts for pretty-printing IPv6 - Renames a lot of IPv4 specific types, such as `Dhcp4Reply` to `DhcpReply`. Types without a prefix will be assumed to refer to IPv4, and IPv6 will always have a version number. - Adds an `icmpv6` layer to `opte` and the `oxide-vpc`, and inserts it in the configuration created by the `xde` driver. This supports ICMPv6 echo requests from the guest to the gateway only. An integration test verifying the hairpinned echo reply is also here. - Updates the API version check script to compare all commits relative to the `master` branch, rather than the last. * Review feedback - Better router error message - Better error messages when parsing IpAddr / IpCidr - Better comments throughout, some better type names - DCE - Fix ARP handling to unconditionally drop outbound requests for anything other than the gateway, and all inbound requests. * Review feedback 2 - Renamed `public_ip` -> `external_ip` fields on NAT-related types. This is important because the "outside" IP address for NAT need not actually be an address that's routable on the public Internet. It can be any address in any network on the other side of the NAT node. - Fix location of Copy derive --- dtrace/common.h | 8 +- dtrace/opte-flow-expire.d | 9 +- dtrace/opte-gen-desc-fail.d | 9 +- dtrace/opte-gen-ht-fail.d | 8 +- dtrace/opte-guest-loopback.d | 10 +- dtrace/opte-ht.d | 11 +- dtrace/opte-layer-process.d | 10 +- dtrace/opte-port-process.d | 6 +- dtrace/opte-rule-match.d | 10 +- dtrace/opte-tcp-flow-state.d | 8 +- dtrace/opte-uft-invaildate.d | 10 +- dtrace/protos.d | 11 + opte-api/check-api-version.sh | 8 +- opte-api/src/cmd.rs | 37 +- opte-api/src/ip.rs | 237 ++++++++++- opte-api/src/lib.rs | 2 +- opte-ioctl/src/lib.rs | 44 +- opte/src/engine/dhcp.rs | 22 +- opte/src/engine/headers.rs | 45 +- opte/src/engine/icmp.rs | 61 +-- opte/src/engine/icmpv6.rs | 174 ++++++++ opte/src/engine/ip4.rs | 2 +- opte/src/engine/ip6.rs | 98 ++++- opte/src/engine/mod.rs | 1 + opte/src/engine/nat.rs | 88 ++-- opte/src/engine/packet.rs | 10 + opte/src/engine/rule.rs | 151 ++++++- opte/src/engine/snat.rs | 272 +++++++----- opteadm/src/lib.rs | 45 +- opteadm/src/main.rs | 105 +++-- oxide-vpc/.gitignore | 4 +- oxide-vpc/src/api.rs | 285 ++++++++++--- oxide-vpc/src/engine/arp.rs | 109 ++--- oxide-vpc/src/engine/{dhcp4.rs => dhcp.rs} | 39 +- oxide-vpc/src/engine/icmp.rs | 19 +- oxide-vpc/src/engine/icmpv6.rs | 49 +++ oxide-vpc/src/engine/mod.rs | 3 +- oxide-vpc/src/engine/nat.rs | 112 ++++- oxide-vpc/src/engine/overlay.rs | 22 +- oxide-vpc/src/engine/router.rs | 236 ++++++----- oxide-vpc/src/lib.rs | 29 +- oxide-vpc/tests/integration_tests.rs | 462 ++++++++++++++++----- xde/src/xde.rs | 213 ++++++---- 43 files changed, 2143 insertions(+), 951 deletions(-) create mode 100644 dtrace/protos.d create mode 100644 opte/src/engine/icmpv6.rs rename oxide-vpc/src/engine/{dhcp4.rs => dhcp.rs} (79%) create mode 100644 oxide-vpc/src/engine/icmpv6.rs diff --git a/dtrace/common.h b/dtrace/common.h index ab5a3939..c5802f43 100644 --- a/dtrace/common.h +++ b/dtrace/common.h @@ -27,13 +27,13 @@ *this->src_ip6 = fvar->src_ip6; \ *this->dst_ip6 = fvar->dst_ip6; \ svar = protos[fvar->proto]; \ - svar = strjoin(svar, ","); \ + svar = strjoin(svar, ",["); \ svar = strjoin(svar, inet_ntoa6(this->src_ip6)); \ - svar = strjoin(svar, ":"); \ + svar = strjoin(svar, "]:"); \ svar = strjoin(svar, lltostr(ntohs(fvar->src_port))); \ - svar = strjoin(svar, ","); \ + svar = strjoin(svar, ",["); \ svar = strjoin(svar, inet_ntoa6(this->dst_ip6)); \ - svar = strjoin(svar, ":"); \ + svar = strjoin(svar, "]:"); \ svar = strjoin(svar, lltostr(ntohs(fvar->dst_port))); #define ETH_FMT(svar, evar) \ diff --git a/dtrace/opte-flow-expire.d b/dtrace/opte-flow-expire.d index c44d14e6..4081e195 100644 --- a/dtrace/opte-flow-expire.d +++ b/dtrace/opte-flow-expire.d @@ -6,18 +6,11 @@ * dtrace -L ./lib -I . -Cqs ./opte-flow-expire.d */ #include "common.h" +#include "protos.d" #define HDR_FMT "%-24s %-18s %s\n" BEGIN { - /* - * Use an associative array to stringify the protocol number. - */ - protos[1]= "ICMP"; - protos[2] = "IGMP"; - protos[6] = "TCP"; - protos[17] = "UDP"; - printf(HDR_FMT, "PORT", "FT NAME", "FLOW"); num = 0; } diff --git a/dtrace/opte-gen-desc-fail.d b/dtrace/opte-gen-desc-fail.d index 44dffca6..d619e123 100644 --- a/dtrace/opte-gen-desc-fail.d +++ b/dtrace/opte-gen-desc-fail.d @@ -4,18 +4,11 @@ * dtrace -L ./lib -I . -Cqs ./opte-gen-desc-fail.d */ #include "common.h" +#include "protos.d" #define HDR_FMT "%-12s %-12s %-4s %-48s %s\n" BEGIN { - /* - * Use an associative array to stringify the protocol number. - */ - protos[1] = "ICMP"; - protos[2] = "IGMP"; - protos[6] = "TCP"; - protos[17] = "UDP"; - printf(HDR_FMT, "PORT", "LAYER", "DIR", "FLOW", "MSG"); num = 0; } diff --git a/dtrace/opte-gen-ht-fail.d b/dtrace/opte-gen-ht-fail.d index 1055b2fe..dff22ed6 100644 --- a/dtrace/opte-gen-ht-fail.d +++ b/dtrace/opte-gen-ht-fail.d @@ -4,17 +4,11 @@ * dtrace -L ./lib -I . -Cqs ./opte-gen-desc-fail.d */ #include "common.h" +#include "protos.d" #define HDR_FMT "%-12s %-12s %-4s %-48s %s\n" BEGIN { - /* - * Use an associative array to stringify the protocol number. - */ - protos[1] = "ICMP"; - protos[2] = "IGMP"; - protos[6] = "TCP"; - protos[17] = "UDP"; printf(HDR_FMT, "PORT", "LAYER", "DIR", "FLOW", "MSG"); num = 0; diff --git a/dtrace/opte-guest-loopback.d b/dtrace/opte-guest-loopback.d index cce94be4..ce6aed9f 100644 --- a/dtrace/opte-guest-loopback.d +++ b/dtrace/opte-guest-loopback.d @@ -4,19 +4,11 @@ * dtrace -L ./lib -I . -Cqs ./opte-guest-loopback.d */ #include "common.h" +#include "protos.d" #define HDR_FMT "%-43s %-12s %-12s\n" BEGIN { - /* - * Use an associative array to stringify the protocol number. - */ - protos[1] = "ICMP"; - protos[2] = "IGMP"; - protos[6] = "TCP"; - protos[17] = "UDP"; - protos[255] = "XXX"; - printf(HDR_FMT, "FLOW", "SRC PORT", "DST PORT"); num = 0; } diff --git a/dtrace/opte-ht.d b/dtrace/opte-ht.d index 4b0f5159..ccf640cf 100644 --- a/dtrace/opte-ht.d +++ b/dtrace/opte-ht.d @@ -1,21 +1,14 @@ /* - * Track Header Transpositions as they happen. + * Track Header Transformations as they happen. * * dtrace -L ./lib -I . -Cqs ./opte-ht.d */ #include "common.h" +#include "protos.d" #define HDR_FMT "%-3s %-12s %-12s %-40s %-40s\n" BEGIN { - /* - * Use an associative array to stringify the protocol number. - */ - protos[1]= "ICMP"; - protos[2] = "IGMP"; - protos[6] = "TCP"; - protos[17] = "UDP"; - printf(HDR_FMT, "DIR", "PORT", "LOCATION", "BEFORE", "AFTER"); num = 0; } diff --git a/dtrace/opte-layer-process.d b/dtrace/opte-layer-process.d index e86c3b44..082f84aa 100644 --- a/dtrace/opte-layer-process.d +++ b/dtrace/opte-layer-process.d @@ -7,19 +7,11 @@ * dtrace -L ./lib -I . -Cqs ./opte-layer-process.d */ #include "common.h" +#include "protos.d" #define HDR_FMT "%-16s %-16s %-3s %-48s %s\n" BEGIN { - /* - * Use an associative array to stringify the protocol number. - */ - protos[1] = "ICMP"; - protos[2] = "IGMP"; - protos[6] = "TCP"; - protos[17] = "UDP"; - protos[255] = "XXX"; - printf(HDR_FMT, "PORT", "LAYER", "DIR", "FLOW", "RES"); num = 0; } diff --git a/dtrace/opte-port-process.d b/dtrace/opte-port-process.d index 09cddac1..f2b91612 100644 --- a/dtrace/opte-port-process.d +++ b/dtrace/opte-port-process.d @@ -4,6 +4,7 @@ * dtrace -L ./lib -I . -Cqs ./opte-port-process.d */ #include "common.h" +#include "protos.d" #define HDR_FMT "%-12s %-3s %-8s %-43s %-5s %s\n" #define LINE_FMT "%-12s %-3s %-8u %-43s %-5u %s\n" @@ -12,11 +13,6 @@ BEGIN { /* * Use an associative array to stringify the protocol number. */ - protos[1] = "ICMP"; - protos[2] = "IGMP"; - protos[6] = "TCP"; - protos[17] = "UDP"; - protos[255] = "XXX"; printf(HDR_FMT, "NAME", "DIR", "EPOCH", "FLOW", "LEN", "RESULT"); num = 0; diff --git a/dtrace/opte-rule-match.d b/dtrace/opte-rule-match.d index 5c2413f9..11ad5f77 100644 --- a/dtrace/opte-rule-match.d +++ b/dtrace/opte-rule-match.d @@ -4,19 +4,11 @@ * dtrace -L ./lib -I . -Cqs ./opte-rule-match.d */ #include "common.h" +#include "protos.d" #define HDR_FMT "%-8s %-12s %-6s %-3s %-43s %s\n" BEGIN { - /* - * Use an associative array to stringify the protocol number. - */ - protos[1] = "ICMP"; - protos[2] = "IGMP"; - protos[6] = "TCP"; - protos[17] = "UDP"; - protos[255] = "XXX"; - printf(HDR_FMT, "PORT", "LAYER", "MATCH", "DIR", "FLOW", "ACTION"); num = 0; } diff --git a/dtrace/opte-tcp-flow-state.d b/dtrace/opte-tcp-flow-state.d index 5684e38c..df2f1825 100644 --- a/dtrace/opte-tcp-flow-state.d +++ b/dtrace/opte-tcp-flow-state.d @@ -4,17 +4,11 @@ * dtrace -L ./lib -I . -Cqs ./opte-tcp-flow-state.d */ #include "common.h" +#include "protos.d" #define FMT "%-16s %-12s %-12s %s\n" BEGIN { - /* - * Use an associative array to stringify the protocol number. - * It's always going to be TCP but we need this declared so - * the FLOW_FMT macros work. - */ - protos[6] = "TCP"; - /* * Use an associative array to stringify the TCP state * values. diff --git a/dtrace/opte-uft-invaildate.d b/dtrace/opte-uft-invaildate.d index 48a63ac6..6abdfe9e 100644 --- a/dtrace/opte-uft-invaildate.d +++ b/dtrace/opte-uft-invaildate.d @@ -8,20 +8,12 @@ * dtrace -L ./lib -I . -Cqs ./opte-uft-invalidate.d */ #include "common.h" +#include "protos.d" #define HDR_FMT "%-8s %-3s %-43s %s\n" #define LINE_FMT "%-8s %-3s %-43s %u\n" BEGIN { - /* - * Use an associative array to stringify the protocol number. - */ - protos[1] = "ICMP"; - protos[2] = "IGMP"; - protos[6] = "TCP"; - protos[17] = "UDP"; - protos[255] = "XXX"; - printf(HDR_FMT, "PORT", "DIR", "FLOW", "EPOCH"); num = 0; } diff --git a/dtrace/protos.d b/dtrace/protos.d new file mode 100644 index 00000000..3592cd6e --- /dev/null +++ b/dtrace/protos.d @@ -0,0 +1,11 @@ +/* + * Definitions of the IP protocol numbers as an associative array. + */ +BEGIN { + protos[1] = "ICMP"; + protos[2] = "IGMP"; + protos[6] = "TCP"; + protos[17] = "UDP"; + protos[58] = "ICMPv6"; + protos[255] = "XXX"; +} diff --git a/opte-api/check-api-version.sh b/opte-api/check-api-version.sh index 59917cf1..7db2fa5b 100755 --- a/opte-api/check-api-version.sh +++ b/opte-api/check-api-version.sh @@ -1,8 +1,8 @@ #!/bin/bash # -# If there is a change to an opte-api source file in the last commit, -# then verify that the API_VERSION value has increased. -if git log -1 -p master..HEAD | grep '^diff.*opte-api/src' +# If there is a change to an opte-api source file relative to the `master` +# branch, # then verify that the API_VERSION value has increased. +if git diff master..HEAD | grep '^diff.*opte-api/src' then - git log -p -1 master..HEAD | awk -f check-api-version.awk + git diff master..HEAD | awk -f check-api-version.awk fi diff --git a/opte-api/src/cmd.rs b/opte-api/src/cmd.rs index d8462137..008e0880 100644 --- a/opte-api/src/cmd.rs +++ b/opte-api/src/cmd.rs @@ -5,6 +5,7 @@ // Copyright 2022 Oxide Computer Company use super::encap::Vni; +use super::ip::IpCidr; use super::mac::MacAddr; use super::API_VERSION; use illumos_sys_hdrs::{c_int, size_t}; @@ -24,21 +25,21 @@ pub const XDE_DLD_OPTE_CMD: i32 = XDE_DLD_PREFIX | 7777; #[derive(Clone, Copy, Debug)] #[repr(C)] pub enum OpteCmd { - ListPorts = 1, // list all ports - AddFwRule = 20, // add firewall rule - RemFwRule = 21, // remove firewall rule - SetFwRules = 22, // set/replace all firewall rules at once - DumpTcpFlows = 30, // dump TCP flows - DumpLayer = 31, // dump the specified Layer - DumpUft = 32, // dump the Unified Flow Table - ListLayers = 33, // list the layers on a given port - ClearUft = 40, // clear the UFT - SetVirt2Phys = 50, // set a v2p mapping - DumpVirt2Phys = 51, // dump the v2p mappings - AddRouterEntryIpv4 = 60, // add a router entry for IPv4 dest - CreateXde = 70, // create a new xde device - DeleteXde = 71, // delete an xde device - SetXdeUnderlay = 72, // set xde underlay devices + ListPorts = 1, // list all ports + AddFwRule = 20, // add firewall rule + RemFwRule = 21, // remove firewall rule + SetFwRules = 22, // set/replace all firewall rules at once + DumpTcpFlows = 30, // dump TCP flows + DumpLayer = 31, // dump the specified Layer + DumpUft = 32, // dump the Unified Flow Table + ListLayers = 33, // list the layers on a given port + ClearUft = 40, // clear the UFT + SetVirt2Phys = 50, // set a v2p mapping + DumpVirt2Phys = 51, // dump the v2p mappings + AddRouterEntry = 60, // add a router entry for IP dest + CreateXde = 70, // create a new xde device + DeleteXde = 71, // delete an xde device + SetXdeUnderlay = 72, // set xde underlay devices } impl TryFrom for OpteCmd { @@ -57,7 +58,7 @@ impl TryFrom for OpteCmd { 40 => Ok(Self::ClearUft), 50 => Ok(Self::SetVirt2Phys), 51 => Ok(Self::DumpVirt2Phys), - 60 => Ok(Self::AddRouterEntryIpv4), + 60 => Ok(Self::AddRouterEntry), 70 => Ok(Self::CreateXde), 71 => Ok(Self::DeleteXde), 72 => Ok(Self::SetXdeUnderlay), @@ -146,7 +147,7 @@ pub enum OpteError { DeserCmdErr(String), DeserCmdReq(String), FlowExists(String), - InvalidRouteDest(String), + InvalidRouterEntry { dest: IpCidr, target: String }, LayerNotFound(String), MacExists { port: String, vni: Vni, mac: MacAddr }, MaxCapacity(u64), @@ -181,7 +182,7 @@ impl OpteError { Self::DeserCmdErr(_) => ENOMSG, Self::DeserCmdReq(_) => ENOMSG, Self::FlowExists(_) => EEXIST, - Self::InvalidRouteDest(_) => EINVAL, + Self::InvalidRouterEntry { .. } => EINVAL, Self::LayerNotFound(_) => ENOENT, Self::MacExists { .. } => EEXIST, Self::MaxCapacity(_) => ENFILE, diff --git a/opte-api/src/ip.rs b/opte-api/src/ip.rs index 7daa3e7e..500248b6 100644 --- a/opte-api/src/ip.rs +++ b/opte-api/src/ip.rs @@ -20,13 +20,39 @@ cfg_if! { } } +/// Generate an ICMPv6 Echo Reply message. +#[derive(Debug, Clone, Copy)] +pub struct Icmpv6EchoReply { + /// The MAC address of the Echo Request source. + pub src_mac: MacAddr, + + /// The IP address of the Echo Request source. + pub src_ip: Ipv6Addr, + + /// The MAC address of the Echo Request destination. + pub dst_mac: MacAddr, + + /// The IP address of the Echo source destination. + pub dst_ip: Ipv6Addr, +} + +impl Display for Icmpv6EchoReply { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "ICMPv6 Echo Reply ({},{}) => ({},{})", + self.dst_mac, self.dst_ip, self.src_mac, self.src_ip, + ) + } +} + /// Generate an ICMPv4 Echo Reply message. /// /// Map an ICMPv4 Echo Message (Type=8, Code=0) from `src` to `dst` /// into an ICMPv4 Echo Reply Message (Type=0, Code=0) from `dst` to /// `src`. #[derive(Clone, Debug)] -pub struct Icmp4EchoReply { +pub struct IcmpEchoReply { /// The MAC address of the sender of the Echo message. The /// destination MAC address of the Echo Reply. pub echo_src_mac: MacAddr, @@ -44,7 +70,7 @@ pub struct Icmp4EchoReply { pub echo_dst_ip: Ipv4Addr, } -impl Display for Icmp4EchoReply { +impl Display for IcmpEchoReply { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, @@ -66,7 +92,7 @@ impl Display for Icmp4EchoReply { /// specifies in the parameter request list. This has worked thus far, /// but we should come back to this and comb over RFC 2131 more /// carefully -- particularly §4.3.1 and §4.3.2. -pub struct Dhcp4Action { +pub struct DhcpAction { /// The client's MAC address. pub client_mac: MacAddr, @@ -96,7 +122,7 @@ pub struct Dhcp4Action { /// The value of the `DHCP Message Type Option (code 53)`. This /// action supports only the Offer and Ack messages. - pub reply_type: Dhcp4ReplyType, + pub reply_type: DhcpReplyType, /// A static route entry, sent to the client via the `Classless /// Static Route Option (code 131)`. @@ -112,19 +138,19 @@ pub struct Dhcp4Action { pub dns_servers: Option<[Option; 3]>, } -impl Display for Dhcp4Action { +impl Display for DhcpAction { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "DHCPv4 {}: {}", self.reply_type, self.client_ip) } } #[derive(Clone, Copy, Debug)] -pub enum Dhcp4ReplyType { +pub enum DhcpReplyType { Offer, Ack, } -impl Display for Dhcp4ReplyType { +impl Display for DhcpReplyType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::Offer => write!(f, "OFFER"), @@ -279,6 +305,18 @@ pub enum IpAddr { Ip6(Ipv6Addr), } +impl From for IpAddr { + fn from(ipv4: Ipv4Addr) -> Self { + IpAddr::Ip4(ipv4) + } +} + +impl From for IpAddr { + fn from(ipv6: Ipv6Addr) -> Self { + IpAddr::Ip6(ipv6) + } +} + impl Default for IpAddr { fn default() -> Self { IpAddr::Ip4(Default::default()) @@ -294,6 +332,19 @@ impl fmt::Display for IpAddr { } } +impl FromStr for IpAddr { + type Err = String; + fn from_str(val: &str) -> result::Result { + if let Ok(ipv4) = val.parse::() { + Ok(ipv4.into()) + } else { + val.parse::() + .map(IpAddr::Ip6) + .map_err(|_| String::from("Invalid IP address")) + } + } +} + /// An IPv4 address. #[derive( Clone, Copy, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, @@ -430,7 +481,7 @@ pub struct Ipv6Addr { } impl Ipv6Addr { - pub const ANY_ADDR: [u8; 16] = [0; 16]; + pub const ANY_ADDR: Self = Self { inner: [0; 16] }; /// Return the bytes of the address. pub fn bytes(&self) -> [u8; 16] { @@ -489,6 +540,15 @@ impl From for Ipv6Addr { } } +impl From for Ipv6Addr { + fn from(ip: smoltcp::wire::Ipv6Address) -> Self { + // Safety: We assume the `smoltcp` type is well-formed, with at least 16 + // octets in the correct order. + let bytes: [u8; 16] = ip.as_bytes().try_into().unwrap(); + Self::from(bytes) + } +} + impl From<&[u8; 16]> for Ipv6Addr { fn from(bytes: &[u8; 16]) -> Ipv6Addr { Ipv6Addr { inner: *bytes } @@ -514,13 +574,13 @@ impl From<[u16; 8]> for Ipv6Addr { } } -#[cfg(any(feature = "std", test))] impl FromStr for Ipv6Addr { type Err = String; fn from_str(val: &str) -> result::Result { - let ip = - val.parse::().map_err(|e| format!("{}", e))?; + let ip = val + .parse::() + .map_err(|_| String::from("Invalid IPv6 address"))?; Ok(ip.into()) } } @@ -532,18 +592,30 @@ pub enum IpCidr { Ip6(Ipv6Cidr), } +impl From for IpCidr { + fn from(cidr: Ipv4Cidr) -> Self { + IpCidr::Ip4(cidr) + } +} + +impl From for IpCidr { + fn from(cidr: Ipv6Cidr) -> Self { + IpCidr::Ip6(cidr) + } +} + impl IpCidr { pub fn is_default(&self) -> bool { match self { Self::Ip4(ip4) => ip4.is_default(), - Self::Ip6(_) => todo!("IPv6 is_default"), + Self::Ip6(ip6) => ip6.is_default(), } } pub fn prefix_len(&self) -> usize { match self { Self::Ip4(ip4) => ip4.prefix_len() as usize, - Self::Ip6(_) => todo!("IPv6 prefix_len"), + Self::Ip6(ip6) => ip6.prefix_len() as usize, } } } @@ -557,10 +629,33 @@ impl fmt::Display for IpCidr { } } +impl FromStr for IpCidr { + type Err = String; + + /// Convert a string like "192.168.2.0/24" into an `IpCidr`. + fn from_str(val: &str) -> result::Result { + match val.parse::() { + Ok(ip4) => Ok(IpCidr::Ip4(ip4)), + Err(_) => val + .parse::() + .map(IpCidr::Ip6) + .map_err(|_| String::from("Invalid IP CIDR")), + } + } +} + /// A valid IPv4 prefix legnth. #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct Ipv4PrefixLen(u8); +impl TryFrom for Ipv4PrefixLen { + type Error = String; + + fn try_from(p: u8) -> Result { + Self::new(p) + } +} + impl Ipv4PrefixLen { pub const NETMASK_NONE: Self = Self(0); pub const NETMASK_ALL: Self = Self(32); @@ -682,7 +777,6 @@ impl fmt::Display for Ipv6Cidr { } } -#[cfg(any(feature = "std", test))] impl FromStr for Ipv6Cidr { type Err = String; @@ -693,9 +787,11 @@ impl FromStr for Ipv6Cidr { None => return Err(format!("no '/' found")), }; - let ip = match ip_s.parse::() { + let ip = match ip_s.parse::() { Ok(v) => v.into(), - Err(e) => return Err(format!("bad IP: {}", e)), + Err(_) => { + return Err(format!("Bad IP address component: '{}'", ip_s)) + } }; let prefix_len = match prefix_s.parse::() { @@ -713,7 +809,18 @@ impl FromStr for Ipv6Cidr { #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct Ipv6PrefixLen(u8); +impl TryFrom for Ipv6PrefixLen { + type Error = String; + + fn try_from(p: u8) -> Result { + Self::new(p) + } +} + impl Ipv6PrefixLen { + pub const NETMASK_NONE: Self = Self(0); + pub const NETMASK_ALL: Self = Self(128); + pub fn new(prefix_len: u8) -> result::Result { if prefix_len > 128 { return Err(format!("bad IPv6 prefix length: {}", prefix_len)); @@ -728,6 +835,11 @@ impl Ipv6PrefixLen { } impl Ipv6Cidr { + pub fn new(ip: Ipv6Addr, prefix_len: Ipv6PrefixLen) -> Self { + let ip = ip.safe_mask(prefix_len); + Ipv6Cidr { ip, prefix_len } + } + pub fn new_checked( ip: Ipv6Addr, prefix_len: u8, @@ -740,6 +852,27 @@ impl Ipv6Cidr { pub fn parts(&self) -> (Ipv6Addr, Ipv6PrefixLen) { (self.ip, self.prefix_len) } + + /// Return `true` if this is the default route subnet + pub fn is_default(&self) -> bool { + let (ip, prefix_len) = self.parts(); + ip == Ipv6Addr::ANY_ADDR && prefix_len.val() == 0 + } + + /// Return the prefix length (netmask). + pub fn prefix_len(self) -> u8 { + self.prefix_len.0 + } + + /// Return the network address of this CIDR. + pub fn ip(&self) -> Ipv6Addr { + self.ip + } + + /// Is this `ip` a member of the CIDR? + pub fn is_member(&self, ip: Ipv6Addr) -> bool { + ip.safe_mask(self.prefix_len) == self.ip + } } #[cfg(test)] @@ -870,6 +1003,76 @@ mod test { assert_eq!(ip6.mask(56).unwrap(), ip6_prefix); let ip6 = Ipv6Addr::from([1; 16]); - assert_eq!(ip6.mask(0).unwrap().bytes(), Ipv6Addr::ANY_ADDR); + assert_eq!(ip6.mask(0).unwrap(), Ipv6Addr::ANY_ADDR); + } + + #[test] + fn ipv6_is_default() { + let cidr = Ipv6Cidr::new_checked(Ipv6Addr::from([1; 16]), 1).unwrap(); + assert!(!cidr.is_default()); + let cidr = Ipv6Cidr::new_checked(Ipv6Addr::from([0; 16]), 1).unwrap(); + assert!(!cidr.is_default()); + + let cidr = Ipv6Cidr::new_checked(Ipv6Addr::from([1; 16]), 0).unwrap(); + assert!(cidr.is_default()); + let cidr = Ipv6Cidr::new_checked(Ipv6Addr::from([0; 16]), 0).unwrap(); + assert!(cidr.is_default()); + } + + #[test] + fn ipv6_prefix_len() { + for i in 0u8..=128 { + let len = Ipv6Cidr::new_checked(Ipv6Addr::from([1; 16]), i) + .unwrap() + .prefix_len(); + assert_eq!(i, len); + } + assert!(Ipv6Cidr::new_checked(Ipv6Addr::from([1; 16]), 129).is_err()); + } + + #[test] + fn ipv6_cidr_is_member() { + let cidr: Ipv6Cidr = "fd00:1::1/16".parse().unwrap(); + assert!(cidr.is_member("fd00:1::1".parse().unwrap())); + assert!(cidr.is_member("fd00:1::10".parse().unwrap())); + assert!(cidr.is_member("fd00:2::1".parse().unwrap())); + + assert!(!cidr.is_member("fd01:1::1".parse().unwrap())); + assert!(!cidr.is_member("fd01:1::10".parse().unwrap())); + assert!(!cidr.is_member("fd01:2::1".parse().unwrap())); + } + + #[test] + fn test_ip_addr_from_str() { + assert_eq!( + IpAddr::Ip4(Ipv4Addr::from([172, 30, 0, 1])), + "172.30.0.1".parse().unwrap() + ); + let bytes = [0xfd00, 0, 0, 0, 0, 0, 0, 1]; + assert_eq!( + IpAddr::Ip6(Ipv6Addr::from(bytes)), + "fd00::1".parse().unwrap() + ); + } + + #[test] + fn test_ip_cidr_from_str() { + assert_eq!( + IpCidr::Ip4( + Ipv4Cidr::new_checked(Ipv4Addr::from([10, 0, 0, 0]), 24) + .unwrap() + ), + "10.0.0.0/24".parse().unwrap(), + ); + assert_eq!( + IpCidr::Ip6( + Ipv6Cidr::new_checked( + Ipv6Addr::from([0xfd00, 0, 0, 0, 0, 0, 0, 1]), + 64 + ) + .unwrap() + ), + "fd00::1/64".parse().unwrap(), + ); } } diff --git a/opte-api/src/lib.rs b/opte-api/src/lib.rs index 779cb2df..5f415d64 100644 --- a/opte-api/src/lib.rs +++ b/opte-api/src/lib.rs @@ -50,7 +50,7 @@ pub use ulp::*; /// /// We rely on CI and the check-api-version.sh script to verify that /// this number is incremented anytime the oxide-api code changes. -pub const API_VERSION: u64 = 11; +pub const API_VERSION: u64 = 12; #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] pub enum Direction { diff --git a/opte-ioctl/src/lib.rs b/opte-ioctl/src/lib.rs index 41b69b42..b60f1625 100644 --- a/opte-ioctl/src/lib.rs +++ b/opte-ioctl/src/lib.rs @@ -4,13 +4,14 @@ // Copyright 2022 Oxide Computer Company +pub use opte::api::OpteError; use opte::api::{ - CmdOk, Ipv4Addr, Ipv4Cidr, MacAddr, NoResp, OpteCmd, OpteCmdIoctl, - OpteError, SetXdeUnderlayReq, Vni, API_VERSION, XDE_DLD_OPTE_CMD, + CmdOk, NoResp, OpteCmd, OpteCmdIoctl, SetXdeUnderlayReq, API_VERSION, + XDE_DLD_OPTE_CMD, }; use oxide_vpc::api::{ - AddRouterEntryIpv4Req, CreateXdeReq, DeleteXdeReq, ListPortsReq, - ListPortsResp, SNatCfg, SetFwRulesReq, SetVirt2PhysReq, + AddRouterEntryReq, CreateXdeReq, DeleteXdeReq, ListPortsReq, ListPortsResp, + SetFwRulesReq, SetVirt2PhysReq, VpcCfg, }; use serde::de::DeserializeOwned; use serde::Serialize; @@ -83,17 +84,7 @@ impl OpteHdl { pub fn create_xde( &self, name: &str, - private_mac: MacAddr, - private_ip: std::net::Ipv4Addr, - vpc_subnet: Ipv4Cidr, - gw_mac: MacAddr, - gw_ip: std::net::Ipv4Addr, - bsvc_addr: std::net::Ipv6Addr, - bsvc_vni: Vni, - vpc_vni: Vni, - src_underlay_addr: std::net::Ipv6Addr, - snat: Option, - external_ips_v4: Option, + cfg: VpcCfg, passthrough: bool, ) -> Result { use libnet::link; @@ -106,22 +97,7 @@ impl OpteHdl { let xde_devname = name.into(); let cmd = OpteCmd::CreateXde; - let req = CreateXdeReq { - xde_devname, - linkid, - private_mac, - private_ip: private_ip.into(), - vpc_subnet, - gw_mac, - gw_ip: gw_ip.into(), - bsvc_addr: bsvc_addr.into(), - bsvc_vni, - vpc_vni, - src_underlay_addr: src_underlay_addr.into(), - snat, - external_ips_v4, - passthrough, - }; + let req = CreateXdeReq { xde_devname, linkid, cfg, passthrough }; let res = run_cmd_ioctl(self.device.as_raw_fd(), cmd, &req); @@ -172,11 +148,11 @@ impl OpteHdl { run_cmd_ioctl(self.device.as_raw_fd(), cmd, &req) } - pub fn add_router_entry_ip4( + pub fn add_router_entry( &self, - req: &AddRouterEntryIpv4Req, + req: &AddRouterEntryReq, ) -> Result { - let cmd = OpteCmd::AddRouterEntryIpv4; + let cmd = OpteCmd::AddRouterEntry; run_cmd_ioctl(self.device.as_raw_fd(), cmd, &req) } diff --git a/opte/src/engine/dhcp.rs b/opte/src/engine/dhcp.rs index 328b0184..390aab7c 100644 --- a/opte/src/engine/dhcp.rs +++ b/opte/src/engine/dhcp.rs @@ -29,7 +29,7 @@ use super::rule::{ IpProtoMatch, Ipv4AddrMatch, PortMatch, Predicate, }; use super::udp::{UdpHdr, UdpMeta}; -use opte_api::{Dhcp4Action, Dhcp4ReplyType, MacAddr, SubnetRouterPair}; +use opte_api::{DhcpAction, DhcpReplyType, MacAddr, SubnetRouterPair}; /// The DHCP message type. /// @@ -55,13 +55,13 @@ impl From for smoltcp::wire::DhcpMessageType { } } -impl From for MessageType { - fn from(rt: Dhcp4ReplyType) -> Self { +impl From for MessageType { + fn from(rt: DhcpReplyType) -> Self { use smoltcp::wire::DhcpMessageType as SmolDMT; match rt { - Dhcp4ReplyType::Offer => Self::from(SmolDMT::Offer), - Dhcp4ReplyType::Ack => Self::from(SmolDMT::Ack), + DhcpReplyType::Offer => Self::from(SmolDMT::Offer), + DhcpReplyType::Ack => Self::from(SmolDMT::Ack), } } } @@ -224,8 +224,8 @@ impl ClasslessStaticRouteOpt { // client/server and those might be unicast, at which point these // preds need to include that possibility. Though it may also require // a whole separate action (and this should perhaps be named the -// Dhcp4LeaseAction). -impl HairpinAction for Dhcp4Action { +// DhcpLeaseAction). +impl HairpinAction for DhcpAction { fn implicit_preds(&self) -> (Vec, Vec) { use smoltcp::wire::DhcpMessageType as SmolDMT; @@ -248,14 +248,14 @@ impl HairpinAction for Dhcp4Action { ]; let data_preds = match self.reply_type { - Dhcp4ReplyType::Offer => { - vec![DataPredicate::Dhcp4MsgType(MessageType::from( + DhcpReplyType::Offer => { + vec![DataPredicate::DhcpMsgType(MessageType::from( SmolDMT::Discover, ))] } - Dhcp4ReplyType::Ack => { - vec![DataPredicate::Dhcp4MsgType(MessageType::from( + DhcpReplyType::Ack => { + vec![DataPredicate::DhcpMsgType(MessageType::from( SmolDMT::Request, ))] } diff --git a/opte/src/engine/headers.rs b/opte/src/engine/headers.rs index 89e82850..c8e7679c 100644 --- a/opte/src/engine/headers.rs +++ b/opte/src/engine/headers.rs @@ -5,8 +5,8 @@ // Copyright 2022 Oxide Computer Company use super::checksum::Checksum; -use super::ip4::{Ipv4Hdr, Ipv4Meta, Ipv4MetaOpt, IPV4_HDR_SZ}; -use super::ip6::{Ipv6Hdr, Ipv6Meta, Ipv6MetaOpt, IPV6_HDR_SZ}; +use super::ip4::{Ipv4Hdr, Ipv4Meta, Ipv4MetaOpt}; +use super::ip6::{Ipv6Hdr, Ipv6Meta, Ipv6MetaOpt}; use super::packet::{PacketRead, ReadErr, WriteError}; use super::tcp::{TcpHdr, TcpMeta, TcpMetaOpt}; use super::udp::{UdpHdr, UdpMeta, UdpMetaOpt}; @@ -124,6 +124,9 @@ macro_rules! assert_ip { } impl IpHdr { + /// Return the total length of the header. + /// + /// In the case of IPv6, this includes any extension headers. pub fn hdr_len(&self) -> usize { match self { Self::Ip4(ip4) => ip4.hdr_len(), @@ -131,6 +134,7 @@ impl IpHdr { } } + /// Return `Some` if this is an IPv4 header, or `None`. pub fn ip4(&self) -> Option<&Ipv4Hdr> { match self { Self::Ip4(ip4) => Some(ip4), @@ -138,6 +142,7 @@ impl IpHdr { } } + /// Return `Some` if this is an IPv6 header, or `None`. pub fn ip6(&self) -> Option<&Ipv6Hdr> { match self { Self::Ip6(ip6) => Some(ip6), @@ -145,6 +150,7 @@ impl IpHdr { } } + /// Return the length of the upper-layer protocol contents. pub fn ulp_len(&self) -> usize { match self { Self::Ip4(ip4) => ip4.ulp_len(), @@ -166,6 +172,7 @@ impl IpHdr { } } + /// Set the total length of the packet, in octets. pub fn set_total_len(&mut self, len: usize) { match self { Self::Ip4(ip4) => ip4.set_total_len(len as u16), @@ -173,17 +180,11 @@ impl IpHdr { } } - pub fn size(&self) -> usize { - match self { - Self::Ip4(_) => IPV4_HDR_SZ, - Self::Ip6(_) => IPV6_HDR_SZ, - } - } - + /// Total length of the packet, including all headers and payload pub fn total_len(&self) -> u16 { match self { Self::Ip4(ip4) => ip4.total_len(), - Self::Ip6(_ip6) => todo!("implement"), + Self::Ip6(ip6) => ip6.total_len(), } } } @@ -201,7 +202,7 @@ impl From for IpHdr { } #[derive( - Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, + Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, Copy, )] pub enum IpMeta { Ip4(Ipv4Meta), @@ -215,6 +216,13 @@ impl IpMeta { _ => None, } } + + pub fn ip6(&self) -> Option<&Ipv6Meta> { + match self { + Self::Ip6(meta) => Some(meta), + _ => None, + } + } } impl From for IpMeta { @@ -253,6 +261,12 @@ impl From for IpMetaOpt { } } +impl From for IpMetaOpt { + fn from(ip6: Ipv6MetaOpt) -> Self { + IpMetaOpt::Ip6(ip6) + } +} + impl HeaderActionModify for IpMeta { fn run_modify(&mut self, spec: &IpMetaOpt) { match (self, spec) { @@ -260,12 +274,15 @@ impl HeaderActionModify for IpMeta { ip4_meta.run_modify(&ip4_spec); } - (IpMeta::Ip6(_ip6_meta), IpMetaOpt::Ip6(_ip6_spec)) => { - todo!("implement IPv6 run_modify()"); + (IpMeta::Ip6(ip6_meta), IpMetaOpt::Ip6(ip6_spec)) => { + ip6_meta.run_modify(&ip6_spec); } (meta, spec) => { - panic!("differeing IP meta and spec: {:?} {:?}", meta, spec); + panic!( + "Different IP versions for meta and spec: {:?} {:?}", + meta, spec + ); } } } diff --git a/opte/src/engine/icmp.rs b/opte/src/engine/icmp.rs index 9a048a82..dd7792fe 100644 --- a/opte/src/engine/icmp.rs +++ b/opte/src/engine/icmp.rs @@ -13,11 +13,10 @@ use super::rule::{ HairpinAction, IpProtoMatch, Ipv4AddrMatch, Predicate, }; use core::fmt::{self, Display}; -pub use opte_api::ip::{Icmp4EchoReply, Protocol}; -use serde::de::{self, Visitor}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +pub use opte_api::ip::{IcmpEchoReply, Protocol}; +use serde::{Deserialize, Serialize}; use smoltcp::phy::{Checksum, ChecksumCapabilities as Csum}; -use smoltcp::wire::{Icmpv4Packet, Icmpv4Repr}; +use smoltcp::wire::{self, Icmpv4Packet, Icmpv4Repr}; cfg_if! { if #[cfg(all(not(feature = "std"), not(test)))] { @@ -27,7 +26,7 @@ cfg_if! { } } -impl HairpinAction for Icmp4EchoReply { +impl HairpinAction for IcmpEchoReply { fn implicit_preds(&self) -> (Vec, Vec) { let hdr_preds = vec![ Predicate::InnerEtherSrc(vec![EtherAddrMatch::Exact( @@ -45,8 +44,8 @@ impl HairpinAction for Icmp4EchoReply { Predicate::InnerIpProto(vec![IpProtoMatch::Exact(Protocol::ICMP)]), ]; - let data_preds = vec![DataPredicate::Icmp4MsgType(MessageType::from( - smoltcp::wire::Icmpv4Message::EchoRequest, + let data_preds = vec![DataPredicate::IcmpMsgType(MessageType::from( + wire::Icmpv4Message::EchoRequest, ))]; (hdr_preds, data_preds) @@ -122,18 +121,19 @@ impl HairpinAction for Icmp4EchoReply { /// predicates. We call this "message type" instead of just "message" /// because that's what it is: the type field of the larger ICMP /// message. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(from = "u8", into = "u8")] pub struct MessageType { - inner: smoltcp::wire::Icmpv4Message, + inner: wire::Icmpv4Message, } -impl From for MessageType { - fn from(inner: smoltcp::wire::Icmpv4Message) -> Self { +impl From for MessageType { + fn from(inner: wire::Icmpv4Message) -> Self { Self { inner } } } -impl From for smoltcp::wire::Icmpv4Message { +impl From for wire::Icmpv4Message { fn from(mt: MessageType) -> Self { mt.inner } @@ -147,42 +147,7 @@ impl From for u8 { impl From for MessageType { fn from(val: u8) -> Self { - Self { inner: smoltcp::wire::Icmpv4Message::from(val) } - } -} - -struct MessageTypeVisitor; - -impl<'de> Visitor<'de> for MessageTypeVisitor { - type Value = MessageType; - - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("an unsigned integer from 0 to 255") - } - - fn visit_u8(self, value: u8) -> Result - where - E: de::Error, - { - Ok(MessageType::from(value)) - } -} - -impl<'de> Deserialize<'de> for MessageType { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_u8(MessageTypeVisitor) - } -} - -impl Serialize for MessageType { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_u8(u8::from(*self)) + Self { inner: wire::Icmpv4Message::from(val) } } } diff --git a/opte/src/engine/icmpv6.rs b/opte/src/engine/icmpv6.rs new file mode 100644 index 00000000..370621f3 --- /dev/null +++ b/opte/src/engine/icmpv6.rs @@ -0,0 +1,174 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// Copyright 2022 Oxide Computer Company + +//! Internet Control Message Protocol version 6 + +use super::ether::{self, EtherHdr, EtherMeta, ETHER_HDR_SZ}; +use super::ip6::Ipv6Hdr; +use super::ip6::Ipv6Meta; +use super::ip6::IPV6_HDR_SZ; +use super::packet::{Packet, PacketMeta, PacketRead, PacketReader, Parsed}; +use super::rule::{ + AllowOrDeny, DataPredicate, EtherAddrMatch, GenErr, GenPacketResult, + HairpinAction, IpProtoMatch, Ipv6AddrMatch, Predicate, +}; +use core::fmt::{self, Display}; +pub use opte_api::ip::{Icmpv6EchoReply, Protocol}; +use serde::{Deserialize, Serialize}; +use smoltcp::phy::{Checksum, ChecksumCapabilities as Csum}; +use smoltcp::wire::{ + Icmpv6Message, Icmpv6Packet, Icmpv6Repr, IpAddress, Ipv6Address, +}; + +cfg_if! { + if #[cfg(all(not(feature = "std"), not(test)))] { + use alloc::vec::Vec; + } else { + use std::vec::Vec; + } +} + +/// An ICMPv6 message type +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(from = "u8", into = "u8")] +pub struct MessageType { + inner: Icmpv6Message, +} + +impl From for MessageType { + fn from(inner: Icmpv6Message) -> MessageType { + MessageType { inner } + } +} + +impl From for Icmpv6Message { + fn from(mt: MessageType) -> Self { + mt.inner + } +} + +impl From for u8 { + fn from(mt: MessageType) -> u8 { + u8::from(mt.inner) + } +} + +impl From for MessageType { + fn from(val: u8) -> Self { + Self { inner: Icmpv6Message::from(val) } + } +} + +impl Display for MessageType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.inner) + } +} + +impl HairpinAction for Icmpv6EchoReply { + fn implicit_preds(&self) -> (Vec, Vec) { + let hdr_preds = vec![ + Predicate::InnerEtherSrc(vec![EtherAddrMatch::Exact( + self.src_mac.into(), + )]), + Predicate::InnerEtherDst(vec![EtherAddrMatch::Exact( + self.dst_mac.into(), + )]), + Predicate::InnerSrcIp6(vec![Ipv6AddrMatch::Exact(self.src_ip)]), + Predicate::InnerDstIp6(vec![Ipv6AddrMatch::Exact(self.dst_ip)]), + Predicate::InnerIpProto(vec![IpProtoMatch::Exact( + Protocol::ICMPv6, + )]), + ]; + + let data_preds = vec![DataPredicate::Icmpv6MsgType(MessageType::from( + Icmpv6Message::EchoRequest, + ))]; + + (hdr_preds, data_preds) + } + + fn gen_packet( + &self, + meta: &PacketMeta, + rdr: &mut PacketReader, + ) -> GenPacketResult { + // Collect the src / dst IP addresses, which are needed to emit the + // resulting ICMPv6 echo reply. + let (src_ip, dst_ip) = if let Some(metadata) = meta.inner_ip6() { + ( + IpAddress::Ipv6(Ipv6Address(metadata.src.bytes())), + IpAddress::Ipv6(Ipv6Address(metadata.dst.bytes())), + ) + } else { + // Getting here implies the predicate matched, but that the + // extracted metadata indicates this isn't an IPv6 packet. That + // should be impossible, but we avoid panicking given the kernel + // context. + return Err(GenErr::Unexpected(format!( + "Expected IPv6 packet metadata, but found: {:?}", + meta + ))); + }; + let body = rdr.copy_remaining(); + let src_pkt = Icmpv6Packet::new_checked(&body)?; + let src_icmp = + Icmpv6Repr::parse(&src_ip, &dst_ip, &src_pkt, &Csum::ignored())?; + + let (src_ident, src_seq_no, src_data) = match src_icmp { + Icmpv6Repr::EchoRequest { ident, seq_no, data } => { + (ident, seq_no, data) + } + + _ => { + // We should never hit this case because the predicate + // should have verified that we are dealing with an + // Echo Request. However, programming error could + // cause this to happen -- let's not take any chances. + return Err(GenErr::Unexpected(format!( + "expected an ICMPv6 Echo Request, got {} {}", + src_pkt.msg_type(), + src_pkt.msg_code() + ))); + } + }; + + let reply = Icmpv6Repr::EchoReply { + ident: src_ident, + seq_no: src_seq_no, + data: src_data, + }; + + let reply_len = reply.buffer_len(); + let mut ulp_body = vec![0u8; reply_len]; + let mut icmp_reply = Icmpv6Packet::new_unchecked(&mut ulp_body); + let mut csum = Csum::ignored(); + csum.icmpv6 = Checksum::Tx; + reply.emit(&dst_ip, &src_ip, &mut icmp_reply, &csum); + + let mut ip = Ipv6Hdr::from(&Ipv6Meta { + src: self.dst_ip, + dst: self.src_ip, + proto: Protocol::ICMPv6, + }); + + // There are no extension headers, so the ULP is the only content. + ip.set_pay_len(reply_len as u16); + + let eth = EtherHdr::from(&EtherMeta { + dst: self.src_mac.into(), + src: self.dst_mac.into(), + ether_type: ether::ETHER_TYPE_IPV6, + }); + + let mut pkt_bytes = + Vec::with_capacity(ETHER_HDR_SZ + IPV6_HDR_SZ + reply_len); + pkt_bytes.extend_from_slice(ð.as_bytes()); + pkt_bytes.extend_from_slice(&ip.as_bytes()); + pkt_bytes.extend_from_slice(&ulp_body); + Ok(AllowOrDeny::Allow(Packet::copy(&pkt_bytes))) + } +} diff --git a/opte/src/engine/ip4.rs b/opte/src/engine/ip4.rs index d9f04715..4f3af82c 100644 --- a/opte/src/engine/ip4.rs +++ b/opte/src/engine/ip4.rs @@ -171,7 +171,7 @@ impl MatchExact for Protocol { } #[derive( - Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, + Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, )] pub struct Ipv4Meta { pub src: Ipv4Addr, diff --git a/opte/src/engine/ip6.rs b/opte/src/engine/ip6.rs index 78b278c6..d927364d 100644 --- a/opte/src/engine/ip6.rs +++ b/opte/src/engine/ip6.rs @@ -6,10 +6,15 @@ use super::checksum::Checksum; use super::headers::{ - Header, HeaderAction, IpMeta, IpMetaOpt, ModifyActionArg, PushActionArg, + Header, HeaderAction, HeaderActionModify, IpMeta, IpMetaOpt, + ModifyActionArg, PushActionArg, }; use super::ip4::Protocol; use super::packet::{PacketRead, ReadErr}; +use crate::engine::rule::MatchExact; +use crate::engine::rule::MatchExactVal; +use crate::engine::rule::MatchPrefix; +use crate::engine::rule::MatchPrefixVal; use core::convert::TryFrom; pub use opte_api::{Ipv6Addr, Ipv6Cidr}; use serde::{Deserialize, Serialize}; @@ -33,8 +38,23 @@ pub const IPV6_HDR_SZ: usize = smoltcp::wire::IPV6_HEADER_LEN; pub const IPV6_VERSION: u8 = 6; pub const DDM_HEADER_ID: u8 = 0xFE; +impl MatchExactVal for Ipv6Addr {} +impl MatchPrefixVal for Ipv6Cidr {} + +impl MatchExact for Ipv6Addr { + fn match_exact(&self, val: &Ipv6Addr) -> bool { + *self == *val + } +} + +impl MatchPrefix for Ipv6Addr { + fn match_prefix(&self, prefix: &Ipv6Cidr) -> bool { + prefix.is_member(*self) + } +} + #[derive( - Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, + Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, )] pub struct Ipv6Meta { pub src: Ipv6Addr, @@ -42,6 +62,17 @@ pub struct Ipv6Meta { pub proto: Protocol, } +impl Ipv6Meta { + // XXX check that at least one field was specified. + pub fn modify( + src: Option, + dst: Option, + proto: Option, + ) -> HeaderAction { + HeaderAction::Modify(Ipv6MetaOpt { src, dst, proto }.into()) + } +} + impl PushActionArg for Ipv6Meta {} impl From<&Ipv6Hdr> for Ipv6Meta { @@ -52,8 +83,9 @@ impl From<&Ipv6Hdr> for Ipv6Meta { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Ipv6MetaOpt { - src: Option<[u8; 16]>, - dst: Option<[u8; 16]>, + src: Option, + dst: Option, + proto: Option, } impl ModifyActionArg for Ipv6MetaOpt {} @@ -68,6 +100,20 @@ impl Ipv6Meta { } } +impl HeaderActionModify for Ipv6Meta { + fn run_modify(&mut self, spec: &Ipv6MetaOpt) { + if let Some(src) = spec.src { + self.src = src; + } + if let Some(dst) = spec.dst { + self.dst = dst; + } + if let Some(proto) = spec.proto { + self.proto = proto; + } + } +} + #[derive(Clone, Debug)] pub struct Ipv6Hdr { vsn_class_flow: [u8; 4], @@ -179,6 +225,12 @@ impl Ipv6Hdr { usize::from(self.payload_len) } + /// Set the payload length of the contained packet, including any extension + /// headers. + pub fn set_pay_len(&mut self, len: u16) { + self.payload_len = len; + } + /// Return the length of the upper-layer protocol payload. pub fn ulp_len(&self) -> usize { self.pay_len() - self.ext_len() @@ -210,6 +262,12 @@ impl Ipv6Hdr { self.payload_len = len - self.hdr_len() as u16; } + /// Return the total length of the packet, including the base header, any + /// extension headers, and the payload itself. + pub fn total_len(&self) -> u16 { + self.payload_len + IPV6_HDR_SZ as u16 + } + /// Return the source IPv6 address pub fn src(&self) -> Ipv6Addr { self.src @@ -366,6 +424,8 @@ impl From<&Ipv6Meta> for Ipv6Hdr { #[cfg(test)] pub(crate) mod test { + use super::Ipv6Addr; + use super::Ipv6Cidr; use super::Ipv6Hdr; use super::DDM_HEADER_ID; use super::IPV6_HDR_SZ; @@ -373,6 +433,8 @@ pub(crate) mod test { use crate::engine::packet::Initialized; use crate::engine::packet::Packet; use crate::engine::packet::PacketReader; + use crate::engine::rule::MatchExact; + use crate::engine::rule::MatchPrefix; use itertools::Itertools; use smoltcp::wire::IpProtocol; use smoltcp::wire::Ipv6Address; @@ -602,5 +664,33 @@ pub(crate) mod test { PAYLOAD_LEN - header.ext_len(), "ULP length is not correct" ); + assert_eq!( + header.total_len(), + (PAYLOAD_LEN + IPV6_HDR_SZ) as u16, + "Total packet length is not correct", + ); + } + + #[test] + fn test_ipv6_addr_match_exact() { + let addr: Ipv6Addr = "fd00::1".parse().unwrap(); + assert!(addr.match_exact(&addr)); + assert!(!addr.match_exact(&("fd00::2".parse().unwrap()))); + } + + #[test] + fn test_ipv6_cidr_match_prefix() { + let cidr: Ipv6Cidr = "fd00::1/16".parse().unwrap(); + let addr: Ipv6Addr = "fd00::1".parse().unwrap(); + assert!(addr.match_prefix(&cidr)); + + let addr: Ipv6Addr = "fd00::2".parse().unwrap(); + assert!(addr.match_prefix(&cidr)); + + let addr: Ipv6Addr = "fd01::1".parse().unwrap(); + assert!(!addr.match_prefix(&cidr)); + + let addr: Ipv6Addr = "fd01::2".parse().unwrap(); + assert!(!addr.match_prefix(&cidr)); } } diff --git a/opte/src/engine/mod.rs b/opte/src/engine/mod.rs index 32cb6239..924f4211 100644 --- a/opte/src/engine/mod.rs +++ b/opte/src/engine/mod.rs @@ -17,6 +17,7 @@ pub mod geneve; #[macro_use] pub mod headers; pub mod icmp; +pub mod icmpv6; pub mod ioctl; #[macro_use] pub mod ip4; diff --git a/opte/src/engine/nat.rs b/opte/src/engine/nat.rs index d81e01e0..01f883f3 100644 --- a/opte/src/engine/nat.rs +++ b/opte/src/engine/nat.rs @@ -6,14 +6,16 @@ use super::ether::EtherMeta; use super::ip4::Ipv4Meta; +use super::ip6::Ipv6Meta; use super::layer::InnerFlowId; use super::port::meta::ActionMeta; use super::rule::{ self, ActionDesc, AllowOrDeny, DataPredicate, HdrTransform, Predicate, StatefulAction, }; +use crate::engine::snat::ConcreteIpAddr; use core::fmt; -use opte_api::{Direction, Ipv4Addr, MacAddr}; +use opte_api::{Direction, IpAddr, MacAddr}; cfg_if! { if #[cfg(all(not(feature = "std"), not(test)))] { @@ -27,42 +29,45 @@ cfg_if! { } } -#[derive(Clone)] -pub struct Nat4 { - priv_ip: Ipv4Addr, - public_ip: Ipv4Addr, +/// A mapping from a private to external IP address for NAT. +#[derive(Debug, Clone, Copy)] +pub struct Nat { + priv_ip: IpAddr, + external_ip: IpAddr, + // XXX-EXT-IP Remove phys_gw_mac: Option, } -impl Nat4 { - pub fn new( - priv_ip: Ipv4Addr, - public_ip: Ipv4Addr, +impl Nat { + /// Create a new NAT mapping from a private to public IP address. + pub fn new( + priv_ip: T, + external_ip: T, phys_gw_mac: Option, ) -> Self { Self { priv_ip: priv_ip.into(), - public_ip: public_ip.into(), + external_ip: external_ip.into(), phys_gw_mac, } } } -impl fmt::Display for Nat4 { +impl fmt::Display for Nat { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} <=> {}", self.priv_ip, self.public_ip) + write!(f, "{} <=> {}", self.priv_ip, self.external_ip) } } -impl StatefulAction for Nat4 { +impl StatefulAction for Nat { fn gen_desc( &self, _flow_id: &InnerFlowId, _meta: &mut ActionMeta, ) -> rule::GenDescResult { - let desc = Nat4Desc { + let desc = NatDesc { priv_ip: self.priv_ip, - public_ip: self.public_ip, + external_ip: self.external_ip, // XXX-EXT-IP This is assuming ext_ip_hack. All packets // outbound for IG will have their dest mac rewritten to // go to physical gateway, which will then properly route @@ -80,27 +85,32 @@ impl StatefulAction for Nat4 { } } -#[derive(Clone)] -pub struct Nat4Desc { - priv_ip: Ipv4Addr, - public_ip: Ipv4Addr, +/// An action descriptor for a NAT action. +#[derive(Debug, Clone, Copy)] +pub struct NatDesc { + priv_ip: IpAddr, + external_ip: IpAddr, // XXX-EXT-IP phys_gw_mac: Option, } -pub const NAT4_NAME: &'static str = "NAT4"; +pub const NAT_NAME: &'static str = "NAT"; -impl ActionDesc for Nat4Desc { +impl ActionDesc for NatDesc { fn gen_ht(&self, dir: Direction) -> HdrTransform { match dir { Direction::Out => { + let inner_ip = match self.external_ip { + IpAddr::Ip4(ipv4) => { + Ipv4Meta::modify(Some(ipv4), None, None) + } + IpAddr::Ip6(ipv6) => { + Ipv6Meta::modify(Some(ipv6), None, None) + } + }; let mut ht = HdrTransform { - name: NAT4_NAME.to_string(), - inner_ip: Ipv4Meta::modify( - Some(self.public_ip), - None, - None, - ), + name: NAT_NAME.to_string(), + inner_ip, ..Default::default() }; @@ -117,16 +127,26 @@ impl ActionDesc for Nat4Desc { ht } - Direction::In => HdrTransform { - name: NAT4_NAME.to_string(), - inner_ip: Ipv4Meta::modify(None, Some(self.priv_ip), None), - ..Default::default() - }, + Direction::In => { + let inner_ip = match self.priv_ip { + IpAddr::Ip4(ipv4) => { + Ipv4Meta::modify(None, Some(ipv4), None) + } + IpAddr::Ip6(ipv6) => { + Ipv6Meta::modify(None, Some(ipv6), None) + } + }; + HdrTransform { + name: NAT_NAME.to_string(), + inner_ip, + ..Default::default() + } + } } } fn name(&self) -> &str { - NAT4_NAME + NAT_NAME } } @@ -151,7 +171,7 @@ mod test { let outside_ip = "76.76.21.21".parse().unwrap(); let outside_port = 80; let gw_mac = MacAddr::from([0x78, 0x23, 0xae, 0x5d, 0x4f, 0x0d]); - let nat = Nat4::new(priv_ip, pub_ip, Some(gw_mac)); + let nat = Nat::new(priv_ip, pub_ip, Some(gw_mac)); let mut ameta = ActionMeta::new(); // ================================================================ diff --git a/opte/src/engine/packet.rs b/opte/src/engine/packet.rs index c6ebba60..83b49a01 100644 --- a/opte/src/engine/packet.rs +++ b/opte/src/engine/packet.rs @@ -396,6 +396,14 @@ impl PacketMeta { } } + /// Return the inner IPv6 metadata. + pub fn inner_ip6(&self) -> Option<&Ipv6Meta> { + match &self.inner.ip { + Some(IpMeta::Ip6(x)) => Some(x), + _ => None, + } + } + /// Return the inner TCP metadata, if the inner ULP is TCP. /// Otherwise, return `None`. pub fn inner_tcp(&self) -> Option<&TcpMeta> { @@ -873,6 +881,8 @@ impl Packet { match proto { Protocol::TCP => Self::parse_hg_tcp(rdr, hg, offsets)?, Protocol::UDP => Self::parse_hg_udp(rdr, hg, offsets)?, + // See comment above about treating ICMP as a header. + Protocol::ICMPv6 => (), _ => return Err(ParseError::UnsupportedProtocol(proto)), } diff --git a/opte/src/engine/rule.rs b/opte/src/engine/rule.rs index 2682295f..fcf9cfb9 100644 --- a/opte/src/engine/rule.rs +++ b/opte/src/engine/rule.rs @@ -15,9 +15,10 @@ use super::headers::{ self, HeaderAction, HeaderActionError, IpAddr, IpMeta, IpMetaOpt, UlpHeaderAction, UlpMeta, UlpMetaOpt, }; -use super::icmp::MessageType as Icmp4MessageType; +use super::icmp::MessageType as IcmpMessageType; +use super::icmpv6::MessageType as Icmpv6MessageType; use super::ip4::{Ipv4Addr, Ipv4Cidr, Ipv4Meta, Protocol}; -use super::ip6::Ipv6Meta; +use super::ip6::{Ipv6Addr, Ipv6Cidr, Ipv6Meta}; use super::layer::InnerFlowId; use super::packet::{ Initialized, Packet, PacketMeta, PacketRead, PacketReader, Parsed, @@ -31,7 +32,10 @@ use illumos_sys_hdrs::c_char; use opte_api::{Direction, MacAddr}; use serde::{Deserialize, Serialize}; use smoltcp::phy::ChecksumCapabilities as Csum; -use smoltcp::wire::{DhcpPacket, DhcpRepr, Icmpv4Packet, Icmpv4Repr}; +use smoltcp::wire::{ + self, DhcpPacket, DhcpRepr, Icmpv4Packet, Icmpv4Repr, Icmpv6Packet, + Icmpv6Repr, +}; cfg_if! { if #[cfg(all(not(feature = "std"), not(test)))] { @@ -52,20 +56,28 @@ cfg_if! { // of payloads include an ARP request, ICMP body, or TCP body. pub trait Payload {} +/// A marker trait for types that can be matched exactly, usually by direct +/// equality comparison. pub trait MatchExactVal {} +/// Trait support matching a value exactly, usually by direct equality +/// comparison. pub trait MatchExact { fn match_exact(&self, val: &M) -> bool; } +/// A marker trait for types that can be match by prefix. pub trait MatchPrefixVal {} +/// A trait describing how to match data by prefix. pub trait MatchPrefix { fn match_prefix(&self, prefix: &M) -> bool; } +/// A marker trait for types that can match a range of values. pub trait MatchRangeVal {} +/// A trait describing how to match data over a range of values. pub trait MatchRange { fn match_range(&self, start: &M, end: &M) -> bool; } @@ -185,9 +197,12 @@ impl Display for ArpOpMatch { } } +/// Describe how to match an IPv4 address #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub enum Ipv4AddrMatch { + /// Match an exact address Exact(Ipv4Addr), + /// Match an address in the same CIDR block Prefix(Ipv4Cidr), } @@ -211,6 +226,35 @@ impl Display for Ipv4AddrMatch { } } +/// Describe how to match an IPv6 address +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum Ipv6AddrMatch { + /// Match an exact address + Exact(Ipv6Addr), + /// Match an address in the same CIDR block + Prefix(Ipv6Cidr), +} + +impl Ipv6AddrMatch { + fn matches(&self, flow_ip: Ipv6Addr) -> bool { + match self { + Self::Exact(ip) => flow_ip.match_exact(ip), + Self::Prefix(cidr) => flow_ip.match_prefix(cidr), + } + } +} + +impl Display for Ipv6AddrMatch { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use Ipv6AddrMatch::*; + + match self { + Exact(ip) => write!(f, "{}", ip), + Prefix(cidr) => write!(f, "{}", cidr), + } + } +} + #[derive(Clone, Debug, Eq, Deserialize, PartialEq, Serialize)] pub enum IpProtoMatch { Exact(Protocol), @@ -275,6 +319,8 @@ pub enum Predicate { InnerArpOp(ArpOpMatch), InnerSrcIp4(Vec), InnerDstIp4(Vec), + InnerSrcIp6(Vec), + InnerDstIp6(Vec), InnerIpProto(Vec), InnerSrcPort(Vec), InnerDstPort(Vec), @@ -353,6 +399,24 @@ impl Display for Predicate { write!(f, "inner.ip.dst={}", s) } + InnerSrcIp6(list) => { + let s = list + .iter() + .map(|v| v.to_string()) + .collect::>() + .join(","); + write!(f, "inner.ip6.src={}", s) + } + + InnerDstIp6(list) => { + let s = list + .iter() + .map(|v| v.to_string()) + .collect::>() + .join(","); + write!(f, "inner.ip6.dst={}", s) + } + InnerSrcPort(list) => { let s = list .iter() @@ -510,6 +574,28 @@ impl Predicate { _ => return false, }, + Self::InnerSrcIp6(list) => match meta.inner.ip { + Some(IpMeta::Ip6(Ipv6Meta { src: ip, .. })) => { + for m in list { + if m.matches(ip) { + return true; + } + } + } + _ => return false, + }, + + Self::InnerDstIp6(list) => match meta.inner.ip { + Some(IpMeta::Ip6(Ipv6Meta { dst: ip, .. })) => { + for m in list { + if m.matches(ip) { + return true; + } + } + } + _ => return false, + }, + Self::InnerSrcPort(list) => match meta.inner.ulp { None => return false, @@ -557,8 +643,9 @@ impl Predicate { #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub enum DataPredicate { - Dhcp4MsgType(DhcpMessageType), - Icmp4MsgType(Icmp4MessageType), + DhcpMsgType(DhcpMessageType), + IcmpMsgType(IcmpMessageType), + Icmpv6MsgType(Icmpv6MessageType), InnerArpTpa(Vec), Not(Box), } @@ -568,14 +655,18 @@ impl Display for DataPredicate { use DataPredicate::*; match self { - Dhcp4MsgType(mt) => { - write!(f, "dhcp4.msg_type={}", mt) + DhcpMsgType(mt) => { + write!(f, "dhcp.msg_type={}", mt) } - Icmp4MsgType(mt) => { + IcmpMsgType(mt) => { write!(f, "icmp.msg_type={}", mt) } + Icmpv6MsgType(mt) => { + write!(f, "icmpv6.msg_type={}", mt) + } + InnerArpTpa(list) => { let s = list .iter() @@ -605,7 +696,7 @@ impl DataPredicate { match self { Self::Not(pred) => return !pred.is_match(meta, rdr), - Self::Dhcp4MsgType(mt) => { + Self::DhcpMsgType(mt) => { let bytes = rdr.copy_remaining(); let pkt = match DhcpPacket::new_checked(&bytes) { Ok(v) => v, @@ -633,7 +724,7 @@ impl DataPredicate { return res; } - Self::Icmp4MsgType(mt) => { + Self::IcmpMsgType(mt) => { let bytes = rdr.copy_remaining(); let pkt = match Icmpv4Packet::new_checked(&bytes) { Ok(v) => v, @@ -656,7 +747,45 @@ impl DataPredicate { } }; - return Icmp4MessageType::from(pkt.msg_type()) == *mt; + return IcmpMessageType::from(pkt.msg_type()) == *mt; + } + + Self::Icmpv6MsgType(mt) => { + // Pull out the IPv6 source / destination addresses. This checks + // that this is actually an IPv6 packet, and these are needed + // for the `smoltcp` packet parsing / validation. + let (src, dst) = if let Some(metadata) = meta.inner_ip6() { + ( + wire::IpAddress::Ipv6(wire::Ipv6Address( + metadata.src.bytes(), + )), + wire::IpAddress::Ipv6(wire::Ipv6Address( + metadata.dst.bytes(), + )), + ) + } else { + // This isn't an IPv6 packet at all + return false; + }; + + let bytes = rdr.copy_remaining(); + let pkt = match Icmpv6Packet::new_checked(&bytes) { + Ok(v) => v, + Err(e) => { + super::err(format!( + "Icmpv6Packet::new_checked() failed: {:?}", + e + )); + return false; + } + }; + if let Err(e) = + Icmpv6Repr::parse(&src, &dst, &pkt, &Csum::ignored()) + { + super::err(format!("Icmpv6Repr::parse() failed: {:?}", e,)); + return false; + } + return Icmpv6MessageType::from(pkt.msg_type()) == *mt; } Self::InnerArpTpa(list) => match meta.inner.arp { diff --git a/opte/src/engine/snat.rs b/opte/src/engine/snat.rs index 780422e4..2c8c8705 100644 --- a/opte/src/engine/snat.rs +++ b/opte/src/engine/snat.rs @@ -4,8 +4,11 @@ // Copyright 2022 Oxide Computer Company +//! Types for working with IP Source NAT, both IPv4 and IPv6. + use super::headers::{UlpGenericModify, UlpHeaderAction, UlpMetaModify}; use super::ip4::Ipv4Meta; +use super::ip6::Ipv6Meta; use super::layer::InnerFlowId; use super::port::meta::ActionMeta; use super::rule::{ @@ -15,7 +18,7 @@ use super::rule::{ use crate::ddi::sync::{KMutex, KMutexType}; use core::fmt; use core::ops::RangeInclusive; -use opte_api::{Direction, Ipv4Addr}; +use opte_api::{Direction, IpAddr, Ipv4Addr, Ipv6Addr}; cfg_if! { if #[cfg(all(not(feature = "std"), not(test)))] { @@ -31,75 +34,100 @@ cfg_if! { } } +/// A single entry in the NAT pool, describing the public IP and port used to +/// NAT a private address. #[derive(Clone, Copy)] pub struct NatPoolEntry { - ip: Ipv4Addr, + ip: IpAddr, port: u16, } +// A public IP and port range for NAT. Includes the list of all possible ports +// and those that are free. +#[derive(Debug, Clone)] +struct PortList { + // The public IP address to which a private IP is mapped + ip: IpAddr, + // The list of all possible ports available in the NAT pool + ports: RangeInclusive, + // The list of unused / free ports in the pool + free_ports: Vec, +} + impl ResourceEntry for NatPoolEntry {} +/// A mapping from private IP addresses to a public IP and a port range used for +/// NAT-ing connections. pub struct NatPool { // Map private IP to public IP + free list of ports - free_list: - KMutex, Vec)>>, + free_list: KMutex>, +} + +mod private { + pub trait Ip: Into {} + impl Ip for super::Ipv4Addr {} + impl Ip for super::Ipv6Addr {} } +/// A marker trait for IP addresses of a concrete protocol version. +/// +/// This can be used to constrain generic types to the same IP address version, +/// but of either IPv4 or IPv6. +pub trait ConcreteIpAddr: private::Ip {} +impl ConcreteIpAddr for T where T: private::Ip {} impl NatPool { - pub fn add( + /// Add a new mapping from private IP to public IP and ports. + pub fn add( &self, - priv_ip: Ipv4Addr, - pub_ip: Ipv4Addr, + priv_ip: T, + pub_ip: T, pub_ports: RangeInclusive, ) { - let free_list = pub_ports.clone().collect(); - self.free_list.lock().insert(priv_ip, (pub_ip, pub_ports, free_list)); + let free_ports = pub_ports.clone().collect(); + let entry = + PortList { ip: pub_ip.into(), ports: pub_ports, free_ports }; + self.free_list.lock().insert(priv_ip.into(), entry); } - pub fn num_avail(&self, priv_ip: Ipv4Addr) -> Result { + /// Return the number of available ports for a given private IP address. + pub fn num_avail(&self, priv_ip: IpAddr) -> Result { match self.free_list.lock().get(&priv_ip) { - Some((_, _, ports)) => Ok(ports.len()), + Some(PortList { free_ports, .. }) => Ok(free_ports.len()), _ => Err(ResourceError::NoMatch(priv_ip.to_string())), } } + /// Return the mapping from a private IP to the public IP and port range. pub fn mapping( &self, - priv_ip: Ipv4Addr, - ) -> Option<(Ipv4Addr, RangeInclusive)> { + priv_ip: IpAddr, + ) -> Option<(IpAddr, RangeInclusive)> { self.free_list .lock() .get(&priv_ip) - .map(|(pub_ip, range, _)| (pub_ip.clone(), range.clone())) + .map(|PortList { ip, ports, .. }| (ip.clone(), ports.clone())) } + /// Create a new NAT pool, with no entries. pub fn new() -> Self { NatPool { free_list: KMutex::new(BTreeMap::new(), KMutexType::Driver) } } // A helper function to verify correct operation during testing. #[cfg(test)] - fn verify_available( + fn verify_available( &self, - priv_ip: Ipv4Addr, - pub_ip: Ipv4Addr, + priv_ip: T, + pub_ip: T, pub_port: u16, ) -> bool { - match self.free_list.lock().get(&priv_ip) { - Some((pip, _, free_list)) => { - if pub_ip != *pip { + match self.free_list.lock().get(&priv_ip.into()) { + Some(PortList { ip, free_ports, .. }) => { + if pub_ip.into() != *ip { return false; } - - for p in free_list { - if pub_port == *p { - return true; - } - } - - false + free_ports.contains(&pub_port) } - None => false, } } @@ -108,27 +136,27 @@ impl NatPool { impl Resource for NatPool {} impl FiniteResource for NatPool { - type Key = Ipv4Addr; + type Key = IpAddr; type Entry = NatPoolEntry; - fn obtain(&self, priv_ip: &Ipv4Addr) -> Result { + fn obtain(&self, priv_ip: &IpAddr) -> Result { match self.free_list.lock().get_mut(&priv_ip) { - Some((ip, _, ports)) => { - if ports.len() == 0 { - return Err(ResourceError::Exhausted); + Some(PortList { ip, free_ports, .. }) => { + if let Some(port) = free_ports.pop() { + Ok(Self::Entry { ip: *ip, port }) + } else { + Err(ResourceError::Exhausted) } - - Ok(Self::Entry { ip: *ip, port: ports.pop().unwrap() }) } None => Err(ResourceError::NoMatch(priv_ip.to_string())), } } - fn release(&self, priv_ip: &Ipv4Addr, entry: Self::Entry) { + fn release(&self, priv_ip: &IpAddr, entry: Self::Entry) { match self.free_list.lock().get_mut(&priv_ip) { - Some((_ip, _, ports)) => { - ports.push(entry.port); + Some(PortList { free_ports, .. }) => { + free_ports.push(entry.port); } None => { @@ -138,26 +166,34 @@ impl FiniteResource for NatPool { } } +/// A NAT pool mapping provided for Source NAT (only outbound connections). #[derive(Clone)] -pub struct SNat4 { - priv_ip: Ipv4Addr, +pub struct SNat { + priv_ip: IpAddr, ip_pool: Arc, } -impl SNat4 { - pub fn new(addr: Ipv4Addr, ip_pool: Arc) -> Self { - SNat4 { priv_ip: addr.into(), ip_pool } +impl SNat { + pub fn new(addr: IpAddr, ip_pool: Arc) -> Self { + SNat { priv_ip: addr, ip_pool } } } -impl fmt::Display for SNat4 { +impl fmt::Display for SNat { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let (pub_ip, ports) = self.ip_pool.mapping(self.priv_ip).unwrap(); - write!(f, "{}:{}-{}", pub_ip, ports.start(), ports.end()) + match pub_ip { + IpAddr::Ip4(ip4) => { + write!(f, "{}:{}-{}", ip4, ports.start(), ports.end()) + } + IpAddr::Ip6(ip6) => { + write!(f, "[{}]:{}-{}", ip6, ports.start(), ports.end()) + } + } } } -impl StatefulAction for SNat4 { +impl StatefulAction for SNat { fn gen_desc( &self, flow_id: &InnerFlowId, @@ -167,7 +203,7 @@ impl StatefulAction for SNat4 { let priv_port = flow_id.src_port; match pool.obtain(&self.priv_ip) { Ok(nat) => { - let desc = SNat4Desc { + let desc = SNatDesc { pool: pool.clone(), priv_ip: self.priv_ip, priv_port: priv_port, @@ -176,11 +212,16 @@ impl StatefulAction for SNat4 { Ok(AllowOrDeny::Allow(Arc::new(desc))) } - - // XXX This needs improving. - Err(_e) => Err(rule::GenDescError::ResourceExhausted { - name: "SNAT Pool".to_string(), - }), + Err(ResourceError::Exhausted) => { + Err(rule::GenDescError::ResourceExhausted { + name: "SNAT Pool (exhausted)".to_string(), + }) + } + Err(ResourceError::NoMatch(ip)) => { + Err(rule::GenDescError::Unexpected { + msg: format!("SNAT pool (no match: {})", ip), + }) + } } } @@ -193,56 +234,68 @@ impl StatefulAction for SNat4 { } #[derive(Clone)] -pub struct SNat4Desc { +pub struct SNatDesc { pool: Arc, nat: NatPoolEntry, - priv_ip: Ipv4Addr, + priv_ip: IpAddr, priv_port: u16, } -pub const SNAT4_NAME: &'static str = "SNAT4"; +pub const SNAT_NAME: &'static str = "SNAT"; -impl ActionDesc for SNat4Desc { +impl ActionDesc for SNatDesc { fn gen_ht(&self, dir: Direction) -> HdrTransform { match dir { - // Outbound traffic needs it's source IP and source port - Direction::Out => HdrTransform { - name: SNAT4_NAME.to_string(), - inner_ip: Ipv4Meta::modify(Some(self.nat.ip), None, None), - inner_ulp: UlpHeaderAction::Modify(UlpMetaModify { - generic: UlpGenericModify { - src_port: Some(self.nat.port), + // Outbound traffic needs its source IP and source port + Direction::Out => { + let inner_ip = match self.nat.ip { + IpAddr::Ip4(ip) => Ipv4Meta::modify(Some(ip), None, None), + IpAddr::Ip6(ip) => Ipv6Meta::modify(Some(ip), None, None), + }; + HdrTransform { + name: SNAT_NAME.to_string(), + inner_ip: inner_ip, + inner_ulp: UlpHeaderAction::Modify(UlpMetaModify { + generic: UlpGenericModify { + src_port: Some(self.nat.port), + ..Default::default() + }, ..Default::default() - }, + }), ..Default::default() - }), - ..Default::default() - }, + } + } // Inbound traffic needs its destination IP and // destination port mapped back to the private values that // the guest expects to see. - Direction::In => HdrTransform { - name: SNAT4_NAME.to_string(), - inner_ip: Ipv4Meta::modify(None, Some(self.priv_ip), None), - inner_ulp: UlpHeaderAction::Modify(UlpMetaModify { - generic: UlpGenericModify { - dst_port: Some(self.priv_port), + Direction::In => { + let inner_ip = match self.priv_ip { + IpAddr::Ip4(ip) => Ipv4Meta::modify(None, Some(ip), None), + IpAddr::Ip6(ip) => Ipv6Meta::modify(None, Some(ip), None), + }; + HdrTransform { + name: SNAT_NAME.to_string(), + inner_ip: inner_ip, + inner_ulp: UlpHeaderAction::Modify(UlpMetaModify { + generic: UlpGenericModify { + dst_port: Some(self.priv_port), + ..Default::default() + }, ..Default::default() - }, + }), ..Default::default() - }), - ..Default::default() - }, + } + } } } fn name(&self) -> &str { - SNAT4_NAME + SNAT_NAME } } -impl Drop for SNat4Desc { +impl Drop for SNatDesc { fn drop(&mut self) { self.pool.release(&self.priv_ip, self.nat); } @@ -252,6 +305,27 @@ impl Drop for SNat4Desc { mod test { use super::*; + #[test] + fn test_nat_pool_different_ip_types() { + let pool = NatPool::new(); + + let ipv4: Ipv4Addr = "172.30.0.1".parse().unwrap(); + let pub_ipv4 = "76.76.21.21".parse().unwrap(); + let ipv6: Ipv6Addr = "fd00::1".parse().unwrap(); + let pub_ipv6 = "2001:db8::1".parse().unwrap(); + + assert!(pool.mapping(ipv4.into()).is_none()); + assert!(pool.mapping(ipv6.into()).is_none()); + + pool.add(ipv4, pub_ipv4, 0..=4096); + assert!(pool.mapping(ipv4.into()).is_some()); + assert!(pool.mapping(ipv6.into()).is_none()); + + pool.add(ipv6, pub_ipv6, 0..=4096); + assert!(pool.mapping(ipv4.into()).is_some()); + assert!(pool.mapping(ipv6.into()).is_some()); + } + #[test] fn snat4_desc_lifecycle() { use crate::engine::ether::{EtherMeta, ETHER_TYPE_IPV4}; @@ -259,22 +333,23 @@ mod test { use crate::engine::ip4::Protocol; use crate::engine::packet::{MetaGroup, PacketMeta}; use crate::engine::tcp::TcpMeta; - use opte_api::MacAddr; + use opte_api::{Ipv4Addr, MacAddr}; let priv_mac = MacAddr::from([0x02, 0x08, 0x20, 0xd8, 0x35, 0xcf]); let dest_mac = MacAddr::from([0x78, 0x23, 0xae, 0x5d, 0x4f, 0x0d]); - let priv_ip = "10.0.0.220".parse().unwrap(); + let priv_ipv4: Ipv4Addr = "10.0.0.220".parse().unwrap(); + let priv_ip = IpAddr::from(priv_ipv4); let priv_port = "4999".parse().unwrap(); - let pub_ip = "52.10.128.69".parse().unwrap(); + let pub_ip: Ipv4Addr = "52.10.128.69".parse().unwrap(); let pub_port = "8765".parse().unwrap(); - let outside_ip = "76.76.21.21".parse().unwrap(); + let outside_ip: Ipv4Addr = "76.76.21.21".parse().unwrap(); let outside_port = 80; let pool = Arc::new(NatPool::new()); - pool.add(priv_ip, pub_ip, 8765..=8765); - let snat = SNat4::new(priv_ip, pool.clone()); + pool.add(priv_ipv4, pub_ip, 8765..=8765); + let snat = SNat::new(priv_ip, pool.clone()); let mut action_meta = ActionMeta::new(); - assert!(pool.verify_available(priv_ip, pub_ip, pub_port)); + assert!(pool.verify_available(priv_ipv4, pub_ip, pub_port)); // ================================================================ // Build the packet metadata @@ -285,7 +360,7 @@ mod test { ether_type: ETHER_TYPE_IPV4, }; let ip = IpMeta::from(Ipv4Meta { - src: priv_ip, + src: priv_ipv4, dst: outside_ip, proto: Protocol::TCP, }); @@ -315,7 +390,7 @@ mod test { Ok(AllowOrDeny::Allow(desc)) => desc, _ => panic!("expected AllowOrDeny::Allow(desc) result"), }; - assert!(!pool.verify_available(priv_ip, pub_ip, pub_port)); + assert!(!pool.verify_available(priv_ipv4, pub_ip, pub_port)); // ================================================================ // Verify outbound header transformation @@ -389,7 +464,7 @@ mod test { }; assert_eq!(ip4_meta.src, outside_ip); - assert_eq!(ip4_meta.dst, priv_ip); + assert_eq!(ip4_meta.dst, priv_ipv4); assert_eq!(ip4_meta.proto, Protocol::TCP); let tcp_meta = match pmi.inner.ulp.as_ref().unwrap() { @@ -406,18 +481,21 @@ mod test { // handed back to the pool. // ================================================================ drop(desc); - assert!(pool.verify_available(priv_ip, pub_ip, pub_port)); + assert!(pool.verify_available(priv_ipv4, pub_ip, pub_port)); } #[test] fn nat_mappings() { let pool = NatPool::new(); - let priv1 = "192.168.2.8".parse::().unwrap(); - let priv2 = "192.168.2.33".parse::().unwrap(); - let public = "52.10.128.69".parse().unwrap(); - - pool.add(priv1, public, 1025..=4096); - pool.add(priv2, public, 4097..=8192); + let priv1_ip = "192.168.2.8".parse::().unwrap(); + let priv1 = IpAddr::Ip4(priv1_ip); + let priv2_ip = "192.168.2.33".parse::().unwrap(); + let priv2 = IpAddr::Ip4(priv2_ip); + let external_ip = "52.10.128.69".parse().unwrap(); + let public = IpAddr::Ip4(external_ip); + + pool.add(priv1_ip, external_ip, 1025..=4096); + pool.add(priv2_ip, external_ip, 4097..=8192); assert_eq!(pool.num_avail(priv1).unwrap(), 3072); let npe1 = match pool.obtain(&priv1) { diff --git a/opteadm/src/lib.rs b/opteadm/src/lib.rs index e362622d..69e3b973 100644 --- a/opteadm/src/lib.rs +++ b/opteadm/src/lib.rs @@ -10,15 +10,13 @@ use std::fs::{File, OpenOptions}; use std::os::unix::io::AsRawFd; -use opte::api::{ - Ipv4Addr, Ipv4Cidr, MacAddr, NoResp, OpteCmd, SetXdeUnderlayReq, Vni, -}; +use opte::api::{NoResp, OpteCmd, SetXdeUnderlayReq}; use opte::engine::ioctl::{self as api}; use opte_ioctl::{run_cmd_ioctl, Error}; use oxide_vpc::api::{ - AddFwRuleReq, AddRouterEntryIpv4Req, CreateXdeReq, DeleteXdeReq, - FirewallRule, ListPortsReq, ListPortsResp, RemFwRuleReq, SNatCfg, - SetFwRulesReq, SetVirt2PhysReq, + AddFwRuleReq, AddRouterEntryReq, CreateXdeReq, DeleteXdeReq, FirewallRule, + ListPortsReq, ListPortsResp, RemFwRuleReq, SetFwRulesReq, SetVirt2PhysReq, + VpcCfg, }; use oxide_vpc::engine::overlay; @@ -36,17 +34,7 @@ impl OpteAdm { pub fn create_xde( &self, name: &str, - private_mac: MacAddr, - private_ip: std::net::Ipv4Addr, - vpc_subnet: Ipv4Cidr, - gw_mac: MacAddr, - gw_ip: std::net::Ipv4Addr, - bsvc_addr: std::net::Ipv6Addr, - bsvc_vni: Vni, - vpc_vni: Vni, - src_underlay_addr: std::net::Ipv6Addr, - snat: Option, - external_ips_v4: Option, + cfg: VpcCfg, passthrough: bool, ) -> Result { use libnet::link; @@ -59,22 +47,7 @@ impl OpteAdm { let xde_devname = name.into(); let cmd = OpteCmd::CreateXde; - let req = CreateXdeReq { - xde_devname, - linkid, - private_mac, - private_ip: private_ip.into(), - vpc_subnet, - gw_mac, - gw_ip: gw_ip.into(), - bsvc_addr: bsvc_addr.into(), - bsvc_vni, - vpc_vni, - src_underlay_addr: src_underlay_addr.into(), - snat, - external_ips_v4, - passthrough, - }; + let req = CreateXdeReq { xde_devname, linkid, cfg, passthrough }; let res = run_cmd_ioctl(self.device.as_raw_fd(), cmd, &req); @@ -235,11 +208,11 @@ impl OpteAdm { ) } - pub fn add_router_entry_ip4( + pub fn add_router_entry( &self, - req: &AddRouterEntryIpv4Req, + req: &AddRouterEntryReq, ) -> Result { - let cmd = OpteCmd::AddRouterEntryIpv4; + let cmd = OpteCmd::AddRouterEntry; run_cmd_ioctl(self.device.as_raw_fd(), cmd, &req) } } diff --git a/opteadm/src/main.rs b/opteadm/src/main.rs index 29dd3a80..4315aeab 100644 --- a/opteadm/src/main.rs +++ b/opteadm/src/main.rs @@ -14,7 +14,7 @@ use std::str::FromStr; use structopt::StructOpt; use opte::api::{ - Direction, IpAddr, Ipv4Addr, Ipv4Cidr, Ipv6Addr, MacAddr, Vni, + Direction, IpCidr, Ipv4Addr, Ipv4Cidr, Ipv6Addr, MacAddr, Vni, }; use opte::engine::flow_table::FlowEntryDump; use opte::engine::ioctl as api; @@ -22,10 +22,11 @@ use opte::engine::layer::InnerFlowId; use opte::engine::rule::RuleDump; use opteadm::OpteAdm; use oxide_vpc::api::{ - Action as FirewallAction, AddRouterEntryIpv4Req, Address, + Action as FirewallAction, AddRouterEntryReq, Address, Filters as FirewallFilters, FirewallRule, GuestPhysAddr, PhysNet, PortInfo, - Ports, ProtoFilter, RemFwRuleReq, RouterTarget, SNatCfg, SetVirt2PhysReq, + Ports, ProtoFilter, RemFwRuleReq, RouterTarget, SNat4Cfg, SetVirt2PhysReq, }; +use oxide_vpc::api::{BoundaryServices, IpCfg, Ipv4Cfg, VpcCfg}; use oxide_vpc::engine::overlay::DumpVirt2PhysResp; /// Administer the Oxide Packet Transformation Engine (OPTE) @@ -128,6 +129,9 @@ enum Command { #[structopt(long)] bsvc_vni: Vni, + #[structopt(long, default_value = "00:00:00:00:00:00")] + bsvc_mac: MacAddr, + #[structopt(long)] vpc_vni: Vni, @@ -144,7 +148,7 @@ enum Command { snat_end: Option, #[structopt(long)] - snat_gw_mac: Option, + phys_gw_mac: Option, #[structopt(long)] external_ipv4: Option, @@ -167,13 +171,14 @@ enum Command { vni: Vni, }, - /// Add a new IPv4 router entry - AddRouterEntryIpv4 { + /// Add a new router entry, either IPv4 or IPv6. + AddRouterEntry { + /// The OPTE port to which the route is added #[structopt(short)] port: String, - - dest: opte::api::Ipv4Cidr, - + /// The network destination to which the route applies. + dest: IpCidr, + /// The location to which traffic matching the destination is sent. target: RouterTarget, }, } @@ -205,17 +210,31 @@ impl From for FirewallFilters { fn print_port_header() { println!( - "{:<32} {:<24} {:<16} {:<8}", - "LINK", "MAC ADDRESS", "IPv4 ADDRESS", "STATE" + "{:<32} {:<24} {:<16} {:<16} {:<40} {:<40} {:<8}", + "LINK", + "MAC ADDRESS", + "IPv4 ADDRESS", + "EXTERNAL IPv4", + "IPv6 ADDRESS", + "EXTERNAL IPv6", + "STATE" ); } fn print_port(pi: PortInfo) { + let none = String::from("None"); println!( - "{:<32} {:<24} {:<16} {:<8}", + "{:<32} {:<24} {:<16} {:<16} {:<40} {:<40} {:<8}", pi.name, pi.mac_addr.to_string(), - pi.ip4_addr.to_string(), + pi.ip4_addr.map(|x| x.to_string()).unwrap_or_else(|| none.clone()), + pi.external_ip4_addr + .map(|x| x.to_string()) + .unwrap_or_else(|| none.clone()), + pi.ip6_addr.map(|x| x.to_string()).unwrap_or_else(|| none.clone()), + pi.external_ip6_addr + .map(|x| x.to_string()) + .unwrap_or_else(|| none.clone()), pi.state, ); } @@ -228,11 +247,6 @@ fn print_flow_header() { } fn print_flow(flow_id: &InnerFlowId, flow_entry: &FlowEntryDump) { - let (src_ip, dst_ip) = match (flow_id.src_ip, flow_id.dst_ip) { - (IpAddr::Ip4(src), IpAddr::Ip4(dst)) => (src, dst), - _ => todo!("support for IPv6"), - }; - // For those types with custom Display implementations // we need to first format in into a String before // passing it to println in order for the format @@ -240,9 +254,9 @@ fn print_flow(flow_id: &InnerFlowId, flow_entry: &FlowEntryDump) { println!( "{:<6} {:<16} {:<6} {:<16} {:<6} {:<8} {:<22}", flow_id.proto.to_string(), - src_ip.to_string(), + flow_id.src_ip.to_string(), flow_id.src_port, - dst_ip.to_string(), + flow_id.dst_ip.to_string(), flow_id.dst_port, flow_entry.hits, flow_entry.state_summary, @@ -499,45 +513,54 @@ fn main() { gateway_ip, bsvc_addr, bsvc_vni, + bsvc_mac, vpc_vni, src_underlay_addr, snat_ip, snat_start, snat_end, - snat_gw_mac, + phys_gw_mac, external_ipv4, passthrough, } => { let hdl = opteadm::OpteAdm::open(OpteAdm::DLD_CTL).unwrap_or_die(); let snat = match snat_ip { - Some(ip) => Some(SNatCfg { - public_ip: ip.into(), + Some(ip) => Some(SNat4Cfg { + external_ip: ip.into(), ports: core::ops::RangeInclusive::new( snat_start.unwrap(), snat_end.unwrap(), ), - phys_gw_mac: snat_gw_mac.unwrap(), }), None => None, }; - hdl.create_xde( - &name, + let cfg = VpcCfg { + ip_cfg: IpCfg::Ipv4(Ipv4Cfg { + vpc_subnet, + private_ip: private_ip.into(), + gateway_ip: gateway_ip.into(), + snat_cfg: snat, + external_ips: external_ipv4, + }), private_mac, - private_ip, - vpc_subnet, gateway_mac, - gateway_ip, - bsvc_addr, - bsvc_vni, - vpc_vni, - src_underlay_addr, - snat, - external_ipv4, - passthrough, - ) - .unwrap_or_die(); + vni: vpc_vni, + phys_ip: src_underlay_addr.into(), + boundary_services: BoundaryServices { + ip: bsvc_addr.into(), + vni: bsvc_vni, + mac: bsvc_mac, + }, + // XXX-EXT-IP: This is part of the external IP hack. We're + // removing this shortly, and won't be supporting creating OPTE + // ports through `opteadm` that use the hack. + proxy_arp_enable: false, + phys_gw_mac, + }; + + hdl.create_xde(&name, cfg, passthrough).unwrap_or_die(); } Command::DeleteXde { name } => { @@ -564,10 +587,10 @@ fn main() { hdl.set_v2p(&req).unwrap_or_die(); } - Command::AddRouterEntryIpv4 { port, dest, target } => { + Command::AddRouterEntry { port, dest, target } => { let hdl = opteadm::OpteAdm::open(OpteAdm::DLD_CTL).unwrap_or_die(); - let req = AddRouterEntryIpv4Req { port_name: port, dest, target }; - hdl.add_router_entry_ip4(&req).unwrap_or_die(); + let req = AddRouterEntryReq { port_name: port, dest, target }; + hdl.add_router_entry(&req).unwrap_or_die(); } } } diff --git a/oxide-vpc/.gitignore b/oxide-vpc/.gitignore index 4e49c675..fe69eee7 100644 --- a/oxide-vpc/.gitignore +++ b/oxide-vpc/.gitignore @@ -1,5 +1,5 @@ -gateway_icmpv4_ping.pcap +gateway_icmpv[46]_ping.pcap overlay_guest_to_guest-guest-1.pcap overlay_guest_to_guest-guest-2.pcap overlay_guest_to_guest-phys-1.pcap -overlay_guest_to_guest-phys-2.pcap \ No newline at end of file +overlay_guest_to_guest-phys-2.pcap diff --git a/oxide-vpc/src/api.rs b/oxide-vpc/src/api.rs index db403242..ead1bf2a 100644 --- a/oxide-vpc/src/api.rs +++ b/oxide-vpc/src/api.rs @@ -21,6 +21,172 @@ cfg_if! { } } +/// Description of Boundary Services, the endpoint used to route traffic +/// to external networks. +// +// NOTE: This is identical to the `PhysNet` type below, but serves a different +// purpose, to identify Boundary Services itself, not a generic physical network +// endpoint in an Oxide rack. +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct BoundaryServices { + /// IPv6 address of the switch running Boundary Services. + pub ip: Ipv6Addr, + /// Dedicated Geneve VNI for Boundary Services traffic. + pub vni: Vni, + /// A MAC address identifying Boundary Services as a logical next + /// hop. + // This value is effectively arbitrary. It's never used to filter or + // direct traffic by the Oxide VPC. It is used to rewrite the + // destination MAC address of the _inner_ guest Ethernet frame, from + // the OPTE virtual gateway MAC, to this one. This serves two + // purposes: OPTE acts "correctly" as a gateway, rewriting the + // destination MAC to the logical next hop; and as an observability + // tool, allowing us to snoop traffic with this MAC. We already have + // the VNI of Boundary Services for that, but it might be useful + // nonetheless. + pub mac: MacAddr, +} + +/// The IPv4 configuration for an OPTE port. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Ipv4Cfg { + /// The private IP subnet of the VPC Subnet. + pub vpc_subnet: Ipv4Cidr, + + /// The guest's private IP address in the VPC Subnet. + pub private_ip: Ipv4Addr, + + /// The IPv4 address for the virtual gateway. + /// + /// The virtual gateway is what the guest sees as its gateway to all other + /// networks, including other VPC guests as well as external networks and + /// the internet. Essentially, this is the IPv4 address of OPTE itself, + /// which is acting as the gateway to the guest. + pub gateway_ip: Ipv4Addr, + + /// The source NAT configuration for making outbound connections + /// from the private network. + /// + /// This allows a guest to make outbound connections to hosts on an external + /// network when there is no external IP address assigned to the guest + /// itself. + // + // XXX Keep this optional for now until NAT'ing is more thoroughly + // implemented in Omicron. + pub snat_cfg: Option, + + /// Optional external IP addresses for this port. + /// + /// This allows hosts on the external network to make inbound connections to + /// the guest. When present, it is also used as 1:1 NAT for outbound + /// connections from the guest to an external network. + // + // XXX For now we only allow one external IP. + pub external_ips: Option, +} + +/// The IPv6 configuration for an OPTE port +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Ipv6Cfg { + /// The private IP subnet of the VPC Subnet. + pub vpc_subnet: Ipv6Cidr, + + /// The guest's private IP address in the VPC Subnet. + pub private_ip: Ipv6Addr, + + /// The IPv6 address for the virtual gateway. + /// + /// The virtual gateway is what the guest sees as its gateway to all other + /// networks, including other VPC guests as well as external networks and + /// the internet. Essentially, this is the IPv6 address of OPTE itself, + /// which is acting as the gateway to the guest. + pub gateway_ip: Ipv6Addr, + + /// The source NAT configuration for making outbound connections + /// from the private network. + /// + /// This allows a guest to make outbound connections to hosts on an external + /// network when there is no external IP address assigned to the guest + /// itself. + // + // XXX Keep this optional for now until NAT'ing is more thoroughly + // implemented in Omicron. + pub snat_cfg: Option, + + /// Optional external IP addresses for this port. + /// + /// This allows hosts on the external network to make inbound connections to + /// the guest. When present, it is also used as 1:1 NAT for outbound + /// connections from the guest to an external network. + // + // XXX For now we only allow one external IP. + pub external_ips: Option, +} + +/// The IP configuration for a port. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum IpCfg { + Ipv4(Ipv4Cfg), + Ipv6(Ipv6Cfg), + DualStack { ipv4: Ipv4Cfg, ipv6: Ipv6Cfg }, +} + +/// The overall configuration for an OPTE port. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VpcCfg { + /// IP address configuration. + pub ip_cfg: IpCfg, + + /// The VPC-private MAC address of the guest. + pub private_mac: MacAddr, + + /// The MAC address for the virtual gateway. + /// + /// The virtual gateway is what the guest sees as its gateway to all other + /// networks, including other VPC guests as well as external networks and + /// the internet. Essentially, this is the MAC address of OPTE itself, + /// which is acting as the gateway to the guest. + pub gateway_mac: MacAddr, + + /// The Geneve Virtual Network Identifier for this VPC in which the guest + /// resides. + pub vni: Vni, + + /// The host (sled) IPv6 address. All guests on the same sled are + /// sourced to a single IPv6 address. + pub phys_ip: Ipv6Addr, + + /// Information for reaching Boundary Services, for traffic destined + /// for external networks. + pub boundary_services: BoundaryServices, + + // XXX-EXT-IP the following two fields are for the external IP hack. + pub proxy_arp_enable: bool, + pub phys_gw_mac: Option, +} + +impl VpcCfg { + /// Return the IPv4 configuration, if it exists, or None. + pub fn ipv4_cfg(&self) -> Option<&Ipv4Cfg> { + match self.ip_cfg { + IpCfg::Ipv4(ref ipv4) | IpCfg::DualStack { ref ipv4, .. } => { + Some(ipv4) + } + _ => None, + } + } + + /// Return the IPv6 configuration, if it exists, or None. + pub fn ipv6_cfg(&self) -> Option<&Ipv6Cfg> { + match self.ip_cfg { + IpCfg::Ipv6(ref ipv6) | IpCfg::DualStack { ref ipv6, .. } => { + Some(ipv6) + } + _ => None, + } + } +} + /// A network destination on the Oxide Rack's physical network. #[derive(Clone, Copy, Debug, Deserialize, Serialize)] pub struct PhysNet { @@ -55,7 +221,7 @@ pub struct GuestPhysAddr { /// abstraction, it's simply allowing one subnet to talk to another. /// There is no separate VPC router process, the real routing is done /// by the underlay. -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Copy, Deserialize, Serialize)] pub enum RouterTarget { Drop, InternetGateway, @@ -84,75 +250,47 @@ impl FromStr for RouterTarget { Ok(Self::VpcSubnet(IpCidr::Ip4(cidr4))) } + Some(("ip6", ip6s)) => { + ip6s.parse().map(|x| Self::Ip(IpAddr::Ip6(x))) + } + + Some(("sub6", cidr6s)) => { + cidr6s.parse().map(|x| Self::VpcSubnet(IpCidr::Ip6(x))) + } + _ => Err(format!("malformed router target: {}", lower)), }, } } } +impl Display for RouterTarget { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Drop => write!(f, "Drop"), + Self::InternetGateway => write!(f, "IG"), + Self::Ip(IpAddr::Ip4(ip4)) => write!(f, "ip4={}", ip4), + Self::Ip(IpAddr::Ip6(ip6)) => write!(f, "ip6={}", ip6), + Self::VpcSubnet(IpCidr::Ip4(sub4)) => write!(f, "sub4={}", sub4), + Self::VpcSubnet(IpCidr::Ip6(sub6)) => write!(f, "sub6={}", sub6), + } + } +} + /// Xde create ioctl parameter data. +/// +/// The bulk of the information is provided via [`VpcCfg`]. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct CreateXdeReq { /// The link identifier of the guest, as provided by dlmgmtd. pub linkid: datalink_id_t, - pub xde_devname: String, - /// The VPC IPv4 address of the guest. - pub private_ip: Ipv4Addr, - - /// The VPC subnet CIDR of the guest. - pub vpc_subnet: Ipv4Cidr, - - /// The VPC MAC address of the guest. - pub private_mac: MacAddr, - - /// The MAC address for the virtual gateway. The virtual gateway - /// is what the guest sees as it's gateway to all other networks, - /// including other VPC guests as well as external networks and - /// the internet. Essentially, this is the MAC address of OPTE - /// itself, which is acting as the gateway to the guest. - pub gw_mac: MacAddr, - - /// The IPv4 address for the virtual gateway. The virtual gateway - /// is what the guest sees as it's gateway to all other networks, - /// including other VPC guests as well as external networks and - /// the internet. Essentially, this is the IPv4 address of OPTE - /// itself, which is acting as the gateway to the guest. - pub gw_ip: Ipv4Addr, - - /// The Boundary Services IPv6 address. Boundary Services is used - /// to transit packets between VPC guests and external networks. - pub bsvc_addr: Ipv6Addr, - - /// The Boundary Services Virtual Network Identifier (VNI). - /// Boundary Services is used to transit packets between VPC - /// guests and external networks. - pub bsvc_vni: Vni, - - /// The host (sled) IPv6 address. All guests on the same sled are - /// sourced to a single IPv6 address. - pub src_underlay_addr: Ipv6Addr, - - /// The Virtual Network Identifier (VNI) of the VPC in which the - /// guest resides. - pub vpc_vni: Vni, - - /// The Source NAT configuration for this guest, consisting of an - /// IP + port range which may be used. This allows a guest to make - /// outbound connections to hosts on an external network when - /// there is no external IP address assigned to the guest itself. - /// - /// XXX Keep this optional for now until NAT'ing is more thoroughly - /// implemented in Omicron. - pub snat: Option, + /// The name of the data link, as it appears to `dlmgmtd`. + pub xde_devname: String, - /// The external IPv4 address of the guest. This allows hosts on - /// the external network to make inbound connections to the guest. - /// Whe present, it is also used as 1:1 NAT for outbound - /// connections from the guest to an external network. - /// - /// XXX For now we only allow one external IP. - pub external_ips_v4: Option, + /// Configuration information describing the device. See [`VpcCfg`] for more + /// details. + pub cfg: VpcCfg, /// This is a development tool for completely bypassing OPTE processing. /// @@ -161,11 +299,20 @@ pub struct CreateXdeReq { pub passthrough: bool, } +/// Configuration of source NAT for a port, describing how a private IP +/// address is mapped to an external IP and port range for outbound connections. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SNat4Cfg { + pub external_ip: Ipv4Addr, + pub ports: core::ops::RangeInclusive, +} + +/// Configuration of source NAT for a port, describing how a private IP +/// address is mapped to an external IP and port range for outbound connections. #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct SNatCfg { - pub public_ip: Ipv4Addr, +pub struct SNat6Cfg { + pub external_ip: Ipv6Addr, pub ports: core::ops::RangeInclusive, - pub phys_gw_mac: MacAddr, } /// Xde delete ioctl parameter data. @@ -185,7 +332,10 @@ pub struct ListPortsReq { pub struct PortInfo { pub name: String, pub mac_addr: MacAddr, - pub ip4_addr: Ipv4Addr, + pub ip4_addr: Option, + pub external_ip4_addr: Option, + pub ip6_addr: Option, + pub external_ip6_addr: Option, pub state: String, } @@ -203,23 +353,24 @@ pub struct SetVirt2PhysReq { pub phys: PhysNet, } -/// Add an entry to the IPv4 router. +/// Add an entry to the router. Addresses may be either IPv4 or IPv6, though the +/// destination and target must match in protocol version. #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct AddRouterEntryIpv4Req { +pub struct AddRouterEntryReq { pub port_name: String, - pub dest: Ipv4Cidr, + pub dest: IpCidr, pub target: RouterTarget, } #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct DelRouterEntryIpv4Req { +pub struct DelRouterEntryReq { pub port_name: String, - pub dest: Ipv4Cidr, + pub dest: IpCidr, pub target: RouterTarget, } #[derive(Clone, Debug, Deserialize, Serialize)] -pub enum DelRouterEntryIpv4Resp { +pub enum DelRouterEntryResp { Ok, NotFound, } diff --git a/oxide-vpc/src/engine/arp.rs b/oxide-vpc/src/engine/arp.rs index 96dc698d..587fdec8 100644 --- a/oxide-vpc/src/engine/arp.rs +++ b/oxide-vpc/src/engine/arp.rs @@ -12,7 +12,7 @@ cfg_if! { } } -use crate::VpcCfg; +use crate::api::VpcCfg; use opte::api::{Direction, MacAddr, OpteError}; use opte::engine::arp::ArpReply; use opte::engine::ether::ETHER_TYPE_ARP; @@ -27,45 +27,71 @@ pub fn setup( cfg: &VpcCfg, ft_limit: core::num::NonZeroU32, ) -> core::result::Result<(), OpteError> { - let mut actions = vec![ - // ARP Reply for gateway's IP. - Action::Hairpin(Arc::new(ArpReply::new(cfg.gw_ip, cfg.gw_mac))), - ]; + // If the guest is configured to use IPv4, we need to respond to its ARP + // requests to resolve the gateway (OPTE) IP address. While the external IP + // hack is still in place, we also need to Proxy ARP external requests for + // the guest's IP address. + // + // Regardless of which IP version the guest is configured to use, we need to + // drop any other ARP request, inbound or outbound. + let mut arp = if let Some(ip_cfg) = cfg.ipv4_cfg() { + let mut actions = vec![ + // ARP Reply for gateway's IPv4 address. + Action::Hairpin(Arc::new(ArpReply::new( + ip_cfg.gateway_ip, + cfg.gateway_mac, + ))), + ]; - if let Some(ip) = cfg.external_ips_v4.as_ref() { - if cfg.proxy_arp_enable { - // XXX-EXT-IP Hack to get remote access to guest instance - // via Proxy ARP. - // - // Reuse the same MAC address for both IPs. This should be - // fine as the VIP is contained solely to the guest - // instance. - actions.push(Action::Hairpin(Arc::new(ArpReply::new( - *ip, - cfg.private_mac, - )))); + if let Some(ip) = ip_cfg.external_ips.as_ref() { + if cfg.proxy_arp_enable { + // XXX-EXT-IP Hack to get remote access to guest instance + // via Proxy ARP. + // + // Reuse the same MAC address for both IPs. This should be + // fine as the VIP is contained solely to the guest + // instance. + actions.push(Action::Hairpin(Arc::new(ArpReply::new( + *ip, + cfg.private_mac, + )))); + } } - } + let mut arp = Layer::new( + "arp", + pb.name(), + // vec![ + // // ARP Reply for gateway's IP. + // Action::Hairpin(Arc::new(ArpReply::new(cfg.gw_ip, cfg.gw_mac))), + // ], + actions, + ft_limit, + ); - let mut arp = Layer::new( - "arp", - pb.name(), - // vec![ - // // ARP Reply for gateway's IP. - // Action::Hairpin(Arc::new(ArpReply::new(cfg.gw_ip, cfg.gw_mac))), - // ], - actions, - ft_limit, - ); + // ================================================================ + // Outbound ARP Request for Gateway, from Guest + // ================================================================ + let mut rule = Rule::new(1, arp.action(0).unwrap().clone()); + rule.add_predicate(Predicate::InnerEtherSrc(vec![ + EtherAddrMatch::Exact(MacAddr::from(cfg.private_mac)), + ])); + arp.add_rule(Direction::Out, rule.finalize()); - // ================================================================ - // Outbound ARP Request for Gateway, from Guest - // ================================================================ - let mut rule = Rule::new(1, arp.action(0).unwrap().clone()); - rule.add_predicate(Predicate::InnerEtherSrc(vec![EtherAddrMatch::Exact( - MacAddr::from(cfg.private_mac), - )])); - arp.add_rule(Direction::Out, rule.finalize()); + // ================================================================ + // Proxy ARP for any incoming requests for guest's external IP. + // + // XXX-EXT-IP This is a hack to get guest access working until we + // have boundary services integrated. + // ================================================================ + if ip_cfg.external_ips.is_some() && cfg.proxy_arp_enable { + let rule = Rule::new(1, arp.action(1).unwrap().clone()); + arp.add_rule(Direction::In, rule.finalize()); + } + + arp + } else { + Layer::new("arp", pb.name(), vec![], ft_limit) + }; // ================================================================ // Drop all other outbound ARP Requests from Guest @@ -76,17 +102,6 @@ pub fn setup( )])); arp.add_rule(Direction::Out, rule.finalize()); - // ================================================================ - // Proxy ARP for any incoming requests for guest's external IP. - // - // XXX-EXT-IP This is a hack to get guest access working until we - // have boundary services integrated. - // ================================================================ - if cfg.external_ips_v4.is_some() && cfg.proxy_arp_enable { - let rule = Rule::new(1, arp.action(1).unwrap().clone()); - arp.add_rule(Direction::In, rule.finalize()); - } - // ================================================================ // Drop all inbound ARP Requests // ================================================================ diff --git a/oxide-vpc/src/engine/dhcp4.rs b/oxide-vpc/src/engine/dhcp.rs similarity index 79% rename from oxide-vpc/src/engine/dhcp4.rs rename to oxide-vpc/src/engine/dhcp.rs index 36320986..a3db6af2 100644 --- a/oxide-vpc/src/engine/dhcp4.rs +++ b/oxide-vpc/src/engine/dhcp.rs @@ -24,9 +24,9 @@ cfg_if! { } } -use crate::VpcCfg; +use crate::api::VpcCfg; use opte::api::{ - Dhcp4Action, Dhcp4ReplyType, Direction, Ipv4Addr, Ipv4PrefixLen, OpteError, + DhcpAction, DhcpReplyType, Direction, Ipv4Addr, Ipv4PrefixLen, OpteError, SubnetRouterPair, }; use opte::engine::ip4::Ipv4Cidr; @@ -39,6 +39,13 @@ pub fn setup( cfg: &VpcCfg, ft_limit: core::num::NonZeroU32, ) -> Result<(), OpteError> { + // The DHCP layer only contains meaningful actions if the port is configured + // to support IPv4. + let ip_cfg = match cfg.ipv4_cfg() { + None => return Ok(()), + Some(cfg) => cfg, + }; + // All guest interfaces live on a `/32`-network in the Oxide VPC; // restricting the L2 domain to two nodes: the guest NIC and the // OPTE Port. This allows OPTE to act as the gateway for which all @@ -50,7 +57,7 @@ pub fn setup( // on the same link (L2 segment) to communicate with each other. // In our case we place the guest in a network of 1, meaning the // router itself must be on a different subnet. However, since the - // router, in this OPTE, is on the same link, we can use the local + // router, in this case OPTE, is on the same link, we can use the local // subnet route feature to deliver packets to the router. // // * `re1`: The local subnet router entry; mapping the gateway @@ -68,20 +75,20 @@ pub fn setup( // // Furthermore, RFC 3442 goes on to say that a DHCP server // administrator should always set both to be on the safe side. - let gw_cidr = Ipv4Cidr::new(cfg.gw_ip, Ipv4PrefixLen::NETMASK_ALL); + let gw_cidr = Ipv4Cidr::new(ip_cfg.gateway_ip, Ipv4PrefixLen::NETMASK_ALL); let re1 = SubnetRouterPair::new(gw_cidr, Ipv4Addr::ANY_ADDR); let re2 = SubnetRouterPair::new( Ipv4Cidr::new(Ipv4Addr::ANY_ADDR, Ipv4PrefixLen::NETMASK_NONE), - cfg.gw_ip, + ip_cfg.gateway_ip, ); - let offer = Action::Hairpin(Arc::new(Dhcp4Action { + let offer = Action::Hairpin(Arc::new(DhcpAction { client_mac: cfg.private_mac.into(), - client_ip: cfg.private_ip, + client_ip: ip_cfg.private_ip, subnet_prefix_len: Ipv4PrefixLen::NETMASK_ALL, - gw_mac: cfg.gw_mac.into(), - gw_ip: cfg.gw_ip, - reply_type: Dhcp4ReplyType::Offer, + gw_mac: cfg.gateway_mac.into(), + gw_ip: ip_cfg.gateway_ip, + reply_type: DhcpReplyType::Offer, re1, re2: Some(re2), re3: None, @@ -94,13 +101,13 @@ pub fn setup( })); let offer_idx = 0; - let ack = Action::Hairpin(Arc::new(Dhcp4Action { + let ack = Action::Hairpin(Arc::new(DhcpAction { client_mac: cfg.private_mac.into(), - client_ip: cfg.private_ip, + client_ip: ip_cfg.private_ip, subnet_prefix_len: Ipv4PrefixLen::NETMASK_ALL, - gw_mac: cfg.gw_mac.into(), - gw_ip: cfg.gw_ip, - reply_type: Dhcp4ReplyType::Ack, + gw_mac: cfg.gateway_mac.into(), + gw_ip: ip_cfg.gateway_ip, + reply_type: DhcpReplyType::Ack, re1, re2: Some(re2), re3: None, @@ -113,7 +120,7 @@ pub fn setup( })); let ack_idx = 1; - let mut dhcp = Layer::new("dhcp4", pb.name(), vec![offer, ack], ft_limit); + let mut dhcp = Layer::new("dhcp", pb.name(), vec![offer, ack], ft_limit); let discover_rule = Rule::new(1, dhcp.action(offer_idx).unwrap().clone()); dhcp.add_rule(Direction::Out, discover_rule.finalize()); diff --git a/oxide-vpc/src/engine/icmp.rs b/oxide-vpc/src/engine/icmp.rs index 3abbf6c4..e2706031 100644 --- a/oxide-vpc/src/engine/icmp.rs +++ b/oxide-vpc/src/engine/icmp.rs @@ -12,9 +12,9 @@ cfg_if! { } } -use crate::VpcCfg; +use crate::api::VpcCfg; use opte::api::{Direction, OpteError}; -use opte::engine::icmp::Icmp4EchoReply; +use opte::engine::icmp::IcmpEchoReply; use opte::engine::layer::Layer; use opte::engine::port::{PortBuilder, Pos}; use opte::engine::rule::{Action, Rule}; @@ -24,13 +24,20 @@ pub fn setup( cfg: &VpcCfg, ft_limit: core::num::NonZeroU32, ) -> core::result::Result<(), OpteError> { - let reply = Action::Hairpin(Arc::new(Icmp4EchoReply { + // The ICMP layer only contains meaningful actions if the port is configured + // to support IPv4. + let ip_cfg = match cfg.ipv4_cfg() { + None => return Ok(()), + Some(cfg) => cfg, + }; + + let reply = Action::Hairpin(Arc::new(IcmpEchoReply { // Map an Echo from guest (src) -> gateway (dst) to an Echo // Reply from gateway (dst) -> guest (src). echo_src_mac: cfg.private_mac.into(), - echo_src_ip: cfg.private_ip, - echo_dst_mac: cfg.gw_mac.into(), - echo_dst_ip: cfg.gw_ip, + echo_src_ip: ip_cfg.private_ip, + echo_dst_mac: cfg.gateway_mac.into(), + echo_dst_ip: ip_cfg.gateway_ip, })); let mut icmp = Layer::new("icmp", pb.name(), vec![reply], ft_limit); diff --git a/oxide-vpc/src/engine/icmpv6.rs b/oxide-vpc/src/engine/icmpv6.rs new file mode 100644 index 00000000..113c8e08 --- /dev/null +++ b/oxide-vpc/src/engine/icmpv6.rs @@ -0,0 +1,49 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// Copyright 2022 Oxide Computer Company + +//! Layer handling ICMPv6 messages + +cfg_if! { + if #[cfg(all(not(feature = "std"), not(test)))] { + use alloc::sync::Arc; + } else { + use std::sync::Arc; + } +} + +use crate::api::VpcCfg; +use opte::api::{Direction, OpteError}; +use opte::engine::icmpv6::Icmpv6EchoReply; +use opte::engine::layer::Layer; +use opte::engine::port::{PortBuilder, Pos}; +use opte::engine::rule::{Action, Rule}; + +pub fn setup( + pb: &mut PortBuilder, + cfg: &VpcCfg, + ft_limit: core::num::NonZeroU32, +) -> core::result::Result<(), OpteError> { + // The ICMPv6 layer only contains meaningful actions if the port is + // configured to support IPv6. + let ip_cfg = match cfg.ipv6_cfg() { + None => return Ok(()), + Some(cfg) => cfg, + }; + + let reply = Action::Hairpin(Arc::new(Icmpv6EchoReply { + // Map an Echo Request from guest (src) -> gateway (dst) to an Echo + // Reply from gateway (dst) -> guest (src). + src_mac: cfg.private_mac.into(), + src_ip: ip_cfg.private_ip, + dst_mac: cfg.gateway_mac.into(), + dst_ip: ip_cfg.gateway_ip, + })); + let mut icmp = Layer::new("icmpv6", pb.name(), vec![reply], ft_limit); + + let rule = Rule::new(1, icmp.action(0).unwrap().clone()); + icmp.add_rule(Direction::Out, rule.finalize()); + pb.add_layer(icmp, Pos::Before("firewall")) +} diff --git a/oxide-vpc/src/engine/mod.rs b/oxide-vpc/src/engine/mod.rs index a2a9f865..76fbcca0 100644 --- a/oxide-vpc/src/engine/mod.rs +++ b/oxide-vpc/src/engine/mod.rs @@ -5,9 +5,10 @@ // Copyright 2022 Oxide Computer Company pub mod arp; -pub mod dhcp4; +pub mod dhcp; pub mod firewall; pub mod icmp; +pub mod icmpv6; pub mod nat; pub mod overlay; pub mod router; diff --git a/oxide-vpc/src/engine/nat.rs b/oxide-vpc/src/engine/nat.rs index d64baab4..68542290 100644 --- a/oxide-vpc/src/engine/nat.rs +++ b/oxide-vpc/src/engine/nat.rs @@ -15,35 +15,54 @@ cfg_if! { } use super::router::{RouterTargetInternal, ROUTER_LAYER_NAME}; -use crate::VpcCfg; +use crate::api::{Ipv4Cfg, Ipv6Cfg, MacAddr, VpcCfg}; +use core::num::NonZeroU32; +use core::result::Result; use opte::api::{Direction, OpteError}; -use opte::engine::ether::ETHER_TYPE_IPV4; +use opte::engine::ether::{ETHER_TYPE_IPV4, ETHER_TYPE_IPV6}; use opte::engine::layer::Layer; -use opte::engine::nat::Nat4; +use opte::engine::nat::Nat; use opte::engine::port::meta::ActionMetaValue; use opte::engine::port::{PortBuilder, Pos}; use opte::engine::rule::{ - Action, EtherTypeMatch, Ipv4AddrMatch, Predicate, Rule, + Action, EtherTypeMatch, Ipv4AddrMatch, Ipv6AddrMatch, Predicate, Rule, }; -use opte::engine::snat::{NatPool, SNat4}; +use opte::engine::snat::{NatPool, SNat}; pub const NAT_LAYER_NAME: &'static str = "nat"; +const ONE_TO_ONE_NAT_PRIORITY: u16 = 10; +const SNAT_PRIORITY: u16 = 100; pub fn setup( pb: &mut PortBuilder, cfg: &VpcCfg, - ft_limit: core::num::NonZeroU32, -) -> core::result::Result<(), OpteError> { + ft_limit: NonZeroU32, +) -> Result<(), OpteError> { let mut layer = Layer::new(NAT_LAYER_NAME, pb.name(), vec![], ft_limit); + if let Some(ipv4_cfg) = cfg.ipv4_cfg() { + setup_ipv4_nat(&mut layer, ipv4_cfg, cfg.phys_gw_mac)?; + } + if let Some(ipv6_cfg) = cfg.ipv6_cfg() { + setup_ipv6_nat(&mut layer, ipv6_cfg, cfg.phys_gw_mac)?; + } + pb.add_layer(layer, Pos::After(ROUTER_LAYER_NAME)) +} +fn setup_ipv4_nat( + layer: &mut Layer, + ip_cfg: &Ipv4Cfg, + // XXX-EXT-IP Remove + phys_gw_mac: Option, +) -> Result<(), OpteError> { // When it comes to NAT we always prefer using 1:1 NAT of external // IP to SNAT. To achieve this we place the NAT rules at a lower // priority than SNAT. - if let Some(ip4) = cfg.external_ips_v4 { - let nat = Arc::new(Nat4::new(cfg.private_ip, ip4, cfg.phys_gw_mac)); + if let Some(ip4) = ip_cfg.external_ips { + let nat = Arc::new(Nat::new(ip_cfg.private_ip, ip4, phys_gw_mac)); // 1:1 NAT outbound packets destined for internet gateway. - let mut out_nat = Rule::new(10, Action::Stateful(nat.clone())); + let mut out_nat = + Rule::new(ONE_TO_ONE_NAT_PRIORITY, Action::Stateful(nat.clone())); out_nat.add_predicate(Predicate::InnerEtherType(vec![ EtherTypeMatch::Exact(ETHER_TYPE_IPV4), ])); @@ -54,22 +73,24 @@ pub fn setup( layer.add_rule(Direction::Out, out_nat.finalize()); // 1:1 NAT inbound packets destined for external IP. - let mut in_nat = Rule::new(10, Action::Stateful(nat)); + let mut in_nat = + Rule::new(ONE_TO_ONE_NAT_PRIORITY, Action::Stateful(nat)); in_nat.add_predicate(Predicate::InnerDstIp4(vec![ Ipv4AddrMatch::Exact(ip4), ])); layer.add_rule(Direction::In, in_nat.finalize()); } - if cfg.snat.is_some() { + if let Some(snat_cfg) = &ip_cfg.snat_cfg { let pool = NatPool::new(); pool.add( - cfg.private_ip, - cfg.snat.as_ref().unwrap().public_ip, - cfg.snat.as_ref().unwrap().ports.clone(), + ip_cfg.private_ip, + snat_cfg.external_ip, + snat_cfg.ports.clone(), ); - let snat = SNat4::new(cfg.private_ip, Arc::new(pool)); - let mut rule = Rule::new(100, Action::Stateful(Arc::new(snat))); + let snat = SNat::new(ip_cfg.private_ip.into(), Arc::new(pool)); + let mut rule = + Rule::new(SNAT_PRIORITY, Action::Stateful(Arc::new(snat))); rule.add_predicate(Predicate::InnerEtherType(vec![ EtherTypeMatch::Exact(ETHER_TYPE_IPV4), @@ -80,6 +101,61 @@ pub fn setup( )); layer.add_rule(Direction::Out, rule.finalize()); } + Ok(()) +} - pb.add_layer(layer, Pos::After(ROUTER_LAYER_NAME)) +fn setup_ipv6_nat( + layer: &mut Layer, + ip_cfg: &Ipv6Cfg, + // XXX-EXT-IP Remove + phys_gw_mac: Option, +) -> Result<(), OpteError> { + // When it comes to NAT we always prefer using 1:1 NAT of external + // IP to SNAT. To achieve this we place the NAT rules at a lower + // priority than SNAT. + if let Some(ip6) = ip_cfg.external_ips { + let nat = Arc::new(Nat::new(ip_cfg.private_ip, ip6, phys_gw_mac)); + + // 1:1 NAT outbound packets destined for internet gateway. + let mut out_nat = + Rule::new(ONE_TO_ONE_NAT_PRIORITY, Action::Stateful(nat.clone())); + out_nat.add_predicate(Predicate::InnerEtherType(vec![ + EtherTypeMatch::Exact(ETHER_TYPE_IPV6), + ])); + out_nat.add_predicate(Predicate::Meta( + RouterTargetInternal::KEY.to_string(), + RouterTargetInternal::InternetGateway.as_meta(), + )); + layer.add_rule(Direction::Out, out_nat.finalize()); + + // 1:1 NAT inbound packets destined for external IP. + let mut in_nat = + Rule::new(ONE_TO_ONE_NAT_PRIORITY, Action::Stateful(nat)); + in_nat.add_predicate(Predicate::InnerDstIp6(vec![ + Ipv6AddrMatch::Exact(ip6), + ])); + layer.add_rule(Direction::In, in_nat.finalize()); + } + + if let Some(ref snat_cfg) = ip_cfg.snat_cfg { + let pool = NatPool::new(); + pool.add( + ip_cfg.private_ip, + snat_cfg.external_ip, + snat_cfg.ports.clone(), + ); + let snat = SNat::new(ip_cfg.private_ip.into(), Arc::new(pool)); + let mut rule = + Rule::new(SNAT_PRIORITY, Action::Stateful(Arc::new(snat))); + + rule.add_predicate(Predicate::InnerEtherType(vec![ + EtherTypeMatch::Exact(ETHER_TYPE_IPV6), + ])); + rule.add_predicate(Predicate::Meta( + RouterTargetInternal::KEY.to_string(), + RouterTargetInternal::InternetGateway.as_meta(), + )); + layer.add_rule(Direction::Out, rule.finalize()); + } + Ok(()) } diff --git a/oxide-vpc/src/engine/overlay.rs b/oxide-vpc/src/engine/overlay.rs index f2ea9d73..a8cc04ba 100644 --- a/oxide-vpc/src/engine/overlay.rs +++ b/oxide-vpc/src/engine/overlay.rs @@ -26,8 +26,7 @@ cfg_if! { use serde::{Deserialize, Serialize}; use super::router::RouterTargetInternal; -use crate::api::{GuestPhysAddr, PhysNet}; -use crate::VpcCfg; +use crate::api::{BoundaryServices, GuestPhysAddr, PhysNet, VpcCfg}; use opte::api::{CmdOk, Direction, Ipv4Addr, MacAddr, OpteError}; use opte::ddi::sync::{KMutex, KMutexType}; use opte::engine::ether::{EtherMeta, ETHER_TYPE_IPV6}; @@ -54,7 +53,7 @@ pub fn setup( ) -> core::result::Result<(), OpteError> { // Action Index 0 let encap = Action::Static(Arc::new(EncapAction::new( - cfg.bsvc_addr, + cfg.boundary_services, cfg.phys_ip, cfg.vni, v2p, @@ -140,7 +139,7 @@ pub const ENCAP_NAME: &'static str = "encap"; /// The mapping itself is available through the port metadata passes /// as argument to the [`StaticAction`] callback. pub struct EncapAction { - bsvc_addr: PhysNet, + boundary_services: PhysNet, // The physical IPv6 ULA of the server that hosts this guest // sending data. phys_ip_src: Ipv6Addr, @@ -150,12 +149,21 @@ pub struct EncapAction { impl EncapAction { pub fn new( - bsvc_addr: PhysNet, + boundary_services: BoundaryServices, phys_ip_src: Ipv6Addr, vni: Vni, v2p: Arc, ) -> Self { - Self { bsvc_addr, phys_ip_src, vni, v2p } + Self { + boundary_services: PhysNet { + ether: boundary_services.mac, + ip: boundary_services.ip, + vni: boundary_services.vni, + }, + phys_ip_src, + vni, + v2p, + } } } @@ -202,7 +210,7 @@ impl StaticAction for EncapAction { }; let phys_target = match target { - RouterTargetInternal::InternetGateway => self.bsvc_addr, + RouterTargetInternal::InternetGateway => self.boundary_services, RouterTargetInternal::Ip(virt_ip) => match self.v2p.get(&virt_ip) { Some(phys) => PhysNet { diff --git a/oxide-vpc/src/engine/router.rs b/oxide-vpc/src/engine/router.rs index 37f4bf34..2c72758c 100644 --- a/oxide-vpc/src/engine/router.rs +++ b/oxide-vpc/src/engine/router.rs @@ -23,9 +23,10 @@ cfg_if! { } use super::firewall as fw; -use crate::api::{DelRouterEntryIpv4Resp, RouterTarget}; -use crate::VpcCfg; -use opte::api::{Direction, Ipv4Addr, Ipv4Cidr, NoResp, OpteError}; +use crate::api::{DelRouterEntryResp, RouterTarget, VpcCfg}; +use opte::api::{ + Direction, Ipv4Addr, Ipv4Cidr, Ipv6Addr, Ipv6Cidr, NoResp, OpteError, +}; use opte::engine::headers::{IpAddr, IpCidr}; use opte::engine::layer::{InnerFlowId, Layer}; use opte::engine::port::meta::{ActionMeta, ActionMetaValue}; @@ -56,39 +57,29 @@ impl ActionMetaValue for RouterTargetInternal { match s { "ig" => Ok(Self::InternetGateway), - _ => { - match s.split_once("=") { - Some(("ip4", ip4_s)) => { - let ip4 = ip4_s.parse::()?; - Ok(Self::Ip(IpAddr::Ip4(ip4))) - } - - Some(("ip6", _ip6_s)) => { - todo!("implement IPv6 support"); - // XXX The parse impl only exists in std envs - // at the moment. - // - // let ip6 = ip6_s.parse::()?; - // Ok(Self::Ip(IpAddr::Ip6(ip6))) - } - - Some(("sub4", cidr4_s)) => { - let cidr4 = cidr4_s.parse::()?; - Ok(Self::VpcSubnet(IpCidr::Ip4(cidr4))) - } - - Some(("sub6", _cidr6_s)) => { - todo!("implement IPv6 subnet support"); - // XXX The parse impl only exists in std envs - // at the moment. - // - // let cidr6 = cidr6_s.parse::()?; - // Ok(Self::VpcSubnet(IpCidr::Ip6(cidr6))) - } - - _ => Err(format!("bad router target: {}", s)), + _ => match s.split_once("=") { + Some(("ip4", ip4_s)) => { + let ip4 = ip4_s.parse::()?; + Ok(Self::Ip(IpAddr::Ip4(ip4))) } - } + + Some(("ip6", ip6_s)) => { + let ip6 = ip6_s.parse::()?; + Ok(Self::Ip(IpAddr::Ip6(ip6))) + } + + Some(("sub4", cidr4_s)) => { + let cidr4 = cidr4_s.parse::()?; + Ok(Self::VpcSubnet(IpCidr::Ip4(cidr4))) + } + + Some(("sub6", cidr6_s)) => { + let cidr6 = cidr6_s.parse::()?; + Ok(Self::VpcSubnet(IpCidr::Ip6(cidr6))) + } + + _ => Err(format!("bad router target: {}", s)), + }, } } @@ -114,14 +105,20 @@ impl fmt::Display for RouterTargetInternal { } } -// The array index represents the subnet prefix length (thus the need -// for 33 entries). The value represents the Rule priority. -fn build_ip4_len_to_pri() -> [u16; 33] { - let mut v = [0; 33]; - for (i, pri) in (0..33).rev().enumerate() { - v[i] = pri + 10; - } - v +// Return a priority for an IP subnet, depending on its prefix length. +// +// The priority is computed as `max_prefix_len - prefix_len + 10`, where +// `max_prefix_len` is the maximum prefix length for the CIDR block of each IP +// version. +fn prefix_len_to_priority(cidr: &IpCidr) -> u16 { + use opte::api::ip::IpCidr::*; + use opte::api::ip::Ipv4PrefixLen; + use opte::api::ip::Ipv6PrefixLen; + let (max_prefix_len, prefix_len) = match cidr { + Ip4(ipv4) => (Ipv4PrefixLen::NETMASK_ALL.val(), ipv4.prefix_len()), + Ip6(ipv6) => (Ipv6PrefixLen::NETMASK_ALL.val(), ipv6.prefix_len()), + }; + (max_prefix_len - prefix_len) as u16 + 10 } pub fn setup( @@ -145,101 +142,120 @@ pub fn setup( pb.add_layer(layer, Pos::After(fw::FW_LAYER_NAME)) } +fn valid_router_dest_target_pair(dest: &IpCidr, target: &RouterTarget) -> bool { + matches!( + (&dest, &target), + // Anything can be dropped + (_, RouterTarget::Drop) | + // IPv4 destination, IPv4 address + (IpCidr::Ip4(_), RouterTarget::Ip(IpAddr::Ip4(_))) | + // IPv4 destination, IPv4 subnet + (IpCidr::Ip4(_), RouterTarget::VpcSubnet(IpCidr::Ip4(_))) | + // IPv6 destination, IPv6 address + (IpCidr::Ip6(_), RouterTarget::Ip(IpAddr::Ip6(_))) | + // IPv6 destination, IPv6 subnet + (IpCidr::Ip6(_), RouterTarget::VpcSubnet(IpCidr::Ip6(_))) + ) || + // Only the default IP addresses are currently allowed to be directed to + // the gateway + (matches!(target, RouterTarget::InternetGateway) && dest.is_default()) +} + fn make_rule( dest: IpCidr, target: RouterTarget, ) -> Result, OpteError> { - let pri_map4 = build_ip4_len_to_pri(); - - match target { - RouterTarget::Drop => match dest { - IpCidr::Ip4(ip4) => { - let mut rule = Rule::new( - pri_map4[ip4.prefix_len() as usize], - Action::Deny, - ); - rule.add_predicate(Predicate::InnerDstIp4(vec![ - rule::Ipv4AddrMatch::Prefix(ip4), - ])); - Ok(rule.finalize()) - } + if !valid_router_dest_target_pair(&dest, &target) { + return Err(OpteError::InvalidRouterEntry { + dest, + target: target.to_string(), + }); + } - IpCidr::Ip6(_) => todo!("IPv6 drop"), - }, + let (predicate, action) = + match target { + RouterTarget::Drop => { + let predicate = match dest { + IpCidr::Ip4(ip4) => Predicate::InnerDstIp4(vec![ + rule::Ipv4AddrMatch::Prefix(ip4), + ]), - RouterTarget::InternetGateway => { - if !dest.is_default() { - return Err(OpteError::InvalidRouteDest(dest.to_string())); + IpCidr::Ip6(ip6) => Predicate::InnerDstIp6(vec![ + rule::Ipv6AddrMatch::Prefix(ip6), + ]), + }; + (predicate, Action::Deny) } - match dest { - IpCidr::Ip4(ip4) => { - let mut rule = Rule::new( - pri_map4[dest.prefix_len()], - Action::Meta(Arc::new(RouterAction::new( - RouterTargetInternal::InternetGateway, - ))), - ); - rule.add_predicate(Predicate::InnerDstIp4(vec![ + RouterTarget::InternetGateway => { + let predicate = match dest { + IpCidr::Ip4(ip4) => Predicate::InnerDstIp4(vec![ rule::Ipv4AddrMatch::Prefix(ip4), - ])); - Ok(rule.finalize()) - } - - IpCidr::Ip6(_) => todo!("IPv6 IG"), + ]), + + IpCidr::Ip6(ip6) => Predicate::InnerDstIp6(vec![ + rule::Ipv6AddrMatch::Prefix(ip6), + ]), + }; + let action = Action::Meta(Arc::new(RouterAction::new( + RouterTargetInternal::InternetGateway, + ))); + (predicate, action) } - } - RouterTarget::Ip(ip) => match dest { - IpCidr::Ip4(ip4) => { - let mut rule = Rule::new( - pri_map4[ip4.prefix_len() as usize], - Action::Meta(Arc::new(RouterAction::new( - RouterTargetInternal::Ip(ip), - ))), - ); - rule.add_predicate(Predicate::InnerDstIp4(vec![ - rule::Ipv4AddrMatch::Prefix(ip4), - ])); - Ok(rule.finalize()) + RouterTarget::Ip(ip) => { + let predicate = match dest { + IpCidr::Ip4(ip4) => Predicate::InnerDstIp4(vec![ + rule::Ipv4AddrMatch::Prefix(ip4), + ]), + + IpCidr::Ip6(ip6) => Predicate::InnerDstIp6(vec![ + rule::Ipv6AddrMatch::Prefix(ip6), + ]), + }; + let action = Action::Meta(Arc::new(RouterAction::new( + RouterTargetInternal::Ip(ip), + ))); + (predicate, action) } - IpCidr::Ip6(_) => todo!("IPv6 IP"), - }, - - RouterTarget::VpcSubnet(vpc) => match dest { - IpCidr::Ip4(ip4) => { - let mut rule = Rule::new( - pri_map4[ip4.prefix_len() as usize], - Action::Meta(Arc::new(RouterAction::new( - RouterTargetInternal::VpcSubnet(vpc), - ))), - ); - rule.add_predicate(Predicate::InnerDstIp4(vec![ - rule::Ipv4AddrMatch::Prefix(ip4), - ])); - Ok(rule.finalize()) + RouterTarget::VpcSubnet(vpc) => { + let predicate = match dest { + IpCidr::Ip4(ip4) => Predicate::InnerDstIp4(vec![ + rule::Ipv4AddrMatch::Prefix(ip4), + ]), + + IpCidr::Ip6(ip6) => Predicate::InnerDstIp6(vec![ + rule::Ipv6AddrMatch::Prefix(ip6), + ]), + }; + let action = Action::Meta(Arc::new(RouterAction::new( + RouterTargetInternal::VpcSubnet(vpc), + ))); + (predicate, action) } + }; - IpCidr::Ip6(_) => todo!("IPv6 router entry"), - }, - } + let priority = prefix_len_to_priority(&dest); + let mut rule = Rule::new(priority, action); + rule.add_predicate(predicate); + Ok(rule.finalize()) } pub fn del_entry( port: &Port, dest: IpCidr, target: RouterTarget, -) -> Result { +) -> Result { let rule = make_rule(dest, target)?; let maybe_id = port.find_rule(ROUTER_LAYER_NAME, Direction::Out, &rule)?; match maybe_id { Some(id) => { port.remove_rule(ROUTER_LAYER_NAME, Direction::Out, id)?; - Ok(DelRouterEntryIpv4Resp::Ok) + Ok(DelRouterEntryResp::Ok) } - None => Ok(DelRouterEntryIpv4Resp::NotFound), + None => Ok(DelRouterEntryResp::NotFound), } } diff --git a/oxide-vpc/src/lib.rs b/oxide-vpc/src/lib.rs index a4718d91..3fc454e0 100644 --- a/oxide-vpc/src/lib.rs +++ b/oxide-vpc/src/lib.rs @@ -41,30 +41,5 @@ extern crate cfg_if; #[cfg(any(feature = "api", test))] pub mod api; -cfg_if! { - if #[cfg(any(feature = "engine", test))] { - use opte::api::{Ipv4Addr, Ipv4Cidr, Ipv6Addr, MacAddr, Vni}; - use crate::api::{PhysNet, SNatCfg}; - - pub mod engine; - - // TODO Tease out generic PortCfg. - #[derive(Clone, Debug)] - pub struct VpcCfg { - pub vpc_subnet: Ipv4Cidr, - pub private_mac: MacAddr, - pub private_ip: Ipv4Addr, - pub gw_mac: MacAddr, - pub gw_ip: Ipv4Addr, - // XXX For now we limit to one external IP. - pub external_ips_v4: Option, - pub snat: Option, - pub vni: Vni, - pub phys_ip: Ipv6Addr, - pub bsvc_addr: PhysNet, - // XXX-EXT-IP the follow two fields are for the external IP hack. - pub proxy_arp_enable: bool, - pub phys_gw_mac: Option, - } - } -} +#[cfg(any(feature = "engine", test))] +pub mod engine; diff --git a/oxide-vpc/tests/integration_tests.rs b/oxide-vpc/tests/integration_tests.rs index 8db166cc..1d17f34a 100644 --- a/oxide-vpc/tests/integration_tests.rs +++ b/oxide-vpc/tests/integration_tests.rs @@ -30,13 +30,13 @@ use opte::engine::arp::{ use opte::engine::checksum::HeaderChecksum; use opte::engine::ether::{ EtherHdr, EtherHdrRaw, EtherMeta, EtherType, ETHER_HDR_SZ, ETHER_TYPE_ARP, - ETHER_TYPE_IPV4, + ETHER_TYPE_IPV4, ETHER_TYPE_IPV6, }; use opte::engine::flow_table::FLOW_DEF_EXPIRE_SECS; use opte::engine::geneve::{self, Vni}; use opte::engine::headers::{IpAddr, IpCidr, IpMeta, UlpMeta}; use opte::engine::ip4::{Ipv4Addr, Ipv4Hdr, Ipv4Meta, Protocol, UlpCsumOpt}; -use opte::engine::ip6::Ipv6Addr; +use opte::engine::ip6::{Ipv6Addr, Ipv6Hdr, Ipv6Meta, IPV6_HDR_SZ}; use opte::engine::packet::{ Initialized, Packet, PacketRead, PacketReader, PacketWriter, ParseError, Parsed, @@ -50,12 +50,12 @@ use opte::engine::tcp::{TcpFlags, TcpHdr}; use opte::engine::udp::{UdpHdr, UdpMeta}; use opte::ExecCtx; use oxide_vpc::api::{ - AddFwRuleReq, FirewallRule, GuestPhysAddr, PhysNet, RouterTarget, SNatCfg, - SetFwRulesReq, + AddFwRuleReq, FirewallRule, GuestPhysAddr, RouterTarget, SNat4Cfg, + SNat6Cfg, SetFwRulesReq, }; +use oxide_vpc::api::{BoundaryServices, IpCfg, Ipv4Cfg, Ipv6Cfg, VpcCfg}; use oxide_vpc::engine::overlay::{self, Virt2Phys}; -use oxide_vpc::engine::{arp, dhcp4, firewall, icmp, nat, router}; -use oxide_vpc::VpcCfg; +use oxide_vpc::engine::{arp, dhcp, firewall, icmp, icmpv6, nat, router}; use pcap_parser::pcap::{self, LegacyPcapBlock, PcapHeader}; use smoltcp::phy::ChecksumCapabilities as CsumCapab; use std::boxed::Box; @@ -381,18 +381,20 @@ impl PcapBuilder { } fn lab_cfg() -> VpcCfg { - VpcCfg { - private_ip: "172.20.14.16".parse().unwrap(), - private_mac: MacAddr::from([0xAA, 0x00, 0x04, 0x00, 0xFF, 0x10]), + let ip_cfg = IpCfg::Ipv4(Ipv4Cfg { vpc_subnet: "172.20.14.0/24".parse().unwrap(), - snat: Some(SNatCfg { - public_ip: "76.76.21.21".parse().unwrap(), + private_ip: "172.20.14.16".parse().unwrap(), + gateway_ip: "172.20.14.1".parse().unwrap(), + snat_cfg: Some(SNat4Cfg { + external_ip: "76.76.21.21".parse().unwrap(), ports: 1025..=4096, - phys_gw_mac: MacAddr::from([0x78, 0x23, 0xae, 0x5d, 0x4f, 0x0d]), }), - external_ips_v4: None, - gw_mac: MacAddr::from([0xAA, 0x00, 0x04, 0x00, 0xFF, 0x01]), - gw_ip: "172.20.14.1".parse().unwrap(), + external_ips: None, + }); + VpcCfg { + ip_cfg, + private_mac: MacAddr::from([0xAA, 0x00, 0x04, 0x00, 0xFF, 0x10]), + gateway_mac: MacAddr::from([0xAA, 0x00, 0x04, 0x00, 0xFF, 0x01]), // XXX These values don't really mean anything in this // context. This "lab cfg" was created during the early days @@ -404,8 +406,8 @@ fn lab_cfg() -> VpcCfg { phys_ip: Ipv6Addr::from([ 0xFD00, 0x0000, 0x00F7, 0x0101, 0x0000, 0x0000, 0x0000, 0x0001, ]), - bsvc_addr: PhysNet { - ether: MacAddr::from([0xA8, 0x40, 0x25, 0x77, 0x77, 0x77]), + boundary_services: BoundaryServices { + mac: MacAddr::from([0xA8, 0x40, 0x25, 0x77, 0x77, 0x77]), ip: Ipv6Addr::from([ 0xFD, 0x00, 0x11, 0x22, 0x33, 0x44, 0x01, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x77, 0x77, @@ -413,7 +415,7 @@ fn lab_cfg() -> VpcCfg { vni: Vni::new(7777u32).unwrap(), }, proxy_arp_enable: false, - phys_gw_mac: None, + phys_gw_mac: Some(MacAddr::from([0x78, 0x23, 0xae, 0x5d, 0x4f, 0x0d])), } } @@ -432,8 +434,9 @@ fn oxide_net_builder( let one_limit = NonZeroU32::new(1).unwrap(); firewall::setup(&mut pb, fw_limit).expect("failed to add firewall layer"); - dhcp4::setup(&mut pb, cfg, one_limit).expect("failed to add dhcp4 layer"); + dhcp::setup(&mut pb, cfg, one_limit).expect("failed to add dhcp layer"); icmp::setup(&mut pb, cfg, one_limit).expect("failed to add icmp layer"); + icmpv6::setup(&mut pb, cfg, one_limit).expect("failed to add icmpv6 layer"); arp::setup(&mut pb, cfg, one_limit).expect("failed to add arp layer"); router::setup(&mut pb, cfg, one_limit).expect("failed to add router layer"); nat::setup(&mut pb, cfg, snat_limit).expect("failed to add nat layer"); @@ -473,7 +476,7 @@ fn oxide_net_setup( "set:arp.rules_in=1,arp.rules_out=2", "set:icmp.rules_out=1", "set:fw.rules_in=1,fw.rules_out=1", - "set:nat.rules_out=1", + "set:nat.rules_out=2", "set:router.rules_out=1", ] ); @@ -485,29 +488,39 @@ const UFT_LIMIT: Option = NonZeroU32::new(16); const TCP_LIMIT: Option = NonZeroU32::new(16); fn g1_cfg() -> VpcCfg { + let ip_cfg = IpCfg::DualStack { + ipv4: Ipv4Cfg { + vpc_subnet: "172.30.0.0/22".parse().unwrap(), + private_ip: "172.30.0.5".parse().unwrap(), + gateway_ip: "172.30.0.1".parse().unwrap(), + snat_cfg: Some(SNat4Cfg { + external_ip: "10.77.77.13".parse().unwrap(), + ports: 1025..=4096, + }), + external_ips: None, + }, + ipv6: Ipv6Cfg { + vpc_subnet: "fd00::/64".parse().unwrap(), + private_ip: "fd00::5".parse().unwrap(), + gateway_ip: "fd00::1".parse().unwrap(), + snat_cfg: Some(SNat6Cfg { + external_ip: "2001:db8::1".parse().unwrap(), + ports: 1025..=4096, + }), + external_ips: None, + }, + }; VpcCfg { - private_ip: "172.30.0.5".parse().unwrap(), + ip_cfg, private_mac: MacAddr::from([0xA8, 0x40, 0x25, 0xFA, 0xFA, 0x37]), - vpc_subnet: "172.30.0.0/22".parse().unwrap(), - snat: Some(SNatCfg { - // NOTE: This is not a routable IP, but remember that a - // "public IP" for an Oxide guest could either be a - // public, routable IP or simply an IP on their wider LAN - // which the oxide Rack is simply a part of. - public_ip: "10.77.77.13".parse().unwrap(), - ports: 1025..=4096, - phys_gw_mac: MacAddr::from([0x78, 0x23, 0xae, 0x5d, 0x4f, 0x0d]), - }), - external_ips_v4: None, - gw_mac: MacAddr::from([0xA8, 0x40, 0x25, 0xFF, 0x77, 0x77]), - gw_ip: "172.30.0.1".parse().unwrap(), + gateway_mac: MacAddr::from([0xA8, 0x40, 0x25, 0xFF, 0x77, 0x77]), vni: Vni::new(1287581u32).unwrap(), // Site 0xF7, Rack 1, Sled 1, Interface 1 phys_ip: Ipv6Addr::from([ 0xFD00, 0x0000, 0x00F7, 0x0101, 0x0000, 0x0000, 0x0000, 0x0001, ]), - bsvc_addr: PhysNet { - ether: MacAddr::from([0xA8, 0x40, 0x25, 0x77, 0x77, 0x77]), + boundary_services: BoundaryServices { + mac: MacAddr::from([0xA8, 0x40, 0x25, 0x77, 0x77, 0x77]), ip: Ipv6Addr::from([ 0xFD, 0x00, 0x99, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, @@ -515,34 +528,44 @@ fn g1_cfg() -> VpcCfg { vni: Vni::new(99u32).unwrap(), }, proxy_arp_enable: false, - phys_gw_mac: None, + phys_gw_mac: Some(MacAddr::from([0x78, 0x23, 0xae, 0x5d, 0x4f, 0x0d])), } } fn g2_cfg() -> VpcCfg { + let ip_cfg = IpCfg::DualStack { + ipv4: Ipv4Cfg { + vpc_subnet: "172.30.0.0/22".parse().unwrap(), + private_ip: "172.30.0.6".parse().unwrap(), + gateway_ip: "172.30.0.1".parse().unwrap(), + snat_cfg: Some(SNat4Cfg { + external_ip: "10.77.77.23".parse().unwrap(), + ports: 4096..=8192, + }), + external_ips: None, + }, + ipv6: Ipv6Cfg { + vpc_subnet: "fd00::/64".parse().unwrap(), + private_ip: "fd00::5".parse().unwrap(), + gateway_ip: "fd00::1".parse().unwrap(), + snat_cfg: Some(SNat6Cfg { + external_ip: "2001:db8::1".parse().unwrap(), + ports: 1025..=4096, + }), + external_ips: None, + }, + }; VpcCfg { - private_ip: "172.30.0.6".parse().unwrap(), + ip_cfg, private_mac: MacAddr::from([0xA8, 0x40, 0x25, 0xF0, 0x00, 0x66]), - vpc_subnet: "172.30.0.0/22".parse().unwrap(), - snat: Some(SNatCfg { - // NOTE: This is not a routable IP, but remember that a - // "public IP" for an Oxide guest could either be a - // public, routable IP or simply an IP on their wider LAN - // which the oxide Rack is simply a part of. - public_ip: "10.77.77.23".parse().unwrap(), - ports: 4097..=8192, - phys_gw_mac: MacAddr::from([0x78, 0x23, 0xae, 0x5d, 0x4f, 0x0d]), - }), - external_ips_v4: None, - gw_mac: MacAddr::from([0xA8, 0x40, 0x25, 0xFF, 0x77, 0x77]), - gw_ip: "172.30.0.1".parse().unwrap(), + gateway_mac: MacAddr::from([0xA8, 0x40, 0x25, 0xFF, 0x77, 0x77]), vni: Vni::new(1287581u32).unwrap(), // Site 0xF7, Rack 1, Sled 22, Interface 1 phys_ip: Ipv6Addr::from([ 0xFD00, 0x0000, 0x00F7, 0x0116, 0x0000, 0x0000, 0x0000, 0x0001, ]), - bsvc_addr: PhysNet { - ether: MacAddr::from([0xA8, 0x40, 0x25, 0x77, 0x77, 0x77]), + boundary_services: BoundaryServices { + mac: MacAddr::from([0xA8, 0x40, 0x25, 0x77, 0x77, 0x77]), ip: Ipv6Addr::from([ 0xFD, 0x00, 0x11, 0x22, 0x33, 0x44, 0x01, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x77, 0x77, @@ -550,7 +573,7 @@ fn g2_cfg() -> VpcCfg { vni: Vni::new(99u32).unwrap(), }, proxy_arp_enable: false, - phys_gw_mac: None, + phys_gw_mac: Some(MacAddr::from([0x78, 0x23, 0xae, 0x5d, 0x4f, 0x0d])), } } @@ -572,13 +595,17 @@ fn tcp_telnet_syn(src: &VpcCfg, dst: &VpcCfg) -> Packet { let mut tcp = TcpHdr::new(7865, 23); tcp.set_flags(TcpFlags::SYN); tcp.set_seq(4224936861); - let mut ip4 = - Ipv4Hdr::new_tcp(&mut tcp, &body, src.private_ip, dst.private_ip); + let mut ip4 = Ipv4Hdr::new_tcp( + &mut tcp, + &body, + src.ipv4_cfg().unwrap().private_ip, + dst.ipv4_cfg().unwrap().private_ip, + ); ip4.compute_hdr_csum(); let tcp_csum = ip4.compute_ulp_csum(UlpCsumOpt::Full, &tcp.as_bytes(), &body); tcp.set_csum(HeaderChecksum::from(tcp_csum).bytes()); - let eth = EtherHdr::new(EtherType::Ipv4, src.private_mac, src.gw_mac); + let eth = EtherHdr::new(EtherType::Ipv4, src.private_mac, src.gateway_mac); let mut bytes = vec![]; bytes.extend_from_slice(ð.as_bytes()); @@ -591,7 +618,11 @@ fn tcp_telnet_syn(src: &VpcCfg, dst: &VpcCfg) -> Packet { // Generate a packet representing the start of a TCP handshake for an // HTTP request from src to dst. fn http_tcp_syn(src: &VpcCfg, dst: &VpcCfg) -> Packet { - http_tcp_syn2(src.private_mac, src.private_ip, dst.private_ip) + http_tcp_syn2( + src.private_mac, + src.ipv4_cfg().unwrap().private_ip, + dst.ipv4_cfg().unwrap().private_ip, + ) } // Generate a packet representing the start of a TCP handshake for an @@ -628,13 +659,17 @@ fn http_tcp_syn_ack(src: &VpcCfg, dst: &VpcCfg) -> Packet { tcp.set_flags(TcpFlags::SYN | TcpFlags::ACK); tcp.set_seq(44161351); tcp.set_ack(2382112980); - let mut ip4 = - Ipv4Hdr::new_tcp(&mut tcp, &body, src.private_ip, dst.private_ip); + let mut ip4 = Ipv4Hdr::new_tcp( + &mut tcp, + &body, + src.ipv4_cfg().unwrap().private_ip, + dst.ipv4_cfg().unwrap().private_ip, + ); ip4.compute_hdr_csum(); let tcp_csum = ip4.compute_ulp_csum(UlpCsumOpt::Full, &tcp.as_bytes(), &body); tcp.set_csum(HeaderChecksum::from(tcp_csum).bytes()); - let eth = EtherHdr::new(EtherType::Ipv4, src.private_mac, src.gw_mac); + let eth = EtherHdr::new(EtherType::Ipv4, src.private_mac, src.gateway_mac); let mut bytes = vec![]; bytes.extend_from_slice(ð.as_bytes()); @@ -655,7 +690,7 @@ fn port_transition_running() { // Add V2P mappings that allow guests to resolve each others // physical addresses. let v2p = Arc::new(Virt2Phys::new()); - v2p.set(IpAddr::Ip4(g2_cfg.private_ip), g2_phys); + v2p.set(IpAddr::Ip4(g2_cfg.ipv4_cfg().unwrap().private_ip), g2_phys); let mut ameta = ActionMeta::new(); let mut g1 = oxide_net_setup("g1_port", &g1_cfg, v2p.clone()); @@ -663,8 +698,10 @@ fn port_transition_running() { // same subnet. router::add_entry( &g1.port, - IpCidr::Ip4(g1_cfg.vpc_subnet), - RouterTarget::VpcSubnet(IpCidr::Ip4(g1_cfg.vpc_subnet)), + IpCidr::Ip4(g1_cfg.ipv4_cfg().unwrap().vpc_subnet), + RouterTarget::VpcSubnet(IpCidr::Ip4( + g1_cfg.ipv4_cfg().unwrap().vpc_subnet, + )), ) .unwrap(); incr!(g1, ["epoch", "router.rules_out"]); @@ -696,7 +733,7 @@ fn port_transition_reset() { // Add V2P mappings that allow guests to resolve each others // physical addresses. let v2p = Arc::new(Virt2Phys::new()); - v2p.set(IpAddr::Ip4(g2_cfg.private_ip), g2_phys); + v2p.set(IpAddr::Ip4(g2_cfg.ipv4_cfg().unwrap().private_ip), g2_phys); let mut ameta = ActionMeta::new(); let mut g1 = oxide_net_setup("g1_port", &g1_cfg, v2p.clone()); @@ -704,8 +741,10 @@ fn port_transition_reset() { // same subnet. router::add_entry( &g1.port, - IpCidr::Ip4(g1_cfg.vpc_subnet), - RouterTarget::VpcSubnet(IpCidr::Ip4(g1_cfg.vpc_subnet)), + IpCidr::Ip4(g1_cfg.ipv4_cfg().unwrap().vpc_subnet), + RouterTarget::VpcSubnet(IpCidr::Ip4( + g1_cfg.ipv4_cfg().unwrap().vpc_subnet, + )), ) .unwrap(); incr!(g1, ["epoch", "router.rules_out"]); @@ -746,8 +785,8 @@ fn port_transition_pause() { // Add V2P mappings that allow guests to resolve each others // physical addresses. let v2p = Arc::new(Virt2Phys::new()); - v2p.set(IpAddr::Ip4(g1_cfg.private_ip), g1_phys); - v2p.set(IpAddr::Ip4(g2_cfg.private_ip), g2_phys); + v2p.set(IpAddr::Ip4(g1_cfg.ipv4_cfg().unwrap().private_ip), g1_phys); + v2p.set(IpAddr::Ip4(g2_cfg.ipv4_cfg().unwrap().private_ip), g2_phys); let mut g1_ameta = ActionMeta::new(); let mut g2_ameta = ActionMeta::new(); let mut g1 = oxide_net_setup("g1_port", &g1_cfg, v2p.clone()); @@ -757,8 +796,10 @@ fn port_transition_pause() { // subnet. router::add_entry( &g1.port, - IpCidr::Ip4(g1_cfg.vpc_subnet), - RouterTarget::VpcSubnet(IpCidr::Ip4(g1_cfg.vpc_subnet)), + IpCidr::Ip4(g1_cfg.ipv4_cfg().unwrap().vpc_subnet), + RouterTarget::VpcSubnet(IpCidr::Ip4( + g1_cfg.ipv4_cfg().unwrap().vpc_subnet, + )), ) .unwrap(); incr!(g1, ["epoch", "router.rules_out"]); @@ -782,8 +823,10 @@ fn port_transition_pause() { // subnet. router::add_entry( &g2.port, - IpCidr::Ip4(g2_cfg.vpc_subnet), - RouterTarget::VpcSubnet(IpCidr::Ip4(g2_cfg.vpc_subnet)), + IpCidr::Ip4(g2_cfg.ipv4_cfg().unwrap().vpc_subnet), + RouterTarget::VpcSubnet(IpCidr::Ip4( + g2_cfg.ipv4_cfg().unwrap().vpc_subnet, + )), ) .unwrap(); incr!(g2, ["epoch", "router.rules_out"]); @@ -825,8 +868,10 @@ fn port_transition_pause() { assert!(matches!( router::del_entry( &g2.port, - IpCidr::Ip4(g2_cfg.vpc_subnet), - RouterTarget::VpcSubnet(IpCidr::Ip4(g2_cfg.vpc_subnet)), + IpCidr::Ip4(g2_cfg.ipv4_cfg().unwrap().vpc_subnet), + RouterTarget::VpcSubnet(IpCidr::Ip4( + g2_cfg.ipv4_cfg().unwrap().vpc_subnet + )), ), Err(OpteError::BadState(_)) )); @@ -905,6 +950,26 @@ fn add_remove_fw_rule() { } fn gen_icmp_echo_req( + eth_src: MacAddr, + eth_dst: MacAddr, + ip_src: IpAddr, + ip_dst: IpAddr, + ident: u16, + seq_no: u16, + data: &[u8], +) -> Packet { + match (ip_src, ip_dst) { + (IpAddr::Ip4(src), IpAddr::Ip4(dst)) => { + gen_icmpv4_echo_req(eth_src, eth_dst, src, dst, ident, seq_no, data) + } + (IpAddr::Ip6(src), IpAddr::Ip6(dst)) => { + gen_icmpv6_echo_req(eth_src, eth_dst, src, dst, ident, seq_no, data) + } + (_, _) => panic!("IP src and dst versions must match"), + } +} + +fn gen_icmpv4_echo_req( eth_src: MacAddr, eth_dst: MacAddr, ip_src: Ipv4Addr, @@ -940,6 +1005,46 @@ fn gen_icmp_echo_req( Packet::copy(&pkt_bytes).parse().unwrap() } +fn gen_icmpv6_echo_req( + eth_src: MacAddr, + eth_dst: MacAddr, + ip_src: Ipv6Addr, + ip_dst: Ipv6Addr, + ident: u16, + seq_no: u16, + data: &[u8], +) -> Packet { + use smoltcp::wire::{Icmpv6Packet, Icmpv6Repr, Ipv6Address}; + + let req = Icmpv6Repr::EchoRequest { ident, seq_no, data }; + let mut body_bytes = vec![0u8; req.buffer_len()]; + let mut req_pkt = Icmpv6Packet::new_unchecked(&mut body_bytes); + let _ = req.emit( + &Ipv6Address::from_bytes(ip_src.bytes().as_slice()).into(), + &Ipv6Address::from_bytes(ip_dst.bytes().as_slice()).into(), + &mut req_pkt, + &Default::default(), + ); + let mut ip6 = Ipv6Hdr::from(&Ipv6Meta { + src: ip_src, + dst: ip_dst, + proto: Protocol::ICMPv6, + }); + ip6.set_total_len(ip6.hdr_len() as u16 + req.buffer_len() as u16); + let eth = EtherHdr::from(&EtherMeta { + dst: eth_dst, + src: eth_src, + ether_type: ETHER_TYPE_IPV6, + }); + + let mut pkt_bytes = + Vec::with_capacity(ETHER_HDR_SZ + ip6.hdr_len() + req.buffer_len()); + pkt_bytes.extend_from_slice(ð.as_bytes()); + pkt_bytes.extend_from_slice(&ip6.as_bytes()); + pkt_bytes.extend_from_slice(&body_bytes); + Packet::copy(&pkt_bytes).parse().unwrap() +} + // Verify that the guest can ping the virtual gateway. #[test] fn gateway_icmp4_ping() { @@ -960,9 +1065,9 @@ fn gateway_icmp4_ping() { // ================================================================ let mut pkt1 = gen_icmp_echo_req( g1_cfg.private_mac, - g1_cfg.gw_mac, - g1_cfg.private_ip, - g1_cfg.gw_ip, + g1_cfg.gateway_mac, + g1_cfg.ipv4_cfg().unwrap().private_ip.into(), + g1_cfg.ipv4_cfg().unwrap().gateway_ip.into(), ident, seq_no, &data[..], @@ -995,7 +1100,7 @@ fn gateway_icmp4_ping() { match meta.inner.ether.as_ref() { Some(eth) => { - assert_eq!(eth.src, g1_cfg.gw_mac); + assert_eq!(eth.src, g1_cfg.gateway_mac); assert_eq!(eth.dst, g1_cfg.private_mac); } @@ -1004,12 +1109,12 @@ fn gateway_icmp4_ping() { match meta.inner.ip.as_ref().unwrap() { IpMeta::Ip4(ip4) => { - assert_eq!(ip4.src, g1_cfg.gw_ip); - assert_eq!(ip4.dst, g1_cfg.private_ip); + assert_eq!(ip4.src, g1_cfg.ipv4_cfg().unwrap().gateway_ip); + assert_eq!(ip4.dst, g1_cfg.ipv4_cfg().unwrap().private_ip); assert_eq!(ip4.proto, Protocol::ICMP); } - ip6 => panic!("execpted inner IPv4 metadata, got IPv6: {:?}", ip6), + ip6 => panic!("expected inner IPv4 metadata, got IPv6: {:?}", ip6), } let mut rdr = PacketReader::new(&reply, ()); @@ -1049,7 +1154,7 @@ fn guest_to_guest_no_route() { // Add V2P mappings that allow guests to resolve each others // physical addresses. let v2p = Arc::new(Virt2Phys::new()); - v2p.set(IpAddr::Ip4(g2_cfg.private_ip), g2_phys); + v2p.set(IpAddr::Ip4(g2_cfg.ipv4_cfg().unwrap().private_ip), g2_phys); let mut ameta = ActionMeta::new(); let mut g1 = oxide_net_setup("g1_port", &g1_cfg, v2p.clone()); g1.port.start(); @@ -1081,7 +1186,7 @@ fn guest_to_guest() { // Add V2P mappings that allow guests to resolve each others // physical addresses. let v2p = Arc::new(Virt2Phys::new()); - v2p.set(IpAddr::Ip4(g2_cfg.private_ip), g2_phys); + v2p.set(IpAddr::Ip4(g2_cfg.ipv4_cfg().unwrap().private_ip), g2_phys); let mut ameta = ActionMeta::new(); let mut g1 = oxide_net_setup("g1_port", &g1_cfg, v2p.clone()); @@ -1091,8 +1196,10 @@ fn guest_to_guest() { // Add router entry that allows Guest 1 to send to Guest 2. router::add_entry( &g1.port, - IpCidr::Ip4(g2_cfg.vpc_subnet), - RouterTarget::VpcSubnet(IpCidr::Ip4(g2_cfg.vpc_subnet)), + IpCidr::Ip4(g2_cfg.ipv4_cfg().unwrap().vpc_subnet), + RouterTarget::VpcSubnet(IpCidr::Ip4( + g2_cfg.ipv4_cfg().unwrap().vpc_subnet, + )), ) .unwrap(); incr!(g1, ["epoch", "router.rules_out"]); @@ -1109,8 +1216,10 @@ fn guest_to_guest() { // once instead of on each port individually. router::add_entry( &g2.port, - IpCidr::Ip4(g1_cfg.vpc_subnet), - RouterTarget::VpcSubnet(IpCidr::Ip4(g1_cfg.vpc_subnet)), + IpCidr::Ip4(g1_cfg.ipv4_cfg().unwrap().vpc_subnet), + RouterTarget::VpcSubnet(IpCidr::Ip4( + g1_cfg.ipv4_cfg().unwrap().vpc_subnet, + )), ) .unwrap(); incr!(g2, ["epoch", "router.rules_out"]); @@ -1199,8 +1308,8 @@ fn guest_to_guest() { match meta.inner.ip.as_ref().unwrap() { IpMeta::Ip4(ip4) => { - assert_eq!(ip4.src, g1_cfg.private_ip); - assert_eq!(ip4.dst, g2_cfg.private_ip); + assert_eq!(ip4.src, g1_cfg.ipv4_cfg().unwrap().private_ip); + assert_eq!(ip4.dst, g2_cfg.ipv4_cfg().unwrap().private_ip); assert_eq!(ip4.proto, Protocol::TCP); } @@ -1254,8 +1363,8 @@ fn guest_to_guest() { match g2_meta.inner.ip.as_ref().unwrap() { IpMeta::Ip4(ip4) => { - assert_eq!(ip4.src, g1_cfg.private_ip); - assert_eq!(ip4.dst, g2_cfg.private_ip); + assert_eq!(ip4.src, g1_cfg.ipv4_cfg().unwrap().private_ip); + assert_eq!(ip4.dst, g2_cfg.ipv4_cfg().unwrap().private_ip); assert_eq!(ip4.proto, Protocol::TCP); } @@ -1291,7 +1400,7 @@ fn guest_to_guest_diff_vpc_no_peer() { // physical addresses. In this case the only guest in VNI 99 is // g1. let v2p = Arc::new(Virt2Phys::new()); - v2p.set(IpAddr::Ip4(g1_cfg.private_ip), g1_phys); + v2p.set(IpAddr::Ip4(g1_cfg.ipv4_cfg().unwrap().private_ip), g1_phys); let mut ameta = ActionMeta::new(); let mut g1 = oxide_net_setup("g1_port", &g1_cfg, v2p.clone()); @@ -1306,8 +1415,10 @@ fn guest_to_guest_diff_vpc_no_peer() { // Peering Gateway they have no way to reach each other. router::add_entry( &g1.port, - IpCidr::Ip4(g1_cfg.vpc_subnet), - RouterTarget::VpcSubnet(IpCidr::Ip4(g1_cfg.vpc_subnet)), + IpCidr::Ip4(g1_cfg.ipv4_cfg().unwrap().vpc_subnet), + RouterTarget::VpcSubnet(IpCidr::Ip4( + g1_cfg.ipv4_cfg().unwrap().vpc_subnet, + )), ) .unwrap(); incr!(g1, ["epoch", "router.rules_out"]); @@ -1324,8 +1435,10 @@ fn guest_to_guest_diff_vpc_no_peer() { // once instead of on each port individually. router::add_entry( &g2.port, - IpCidr::Ip4(g1_cfg.vpc_subnet), - RouterTarget::VpcSubnet(IpCidr::Ip4(g1_cfg.vpc_subnet)), + IpCidr::Ip4(g1_cfg.ipv4_cfg().unwrap().vpc_subnet), + RouterTarget::VpcSubnet(IpCidr::Ip4( + g1_cfg.ipv4_cfg().unwrap().vpc_subnet, + )), ) .unwrap(); incr!(g2, ["epoch", "router.rules_out"]); @@ -1375,7 +1488,11 @@ fn guest_to_internet() { // Generate a TCP SYN packet from g1 to zinascii.com // ================================================================ let dst_ip = "52.10.128.69".parse().unwrap(); - let mut pkt1 = http_tcp_syn2(g1_cfg.private_mac, g1_cfg.private_ip, dst_ip); + let mut pkt1 = http_tcp_syn2( + g1_cfg.private_mac, + g1_cfg.ipv4_cfg().unwrap().private_ip, + dst_ip, + ); // ================================================================ // Run the packet through g1's port in the outbound direction and @@ -1410,7 +1527,7 @@ fn guest_to_internet() { match meta.outer.ip.as_ref().unwrap() { IpMeta::Ip6(ip6) => { assert_eq!(ip6.src, g1_cfg.phys_ip); - assert_eq!(ip6.dst, g1_cfg.bsvc_addr.ip); + assert_eq!(ip6.dst, g1_cfg.boundary_services.ip); } val => panic!("expected outer IPv6, got: {:?}", val), @@ -1427,7 +1544,7 @@ fn guest_to_internet() { match meta.outer.encap.as_ref() { Some(geneve) => { - assert_eq!(geneve.vni, g1_cfg.bsvc_addr.vni); + assert_eq!(geneve.vni, g1_cfg.boundary_services.vni); } None => panic!("expected outer Geneve metadata"), @@ -1436,7 +1553,7 @@ fn guest_to_internet() { match meta.inner.ether.as_ref() { Some(eth) => { assert_eq!(eth.src, g1_cfg.private_mac); - assert_eq!(eth.dst, g1_cfg.bsvc_addr.ether.into()); + assert_eq!(eth.dst, g1_cfg.boundary_services.mac); assert_eq!(eth.ether_type, ETHER_TYPE_IPV4); } @@ -1445,7 +1562,16 @@ fn guest_to_internet() { match meta.inner.ip.as_ref().unwrap() { IpMeta::Ip4(ip4) => { - assert_eq!(ip4.src, g1_cfg.snat.as_ref().unwrap().public_ip); + assert_eq!( + ip4.src, + g1_cfg + .ipv4_cfg() + .unwrap() + .snat_cfg + .as_ref() + .unwrap() + .external_ip + ); assert_eq!(ip4.dst, dst_ip); assert_eq!(ip4.proto, Protocol::TCP); } @@ -1458,7 +1584,9 @@ fn guest_to_internet() { assert_eq!( tcp.src, g1_cfg - .snat + .ipv4_cfg() + .unwrap() + .snat_cfg .as_ref() .unwrap() .ports @@ -1570,9 +1698,9 @@ fn arp_gateway() { let arp = ArpEth4Payload { sha: cfg.private_mac, - spa: cfg.private_ip, + spa: cfg.ipv4_cfg().unwrap().private_ip, tha: MacAddr::from([0x00; 6]), - tpa: cfg.gw_ip, + tpa: cfg.ipv4_cfg().unwrap().gateway_ip, }; let mut wtr = PacketWriter::new(pkt, None); @@ -1589,7 +1717,7 @@ fn arp_gateway() { let ethm = meta.inner.ether.as_ref().unwrap(); let arpm = meta.inner.arp.as_ref().unwrap(); assert_eq!(ethm.dst, cfg.private_mac); - assert_eq!(ethm.src, cfg.gw_mac); + assert_eq!(ethm.src, cfg.gateway_mac); assert_eq!(ethm.ether_type, ETHER_TYPE_ARP); assert_eq!(arpm.op, ArpOp::Reply); assert_eq!(arpm.ptype, ETHER_TYPE_IPV4); @@ -1600,10 +1728,10 @@ fn arp_gateway() { &ArpEth4PayloadRaw::parse(&mut rdr).unwrap(), ); - assert_eq!(arp.sha, cfg.gw_mac); - assert_eq!(arp.spa, cfg.gw_ip); + assert_eq!(arp.sha, cfg.gateway_mac); + assert_eq!(arp.spa, cfg.ipv4_cfg().unwrap().gateway_ip); assert_eq!(arp.tha, cfg.private_mac); - assert_eq!(arp.tpa, cfg.private_ip); + assert_eq!(arp.tpa, cfg.ipv4_cfg().unwrap().private_ip); } res => panic!("expected a Hairpin, got {:?}", res), @@ -1621,7 +1749,7 @@ fn flow_expiration() { // Add V2P mappings that allow guests to resolve each others // physical addresses. let v2p = Arc::new(Virt2Phys::new()); - v2p.set(IpAddr::Ip4(g2_cfg.private_ip), g2_phys); + v2p.set(IpAddr::Ip4(g2_cfg.ipv4_cfg().unwrap().private_ip), g2_phys); let mut ameta = ActionMeta::new(); let mut g1 = oxide_net_setup("g1_port", &g1_cfg, v2p.clone()); @@ -1632,8 +1760,10 @@ fn flow_expiration() { // Add router entry that allows Guest 1 to send to Guest 2. router::add_entry( &g1.port, - IpCidr::Ip4(g2_cfg.vpc_subnet), - RouterTarget::VpcSubnet(IpCidr::Ip4(g2_cfg.vpc_subnet)), + IpCidr::Ip4(g2_cfg.ipv4_cfg().unwrap().vpc_subnet), + RouterTarget::VpcSubnet(IpCidr::Ip4( + g2_cfg.ipv4_cfg().unwrap().vpc_subnet, + )), ) .unwrap(); incr!(g1, ["epoch", "router.rules_out"]); @@ -1671,7 +1801,7 @@ fn firewall_replace_rules() { // Add V2P mappings that allow guests to resolve each others // physical addresses. let v2p = Arc::new(Virt2Phys::new()); - v2p.set(IpAddr::Ip4(g2_cfg.private_ip), g2_phys); + v2p.set(IpAddr::Ip4(g2_cfg.ipv4_cfg().unwrap().private_ip), g2_phys); let mut ameta = ActionMeta::new(); let mut g1 = oxide_net_setup("g1_port", &g1_cfg, v2p.clone()); @@ -1681,8 +1811,10 @@ fn firewall_replace_rules() { // Add router entry that allows Guest 1 to send to Guest 2. router::add_entry( &g1.port, - IpCidr::Ip4(g2_cfg.vpc_subnet), - RouterTarget::VpcSubnet(IpCidr::Ip4(g2_cfg.vpc_subnet)), + IpCidr::Ip4(g2_cfg.ipv4_cfg().unwrap().vpc_subnet), + RouterTarget::VpcSubnet(IpCidr::Ip4( + g2_cfg.ipv4_cfg().unwrap().vpc_subnet, + )), ) .unwrap(); incr!(g1, ["epoch", "router.rules_out"]); @@ -1792,3 +1924,103 @@ fn firewall_replace_rules() { } update!(g2, ["set:uft.flows_in=0"]); } + +// Test that a guest can send an ICMPv6 echo request / reply to the gateway. +#[test] +fn gateway_icmpv6_ping() { + use smoltcp::wire::{Icmpv6Packet, Icmpv6Repr, Ipv6Address}; + + let g1_cfg = g1_cfg(); + let v2p = Arc::new(Virt2Phys::new()); + let mut ameta = ActionMeta::new(); + let mut g1 = oxide_net_setup("g1_port", &g1_cfg, v2p.clone()); + g1.port.start(); + set_state!(g1, PortState::Running); + let mut pcap = PcapBuilder::new("gateway_icmpv6_ping.pcap"); + let ident = 7; + let seq_no = 777; + let data = b"reunion\0"; + + // ================================================================ + // Generate an ICMP Echo Request from G1 to Virtual GW + // ================================================================ + let mut pkt1 = gen_icmp_echo_req( + g1_cfg.private_mac, + g1_cfg.gateway_mac, + g1_cfg.ipv6_cfg().unwrap().private_ip.into(), + g1_cfg.ipv6_cfg().unwrap().gateway_ip.into(), + ident, + seq_no, + &data[..], + ); + pcap.add_pkt(&pkt1); + + // ================================================================ + // Run the Echo Request through g1's port in the outbound + // direction and verify it results in an Echo Reply Hairpin packet + // back to guest. + // ================================================================ + let res = g1.port.process(Out, &mut pkt1, &mut ameta); + let hp = match res { + Ok(Hairpin(hp)) => hp, + _ => panic!("expected Hairpin, got {:?}", res), + }; + assert_port!(g1); + + let reply = hp.parse().unwrap(); + pcap.add_pkt(&reply); + + // Ether + IPv6 + assert_eq!(reply.body_offset(), ETHER_HDR_SZ + IPV6_HDR_SZ); + assert_eq!(reply.body_seg(), 0); + + let meta = reply.meta(); + assert!(meta.outer.ether.is_none()); + assert!(meta.outer.ip.is_none()); + assert!(meta.outer.ulp.is_none()); + + match meta.inner.ether.as_ref() { + Some(eth) => { + assert_eq!(eth.src, g1_cfg.gateway_mac); + assert_eq!(eth.dst, g1_cfg.private_mac); + } + + None => panic!("no inner ether header"), + } + + let (src, dst) = match meta.inner.ip.as_ref().unwrap() { + IpMeta::Ip6(ip6) => { + assert_eq!(ip6.src, g1_cfg.ipv6_cfg().unwrap().gateway_ip); + assert_eq!(ip6.dst, g1_cfg.ipv6_cfg().unwrap().private_ip); + assert_eq!(ip6.proto, Protocol::ICMPv6); + ( + Ipv6Address::from_bytes(ip6.src.bytes().as_slice()), + Ipv6Address::from_bytes(ip6.dst.bytes().as_slice()), + ) + } + ip4 => panic!("expected inner IPv6 metadata, got IPv4: {:?}", ip4), + }; + + let mut rdr = PacketReader::new(&reply, ()); + // Need to seek to body. + rdr.seek(ETHER_HDR_SZ + IPV6_HDR_SZ).unwrap(); + let reply_body = rdr.copy_remaining(); + let reply_pkt = Icmpv6Packet::new_checked(&reply_body).unwrap(); + let mut csum = CsumCapab::ignored(); + csum.icmpv6 = smoltcp::phy::Checksum::Rx; + let reply_icmp = + Icmpv6Repr::parse(&src.into(), &dst.into(), &reply_pkt, &csum).unwrap(); + match reply_icmp { + Icmpv6Repr::EchoReply { + ident: r_ident, + seq_no: r_seq_no, + data: r_data, + } => { + assert_eq!(r_ident, ident); + assert_eq!(r_seq_no, seq_no); + assert_eq!(r_data, data); + } + + _ => panic!("expected Echo Reply, got {:?}", reply_icmp), + } +} diff --git a/xde/src/xde.rs b/xde/src/xde.rs index 47b1a804..09b68ace 100644 --- a/xde/src/xde.rs +++ b/xde/src/xde.rs @@ -35,7 +35,7 @@ use opte::ddi::sync::{KMutex, KMutexType, KRwLock, KRwLockType}; use opte::ddi::time::{Interval, Moment, Periodic}; use opte::engine::ether::EtherAddr; use opte::engine::geneve::Vni; -use opte::engine::headers::{IpAddr, IpCidr}; +use opte::engine::headers::IpAddr; use opte::engine::ioctl::{self as api}; use opte::engine::ip6::Ipv6Addr; use opte::engine::packet::{ @@ -45,12 +45,13 @@ use opte::engine::port::meta::ActionMeta; use opte::engine::port::{Port, PortBuilder, ProcessResult}; use opte::ExecCtx; use oxide_vpc::api::{ - AddFwRuleReq, AddRouterEntryIpv4Req, CreateXdeReq, DeleteXdeReq, + AddFwRuleReq, AddRouterEntryReq, CreateXdeReq, DeleteXdeReq, IpCfg, ListPortsReq, ListPortsResp, PhysNet, PortInfo, RemFwRuleReq, - SetFwRulesReq, SetVirt2PhysReq, + SetFwRulesReq, SetVirt2PhysReq, VpcCfg, +}; +use oxide_vpc::engine::{ + arp, dhcp, firewall, icmp, icmpv6, nat, overlay, router, }; -use oxide_vpc::engine::{arp, dhcp4, firewall, icmp, nat, overlay, router}; -use oxide_vpc::VpcCfg; // Entry limits for the varous flow tables. // @@ -390,7 +391,7 @@ unsafe extern "C" fn xde_dld_ioc_opte_cmd( hdlr_resp(&mut env, resp) } - OpteCmd::AddRouterEntryIpv4 => { + OpteCmd::AddRouterEntry => { let resp = add_router_entry_hdlr(&mut env); hdlr_resp(&mut env, resp) } @@ -402,7 +403,6 @@ unsafe extern "C" fn xde_dld_ioc_opte_cmd( } } -const NANOS: i64 = 1_000_000_000; const ONE_SECOND: Interval = Interval::from_duration(Duration::new(1, 0)); #[no_mangle] @@ -441,55 +441,49 @@ fn create_xde(req: &CreateXdeReq) -> Result { None => (), }; + let cfg = &req.cfg; match devs.iter().find(|x| { - x.vni == req.vpc_vni && x.port.mac_addr() == req.private_mac.into() + x.vni == cfg.vni && x.port.mac_addr() == cfg.private_mac.into() }) { Some(_) => { return Err(OpteError::MacExists { port: req.xde_devname.clone(), - vni: req.vpc_vni, - mac: req.private_mac, + vni: cfg.vni, + mac: cfg.private_mac, }) } None => (), } - // XXX-EXT-IP This is for the external IP hack. - let phys_gw_mac = if unsafe { xde_ext_ip_hack == 1 } && req.snat.is_some() { - Some(req.snat.as_ref().unwrap().phys_gw_mac) - } else { - None - }; - + // XXX-EXT-IP Copy the configuration, modifying the external IP hack + // fields depending on the value of the `xde_ext_ip_hack` tunable. + let proxy_arp_enable = unsafe { xde_ext_ip_hack == 1 }; let vpc_cfg = VpcCfg { - vpc_subnet: req.vpc_subnet, - private_mac: req.private_mac, - private_ip: req.private_ip, - gw_mac: req.gw_mac, - gw_ip: req.gw_ip, - external_ips_v4: req.external_ips_v4, - snat: req.snat.clone(), - vni: req.vpc_vni, - phys_ip: req.src_underlay_addr, - bsvc_addr: PhysNet { - ether: MacAddr::from([0; 6]), //XXX this should not be needed - ip: req.bsvc_addr, - vni: req.bsvc_vni, - }, - proxy_arp_enable: unsafe { xde_ext_ip_hack == 1 }, - phys_gw_mac, + proxy_arp_enable, + phys_gw_mac: if proxy_arp_enable { cfg.phys_gw_mac } else { None }, + ..cfg.clone() }; // If this is the first guest in this VPC, then create a new // mapping for said VPC. Otherwise, pull the existing one. - let port_v2p = state.vpc_map.add( - IpAddr::Ip4(req.private_ip), - PhysNet { - ether: req.private_mac, - ip: req.src_underlay_addr, - vni: req.vpc_vni, - }, - ); + // + // We need to insert mappings for both IPv4 and IPv6 addresses, should the + // guest have them. They should return the same `Virt2Phys` mapping, since + // they're mapping both IP addresses to the same host. + let phys_net = + PhysNet { ether: cfg.private_mac, ip: cfg.phys_ip, vni: cfg.vni }; + let port_v2p = match vpc_cfg.ip_cfg { + IpCfg::Ipv4(ref ipv4) => { + state.vpc_map.add(IpAddr::Ip4(ipv4.private_ip), phys_net) + } + IpCfg::Ipv6(ref ipv6) => { + state.vpc_map.add(IpAddr::Ip6(ipv6.private_ip), phys_net) + } + IpCfg::DualStack { ref ipv4, ref ipv6 } => { + state.vpc_map.add(IpAddr::Ip4(ipv4.private_ip), phys_net); + state.vpc_map.add(IpAddr::Ip6(ipv6.private_ip), phys_net) + } + }; let port = new_port( req.xde_devname.clone(), @@ -515,7 +509,7 @@ fn create_xde(req: &CreateXdeReq) -> Result { port_v2p, vpc_cfg, passthrough: req.passthrough, - vni: req.vpc_vni.into(), + vni: cfg.vni, u1: underlay.u1.clone(), u2: underlay.u2.clone(), }); @@ -1210,28 +1204,60 @@ fn guest_loopback( // have the overlay layer in place which would // normally rewrite the dst MAC addr to that of the // dest guest. - if let Some(ip4) = ip_hdr.ip4() { - let res = devs.iter().find(|x| { - opte::engine::dbg(format!( - "dev ip: {} pkt dest: {}", - x.vpc_cfg.private_ip, - ip4.dst(), - )); - x.vpc_cfg.private_ip == ip4.dst() - }); - if let Some(dev) = res { + // Check the IP address against the guest's IPv4 and IPv6 + // addresses, in that order. If either matches, we need to + // rewrite the destination MAC to that of the guest. + let maybe_dev = { + if let Some(ip4) = ip_hdr.ip4() { + // Find device with matching IPv4 address + devs.iter().find(|x| { + if let Some(cfg) = x.vpc_cfg.ipv4_cfg() { + opte::engine::dbg(format!( + "dev ipv4: {} pkt dest: {}", + cfg.private_ip, + ip4.dst(), + )); + cfg.private_ip == ip4.dst() + } else { + false + } + }) + } else if let Some(ip6) = ip_hdr.ip6() { + // Find device with matching IPv6 address + devs.iter().find(|x| { + if let Some(cfg) = x.vpc_cfg.ipv6_cfg() { + opte::engine::dbg(format!( + "dev ipv6: {} pkt dest: {}", + cfg.private_ip, + ip6.dst(), + )); + cfg.private_ip == ip6.dst() + } else { + false + } + }) + } else { + // This should be unreachable!(). We have an IP header, + // but neither `ip4()` nor `ip6()` returned something. opte::engine::dbg(format!( - "rewriting packet dst mac to: {}", - dev.vpc_cfg.private_mac, + "Packet contains IP header, but neither `ip4()` \ + nor `ip6()` returned `Some`. IP header: {:?}", + ip_hdr, )); - pkt.write_dst_mac(dev.vpc_cfg.private_mac.into()); + None } + }; - res - } else { - None + // If we found a guest with the dst address, rewrite the MAC + if let Some(dev) = maybe_dev { + opte::engine::dbg(format!( + "rewriting packet dst mac to: {}", + dev.vpc_cfg.private_mac, + )); + pkt.write_dst_mac(dev.vpc_cfg.private_mac.into()); } + maybe_dev } } } else { @@ -1386,23 +1412,42 @@ unsafe extern "C" fn xde_mc_tx( match res { Ok(ProcessResult::Modified) => { if xde_ext_ip_hack == 1 { - let mut local = false; - - match pkt.headers().inner.ip.as_ref() { - Some(ip_hdr) => { + let local = + if let Some(ip_hdr) = pkt.headers().inner.ip.as_ref() { if let Some(ip4) = ip_hdr.ip4() { let devs = xde_devs.read(); - let res = devs - .iter() - .find(|x| x.vpc_cfg.private_ip == ip4.dst()); - - if res.is_some() { - local = true; - } + let res = devs.iter().find(|x| { + if let Some(cfg) = x.vpc_cfg.ipv4_cfg() { + cfg.private_ip == ip4.dst() + } else { + false + } + }); + res.is_some() + } else if let Some(ip6) = ip_hdr.ip6() { + let devs = xde_devs.read(); + let res = devs.iter().find(|x| { + if let Some(cfg) = x.vpc_cfg.ipv6_cfg() { + cfg.private_ip == ip6.dst() + } else { + false + } + }); + res.is_some() + } else { + // This should be unreachable!(). We have an IP header, + // but neither `ip4()` nor `ip6()` returned something. + opte::engine::dbg(format!( + "Packet contains IP header, but neither \ + `ip4()` nor `ip6()` returned `Some`. \ + IP header: {:?}", + ip_hdr, + )); + false } - } - None => (), - } + } else { + false + }; opte::engine::dbg(format!( "[Tx] ext_ip_hack local: {:?}", @@ -1868,8 +1913,9 @@ fn new_port( firewall::setup(&mut pb, FW_FT_LIMIT.unwrap())?; // XXX some layers have no need for LFT, perhaps have two types // of Layer: one with, one without? - dhcp4::setup(&mut pb, &cfg, FT_LIMIT_ONE.unwrap())?; + dhcp::setup(&mut pb, &cfg, FT_LIMIT_ONE.unwrap())?; icmp::setup(&mut pb, &cfg, FT_LIMIT_ONE.unwrap())?; + icmpv6::setup(&mut pb, &cfg, FT_LIMIT_ONE.unwrap())?; arp::setup(&mut pb, &cfg, FT_LIMIT_ONE.unwrap())?; router::setup(&mut pb, &cfg, FT_LIMIT_ONE.unwrap())?; nat::setup(&mut pb, &cfg, NAT_FT_LIMIT.unwrap())?; @@ -2042,7 +2088,7 @@ unsafe extern "C" fn xde_rx( #[no_mangle] fn add_router_entry_hdlr(env: &mut IoctlEnvelope) -> Result { - let req: AddRouterEntryIpv4Req = env.copy_in_req()?; + let req: AddRouterEntryReq = env.copy_in_req()?; let devs = unsafe { xde_devs.read() }; let mut iter = devs.iter(); let dev = match iter.find(|x| x.devname == req.port_name) { @@ -2050,11 +2096,7 @@ fn add_router_entry_hdlr(env: &mut IoctlEnvelope) -> Result { None => return Err(OpteError::PortNotFound(req.port_name)), }; - router::add_entry( - &dev.port, - IpCidr::Ip4(req.dest.into()), - req.target.into(), - ) + router::add_entry(&dev.port, req.dest.into(), req.target.into()) } #[no_mangle] @@ -2202,7 +2244,18 @@ fn list_ports_hdlr( resp.ports.push(PortInfo { name: dev.port.name().to_string(), mac_addr: dev.port.mac_addr().into(), - ip4_addr: dev.vpc_cfg.private_ip, + ip4_addr: dev.vpc_cfg.ipv4_cfg().map(|cfg| cfg.private_ip), + external_ip4_addr: dev + .vpc_cfg + .ipv4_cfg() + .map(|cfg| cfg.external_ips) + .flatten(), + ip6_addr: dev.vpc_cfg.ipv6_cfg().map(|cfg| cfg.private_ip), + external_ip6_addr: dev + .vpc_cfg + .ipv6_cfg() + .map(|cfg| cfg.external_ips) + .flatten(), state: dev.port.state().to_string(), }); }