Skip to content

Commit 7a5f735

Browse files
[multicast] implicit group lifecycle with IP pool integration (#9450)
This PR also addresses permission models, object deletion, and error handling questions related to reserved addresses presented in @askfongjojo's testing Google Doc (default IP Pools are covered in mainline already). In thinking through the *Groups* API, permission scopes, and flexibility, @rcgoodfellow mentioned this consideration: > Do we need an explicit notion of a group object at all? Or can instances simply allocate/deallocate group IPs from pools, and there is no explicit management of group objects. With Fleet admins having access control to create pools and link silos to a pool, we arrived at the idea of replacing the current explicit multicast group CRUD with an implicit lifecycle, where groups are created upon the first member join and deleted when the last member leaves. **Auth Model:** - Discovery (fleet-scoped): - Read/list groups and list members: any authenticated user in the same fleet. - Membership (project-scoped): - Join/leave requires Instance::Modify on the specific instance. - Creation control: - Implicit group creation only when the caller's silo is linked to a suitable multicast pool (by name or by explicit IP in that pool). **Behavior:** - Implicit lifecycle: - Create on first join (idempotent); delete when last member leaves (atomic mark-for-removal, reconciler schedules cleanup). - Addressing and validation: - Implicit allocation from the caller's linked multicast pools. - SSM/ASM semantics enforced: - IPv4 SSM 232/8 and IPv6 ff3x::/32 require ≥1 source IP. - ASM groups may optionally specify sources (can be `None`). - When joining by explicit IP: resolve the pool containing the IP, verify the silo link before creating. - Error handling: - Reserved/invalid multicast ranges rejected at pool/range add time. **API:** - Primary flows: - Group-centric member management: POST/DELETE /v1/multicast-groups/{group}/members - Instance-centric join/leave: PUT/DELETE /v1/instances/{instance}/multicast-groups/{group} - Discovery endpoints remain for list/view; there is no explicit group create/update/delete. - This is a *breaking* change, but multicast is not yet enabled or available in production. **Key changes:** - Implicit group model; groups exist while they have members. - IP pool integration for multicast allocation with silo link gating. - Simplified API centered on join/leave flows. - Add multicast_ip to the member table for responses. - For consistency, move to `Instant` type over `SystemTime` for mcast-related caches. This also fixes the flaky test issue in #9588.
1 parent 63d8904 commit 7a5f735

File tree

83 files changed

+45284
-8924
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+45284
-8924
lines changed

Cargo.lock

Lines changed: 9 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -629,7 +629,7 @@ oxide-client = { path = "clients/oxide-client" }
629629
oxide-tokio-rt = "0.1.2"
630630
oxide-vpc = { git = "https://github.com/oxidecomputer/opte", rev = "795a1e0aeefb7a2c6fe4139779fdf66930d09b80", features = [ "api", "std" ] }
631631
oxlog = { path = "dev-tools/oxlog" }
632-
oxnet = "0.1.3"
632+
oxnet = "0.1.4"
633633
once_cell = "1.21.3"
634634
openapi-lint = { git = "https://github.com/oxidecomputer/openapi-lint", branch = "main" }
635635
openapiv3 = "2.2.0"

common/src/address.rs

Lines changed: 144 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -25,82 +25,186 @@ pub const SLED_PREFIX: u8 = 64;
2525

2626
// Multicast constants
2727

28-
/// IPv4 Source-Specific Multicast (SSM) subnet as defined in RFC 4607:
29-
/// <https://tools.ietf.org/html/rfc4607>.
28+
/// IPv4 Source-Specific Multicast (SSM) subnet.
3029
///
31-
/// RFC 4607 Section 3 allocates 232.0.0.0/8 as the IPv4 SSM address range.
30+
/// See [RFC 4607 §3] for the IPv4 SSM address range allocation (232.0.0.0/8).
3231
/// This is a single contiguous block, unlike IPv6 which has per-scope ranges.
33-
pub const IPV4_SSM_SUBNET: oxnet::Ipv4Net =
34-
oxnet::Ipv4Net::new_unchecked(Ipv4Addr::new(232, 0, 0, 0), 8);
32+
///
33+
/// [RFC 4607 §3]: https://www.rfc-editor.org/rfc/rfc4607#section-3
34+
pub const IPV4_SSM_SUBNET: Ipv4Net =
35+
Ipv4Net::new_unchecked(Ipv4Addr::new(232, 0, 0, 0), 8);
3536

36-
/// IPv6 Source-Specific Multicast (SSM) subnet as defined in RFC 4607:
37-
/// <https://tools.ietf.org/html/rfc4607>.
37+
/// IPv6 Source-Specific Multicast (SSM) subnet.
3838
///
39-
/// RFC 4607 Section 3 specifies "FF3x::/32 for each scope x" - meaning one
40-
/// /32 block per scope (FF30::/32, FF31::/32, ..., FF3F::/32).
39+
/// See [RFC 4607 §3] for SSM scope allocation. The RFC specifies "ff3x::/32
40+
/// for each scope x" - meaning one /32 block per scope (ff30::/32, ff31::/32,
41+
/// ..., ff3f::/32).
4142
///
4243
/// We use /12 as an implementation convenience to match all these blocks with
4344
/// a single subnet. This works because all SSM addresses share the same first
4445
/// 12 bits:
45-
/// - Bits 0-7: 11111111 (0xFF, multicast prefix)
46+
/// - Bits 0-7: 11111111 (0xff, multicast prefix)
4647
/// - Bits 8-11: 0011 (flag field = 3, indicating SSM)
47-
/// - Bits 12-15: xxxx (scope field, any value 0-F)
48+
/// - Bits 12-15: xxxx (scope field, any value 0-f)
4849
///
49-
/// Thus FF30::/12 efficiently matches FF30:: through FF3F:FFFF:...:FFFF,
50+
/// Thus ff30::/12 efficiently matches ff30:: through ff3f:ffff:...:ffff,
5051
/// covering all SSM scopes.
51-
pub const IPV6_SSM_SUBNET: oxnet::Ipv6Net = oxnet::Ipv6Net::new_unchecked(
52-
Ipv6Addr::new(0xff30, 0, 0, 0, 0, 0, 0, 0),
53-
12,
54-
);
52+
///
53+
/// This superset is used only for contains-based classification and validation
54+
/// (e.g., `contains()` checks). It is not an allocation boundary.
55+
///
56+
/// [RFC 4607 §3]: https://www.rfc-editor.org/rfc/rfc4607#section-3
57+
pub const IPV6_SSM_SUBNET: Ipv6Net =
58+
Ipv6Net::new_unchecked(Ipv6Addr::new(0xff30, 0, 0, 0, 0, 0, 0, 0), 12);
59+
60+
/// Maximum source IPs per SSM group member (per [RFC 3376] IGMPv3).
61+
///
62+
/// [RFC 3376]: https://www.rfc-editor.org/rfc/rfc3376
63+
pub const MAX_SSM_SOURCE_IPS: usize = 64;
64+
65+
/// Check if an IP is in the SSM (Source-Specific Multicast) range.
66+
///
67+
/// SSM ranges per [RFC 4607 §3]:
68+
/// - IPv4: 232.0.0.0/8
69+
/// - IPv6: ff3x::/32 (all SSM scopes)
70+
///
71+
/// [RFC 4607 §3]: https://www.rfc-editor.org/rfc/rfc4607#section-3
72+
pub fn is_ssm_address(ip: std::net::IpAddr) -> bool {
73+
match ip {
74+
IpAddr::V4(addr) => IPV4_SSM_SUBNET.contains(addr),
75+
IpAddr::V6(addr) => IPV6_SSM_SUBNET.contains(addr),
76+
}
77+
}
5578

5679
/// IPv4 multicast address range (224.0.0.0/4).
57-
/// See RFC 5771 (IPv4 Multicast Address Assignments):
58-
/// <https://www.rfc-editor.org/rfc/rfc5771>
80+
///
81+
/// See [RFC 5771] for IPv4 multicast address assignments.
82+
///
83+
/// [RFC 5771]: https://www.rfc-editor.org/rfc/rfc5771
5984
pub const IPV4_MULTICAST_RANGE: Ipv4Net =
6085
Ipv4Net::new_unchecked(Ipv4Addr::new(224, 0, 0, 0), 4);
6186

6287
/// IPv4 link-local multicast subnet (224.0.0.0/24).
88+
///
6389
/// This range is reserved for local network control protocols and should not
6490
/// be routed beyond the local link. Includes addresses for protocols like
6591
/// OSPF (224.0.0.5), RIPv2 (224.0.0.9), and other local routing protocols.
66-
/// See RFC 5771 Section 4:
67-
/// <https://www.rfc-editor.org/rfc/rfc5771#section-4>
92+
///
93+
/// See [RFC 5771 §4] for link-local multicast address assignments. The IANA
94+
/// IPv4 Multicast Address Space registry is the canonical source for
95+
/// assignments.
96+
///
97+
/// [RFC 5771 §4]: https://www.rfc-editor.org/rfc/rfc5771#section-4
6898
pub const IPV4_LINK_LOCAL_MULTICAST_SUBNET: Ipv4Net =
6999
Ipv4Net::new_unchecked(Ipv4Addr::new(224, 0, 0, 0), 24);
70100

71101
/// IPv6 multicast address range (ff00::/8).
72-
/// See RFC 4291 (IPv6 Addressing Architecture):
73-
/// <https://www.rfc-editor.org/rfc/rfc4291>
102+
///
103+
/// See [RFC 4291] for IPv6 addressing architecture.
104+
///
105+
/// [RFC 4291]: https://www.rfc-editor.org/rfc/rfc4291
74106
pub const IPV6_MULTICAST_RANGE: Ipv6Net =
75107
Ipv6Net::new_unchecked(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0, 0), 8);
76108

