Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions dpd-client/tests/integration_tests/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,62 @@ async fn test_ndp_needed() -> TestResult {
Ok(())
}

// An IPv4 packet routed via an IPv6 nexthop (e.g. BGP unnumbered) with no
// neighbor entry should come up to the service port asking for NDP resolution
// of the IPv6 nexthop.
#[tokio::test]
#[ignore]
async fn test_ndp_needed_ipv4_via_ipv6() -> TestResult {
let switch = &*get_switch().await;

let ingress_port = PhysPort(10);
let egress_port = PhysPort(15);
let router_ip = "fe80::50";
let src_mac = "e0:d5:5e:67:89:ab";
let router_mac = switch.get_port_mac(ingress_port).unwrap();

common::set_route_ipv4_over_ipv6(
switch,
"10.10.50.0/24",
egress_port,
router_ip,
)
.await?;

let (send_pkt, mut recv_pkt) = common::gen_udp_routed_pair(
switch,
egress_port,
router_mac,
Endpoint::parse(src_mac, "10.10.10.10", 3333).unwrap(),
Endpoint::parse("e0:d5:5e:67:89:ac", "10.10.50.11", 4444).unwrap(),
);

let send = TestPacket { packet: Arc::new(send_pkt), port: ingress_port };

let payload = router_ip.parse::<Ipv6Addr>().unwrap().octets();
common::add_sidecar_hdr(
switch,
&mut recv_pkt,
sidecar::SC_NEIGHBOR_NEEDED,
ingress_port,
egress_port,
Some(&payload[..]),
);

// Because the packet wasn't successfully routed, the src/dst mac addresses
// should be unchanged.
let src_eth = send.packet.hdrs.eth_hdr.as_ref().unwrap();
eth::EthHdr::rewrite_smac(&mut recv_pkt, src_eth.eth_smac);
eth::EthHdr::rewrite_dmac(&mut recv_pkt, src_eth.eth_dmac);

let expected =
TestPacket { packet: Arc::new(recv_pkt), port: SERVICE_PORT };

assert!(one_counted_test(switch, send, Some(expected), "ndp_miss").await?);

Ok(())
}

#[tokio::test]
#[ignore]
async fn test_ttl_exceeded() -> TestResult {
Expand Down
2 changes: 1 addition & 1 deletion packet/src/sidecar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// 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 2025 Oxide Computer Company
// Copyright 2026 Oxide Computer Company

use std::convert::TryInto;
use std::fmt;
Expand Down
54 changes: 21 additions & 33 deletions tfportd/src/sidecar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// 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 2025 Oxide Computer Company
// Copyright 2026 Oxide Computer Company

use std::net::IpAddr;
use std::sync::Arc;
Expand Down Expand Up @@ -100,39 +100,12 @@ pub fn resolution_needed(
}
}

