Skip to content

Commit 375a0f2

Browse files
[multicast] prevent VLAN translation via match key enforcement (#195)
Fixes #107. This PR prevents cross-VLAN translation in multicast NAT ingress. Previously, a packet arriving with VLAN 100 destined to a multicast group configured for VLAN 200 would be NAT encapsulated and forwarded, translating the packet to the wrong customer's network. Includes: - Adds `Ipv4VlanMatchKey` and `Ipv6VlanMatchKey` that match on destination address, VLAN header validity, and VLAN ID - Each group now installs a single NAT entry with an exact VLAN match key. - Fixes MulticastRouter4 using IPv6 ICMP error codes instead of IPv4 - Removes dead NAT rollback branches for internal groups (no NAT entries) - Adds rollback support for VLAN changes in NAT and route tables
1 parent c0bf0a3 commit 375a0f2

File tree

13 files changed

+1079
-298
lines changed

13 files changed

+1079
-298
lines changed

dpd-client/tests/integration_tests/mcast.rs

Lines changed: 535 additions & 45 deletions
Large diffs are not rendered by default.

dpd-client/tests/integration_tests/table_tests.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//
55
// Copyright 2026 Oxide Computer Company
66

7+
#[cfg(feature = "multicast")]
78
use std::net::IpAddr;
89
use std::net::Ipv4Addr;
910
use std::net::Ipv6Addr;
@@ -41,8 +42,11 @@ use crate::integration_tests::common::prelude::*;
4142
// investigating. If it only changes by an entry or two, it's fine to just
4243
// adjust the constant below to match the observed result.
4344
//
45+
// TODO: Multicast drops IPv4 LPM capacity to 7164 (from 8187) due to
46+
// ingress TCAM pressure. Investigate moving MulticastRouter4/6 into the
47+
// egress pipeline to reclaim capacity.
4448
#[cfg(feature = "multicast")]
45-
const IPV4_LPM_SIZE: usize = 8175; // ipv4 forwarding table
49+
const IPV4_LPM_SIZE: usize = 7164; // ipv4 forwarding table
4650
#[cfg(not(feature = "multicast"))]
4751
const IPV4_LPM_SIZE: usize = 8187; // ipv4 forwarding table
4852

dpd/p4/sidecar.p4

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -608,8 +608,17 @@ control NatIngress (
608608
}
609609

610610
// Separate table for IPv4 multicast packets that need to be encapsulated.
611+
//
612+
// Each group has a single entry keyed on (dst_addr, vlan_valid, vlan_id).
613+
// Groups with a VLAN match on (addr, true, vlan_id). Groups without VLAN
614+
// match on (addr, false, 0). Packets with the wrong VLAN miss and are
615+
// not NAT encapsulated.
611616
table ingress_ipv4_mcast {
612-
key = { hdr.ipv4.dst_addr : exact; }
617+
key = {
618+
hdr.ipv4.dst_addr : exact;
619+
hdr.vlan.isValid() : exact;
620+
hdr.vlan.vlan_id : exact;
621+
}
613622
actions = { mcast_forward_ipv4_to; }
614623
const size = IPV4_MULTICAST_TABLE_SIZE;
615624
counters = mcast_ipv4_ingress_ctr;
@@ -625,9 +634,14 @@ control NatIngress (
625634
mcast_ipv6_ingress_ctr.count();
626635
}
627636

628-
// Separate table for IPv6 multicast packets that need to be encapsulated.
637+
// IPv6 counterpart of ingress_ipv4_mcast for IPv6 packets that need to be
638+
// encapsulated. See ingress_ipv4_mcast for details on VLAN matching.
629639
table ingress_ipv6_mcast {
630-
key = { hdr.ipv6.dst_addr : exact; }
640+
key = {
641+
hdr.ipv6.dst_addr : exact;
642+
hdr.vlan.isValid() : exact;
643+
hdr.vlan.vlan_id : exact;
644+
}
631645
actions = { mcast_forward_ipv6_to; }
632646
const size = IPV6_MULTICAST_TABLE_SIZE;
633647
counters = mcast_ipv6_ingress_ctr;
@@ -1330,15 +1344,17 @@ control MulticastRouter4(
13301344
apply {
13311345
// If the packet came in with a VLAN tag, we need to invalidate
13321346
// the VLAN header before we do the lookup. The VLAN header
1333-
// will be re-attached if set in the forward_vlan action.
1347+
// will be re-attached if set in the forward_vlan action (or
1348+
// untagged for groups without VLAN). This prevents unintended
1349+
// VLAN translation.
13341350
if (hdr.vlan.isValid()) {
13351351
hdr.ethernet.ether_type = hdr.vlan.ether_type;
13361352
hdr.vlan.setInvalid();
13371353
}
13381354

13391355
if (!tbl.apply().hit) {
1340-
icmp_error(ICMP6_DST_UNREACH, ICMP6_DST_UNREACH_NOROUTE);
1341-
meta.drop_reason = DROP_IPV6_UNROUTEABLE;
1356+
icmp_error(ICMP_DEST_UNREACH, ICMP_DST_UNREACH_NET);
1357+
meta.drop_reason = DROP_IPV4_UNROUTEABLE;
13421358
// Dont set meta.dropped because we want an error packet
13431359
// to go out.
13441360
} else if (hdr.ipv4.ttl == 1 && !meta.service_routed) {
@@ -1470,7 +1486,9 @@ control MulticastRouter6 (
14701486
apply {
14711487
// If the packet came in with a VLAN tag, we need to invalidate
14721488
// the VLAN header before we do the lookup. The VLAN header
1473-
// will be re-attached if set in the forward_vlan action.
1489+
// will be re-attached if set in the forward_vlan action (or
1490+
// untagged for groups without VLAN). This prevents unintended
1491+
// VLAN translation.
14741492
if (hdr.vlan.isValid()) {
14751493
hdr.ethernet.ether_type = hdr.vlan.ether_type;
14761494
hdr.vlan.setInvalid();

0 commit comments

Comments
 (0)