77109
/// IPv6 multicast prefix (ff00::/8) mask/value for scope checking.
110+
///
111+
/// See [RFC 4291 §2.7] for multicast address format.
112+
///
113+
/// [RFC 4291 §2.7]: https://www.rfc-editor.org/rfc/rfc4291#section-2.7
78114
pub const IPV6_MULTICAST_PREFIX: u16 = 0xff00;
79115

80-
/// Admin-scoped IPv6 multicast prefix (ff04::/16) as u16 for address
116+
/// Admin-local IPv6 multicast prefix (ff04::/16) as u16 for address
81117
/// construction and normalization of underlay multicast addresses.
118+
///
119+
/// See [RFC 4291 §2.7] and [RFC 7346] for IPv6 multicast address format
120+
/// and scope definitions.
121+
///
122+
/// [RFC 4291 §2.7]: https://www.rfc-editor.org/rfc/rfc4291#section-2.7
123+
/// [RFC 7346]: https://www.rfc-editor.org/rfc/rfc7346
82124
pub const IPV6_ADMIN_SCOPED_MULTICAST_PREFIX: u16 = 0xff04;
83125

126+
/// Fixed underlay admin-local IPv6 multicast subnet (ff04::/64) used for
127+
/// internal multicast group allocation and external→underlay mapping.
128+
///
129+
/// Admin-local scope (4) is defined in [RFC 7346] as "the smallest scope that
130+
/// must be administratively configured."
131+
///
132+
/// Static for consistency across racks. The XOR-fold algorithm maps external
133+
/// multicast IPs into this /64 with an 8-bit salt, guaranteeing 256 unique
134+
/// addresses per external IP for collision retries. IP pool validation rejects
135+
/// ranges overlapping this prefix.
136+
///
137+
/// External pools may use other admin-local prefixes (e.g., `ff04:0:0:1::/64`)
138+
/// outside this range or other administratively configured scopes
139+
/// (e.g., site-local `ff05::/16`).
140+
///
141+
/// [RFC 7346]: https://www.rfc-editor.org/rfc/rfc7346
142+
// TODO: Expose this subnet via rack API (e.g., in `Rack` view or a dedicated
143+
// networking info endpoint) so operators can see reserved address ranges.
144+
pub const UNDERLAY_MULTICAST_SUBNET: Ipv6Net = Ipv6Net::new_unchecked(
145+
Ipv6Addr::new(IPV6_ADMIN_SCOPED_MULTICAST_PREFIX, 0, 0, 0, 0, 0, 0, 0),
146+
64,
147+
);
148+
149+
/// Last address in the underlay multicast subnet (ff04::ffff:ffff:ffff:ffff).
150+
pub const UNDERLAY_MULTICAST_SUBNET_LAST: Ipv6Addr = Ipv6Addr::new(
151+
IPV6_ADMIN_SCOPED_MULTICAST_PREFIX,
152+
0,
153+
0,
154+
0,
155+
0xffff,
156+
0xffff,
157+
0xffff,
158+
0xffff,
159+
);
160+
84161
/// IPv6 interface-local multicast subnet (ff01::/16).
162+
///
85163
/// These addresses are not routable and should not be added to IP pools.
86-
/// See RFC 4291 Section 2.7 (multicast scope field):
87-
/// <https://www.rfc-editor.org/rfc/rfc4291#section-2.7>
88-
pub const IPV6_INTERFACE_LOCAL_MULTICAST_SUBNET: oxnet::Ipv6Net =
89-
oxnet::Ipv6Net::new_unchecked(
90-
Ipv6Addr::new(0xff01, 0, 0, 0, 0, 0, 0, 0),
91-
16,
92-
);
164+
///
165+
/// See [RFC 4291 §2.7] for multicast scope field definitions.
166+
///
167+
/// [RFC 4291 §2.7]: https://www.rfc-editor.org/rfc/rfc4291#section-2.7
168+
pub const IPV6_INTERFACE_LOCAL_MULTICAST_SUBNET: Ipv6Net =
169+
Ipv6Net::new_unchecked(Ipv6Addr::new(0xff01, 0, 0, 0, 0, 0, 0, 0), 16);
170+
171+
/// Last address in the IPv6 interface-local multicast subnet.
172+
pub const IPV6_INTERFACE_LOCAL_MULTICAST_LAST: Ipv6Addr = Ipv6Addr::new(
173+
0xff01, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff,
174+
);
93175