pub fn handle_neighbor_needed(g: &Global, p: packet::Packet) {
if p.hdrs.ipv6_hdr.is_none() {
return error!(g.log, "not an ipv6 packet");
}
Comment on lines -104 to -106

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's my understanding of this PR. Please lmk if something is missing:

  • Say we have an IPv4 packet with an IPv6 next hop. BGP unnumbered is a common case.
  • And the next hop IPv6 address needs NDP resolution. Then we follow this chain up to tfportd:

    dendrite/dpd/p4/sidecar.p4

    Lines 1035 to 1040 in 6385c1e

    action forward_v6(PortId_t port, ipv6_addr_t nexthop) {
    res.port = port;
    res.nexthop6 = nexthop;
    res.is_v6 = true;
    forward_ctr.count();
    }

    dendrite/dpd/p4/sidecar.p4

    Lines 1256 to 1261 in 6385c1e

    hdr.ipv4.ttl = hdr.ipv4.ttl - 1;
    ig_tm_md.ucast_egress_port = fwd.port;
    meta.nexthop_ipv4 = fwd.nexthop;
    meta.nexthop_ipv6 = fwd.nexthop6;
    meta.resolve_nexthop = true;

    dendrite/dpd/p4/sidecar.p4

    Lines 1180 to 1194 in 6385c1e

    action request() {
    hdr.sidecar.sc_code = SC_NEIGHBOR_NEEDED;
    hdr.sidecar.sc_ingress = (bit<16>)ig_intr_md.ingress_port;
    hdr.sidecar.sc_egress = (bit<16>)ig_tm_md.ucast_egress_port;
    hdr.sidecar.sc_ether_type = hdr.ethernet.ether_type;
    hdr.sidecar.sc_payload = (bit<128>)meta.nexthop_ipv6;
    hdr.sidecar.setValid();
    hdr.ethernet.ether_type = ETHERTYPE_SIDECAR;
    meta.service_routed = true;
    meta.drop_reason = DROP_NDP_MISS;
    // Dont set meta.dropped because we want an error packet
    // to go up to the switch.
    ig_tm_md.ucast_egress_port = USER_SPACE_SERVICE_PORT;
    ctr.count();
    }
  • The sc_code is for NEIGHBOR_NEEDED, but the packet is IPv4. The old version ignored such a packet.
  • In this PR, we combine these duplicated handlers and serve the resolution sc_code independent of its packet's particular IP version.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, exactly. That's the dream anyway


let sc = p.hdrs.sidecar_hdr.unwrap();
let asic_id = p.hdrs.sidecar_hdr.as_ref().unwrap().sc_egress;
let ipv6 = {
let x = u128::from_be_bytes(sc.sc_payload[0..16].try_into().unwrap());
x.into()
};
resolution_needed(g, asic_id, IpAddr::V6(ipv6), p);
}

pub fn handle_arp_needed(g: &Global, p: packet::Packet) {
if p.hdrs.ipv4_hdr.is_none() {
return error!(g.log, "not an ipv4 packet: {:?}", p);
}

let sc = p.hdrs.sidecar_hdr.unwrap();
let asic_id = p.hdrs.sidecar_hdr.as_ref().unwrap().sc_egress;
let ipv4 = {
let x = u32::from_be_bytes(sc.sc_payload[12..16].try_into().unwrap());
x.into()
};
resolution_needed(g, asic_id, IpAddr::V4(ipv4), p);
}

fn handle_packet(g: &Global, p: packet::Packet) {
let sc_code = match &p.hdrs.sidecar_hdr {
Some(sc) => sc.sc_code,
let sc = match &p.hdrs.sidecar_hdr {
Some(sc) => sc,
None => return debug!(g.log, "received a non-sidecar packet"),
};
let sc_code = sc.sc_code;

match sc_code {
sidecar::SC_FWD_FROM_USERSPACE => {
Expand All @@ -142,8 +115,23 @@ fn handle_packet(g: &Global, p: packet::Packet) {
// These packets were addressed to a port that doesn't have a corresponding
// tfport link. There's nothing we want to do with them.
}
sidecar::SC_ARP_NEEDED => handle_arp_needed(g, p),
sidecar::SC_NEIGHBOR_NEEDED => handle_neighbor_needed(g, p),
sidecar::SC_ARP_NEEDED => {
if p.hdrs.ipv4_hdr.is_none() && p.hdrs.ipv6_hdr.is_none() {
error!(g.log, "not an ip packet (sc_code: {sc_code}): {p:?}");
return None;
}
let [.., a, b, c, d] = sc.sc_payload;
let ip = IpAddr::V4(Ipv4Addr::new(a, b, c, d));
resolution_needed(g, sc.sc_egress, ip, p)
}
sidecar::SC_NEIGHBOR_NEEDED => {
if p.hdrs.ipv6_hdr.is_none() {
error!(g.log, "not an ipv6 packet (sc_code: {sc_code}): {p:?}");
return None;
}
let ip = IpAddr::V6(Ipv6Addr::from(sc.sc_payload));
resolution_needed(g, sc.sc_egress, ip, p)
}
// TODO-completeness: implement this as per dendrite#156
sidecar::SC_ICMP_NEEDED => {}
x => debug!(g.log, "unrecognized sidecar code: {:x}", x),
Expand Down
Loading