diff --git a/dpd-client/tests/integration_tests/mcast.rs b/dpd-client/tests/integration_tests/mcast.rs index b98b8e76..c7997e94 100644 --- a/dpd-client/tests/integration_tests/mcast.rs +++ b/dpd-client/tests/integration_tests/mcast.rs @@ -17,7 +17,7 @@ use anyhow::anyhow; use dpd_client::{Error, types}; use futures::TryStreamExt; use oxnet::MulticastMac; -use packet::{Endpoint, eth, geneve, ipv4, ipv6, udp}; +use packet::{Endpoint, eth, geneve, ipv4, ipv6, sidecar, udp}; const MULTICAST_TEST_IPV4: Ipv4Addr = Ipv4Addr::new(224, 0, 1, 0); const MULTICAST_TEST_IPV6: Ipv6Addr = @@ -2147,9 +2147,27 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_external_members() ]; let port_label_ingress = switch.port_label(ingress).unwrap(); + let port_label_egress1 = switch.port_label(egress1).unwrap(); + let port_label_egress2 = switch.port_label(egress2).unwrap(); let ctr_baseline_ingress = switch.get_counter(&port_label_ingress, Some("ingress")).await.unwrap(); + let ctr_baseline_external_egress1 = switch + .get_counter(&port_label_egress1, Some("multicast_external")) + .await + .unwrap(); + let ctr_baseline_external_egress2 = switch + .get_counter(&port_label_egress2, Some("multicast_external")) + .await + .unwrap(); + let ctr_baseline_underlay_egress1 = switch + .get_counter(&port_label_egress1, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_baseline_underlay_egress2 = switch + .get_counter(&port_label_egress2, Some("multicast_underlay")) + .await + .unwrap(); switch.packet_test(vec![test_pkt], expected_pkts).unwrap(); @@ -2163,6 +2181,44 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_external_members() .await .unwrap(); + // Decapped replicas to external members must be attributed to the + // external counter, not the underlay one. + check_counter_incremented( + switch, + &port_label_egress1, + ctr_baseline_external_egress1, + 1, + Some("multicast_external"), + ) + .await + .unwrap(); + check_counter_incremented( + switch, + &port_label_egress2, + ctr_baseline_external_egress2, + 1, + Some("multicast_external"), + ) + .await + .unwrap(); + + let ctr_underlay_egress1 = switch + .get_counter(&port_label_egress1, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_underlay_egress2 = switch + .get_counter(&port_label_egress2, Some("multicast_underlay")) + .await + .unwrap(); + assert_eq!( + ctr_underlay_egress1, ctr_baseline_underlay_egress1, + "decapped replicas must not hit the underlay counter" + ); + assert_eq!( + ctr_underlay_egress2, ctr_baseline_underlay_egress2, + "decapped replicas must not hit the underlay counter" + ); + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); @@ -2277,9 +2333,27 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_underlay_members() ]; let port_label_ingress = switch.port_label(ingress).unwrap(); + let port_label_egress3 = switch.port_label(egress3).unwrap(); + let port_label_egress4 = switch.port_label(egress4).unwrap(); let ctr_baseline_ingress = switch.get_counter(&port_label_ingress, Some("ingress")).await.unwrap(); + let ctr_baseline_underlay_egress3 = switch + .get_counter(&port_label_egress3, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_baseline_underlay_egress4 = switch + .get_counter(&port_label_egress4, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_baseline_external_egress3 = switch + .get_counter(&port_label_egress3, Some("multicast_external")) + .await + .unwrap(); + let ctr_baseline_external_egress4 = switch + .get_counter(&port_label_egress4, Some("multicast_external")) + .await + .unwrap(); switch.packet_test(vec![test_pkt], expected_pkts).unwrap(); @@ -2293,6 +2367,43 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_underlay_members() .await .unwrap(); + // Encapped replicas to underlay members increment the underlay counter. + check_counter_incremented( + switch, + &port_label_egress3, + ctr_baseline_underlay_egress3, + 1, + Some("multicast_underlay"), + ) + .await + .unwrap(); + check_counter_incremented( + switch, + &port_label_egress4, + ctr_baseline_underlay_egress4, + 1, + Some("multicast_underlay"), + ) + .await + .unwrap(); + + let ctr_external_egress3 = switch + .get_counter(&port_label_egress3, Some("multicast_external")) + .await + .unwrap(); + let ctr_external_egress4 = switch + .get_counter(&port_label_egress4, Some("multicast_external")) + .await + .unwrap(); + assert_eq!( + ctr_external_egress3, ctr_baseline_external_egress3, + "underlay-tagged replicas must not hit the external counter" + ); + assert_eq!( + ctr_external_egress4, ctr_baseline_external_egress4, + "underlay-tagged replicas must not hit the external counter" + ); + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); @@ -2420,9 +2531,45 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_underlay_and_external_membe ]; let port_label_ingress = switch.port_label(ingress).unwrap(); + let port_label_egress1 = switch.port_label(egress1).unwrap(); + let port_label_egress2 = switch.port_label(egress2).unwrap(); + let port_label_egress3 = switch.port_label(egress3).unwrap(); + let port_label_egress4 = switch.port_label(egress4).unwrap(); let ctr_baseline_ingress = switch.get_counter(&port_label_ingress, Some("ingress")).await.unwrap(); + let ctr_baseline_underlay_egress1 = switch + .get_counter(&port_label_egress1, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_baseline_underlay_egress2 = switch + .get_counter(&port_label_egress2, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_baseline_underlay_egress3 = switch + .get_counter(&port_label_egress3, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_baseline_underlay_egress4 = switch + .get_counter(&port_label_egress4, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_baseline_external_egress1 = switch + .get_counter(&port_label_egress1, Some("multicast_external")) + .await + .unwrap(); + let ctr_baseline_external_egress2 = switch + .get_counter(&port_label_egress2, Some("multicast_external")) + .await + .unwrap(); + let ctr_baseline_external_egress3 = switch + .get_counter(&port_label_egress3, Some("multicast_external")) + .await + .unwrap(); + let ctr_baseline_external_egress4 = switch + .get_counter(&port_label_egress4, Some("multicast_external")) + .await + .unwrap(); switch.packet_test(vec![test_pkt], expected_pkts).unwrap(); @@ -2436,12 +2583,167 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_underlay_and_external_membe .await .unwrap(); + // Still-encapsulated replicas to underlay members count as underlay, + // not external, for UNDERLAY_EXTERNAL tagged groups. + check_counter_incremented( + switch, + &port_label_egress3, + ctr_baseline_underlay_egress3, + 1, + Some("multicast_underlay"), + ) + .await + .unwrap(); + check_counter_incremented( + switch, + &port_label_egress4, + ctr_baseline_underlay_egress4, + 1, + Some("multicast_underlay"), + ) + .await + .unwrap(); + + // Decapped replicas to external members count as external. + check_counter_incremented( + switch, + &port_label_egress1, + ctr_baseline_external_egress1, + 1, + Some("multicast_external"), + ) + .await + .unwrap(); + check_counter_incremented( + switch, + &port_label_egress2, + ctr_baseline_external_egress2, + 1, + Some("multicast_external"), + ) + .await + .unwrap(); + + let ctr_external_egress3 = switch + .get_counter(&port_label_egress3, Some("multicast_external")) + .await + .unwrap(); + let ctr_external_egress4 = switch + .get_counter(&port_label_egress4, Some("multicast_external")) + .await + .unwrap(); + assert_eq!( + ctr_external_egress3, ctr_baseline_external_egress3, + "underlay replicas must not hit the external counter" + ); + assert_eq!( + ctr_external_egress4, ctr_baseline_external_egress4, + "underlay replicas must not hit the external counter" + ); + + let ctr_underlay_egress1 = switch + .get_counter(&port_label_egress1, Some("multicast_underlay")) + .await + .unwrap(); + let ctr_underlay_egress2 = switch + .get_counter(&port_label_egress2, Some("multicast_underlay")) + .await + .unwrap(); + assert_eq!( + ctr_underlay_egress1, ctr_baseline_underlay_egress1, + "decapped replicas must not hit the underlay counter" + ); + assert_eq!( + ctr_underlay_egress2, ctr_baseline_underlay_egress2, + "decapped replicas must not hit the underlay counter" + ); + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); cleanup_test_group(switch, MULTICAST_NAT_IP.into(), TEST_TAG).await } +/// Link-local replicas reach egress with a zero replication id, so the +/// counter block reaches them through `is_link_local_ipv6_mcast` rather +/// than `egress_rid`. Both the underlay and external conditions exclude +/// that case, and this checks the exclusion remains after compilation. +#[tokio::test] +#[ignore] +async fn test_link_local_multicast_counter_attribution() -> TestResult { + let switch = &*get_switch().await; + + let egress = PhysPort(10); + + let src = + Endpoint::parse("e0:d5:5e:67:89:ab", "fd00:1122:7788:0101::4", 3333) + .unwrap(); + let dst = Endpoint::parse("33:33:00:00:00:01", "ff02::1", 4444).unwrap(); + + // Sourced from userspace so the packet egresses without replication, + // mirroring test_link_local_multicast_outbound in route_ipv6. + let mut send = common::gen_udp_packet(src, dst); + common::add_sidecar_hdr( + switch, + &mut send, + sidecar::SC_FWD_FROM_USERSPACE, + NO_PORT, + egress, + None, + ); + let test_pkt = TestPacket { packet: Arc::new(send), port: SERVICE_PORT }; + let expected_pkts = vec![TestPacket { + packet: Arc::new(common::gen_udp_packet(src, dst)), + port: egress, + }]; + + let port_label_egress = switch.port_label(egress).unwrap(); + + let ctr_baseline_link_local = switch + .get_counter(&port_label_egress, Some("multicast_link_local")) + .await + .unwrap(); + let ctr_baseline_external = switch + .get_counter(&port_label_egress, Some("multicast_external")) + .await + .unwrap(); + let ctr_baseline_underlay = switch + .get_counter(&port_label_egress, Some("multicast_underlay")) + .await + .unwrap(); + + switch.packet_test(vec![test_pkt], expected_pkts).unwrap(); + + check_counter_incremented( + switch, + &port_label_egress, + ctr_baseline_link_local, + 1, + Some("multicast_link_local"), + ) + .await + .unwrap(); + + let ctr_external = switch + .get_counter(&port_label_egress, Some("multicast_external")) + .await + .unwrap(); + let ctr_underlay = switch + .get_counter(&port_label_egress, Some("multicast_underlay")) + .await + .unwrap(); + assert_eq!( + ctr_external, ctr_baseline_external, + "link-local replicas must not hit the external counter" + ); + assert_eq!( + ctr_underlay, ctr_baseline_underlay, + "link-local replicas must not hit the underlay counter" + ); + + Ok(()) +} + #[tokio::test] #[ignore] async fn test_ipv4_multicast_drops_ingress_is_egress_port() -> TestResult { diff --git a/dpd/p4/sidecar.p4 b/dpd/p4/sidecar.p4 index 37a394cd..f8817d13 100644 --- a/dpd/p4/sidecar.p4 +++ b/dpd/p4/sidecar.p4 @@ -1823,7 +1823,7 @@ control MulticastIngress ( } else if (hdr.geneve.isValid() && hdr.inner_ipv6.isValid()) { // Check if the inner destination address is an IPv6 multicast // address (ff00::/8). Apply source filtering for both SSM - // (ff3x::/16) and ASM ranges. + // (ff3x::/32) and ASM ranges. if (hdr.inner_ipv6.dst_addr[127:120] == 8w0xff) { mcast_source_filter_ipv6.apply(); } else { @@ -2346,15 +2346,38 @@ control Egress( } else if (is_mcast == true) { mcast_ctr.count(eg_intr_md.egress_port); + // Count each multicast replica by type, one simple gateway + // per condition: + // + // - link-local (ff02::/16 outer dst) — never replicated by + // the PRE; it arrives with egress_rid == 0. + // - egress_rid > 0, Geneve hdr still valid — bound for an + // underlay port. + // - egress_rid > 0, no Geneve hdr — decapped at ingress + // (external-only groups) or by the mcast_egress control + // above (bifurcated, external + underlay members), and + // bound for an external member. + // + // The mcast_tag option is not read here: a replica headed + // for the underlay still carries its encapsulation whether + // or not the group is bifurcated. + // + // The rid arms don't test !is_link_local_ipv6_mcast: the + // compiler doesn't carry the negation across gateways. + // They don't need to either, since scope 2 is refused at + // group creation, so a replica never carries a link-local + // outer destination. + // + // The IngressDeparser above notes the same compiler limitation. if (is_link_local_ipv6_mcast) { link_local_mcast_ctr.count(eg_intr_md.egress_port); - } else if (hdr.geneve.isValid()) { - external_mcast_ctr.count(eg_intr_md.egress_port); - } else if (hdr.geneve.isValid() && - hdr.geneve_opts.oxg_mcast.isValid() && - hdr.geneve_opts.oxg_mcast.mcast_tag == MULTICAST_TAG_UNDERLAY) { + } + if (is_egress_rid_mcast && hdr.geneve.isValid()) { underlay_mcast_ctr.count(eg_intr_md.egress_port); } + if (is_egress_rid_mcast && !hdr.geneve.isValid()) { + external_mcast_ctr.count(eg_intr_md.egress_port); + } } else { unicast_ctr.count(eg_intr_md.egress_port); }