diff --git a/dpd-client/tests/integration_tests/common.rs b/dpd-client/tests/integration_tests/common.rs index 0fcebdc8..c7e68a40 100644 --- a/dpd-client/tests/integration_tests/common.rs +++ b/dpd-client/tests/integration_tests/common.rs @@ -4,6 +4,7 @@ // // Copyright 2026 Oxide Computer Company +use std::borrow::Cow; use std::fmt::Write; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::sync::atomic::AtomicBool; @@ -168,10 +169,18 @@ impl TestPacket { println!("port: {}: {:?}", self.port, self.packet); } if verbosity & SHOW_HEX != 0 { - let data = self.packet.deparse().unwrap(); - dump_hex(&data); + let data = self.deconstruct_pkt(); + dump_hex(data.as_ref()); } } + + /// Returns the deparsed bytes of the contained packet if possible. + /// Otherwise, just returns the body bytes. + fn deconstruct_pkt(&self) -> Cow<'_, [u8]> { + self.packet.deparse().map(|pkt| pkt.to_vec().into()).unwrap_or_else( + |_| Cow::Borrowed(self.packet.body.as_deref().unwrap_or(&[])), + ) + } } impl PartialEq for TestPacket { @@ -579,7 +588,7 @@ impl Switch { if phys_port == NO_PORT { None } else if let Some(port) = &self.ports[idx] { - Some((port.port_id.clone(), port.link_id.clone())) + Some((port.port_id.clone(), port.link_id)) } else { panic!("request for missing port: {phys_port}"); } @@ -607,7 +616,7 @@ impl Switch { let Some(p) = p else { unreachable!("Filtered to only Some(_)"); }; - (p.port_id.clone(), p.link_id.clone()) + (p.port_id.clone(), p.link_id) }) } @@ -631,21 +640,21 @@ impl Switch { tx: mpsc::Sender, ) { loop { - match hdl.next_owned() { + let raw = match hdl.next_owned() { pcap::Ternary::None => break, pcap::Ternary::Err(e) => { println!("port {port} got pcap error: {e}"); break; } - pcap::Ternary::Ok(data) => { - let packet = packet::Packet::parse(&data).unwrap(); - let copy = - Packet { hdrs: packet.hdrs.clone(), body: packet.body }; + pcap::Ternary::Ok(d) => d, + }; - let cap = TestPacket { port, packet: Arc::new(copy) }; - tx.send(cap).unwrap(); - } - } + let packet = packet::Packet::parse(&raw).unwrap_or_else(|e| { + eprintln!("Packet failed to parse: {e:?}"); + Packet { body: Some(raw), ..Default::default() } + }); + + tx.send(TestPacket { port, packet: Arc::new(packet) }).unwrap(); } } @@ -736,7 +745,11 @@ impl Switch { let mut found = Vec::new(); for (s, sent) in send.iter().enumerate() { for (g, got) in captured.iter().enumerate() { - if sent == got { + // Compare raw bytes to support sending degenerate packets + // that might not parse into a normal header. + if sent.port == got.port + && sent.deconstruct_pkt() == got.deconstruct_pkt() + { found.push(s); captured.remove(g); break; diff --git a/dpd-client/tests/integration_tests/geneve.rs b/dpd-client/tests/integration_tests/geneve.rs index 6f5a49bf..eed4780e 100644 --- a/dpd-client/tests/integration_tests/geneve.rs +++ b/dpd-client/tests/integration_tests/geneve.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use packet::Endpoint; +use packet::Packet; use packet::eth; use packet::geneve; @@ -19,7 +20,17 @@ use crate::integration_tests::common::prelude::*; #[ignore] async fn test_geneve() -> TestResult { let switch = &*get_switch().await; + test_geneve_impl(switch, |_| Ok(())).await +} +/// Runs a packet test between VPC peers. +/// +/// The `modify_inner` callback can be used to arbitrarily +/// change the packet before encapsulation. +async fn test_geneve_impl( + switch: &Switch, + modify_inner: impl FnOnce(&mut Packet) -> anyhow::Result<()>, +) -> TestResult { let ingress_port = PhysPort(10); let uplink_port = PhysPort(14); let router_ip = "fd00:1122:3344:0101::1"; @@ -35,13 +46,12 @@ async fn test_geneve() -> TestResult { let vpc_dst_ip = "10.10.10.2"; let vpc_dst_mac = "04:01:01:01:01:02"; let vpc_dst_port = 4444; - let payload = common::gen_udp_packet( + let mut inner = common::gen_udp_packet( Endpoint::parse(vpc_src_mac, vpc_src_ip, vpc_src_port).unwrap(), Endpoint::parse(vpc_dst_mac, vpc_dst_ip, vpc_dst_port).unwrap(), - ) - .deparse() - .unwrap() - .to_vec(); + ); + modify_inner(&mut inner)?; + let payload = inner.deparse().unwrap().to_vec(); let mut to_send = common::gen_geneve_packet( Endpoint::parse("e0:d5:5e:67:89:ab", "fd00:1122:7788:0101::4", 3333) @@ -52,7 +62,7 @@ async fn test_geneve() -> TestResult { geneve::GENEVE_UDP_PORT, ) .unwrap(), - eth::ETHER_IPV4, + eth::ETHER_ETHER, 345, &[], &payload, @@ -105,7 +115,7 @@ async fn test_geneve_multiopt() -> TestResult { geneve::GENEVE_UDP_PORT, ) .unwrap(), - eth::ETHER_IPV4, + eth::ETHER_ETHER, 345, &[ // Does this combination make sense? Maybe not today, but @@ -137,7 +147,7 @@ async fn test_geneve_multiopt() -> TestResult { geneve::GENEVE_UDP_PORT, ) .unwrap(), - eth::ETHER_IPV4, + eth::ETHER_ETHER, 345, &[ // Swapped! diff --git a/dpd-client/tests/integration_tests/nat.rs b/dpd-client/tests/integration_tests/nat.rs index f8fba25b..e9e50808 100644 --- a/dpd-client/tests/integration_tests/nat.rs +++ b/dpd-client/tests/integration_tests/nat.rs @@ -16,6 +16,7 @@ use ::common::network::Vni; use dpd_client::ClientInfo; use dpd_client::types; use packet::Endpoint; +use packet::Packet; use packet::eth; use packet::geneve; use packet::icmp; @@ -187,6 +188,99 @@ struct NatTest { nat_l4_port: u16, l4_protocol: L4Protocol, geneve_vni: u32, + + // Callbacks to modify standard harness behavior. + + // Called on the inner packet. Test fails if this returns error. + // Useful for injecting special cases. + modify_inner: fn(&mut Packet) -> anyhow::Result<()>, + + // Called on the (to_send, expect_recv) tuple immediately before + // running the packet test. + modify_result: + fn(&mut Vec, &mut Vec) -> anyhow::Result<()>, +} + +impl NatTest { + /// Creates a default config usable for IPv4 ingress tests. + fn ingress_ipv4() -> Self { + NatTest { + uplink_port: PhysPort(14), + uplink_port_external: "192.168.1.2".to_string(), + uplink_port_registered: true, + uplink_route: "unused".to_string(), + router_ip: "192.168.1.1".to_string(), + router_mac: "02:aa:bb:cc:dd:ee".to_string(), + + vpc_src_ip: "10.10.10.32".to_string(), + vpc_src_mac: "04:01:01:01:01:02".to_string(), + vpc_src_port: 4444, + vpc_dst_ip: "231.44.22.11".to_string(), + vpc_dst_mac: "04:01:01:01:01:01".to_string(), + vpc_dst_port: 3333, + + gimlet_port: PhysPort(10), + gimlet_ip: "fd00:1122:7788:0101::4".to_string(), + gimlet_mac: "11:22:33:44:55:66".to_string(), + gimlet_port_ip: "fd00:1122:3344:0101::5".to_string(), + + nat_l4_port: 2000, + l4_protocol: L4Protocol::Udp, + geneve_vni: 7654, + + modify_inner: |_| Ok(()), + modify_result: |_, _| Ok(()), + } + } + + /// Creates a default config usable for IPv4 egress tests. + fn egress_ipv4() -> Self { + NatTest { + uplink_port: PhysPort(14), + uplink_port_external: "192.168.1.2".to_string(), + uplink_port_registered: false, + uplink_route: "0.0.0.0/0".to_string(), + router_ip: "192.168.1.1".to_string(), + router_mac: "02:aa:bb:cc:dd:ee".to_string(), + + vpc_src_ip: "172.16.10.33".to_string(), + vpc_src_mac: "04:01:01:01:01:01".to_string(), + vpc_src_port: 3333, + vpc_dst_ip: "10.10.10.32".to_string(), + vpc_dst_mac: "04:01:01:01:01:02".to_string(), + vpc_dst_port: 4444, + + gimlet_port: PhysPort(10), + gimlet_ip: "fd00:1122:7788:0101::4".to_string(), + gimlet_mac: "11:22:33:44:55:66".to_string(), + gimlet_port_ip: "fd00:1122:3344:0101::5".to_string(), + + nat_l4_port: 10, + l4_protocol: L4Protocol::Udp, + geneve_vni: 1, // not used on egress tests + modify_inner: |_| Ok(()), + modify_result: |_, _| Ok(()), + } + } + + /// Creates a switch NAT config for `self`. + async fn create_ipv4_nat(&self, switch: &Switch) -> anyhow::Result<()> { + let nat_ip = self.uplink_port_external.parse()?; + let internal_ip = self.gimlet_ip.parse()?; + let tgt = types::NatTarget { + internal_ip, + inner_mac: self.vpc_dst_mac.parse::()?.into(), + vni: Vni::new(self.geneve_vni) + .ok_or_else(|| anyhow!("Invalid VNI: {:?}", self.geneve_vni))? + .into(), + }; + + let nat_low = 1024; + let nat_high = 2048; + switch.client.nat_ipv4_create(&nat_ip, nat_low, nat_high, &tgt).await?; + + Ok(()) + } } async fn test_nat_egress(switch: &Switch, test: &NatTest) -> TestResult { @@ -241,10 +335,10 @@ async fn test_nat_egress(switch: &Switch, test: &NatTest) -> TestResult { // responsibility, then this needs to be done on the 'expected' packet // rather than the 'to_send' packet. let inner = { - if let Some(mut ipv4) = payload_pkt.hdrs.ipv4_hdr { + if let Some(ipv4) = &mut payload_pkt.hdrs.ipv4_hdr { ipv4.ipv4_src_ip = test.uplink_port_external.parse()?; Ok(eth::ETHER_IPV4) - } else if let Some(mut ipv6) = payload_pkt.hdrs.ipv6_hdr { + } else if let Some(ipv6) = &mut payload_pkt.hdrs.ipv6_hdr { ipv6.ipv6_src_ip = test.uplink_port_external.parse()?; Ok(eth::ETHER_IPV6) } else { @@ -274,6 +368,8 @@ async fn test_nat_egress(switch: &Switch, test: &NatTest) -> TestResult { ipv4::Ipv4Hdr::update_checksum(&mut payload_pkt); } + (test.modify_inner)(&mut payload_pkt)?; + let switch_mac = switch.get_port_mac(test.gimlet_port).unwrap().to_string(); let payload = payload_pkt.deparse().unwrap().to_vec(); let to_send = common::gen_geneve_packet( @@ -290,7 +386,7 @@ async fn test_nat_egress(switch: &Switch, test: &NatTest) -> TestResult { &payload[14..], ); - let expected = match test.uplink_port_registered { + let mut expected = match test.uplink_port_registered { true => { let mut to_recv = common::gen_packet_routed( switch, @@ -306,9 +402,11 @@ async fn test_nat_egress(switch: &Switch, test: &NatTest) -> TestResult { false => Vec::new(), }; - let send = TestPacket { packet: Arc::new(to_send), port: test.gimlet_port }; + let mut send = + vec![TestPacket { packet: Arc::new(to_send), port: test.gimlet_port }]; + (test.modify_result)(&mut send, &mut expected)?; - switch.packet_test(vec![send], expected) + switch.packet_test(send, expected) } async fn test_nat_ingress(switch: &Switch, test: &NatTest) -> TestResult { @@ -320,6 +418,8 @@ async fn test_nat_ingress(switch: &Switch, test: &NatTest) -> TestResult { tag: switch.client.inner().tag.clone(), }; switch.client.link_ipv6_create(&port_id, &link_id, &entry).await.unwrap(); + switch.set_uplink(test.uplink_port, test.uplink_port_registered).await; + let cidr = Ipv6Net::new(test.gimlet_ip.parse().unwrap(), 64).unwrap(); let route = types::Ipv6RouteUpdate { cidr, @@ -348,7 +448,7 @@ async fn test_nat_ingress(switch: &Switch, test: &NatTest) -> TestResult { test.nat_l4_port, ) .unwrap(); - let (ingress_pkt, csum_generated) = match test.l4_protocol { + let (mut ingress_pkt, csum_generated) = match test.l4_protocol { L4Protocol::Tcp => { (common::gen_tcp_packet_loaded(src, dst, &load), true) } @@ -360,6 +460,8 @@ async fn test_nat_ingress(switch: &Switch, test: &NatTest) -> TestResult { } }; + (test.modify_inner)(&mut ingress_pkt)?; + // Deparse the incoming packet so we can copy it into the encapsulated // packet let ingress_payload = { @@ -405,14 +507,15 @@ async fn test_nat_ingress(switch: &Switch, test: &NatTest) -> TestResult { forward_pkt.hdrs.udp_hdr.as_mut().unwrap().udp_sum = 0; } - let send = vec![TestPacket { + let mut send = vec![TestPacket { packet: Arc::new(ingress_pkt), port: test.uplink_port, }]; - let expected = vec![TestPacket { + let mut expected = vec![TestPacket { packet: Arc::new(forward_pkt), port: test.gimlet_port, }]; + (test.modify_result)(&mut send, &mut expected)?; switch.packet_test(send, expected) } @@ -423,30 +526,9 @@ async fn test_egress_ipv4( l4_protocol: L4Protocol, uplink_port_registered: bool, ) -> TestResult { - let test = NatTest { - uplink_port: PhysPort(14), - uplink_port_external: "192.168.1.2".to_string(), - uplink_port_registered, - uplink_route: "0.0.0.0/0".to_string(), - router_ip: "192.168.1.1".to_string(), - router_mac: "02:aa:bb:cc:dd:ee".to_string(), - - vpc_src_ip: "172.16.10.33".to_string(), - vpc_src_mac: "04:01:01:01:01:01".to_string(), - vpc_src_port: 3333, - vpc_dst_ip: "10.10.10.32".to_string(), - vpc_dst_mac: "04:01:01:01:01:02".to_string(), - vpc_dst_port: 4444, - - gimlet_port: PhysPort(10), - gimlet_ip: "fd00:1122:7788:0101::4".to_string(), - gimlet_mac: "11:22:33:44:55:66".to_string(), - gimlet_port_ip: "fd00:1122:3344:0101::5".to_string(), - - nat_l4_port: 10, - l4_protocol, - geneve_vni: 1, // not used on egress tests - }; + let mut test = NatTest::egress_ipv4(); + test.l4_protocol = l4_protocol; + test.uplink_port_registered = uplink_port_registered; test_nat_egress(switch, &test).await } @@ -487,46 +569,11 @@ async fn test_ingress_ipv4( switch: &Switch, l4_protocol: L4Protocol, ) -> TestResult { - let test = NatTest { - uplink_port: PhysPort(14), - uplink_port_external: "192.168.1.2".to_string(), - uplink_port_registered: true, - uplink_route: "unused".to_string(), - router_ip: "192.168.1.1".to_string(), - router_mac: "02:aa:bb:cc:dd:ee".to_string(), - - vpc_src_ip: "10.10.10.32".to_string(), - vpc_src_mac: "04:01:01:01:01:02".to_string(), - vpc_src_port: 4444, - vpc_dst_ip: "231.44.22.11".to_string(), - vpc_dst_mac: "04:01:01:01:01:01".to_string(), - vpc_dst_port: 3333, - - gimlet_port: PhysPort(10), - gimlet_ip: "fd00:1122:7788:0101::4".to_string(), - gimlet_mac: "11:22:33:44:55:66".to_string(), - gimlet_port_ip: "fd00:1122:3344:0101::5".to_string(), - - nat_l4_port: 2000, - l4_protocol, - geneve_vni: 7654, - }; + let mut test = NatTest::ingress_ipv4(); + test.l4_protocol = l4_protocol; - let nat_ip = test.uplink_port_external.parse().unwrap(); - let internal_ip = test.gimlet_ip.parse().unwrap(); - let tgt = types::NatTarget { - internal_ip, - inner_mac: test.vpc_dst_mac.parse::()?.into(), - vni: Vni::new(test.geneve_vni).unwrap().into(), - }; + test.create_ipv4_nat(switch).await?; - let nat_low = 1024; - let nat_high = 2048; - switch - .client - .nat_ipv4_create(&nat_ip, nat_low, nat_high, &tgt) - .await - .unwrap(); test_nat_ingress(switch, &test).await } @@ -580,6 +627,9 @@ async fn test_egress_ipv6( nat_l4_port: 10, l4_protocol, geneve_vni: 1, // not used on egress tests + + modify_inner: |_| Ok(()), + modify_result: |_, _| Ok(()), }; test_nat_egress(switch, &test).await @@ -633,6 +683,9 @@ async fn test_ingress_ipv6( nat_l4_port: 2000, l4_protocol, geneve_vni: 9876, + + modify_inner: |_| Ok(()), + modify_result: |_, _| Ok(()), }; let nat_ip = test.uplink_port_external.parse().unwrap(); diff --git a/dpd-client/tests/integration_tests/route_ipv4.rs b/dpd-client/tests/integration_tests/route_ipv4.rs index 7e2d2aac..a52f8d3a 100644 --- a/dpd-client/tests/integration_tests/route_ipv4.rs +++ b/dpd-client/tests/integration_tests/route_ipv4.rs @@ -10,6 +10,8 @@ use std::sync::Arc; use anyhow::anyhow; use oxnet::Ipv4Net; +use packet::Packet; +use packet::eth; use reqwest::StatusCode; use crate::integration_tests::common; @@ -76,7 +78,7 @@ fn build_route_update( replace: bool, ) -> types::Ipv4RouteUpdate { types::Ipv4RouteUpdate { - cidr: subnet.into(), + cidr: subnet, target: types::RouteTarget::V4(target.clone()), replace, } @@ -97,7 +99,7 @@ async fn validate_routes( } } else { // Verify that the set of routes on the switch match those we just set - let found = client.route_ipv4_get(&cidr).await?; + let found = client.route_ipv4_get(cidr).await?; assert_eq!(found.len(), expected.len()); for target in expected { assert!( @@ -108,41 +110,86 @@ async fn validate_routes( } } -async fn test_route_ipv4_unicast_impl(switch: &Switch) -> TestResult { - let ingress = PhysPort(10); - let egress = PhysPort(14); - - let router = Router::new(10, "10.10.10.1", "02:78:39:45:b9:00", None); +/// Configuration for a point to point IPv4 routing test. +struct RouteTest { + ingress: PhysPort, + egress: PhysPort, + router: Router, + router_subnet: &'static str, + src: Endpoint, + dst: Endpoint, + + // Called on the tx packet before generating the expected response. + // Test fails if this returns error. Useful for injecting special cases. + modify_pkt: fn(&mut Packet) -> anyhow::Result<()>, + + // Called on the (to_send, expect_recv) tuple immediately before + // running the packet test. + modify_result: + fn(&mut Vec, &mut Vec) -> anyhow::Result<()>, +} - common::set_route_ipv4(switch, "10.10.10.0/24", egress, &router.ip).await?; - common::add_arp_ipv4(switch, &router.ip, router.mac).await?; +impl Default for RouteTest { + fn default() -> Self { + Self { + ingress: PhysPort(10), + egress: PhysPort(14), + router: Router::new(10, "10.10.10.1", "02:78:39:45:b9:00", None), + router_subnet: "10.10.10.0/24", + src: Endpoint::parse("e0:d5:5e:67:89:ab", "10.10.10.10", 3333) + .unwrap(), + dst: Endpoint::parse("e0:d5:5e:67:89:ac", "10.10.10.11", 4444) + .unwrap(), + + modify_pkt: |_| Ok(()), + modify_result: |_, _| Ok(()), + } + } +} - let (to_send, to_recv) = common::gen_udp_routed_pair( +/// Executes a routing test according to the given configuration. +async fn test_route_ipv4_unicast_impl( + switch: &Switch, + conf: RouteTest, +) -> TestResult { + common::set_route_ipv4( switch, - egress, - router.mac, - Endpoint::parse("e0:d5:5e:67:89:ab", "10.10.10.10", 3333).unwrap(), - Endpoint::parse("e0:d5:5e:67:89:ac", "10.10.10.11", 4444).unwrap(), - ); + conf.router_subnet, + conf.egress, + &conf.router.ip, + ) + .await?; + common::add_arp_ipv4(switch, &conf.router.ip, conf.router.mac).await?; - let send = TestPacket { packet: Arc::new(to_send), port: ingress }; - let expected = TestPacket { packet: Arc::new(to_recv), port: egress }; - switch.packet_test(vec![send], vec![expected]) + let mut to_send = common::gen_udp_packet_loaded(conf.src, conf.dst, &[]); + eth::EthHdr::rewrite_dmac(&mut to_send, conf.router.mac); + (conf.modify_pkt)(&mut to_send)?; + + let to_recv = common::gen_packet_routed(switch, conf.egress, &to_send); + + let mut send = + vec![TestPacket { packet: Arc::new(to_send), port: conf.ingress }]; + let mut expected = + vec![TestPacket { packet: Arc::new(to_recv), port: conf.egress }]; + (conf.modify_result)(&mut send, &mut expected)?; + + switch.packet_test(send, expected) } #[tokio::test] #[ignore] async fn test_route_ipv4_unicast() -> TestResult { let switch = &*get_switch().await; - test_route_ipv4_unicast_impl(switch).await + test_route_ipv4_unicast_impl(switch, RouteTest::default()).await } async fn test_deleted_route_ipv4_impl(switch: &Switch) -> TestResult { - let ingress = PhysPort(10); + let conf = RouteTest::default(); + let ingress = conf.ingress; let egress = SERVICE_PORT; // Verify that we can still unicast via the correctly set-up route. - test_route_ipv4_unicast_impl(switch).await?; + test_route_ipv4_unicast_impl(switch, conf).await?; // Delete that route entry, and check that we cannot send out the egress // port. @@ -501,8 +548,7 @@ fn shrink_test_route( tag: "testing".into(), port_id, link_id, - tgt_ip: Ipv4Addr::new(ip_prefix.0, ip_prefix.1, ip_prefix.2, target) - .into(), + tgt_ip: Ipv4Addr::new(ip_prefix.0, ip_prefix.1, ip_prefix.2, target), vlan_id: None, } } diff --git a/packet/src/eth.rs b/packet/src/eth.rs index 0cff359a..6cac78d2 100644 --- a/packet/src/eth.rs +++ b/packet/src/eth.rs @@ -170,7 +170,11 @@ impl Protocol for EthHdr { } let mut v = BytesMut::with_capacity(total_size); - let hdr = &(packet.hdrs.eth_hdr.as_ref().unwrap()); + let hdr = packet + .hdrs + .eth_hdr + .as_ref() + .ok_or_else(|| crate::deparse_error("no Ethernet header"))?; v.put_slice(&hdr.eth_dmac.to_vec()); v.put_slice(&hdr.eth_smac.to_vec()); if let Some(q) = hdr.eth_8021q { @@ -230,3 +234,33 @@ impl fmt::Display for EthHdr { ) } } + +#[cfg(test)] +mod eth_tests { + use common::network::MacAddr; + + use crate::Packet; + use crate::PacketError; + use crate::Protocol; + use crate::eth::EthHdr; + + /// Exercises basic deparse on an ethernet header. + #[test] + fn basic_deparse() { + let mut pkt = Packet::default(); + // Sanity check + pkt.hdrs.eth_hdr = Some(EthHdr { + eth_dmac: MacAddr::ZERO, + eth_smac: MacAddr::ZERO, + eth_8021q: None, + eth_type: 0x88B5, + eth_size: 1500, + }); + EthHdr::deparse(&pkt, 0).expect("Should deparse with good inputs"); + + pkt.hdrs.eth_hdr = None; + let err = + EthHdr::deparse(&pkt, 0).expect_err("No eth header to deparse"); + assert!(matches!(err, PacketError::Deparse(_))); + } +}