94176
/// IPv6 link-local multicast subnet (ff02::/16).
177+
///
95178
/// These addresses are not routable beyond the local link and should not be
96179
/// added to IP pools.
97-
/// See RFC 4291 Section 2.7 (multicast scope field):
98-
/// <https://www.rfc-editor.org/rfc/rfc4291#section-2.7>
99-
pub const IPV6_LINK_LOCAL_MULTICAST_SUBNET: oxnet::Ipv6Net =
100-
oxnet::Ipv6Net::new_unchecked(
101-
Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 0),
102-
16,
103-
);
180+
///
181+
/// See [RFC 4291 §2.7] for multicast scope field definitions.
182+
///
183+
/// [RFC 4291 §2.7]: https://www.rfc-editor.org/rfc/rfc4291#section-2.7
184+
pub const IPV6_LINK_LOCAL_MULTICAST_SUBNET: Ipv6Net =
185+
Ipv6Net::new_unchecked(Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 0), 16);
186+
187+
/// Last address in the IPv6 link-local multicast subnet.
188+
pub const IPV6_LINK_LOCAL_MULTICAST_LAST: Ipv6Addr = Ipv6Addr::new(
189+
0xff02, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff,
190+
);
191+
192+
/// IPv6 reserved-scope multicast subnet (ff00::/16).
193+
///
194+
/// Scope 0 is reserved - packets with this scope must not be originated and
195+
/// must be silently dropped if received. These addresses should not be added
196+
/// to IP pools.
197+
///
198+
/// See [RFC 4291 §2.7] for multicast scope field definitions.
199+
///
200+
/// [RFC 4291 §2.7]: https://www.rfc-editor.org/rfc/rfc4291#section-2.7
201+
pub const IPV6_RESERVED_SCOPE_MULTICAST_SUBNET: Ipv6Net =
202+
Ipv6Net::new_unchecked(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0, 0), 16);
203+
204+
/// Last address in the IPv6 reserved-scope multicast subnet.
205+
pub const IPV6_RESERVED_SCOPE_MULTICAST_LAST: Ipv6Addr = Ipv6Addr::new(
206+
0xff00, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff,
207+
);
104208

