From 1d5c2961981d149d85ea08c9ea06f7510a72adbf Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Wed, 4 Nov 2026 02:18:43 +0000 Subject: [PATCH] [mcast, p4] drop mcast pkts coming from the userspace service port Any multicast packets that the switch zone hands back through the service port (with a sidecar header) shouldn't enter into replication. This update drops such packets as the first arm of `MulticastIngress` (with a port + sidecar check) and introduces an associated counter for this event. Link-local multicast v6 packets won't hit this condition, while IPv4 `224.0.0.0/24` dst packets also get dropped, same as oxidecomputer/sidecar-lite#181. We remain at 19 stages with this flow. --- dpd-client/tests/integration_tests/mcast.rs | 128 ++++++++++++++++++++ dpd/p4/constants.p4 | 3 +- dpd/p4/sidecar.p4 | 10 +- dpd/src/counters.rs | 5 + 4 files changed, 144 insertions(+), 2 deletions(-) diff --git a/dpd-client/tests/integration_tests/mcast.rs b/dpd-client/tests/integration_tests/mcast.rs index 6be56e65..41b1ab02 100644 --- a/dpd-client/tests/integration_tests/mcast.rs +++ b/dpd-client/tests/integration_tests/mcast.rs @@ -3601,6 +3601,134 @@ async fn test_link_local_multicast_counter_attribution() -> TestResult { Ok(()) } +async fn run_userspace_multicast_drop_test( + switch: &Switch, + src: Endpoint, + dst: Endpoint, + nat_target: types::ExternalNatTarget, +) -> TestResult { + let egress1 = PhysPort(15); + let egress2 = PhysPort(17); + let egress3 = PhysPort(19); + + let internal_multicast_ip = IpAddr::V6(MULTICAST_NAT_IP); + create_test_multicast_group( + switch, + internal_multicast_ip, + Some(&TEST_TAG), + &[ + (egress1, types::Direction::Underlay), + (egress3, types::Direction::Underlay), + ], + None, + types::ExternalForwarding { vlan_id: None }, + None, + ) + .await; + + let multicast_ip = dst.get_ip("destination").unwrap(); + create_test_multicast_group( + switch, + multicast_ip, + Some(&TEST_TAG), + &[ + (egress1, types::Direction::External), + (egress2, types::Direction::External), + ], + Some(nat_target), + types::ExternalForwarding { vlan_id: None }, + None, + ) + .await; + + let mut send = common::gen_udp_packet(src, dst); + common::add_sidecar_hdr( + switch, + &mut send, + sidecar::SC_FWD_FROM_USERSPACE, + NO_PORT, + PhysPort(10), + None, + ); + let test_pkt = TestPacket { packet: Arc::new(send), port: SERVICE_PORT }; + + let ctr_baseline_userspace = + switch.get_counter("multicast_from_userspace", None).await.unwrap(); + let ctr_baseline_no_group = + switch.get_counter("multicast_no_group", None).await.unwrap(); + + switch.packet_test(vec![test_pkt], Vec::new()).unwrap(); + + check_counter_incremented( + switch, + "multicast_from_userspace", + ctr_baseline_userspace, + 1, + None, + ) + .await + .unwrap(); + check_counter_incremented( + switch, + "multicast_no_group", + ctr_baseline_no_group, + 0, + None, + ) + .await + .unwrap(); + + cleanup_test_group(switch, multicast_ip, &TEST_TAG).await.unwrap(); + cleanup_test_group(switch, internal_multicast_ip, &TEST_TAG).await +} + +#[tokio::test] +#[ignore] +async fn test_userspace_multicast_v4_dropped() -> TestResult { + let switch = &*get_switch().await; + + let src = + Endpoint::parse("e0:d5:5e:67:89:ab", "10.10.10.10", 3333).unwrap(); + let dst = Endpoint::parse( + "01:00:5e:00:01:00", + &MULTICAST_TEST_IPV4.to_string(), + 4444, + ) + .unwrap(); + + run_userspace_multicast_drop_test( + switch, + src, + dst, + create_nat_target_ipv4(), + ) + .await +} + +#[tokio::test] +#[ignore] +async fn test_userspace_multicast_v6_dropped() -> TestResult { + let switch = &*get_switch().await; + + let src = + Endpoint::parse("e0:d5:5e:67:89:ab", "fd00:1122:7788:0101::4", 3333) + .unwrap(); + let dst = Endpoint::parse( + "33:33:00:01:10:10", + &MULTICAST_TEST_IPV6.to_string(), + 4444, + ) + .unwrap(); + + run_userspace_multicast_drop_test( + switch, + src, + dst, + create_nat_target_ipv6(), + ) + .await +} + #[tokio::test] #[ignore] async fn test_link_local_multicast_inbound_counter_attribution() -> TestResult { diff --git a/dpd/p4/constants.p4 b/dpd/p4/constants.p4 index 9913bdaf..fe0b1779 100644 --- a/dpd/p4/constants.p4 +++ b/dpd/p4/constants.p4 @@ -115,5 +115,6 @@ const bit<8> DROP_SCTP = 0x1B; // TTL/hop limit of 1 in the parser, or inner TTL/hop limit of 0 or 1 // on the decapsulation path (see RFC 1812 §4.3.2.7, §5.3.1; RFC 4443 §2.4(e.3)) const bit<8> DROP_MULTICAST_TTL_EXCEEDED = 0x1C; +const bit<8> DROP_MULTICAST_FROM_USERSPACE = 0x1D; // MAX(DROP_xxx) + 1 -const bit<32> DROP_REASON_MAX = 0x1D; +const bit<32> DROP_REASON_MAX = 0x1E; diff --git a/dpd/p4/sidecar.p4 b/dpd/p4/sidecar.p4 index eb44ff13..5a7e06d0 100644 --- a/dpd/p4/sidecar.p4 +++ b/dpd/p4/sidecar.p4 @@ -1662,6 +1662,11 @@ control MulticastIngress ( meta.dropped = true; } + action drop_mcast_from_userspace() { + meta.drop_reason = DROP_MULTICAST_FROM_USERSPACE; + meta.dropped = true; + } + // Drop action for IPv6 multicast packets with no group // that is a valid admin-scoped multicast group. action drop_mcastv6_admin_scoped_no_group() { @@ -1833,7 +1838,10 @@ control MulticastIngress ( // Note: SSM tables currently take one extra stage in the pipeline (17->18). apply { - if (hdr.geneve.isValid() && hdr.inner_ipv4.isValid()) { + if (ig_intr_md.ingress_port == USER_SPACE_SERVICE_PORT && + hdr.sidecar.isValid()) { + drop_mcast_from_userspace(); + } else if (hdr.geneve.isValid() && hdr.inner_ipv4.isValid()) { // Check if the inner destination address is an IPv4 multicast // address (224.0.0.0/4). Apply source filtering for both SSM // (232.0.0.0/8) and ASM ranges. diff --git a/dpd/src/counters.rs b/dpd/src/counters.rs index e65cd33c..364f00ea 100644 --- a/dpd/src/counters.rs +++ b/dpd/src/counters.rs @@ -200,6 +200,7 @@ enum DropReason { GeneveOptionUnknown, DropSctp, MulticastTtlExceeded, + MulticastFromUserspace, } impl TryFrom for DropReason { @@ -236,6 +237,7 @@ impl TryFrom for DropReason { 26 => Ok(DropReason::GeneveOptionUnknown), 27 => Ok(DropReason::DropSctp), 28 => Ok(DropReason::MulticastTtlExceeded), + 29 => Ok(DropReason::MulticastFromUserspace), x => Err(format!("Unrecognized drop reason: {x}")), } } @@ -286,6 +288,9 @@ fn reason_label(ctr: u8) -> Result, String> { DropReason::MulticastTtlExceeded => { "multicast_ttl_exceeded".to_string() } + DropReason::MulticastFromUserspace => { + "multicast_from_userspace".to_string() + } }; Ok(Some(label)) }