Skip to content

[multicast] Bitmap-based multicast replication for sidecar-lite - #152

Open
zeeshanlakhani wants to merge 6 commits into
mainfrom
zl/multicast
Open

zeeshanlakhani wants to merge 6 commits into
mainfrom
zl/multicast

Conversation

@zeeshanlakhani

@zeeshanlakhani zeeshanlakhani commented Mar 23, 2026 •

Copy link
Copy Markdown

This adds multicast replication to sidecar-lite, emulating DPD's sidecar.p4 on dendrite @ zl/mcast-swadm, following RFD 488.

We manage 2 bitmaps per underlay group (external and underlay), for which replication is driven by p4rs replicate (dep @ p4#240). Ingress zeroes out at most one of these bitmaps determined by the geneve multicast option tag (tag 0 zeroes out underlay; tag 1 zeroes out external), and merges both.

Tag 0 (External) also decaps upon ingress. With no tag given, as on external NAT ingress packets, both bitmaps remain in place.

Only geneve-encapsulated multicast commits to replication. Multicast packets that miss ingress NAT drop on the empty bitmap, matching DPD's drop_mcastv{4,6}_no_group semantics. Packet copies that remain encapsulated lose a hop on the outer IPv6 header, as modelled in MulticastRouter6.

External multicast is NAT-encapsulated into the underlay via nat_v{4,6_mcast(_untagged), reusing forward_to_sled. Tagged and untagged variants exist as separate tables since x4c cannot key directly on header validity. The encap piece now zeroes out the outer src, traffic class, flow label, and inner eth src, like DPD's add_encap_headers.

Egress decap checks ff04::/64 and tag=2, matching DPD's egress mcast_tag_check, with per-port decap from a RID-keyed bitmap (tbl_decap_ports) and optional VLAN. tag=2 decapsulation also rejects inner unicast. tag=0 copies (already decapped in ingress) use the table for the VLAN. Dst MAC derivation (RFC 1112, RFC 2464) and per-port src MAC rewriting run on every packet copy.

Includes:

  • The parser rejects ff01 addresses outright, as well as ff02 destined packets with a hop limit of 0 (ff02 does still go to the scrimlet).
  • Multicast packets with a TTL/hop_limit <= 1 get dropped in ingress after the fwd_from_scrimlet return, letting scrimlet transmissions pass through.
  • Both decapsulation paths drop inner TTL <= 1. IPv4 checksums use RFC 1624 (eqn. 3): a checksum of 0xfeff turns into 0x0000 after decrementing the TTL (XOR used here due to ~ not being parsable by x4c).
  • scadm gets add/remove/dump for replication (v6), source filters, mcast NAT, decap ports, src MAC, and asic-id-to-port (for logical mapping).
  • Unicast modifications:
    • The router drops TTL/hop_limit <= 1 and decrements remaining packets on routes to nonzero ports, updating the IPv4 checksum.
      • Port 0 (the scrimlet) skips both the TTL check and decrement.
      • DPD's Router4/6 still decrement service-bound packets.
    • The NAT encap zeroing described above is shared with unicast NAT.
    • Packets with a parsed Geneve header skip ingress NAT, including unicast.

References

This work depends on the P4(rs) work in #240 and softnpu's zl/multicast.

@zeeshanlakhani
zeeshanlakhani marked this pull request as ready for review March 24, 2026 00:40
@zeeshanlakhani zeeshanlakhani changed the title [multicast] adds multicast replication for sidecar-lite [multicast] Bitmap-based multicast replication for sidecar-lite Mar 25, 2026
@zeeshanlakhani
zeeshanlakhani marked this pull request as draft March 26, 2026 15:54
@zeeshanlakhani
zeeshanlakhani marked this pull request as ready for review March 30, 2026 05:48
zeeshanlakhani added a commit to oxidecomputer/dendrite that referenced this pull request Mar 30, 2026
This wires up the softnpu ASIC backend to support multicast end-to-end by
translating DPD's sidecar.p4 table operations into sidecar-lite.p4's
simplified P4 pipeline.

## AsicMulticastOps

We replace the stubbed AsicMulticastOps implementation (which returned
"OperationUnsupported" for group creation and port addition with in-memory group
tracking via McGroupData, following the tofino_stub pattern.

Group membership is used by the table translation layer to build port
bitmaps for sidecar-lite's Replicate extern. Ports >= 128 are
rejected at add time to match sidecar-lite's 128-bit bitmap width.

## Table translation (asic/src/softnpu/table.rs)

We map sidecar.p4 table names to sidecar-lite equivalents and translate
action parameters where the designs differ for emulation.

All multicast action arms are gated with #[cfg(feature = "multicast")].

## References

- [softnpu #183](oxidecomputer/softnpu#183)
- [propolis #1093](oxidecomputer/propolis#1093)
- [p4rs #240](oxidecomputer/p4#240)
- [sidecar-lite #152](oxidecomputer/sidecar-lite#152)
- tokio: 1.50 (due to softnpu)
- oxide-tokio-rt: 0.1.3 (following-up from tokio's move to 1.50)
zeeshanlakhani added a commit to oxidecomputer/dendrite that referenced this pull request Apr 1, 2026
This wires up the softnpu ASIC backend to support multicast end-to-end by
translating DPD's sidecar.p4 table operations into sidecar-lite.p4's
simplified P4 pipeline.

## AsicMulticastOps

We replace the stubbed AsicMulticastOps implementation (which returned
"OperationUnsupported" for group creation and port addition with in-memory group
tracking via McGroupData, following the tofino_stub pattern.

Group membership is used by the table translation layer to build port
bitmaps for sidecar-lite's Replicate extern. Ports >= 128 are
rejected at add time to match sidecar-lite's 128-bit bitmap width.

## Table translation (asic/src/softnpu/table.rs)

We map sidecar.p4 table names to sidecar-lite equivalents and translate
action parameters where the designs differ for emulation.

All multicast action arms are gated with #[cfg(feature = "multicast")].

## References

- [softnpu #183](oxidecomputer/softnpu#183)
- [propolis #1093](oxidecomputer/propolis#1093)
- [p4rs #240](oxidecomputer/p4#240)
- [sidecar-lite #152](oxidecomputer/sidecar-lite#152)
- tokio: 1.50 (due to softnpu)
- oxide-tokio-rt: 0.1.3 (following-up from tokio's move to 1.50)
zeeshanlakhani added a commit to oxidecomputer/dendrite that referenced this pull request Apr 28, 2026
This wires up the softnpu ASIC backend to support multicast end-to-end by translating DPD's sidecar.p4 table operations into sidecar-lite.p4's simplified P4 pipeline.

## AsicMulticastOps

We replace the stubbed AsicMulticastOps implementation (which returned "OperationUnsupported" for group creation and port addition with in-memory group tracking via McGroupData, following the tofino_stub pattern.

Group membership is used by the table translation layer to build port bitmaps for sidecar-lite's Replicate extern. Ports >= 128 are rejected at add time to match sidecar-lite's 128-bit bitmap width.

## Table translation (asic/src/softnpu/table.rs)

We map sidecar.p4 table names to sidecar-lite equivalents and translate action parameters where the designs differ for emulation.

## Fix update semantics

We fix an issue with stale in-memory sources after update with absent source list(s). `modify_group_external` wrote back `new.sources.or(existing)` to the in-memory record while `update_external_tables` in the same call canonicalized `new.sources` directly to rewrite the P4 source-filter entries. A `None` update therefore cleared the P4 table but left a stale `Some([...])` in memory.

All multicast action arms are gated with #[cfg(feature = "multicast")].



## References and Deps

- [softnpu #183](oxidecomputer/softnpu#183)
- [propolis #1093](oxidecomputer/propolis#1093)
- [p4rs #240](oxidecomputer/p4#240)
- [sidecar-lite #152](oxidecomputer/sidecar-lite#152)
- tokio: 1.50 (due to softnpu)
- oxide-tokio-rt: 0.1.3 (following-up from tokio's move to 1.50)
- bump oxide-tokio-rt to 0.1.4, tokio to 1.52.1 (matching Omicron)
@zeeshanlakhani zeeshanlakhani self-assigned this May 7, 2026
@zeeshanlakhani
zeeshanlakhani force-pushed the zl/multicast branch 2 times, most recently from 8757f58 to bcceb2d Compare June 4, 2026 03:42
This adds multicast replication to sidecar-lite, emulating DPD's
sidecar.p4 on dendrite @ zl/mcast-swadm, following RFD 488.

We manage 2 bitmaps per underlay group (external and underlay), for
which replication is driven by p4rs `replicate` (dep @ p4#240). Ingress
zeroes out at most one of these bitmaps determined by the geneve
multicast option tag (tag 0 zeroes out underlay; tag 1 zeroes out
external), and merges both.

Tag 0 (External) also decaps upon ingress. With no tag given, as on
external NAT ingress packets, both bitmaps remain in place.

Only geneve-encapsulated multicast commits to replication. Multicast
packets that miss ingress NAT drop on the empty bitmap, matching DPD's
`drop_mcastv{4,6}_no_group` semantics. Packet copies that
remain encapsulated lose a hop on the outer IPv6 header, as modelled in
`MulticastRouter6`.

External multicast is NAT-encapsulated into the underlay via
`nat_v{4,6}_mcast(_untagged)`, reusing `forward_to_sled`. Tagged and
untagged variants exist as separate tables since x4c cannot key
directly on header validity. The encap piece now zeroes out the outer
src, traffic class, flow label, and inner eth src, like DPD's
`add_encap_headers`.

Egress decap checks `ff04::/64` and `tag=2`, matching DPD's egress
`mcast_tag_check`, with per-port decap from a RID-keyed bitmap
(`tbl_decap_ports`) and optional VLAN. `tag=2` decapsulation also
rejects inner unicast. `tag=0` copies (already decapped in ingress)
use the table for the VLAN. Dst MAC derivation (RFC 1112, RFC 2464)
and per-port src MAC rewriting run on every packet copy.

Includes:

- The parser rejects `ff01` addresses outright, as well as `ff02` destined
  packets with a hop limit of 0 (`ff02` *does* still go to the scrimlet).
- Multicast packets with a TTL/hop_limit <= 1 get dropped in ingress
  after the `fwd_from_scrimlet` return, letting scrimlet transmissions
  pass through.
- Both decapsulation paths drop inner TTL <= 1. IPv4 checksums use
  RFC 1624 (eqn. 3): a checksum of 0xfeff turns into 0x0000 after
  decrementing the TTL (XOR used here due to `~` not being parsable by x4c).
- scadm gets add/remove/dump for replication (v6), source filters, mcast
  NAT, decap ports, src MAC, and asic-id-to-port (for logical mapping).
- Unicast modifications:
  - The router drops TTL/hop_limit <= 1 and decrements remaining packets
    on routes to nonzero ports, updating the IPv4 checksum.
    - Port 0 (the scrimlet) skips both the TTL check and decrement.
    - DPD's Router4/6 still decrement service-bound packets.
  - The NAT encap zeroing described above is shared with unicast NAT.
  - Packets with a parsed Geneve header skip ingress NAT, including
    unicast.

### References

This work depends on the P4(rs) work in
[#240](oxidecomputer/p4#240) and softnpu
zl/multicast.
Comment thread Cargo.toml Outdated
Comment thread scadm/Cargo.toml Outdated
Comment thread p4/parser.p4 Outdated
Comment thread p4/sidecar-lite.p4 Outdated
Comment thread p4/sidecar-lite.p4 Outdated
Comment thread p4/sidecar-lite.p4 Outdated
//
// These are keyed on both the destination address and VLAN ID.
//
// Packets with a mismatched VLAN miss the table and are not NAT'd

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.

Our disposition on VLANs to date is that we'll do tagging to participate in segmented upstream networks e.g., the vlan variants of the forwarding actions on routing tables. But we've not done anything in terms of ingress isolation/segmentation with VLANs. What is driving keying ingress NAT tables for mcast on vlan ids?

@zeeshanlakhani zeeshanlakhani Oct 19, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yep, this adds ingress VLAN filtering for multicast. It came from oxidecomputer/dendrite#195, which fixed oxidecomputer/dendrite#107 (a packet arriving on one VLAN got NAT-encapped into a group configured for another). In the thread, @FelixMcFelix suggested using a single exact VLAN entry per group for customer ingress, and that's what merged. Geneve packets bypass NatIngress and are decapsulated later without returning to it, which means the lookup sees the incoming (customer's) packet's original VLAN state.

In dpd, an external group's optional vlan_id feeds the NAT ingress key and egress route, and gets propagated to the underlay group's external decap bitmap. This linked setting controls ingress admission and VLAN tagging on decapsulated external packet copies.

I carried this over here for emulation parity with dendrite's sidecar (mcast_nat_v4_wrong_vlan_miss covers this miss).

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.

a packet arriving on one VLAN got NAT-encapped into a group configured for another

This implies we are attempting to support overlapping address spaces? Supporting overlapping address spaces is not something we support, independent of address type. I think would be a pretty big lift in control plane to get there. It seems to me that the destination group address alone needs to be sufficient to determine where a packet needs to go within the rack on ingress.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I didn't mean to imply overlapping spaces actually. More so, the focus is on VLAN translation. Dpd rejects duplicate group IPs, and the VLAN match is about controlling admission. The destination group address already decides where it goes in the rack.

As mentioned, the check came from oxidecomputer/dendrite#195, fixing oxidecomputer/dendrite#107, which recorded a legit, failing test. The intent was to prevent cross-VLAN translation; #107 captured an encapsulated packet leaving an external port.

If ingress should accept the packet regardless of VLAN, I'm down to update dendrite off main and here as well. We'd need to enforce the external-to-external replication restriction separately, while also allowing delivery to internal receivers. Thoughts?

@zeeshanlakhani zeeshanlakhani Oct 19, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

There are a few alternative directions I take this too @rcgoodfellow, but I thought to just bring this up first as a question. @FelixMcFelix may have thoughts here too?

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.

If ingress should accept the packet regardless of VLAN, I'm down to update dendrite off main and here as well.

This is what we do for unicast. We are a router, and it's not generally expected that VLANs will be preserved across a routed boundary. There is no concept of VLAN isolation within the rack, so there are no VLAN-based boundaries we can meaningfully enforce on ingress.

I understand there was a failing test before, but it's not clear to me that we want multicast ingress tied to VLANs in the way that the test was specifying.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I gotcha.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I'll update here and with a PR off dendrite/main too.

Comment thread p4/sidecar-lite.p4
Comment thread p4/sidecar-lite.p4 Outdated
zeeshanlakhani added a commit to oxidecomputer/dendrite that referenced this pull request Oct 19, 2026
Ry flagged this on oxidecomputer/sidecar-lite#152 (its handling of
ff02 reject was emulating this sidecar). Ever since #14, the parser dropped
ttl/hl (hop-limit) 0 before `services` was involved, which meant that
a unicast packet headed to a switch address and `ff02::/16` headed to the
scrimlet would both be dropped. RFC 1812 §4.2.2.9 and RFC 8200 §3 mention
that the destination should still accept it, while the forwarding pass
still has to drop (per RFC 1812 §5.3.1).

This work updates the sidecar parser to record the reason via meta
and then continue parsing. For unicast and IPv6 link-local multicast,
the drop has been moved to the forwarding side.

Note: some of the arrangement here is to make this change while operating
at our 20-stage max.

Includes:

- mcast packets at ttl/hl 1 now get counted as `multicast_ttl_exceeded`
- Geneve encapped packets headed to a switch address with an outer hop
  limit of 0 get decapped again, as it was before #14
zeeshanlakhani added a commit to oxidecomputer/dendrite that referenced this pull request Oct 19, 2026
Ry flagged this on oxidecomputer/sidecar-lite#152 (its handling of
ff02 reject was emulating this sidecar). Ever since #14, the parser dropped
ttl/hl (hop-limit) 0 before `services` was involved, which meant that
a unicast packet headed to a switch address and `ff02::/16` headed to the
scrimlet would both be dropped. RFC 1812 §4.2.2.9 and RFC 8200 §3 mention
that the destination should still accept it, while the forwarding pass
still has to drop (per RFC 1812 §5.3.1).

This work updates the sidecar parser to record the reason via meta
and then continue parsing. For unicast and IPv6 link-local multicast,
the drop has been moved to the forwarding side.

Note: some of the arrangement here is to make this change while operating
at our 20-stage max.

Includes:

- mcast packets at ttl/hl 1 now get counted as `multicast_ttl_exceeded`
- Geneve encapped packets headed to a switch address with an outer hop
  limit of 0 get decapped again, as it was before #14
Note: nonzero ports drop at <= 8w1, so an exempt ttl
of 0 can't decrement and wrap around.
Per the discussion in
#152 (comment),
we're a router, and it's not expected that VLANs are preserved across a routed
boundary. There's also no VLAN isolation in the rack to enforce on
ingress.
@zeeshanlakhani

Copy link
Copy Markdown
Author

@rcgoodfellow updated on this side. The dendrite fix will go in the mcast stack of PRs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants