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
136 changes: 135 additions & 1 deletion packet/src/sidecar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
// 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;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

use bytes::BufMut;
use bytes::BytesMut;
Expand Down Expand Up @@ -132,6 +133,32 @@ pub fn code_to_str(code: u8) -> String {
.to_string()
}

/// For an SC_ARP_NEEDED or SC_NEIGHBOR_NEEDED packet, returns the egress
/// ASIC port and the nexthop address awaiting resolution. The nexthop's
/// address family comes from the sidecar code rather than the encapsulated
/// packet, since a route may have a nexthop of the other address family
/// (e.g. 10.0.0.0/8 via fe80::1).
pub fn nexthop_needed(p: &Packet) -> Option<(u16, IpAddr)> {
if p.hdrs.ipv4_hdr.is_none() && p.hdrs.ipv6_hdr.is_none() {
// not an IP packet, nothing to resolve
return None;
}

let sc = p.hdrs.sidecar_hdr.as_ref()?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

sidecar header presence and matching on sc_code is already done in handle_packet, so this is duplicating that logic. That can be avoided with separate handle_arp_needed and handle_sc_neighbor_needed functions that operate on the already extracted sidecar header to get at sc_payload and sc_egress.

That would allow the ip header check to be different for as well, as I think we only need to check for ipv4 for arp needed since we don't forward v6 over v4.

let ip = match sc.sc_code {
// The P4 code zero-extends the 32-bit nexthop to fill the 128-bit
// payload, so the address is in the last four bytes.
SC_ARP_NEEDED => {
let [.., a, b, c, d] = sc.sc_payload;
IpAddr::V4(Ipv4Addr::new(a, b, c, d))
}
SC_NEIGHBOR_NEEDED => IpAddr::V6(Ipv6Addr::from(sc.sc_payload)),
// wrong sidecar code, nothing to resolve
_ => return None,
};
Some((sc.sc_egress, ip))
}

impl fmt::Display for SidecarHdr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
Expand All @@ -157,3 +184,110 @@ impl fmt::Debug for SidecarHdr {
)
}
}

#[cfg(test)]
mod tests {
use super::*;

const SC_EGRESS: u16 = 7;

fn endpoints(src: &str, dst: &str) -> (Endpoint, Endpoint) {
(
Endpoint::parse("e0:d5:5e:67:89:ab", src, 3333).unwrap(),
Endpoint::parse("e0:d5:5e:67:89:ac", dst, 4444).unwrap(),
)
}

fn udp_pkt(src: &str, dst: &str) -> Packet {
let (src_ep, dst_ep) = endpoints(src, dst);
let stack = if src.contains(':') {
vec![ipv6::IPPROTO_UDP.into(), eth::ETHER_IPV6]
} else {
vec![ipv4::IPPROTO_UDP.into(), eth::ETHER_IPV4]
};
Packet::generate(src_ep, dst_ep, stack, None).unwrap()
}

// Wrap `p` in a sidecar header, as the switch does when it punts a packet
// to userspace.
fn add_sidecar(mut p: Packet, sc_code: u8, sc_payload: [u8; 16]) -> Packet {
let eth = p.hdrs.eth_hdr.as_mut().unwrap();
p.hdrs.sidecar_hdr = Some(SidecarHdr {
sc_code,
sc_pad: 0,
sc_ingress: 1,
sc_egress: SC_EGRESS,
sc_ether_type: eth.eth_type,
sc_payload,
});
eth.eth_type = eth::ETHER_SIDECAR;
p
}

// The P4 code widens the 32-bit IPv4 nexthop to 128 bits, so the address
// lands in the last four bytes of the payload.
fn v4_payload(ip: Ipv4Addr) -> [u8; 16] {
let mut payload = [0u8; 16];
payload[12..].copy_from_slice(&ip.octets());
payload
}

#[test]
fn nexthop_needed_ipv4_via_ipv4() {
let nh: Ipv4Addr = "192.0.2.1".parse().unwrap();
let p = add_sidecar(
udp_pkt("10.0.0.1", "10.0.0.2"),
SC_ARP_NEEDED,
v4_payload(nh),
);
assert_eq!(nexthop_needed(&p), Some((SC_EGRESS, nh.into())));
}

#[test]
fn nexthop_needed_ipv6_via_ipv6() {
let nh: Ipv6Addr = "fd00:1122:3344::1".parse().unwrap();
let p = add_sidecar(
udp_pkt("fd00::1", "fd00::2"),
SC_NEIGHBOR_NEEDED,
nh.octets(),
);
assert_eq!(nexthop_needed(&p), Some((SC_EGRESS, nh.into())));
}

#[test]
fn nexthop_needed_ipv4_via_ipv6() {
let nh: Ipv6Addr = "fe80::1".parse().unwrap();
let p = add_sidecar(
udp_pkt("10.0.0.1", "10.0.0.2"),
SC_NEIGHBOR_NEEDED,
nh.octets(),
);
assert_eq!(nexthop_needed(&p), Some((SC_EGRESS, nh.into())));
}

#[test]
fn nexthop_needed_ipv6_via_ipv4() {
let nh: Ipv4Addr = "192.0.2.1".parse().unwrap();
let p = add_sidecar(
udp_pkt("fd00::1", "fd00::2"),
SC_ARP_NEEDED,
v4_payload(nh),
);
assert_eq!(nexthop_needed(&p), Some((SC_EGRESS, nh.into())));
}

#[test]
fn nexthop_needed_non_ip_pkt() {
let (src, dst) = endpoints("10.0.0.1", "10.0.0.2");
let arp_pkt = Packet::generate(
src,
dst,
vec![arp::ARPOP_REQUEST, eth::ETHER_ARP],
None,
)
.unwrap();
let nh: Ipv4Addr = "192.0.2.1".parse().unwrap();
let p = add_sidecar(arp_pkt, SC_ARP_NEEDED, v4_payload(nh));
assert_eq!(nexthop_needed(&p), None);
}
}
41 changes: 10 additions & 31 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,34 +100,6 @@ 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,
Expand All @@ -142,8 +114,15 @@ 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 | sidecar::SC_NEIGHBOR_NEEDED => {
match sidecar::nexthop_needed(&p) {
Some((asic_id, ip)) => resolution_needed(g, asic_id, ip, p),
None => error!(
g.log,
"not an ip packet (sc_code: {sc_code}): {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