105209
/// maximum possible value for a tcp or udp port
106210
pub const MAX_PORT: u16 = u16::MAX;
@@ -254,8 +358,9 @@ pub static NTP_OPTE_IPV6_SUBNET: LazyLock<Ipv6Net> = LazyLock::new(|| {
254358
// Anycast is a mechanism in which a single IP address is shared by multiple
255359
// devices, and the destination is located based on routing distance.
256360
//
257-
// This is covered by RFC 4291 in much more detail:
258-
// <https://datatracker.ietf.org/doc/html/rfc4291#section-2.6>
361+
// See [RFC 4291 §2.6] for anycast address allocation.
362+
//
363+
// [RFC 4291 §2.6]: https://www.rfc-editor.org/rfc/rfc4291#section-2.6
259364
//
260365
// Anycast addresses are always the "zeroeth" address within a subnet. We
261366
// always explicitly skip these addresses within our network.

common/src/api/external/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2558,6 +2558,11 @@ impl Vni {
25582558
pub fn random_system() -> Self {
25592559
Self(rand::rng().random_range(0..Self::MIN_GUEST_VNI))
25602560
}
2561+
2562+
/// Returns the VNI as a raw u32.
2563+
pub const fn as_u32(&self) -> u32 {
2564+
self.0
2565+
}
25612566
}
25622567

25632568
impl From<Vni> for u32 {

dev-tools/omdb/tests/successes.out

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -713,7 +713,7 @@ task: "multicast_reconciler"
713713
configured period: every <REDACTED_DURATION>m
714714
last completed activation: <REDACTED ITERATIONS>, triggered by <TRIGGERED_BY_REDACTED>
715715
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
716-
warning: unknown background task: "multicast_reconciler" (don't know how to interpret details: Object {"disabled": Bool(false), "errors": Array [], "groups_created": Number(0), "groups_deleted": Number(0), "groups_verified": Number(0), "members_deleted": Number(0), "members_processed": Number(0)})
716+
warning: unknown background task: "multicast_reconciler" (don't know how to interpret details: Object {"disabled": Bool(false), "empty_groups_marked": Number(0), "errors": Array [], "groups_created": Number(0), "groups_deleted": Number(0), "groups_verified": Number(0), "members_deleted": Number(0), "members_processed": Number(0)})
717717

718718
task: "phantom_disks"
719719
configured period: every <REDACTED_DURATION>s
@@ -1281,7 +1281,7 @@ task: "multicast_reconciler"
12811281
configured period: every <REDACTED_DURATION>m
12821282
last completed activation: <REDACTED ITERATIONS>, triggered by <TRIGGERED_BY_REDACTED>
12831283
started at <REDACTED_TIMESTAMP> (<REDACTED DURATION>s ago) and ran for <REDACTED DURATION>ms
1284-
warning: unknown background task: "multicast_reconciler" (don't know how to interpret details: Object {"disabled": Bool(false), "errors": Array [], "groups_created": Number(0), "groups_deleted": Number(0), "groups_verified": Number(0), "members_deleted": Number(0), "members_processed": Number(0)})
1284+
warning: unknown background task: "multicast_reconciler" (don't know how to interpret details: Object {"disabled": Bool(false), "empty_groups_marked": Number(0), "errors": Array [], "groups_created": Number(0), "groups_deleted": Number(0), "groups_verified": Number(0), "members_deleted": Number(0), "members_processed": Number(0)})
12851285

12861286
task: "phantom_disks"
12871287
configured period: every <REDACTED_DURATION>s

illumos-utils/src/opte/port_manager.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ struct RouteSet {
9191
pub struct MulticastGroupCfg {
9292
/// The multicast group IP address (IPv4 or IPv6).
9393
pub group_ip: IpAddr,
94-
/// For Source-Specific Multicast (SSM), list of source addresses.
94+
/// Source addresses for source-filtered multicast (optional for ASM,
95+
/// required for SSM).
9596
pub sources: Vec<IpAddr>,
9697
}
9798

@@ -752,7 +753,7 @@ impl PortManager {
752753
///
753754
/// TODO: Once OPTE kernel module supports multicast group APIs, this
754755
/// method should be updated to configure OPTE port-level multicast
755-
/// group membership. Note: multicast groups are fleet-wide and can span
756+
/// group membership. Note: multicast groups are fleet-scoped and can span
756757
/// across VPCs.
757758
pub fn multicast_groups_ensure(
758759
&self,

nexus-config/src/nexus_config.rs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ use anyhow::anyhow;
1010
use camino::{Utf8Path, Utf8PathBuf};
1111
use dropshot::ConfigDropshot;
1212
use dropshot::ConfigLogging;
13-
use ipnet::Ipv6Net;
1413
use nexus_types::deployment::ReconfiguratorConfig;
15-
use omicron_common::address::IPV6_ADMIN_SCOPED_MULTICAST_PREFIX;
1614
use omicron_common::address::Ipv6Subnet;
1715
pub use omicron_common::address::MAX_VPC_IPV4_SUBNET_PREFIX;
1816
pub use omicron_common::address::MIN_VPC_IPV4_SUBNET_PREFIX;
@@ -31,7 +29,6 @@ use serde_with::serde_as;
3129
use std::collections::HashMap;
3230
use std::fmt;
3331
use std::net::IpAddr;
34-
use std::net::Ipv6Addr;
3532
use std::net::SocketAddr;
3633
use std::time::Duration;
3734
use uuid::Uuid;
@@ -944,15 +941,6 @@ impl Default for FmTasksConfig {
944941
}
945942
}
946943

947-
/// Fixed underlay admin-scoped IPv6 multicast network (ff04::/64) used for
948-
/// internal multicast group allocation and external→underlay mapping.
949-
/// This /64 subnet within the admin-scoped space provides 2^64 host addresses
950-
/// (ample for collision resistance) and is not configurable.
951-
pub const DEFAULT_UNDERLAY_MULTICAST_NET: Ipv6Net = Ipv6Net::new_assert(
952-
Ipv6Addr::new(IPV6_ADMIN_SCOPED_MULTICAST_PREFIX, 0, 0, 0, 0, 0, 0, 0),
953-
64,
954-
);
955-
956944
/// Configuration for multicast options.
957945
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
958946
pub struct MulticastConfig {

0 commit comments

Comments
 (0)