Skip to content

Commit 8991e13

Browse files
[api-types] align multicast validation with dendrite/omicron policy
Here, we match the canonical destination policy shared by dendrite's DPD validator and thundermuffin's SSM join validation per RFC 4607 and RFC 7346, including: - replacing the ff30::/12 IPV6_SSM_SUBNET in client-common with per-scope ff3x::/32 blocks and an `is_ssm_address` helper, mirroring the omicron constants. RFC 3306 unicast-prefix-based addresses with a nonzero network prefix are now classified as ASM - rejecting the reserved IPv4 SSM subnet 232.0.0.0/24 (RFC 4607 4.3) - restricting IPv6 scopes to admin-local, site-local, organization-local, and global - rejecting external groups in the reserved underlay multicast subnet (ff04::/64), which Omicron allocates for internal underlay mapping - requiring allocatable IPv6 SSM group IDs
1 parent 2466950 commit 8991e13

6 files changed

Lines changed: 264 additions & 186 deletions

File tree

client-common/src/address.rs

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,26 +22,75 @@
2222
//! [RFC 7346]: https://www.rfc-editor.org/rfc/rfc7346
2323
2424
use oxnet::{Ipv4Net, Ipv6Net};
25-
use std::net::{Ipv4Addr, Ipv6Addr};
25+
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
2626

2727
// TODO: Consolidate these constants and the `omicron_common::address`
2828
// originals into `oxnet`, the cycle-free leaf crate that maghemite, dendrite,
2929
// and omicron already share, so the duplication can be removed.
3030

31-
/// IPv4 Source-Specific Multicast (SSM) subnet (232.0.0.0/8) per RFC 4607 §3.
31+
/// IPv4 Source-Specific Multicast (SSM) subnet (232.0.0.0/8) per RFC 4607 §1.
3232
pub const IPV4_SSM_SUBNET: Ipv4Net =
3333
Ipv4Net::new_unchecked(Ipv4Addr::new(232, 0, 0, 0), 8);
3434

35-
/// IPv6 Source-Specific Multicast (SSM) subnet.
35+
/// Reserved IPv4 SSM subnet (232.0.0.0/24).
3636
///
37-
/// RFC 4607 §3 specifies ff3x::/32, where the `x` nibble is the multicast
38-
/// scope. We use /12 as an implementation convenience matching all per-scope
39-
/// blocks (ff30:: through ff3f:ffff:..:ffff) with a single subnet, since all
40-
/// SSM addresses share the first 12 bits (0xff prefix plus flag field 3).
41-
/// This superset is used only for contains-based classification, not as an
42-
/// allocation boundary.
43-
pub const IPV6_SSM_SUBNET: Ipv6Net =
44-
Ipv6Net::new_unchecked(Ipv6Addr::new(0xff30, 0, 0, 0, 0, 0, 0, 0), 12);
37+
/// RFC 4607 §4.3 reserves 232.0.0.0 (must not be assigned to any
38+
/// application) and notes that IANA holds 232.0.0.1 through 232.0.0.255
39+
/// in reserve, so the entire first /24 is excluded from allocation.
40+
pub const IPV4_SSM_RESERVED_SUBNET: Ipv4Net =
41+
Ipv4Net::new_unchecked(Ipv4Addr::new(232, 0, 0, 0), 24);
42+
43+
const fn ipv6_ssm_subnet(scope: u16) -> Ipv6Net {
44+
Ipv6Net::new_unchecked(
45+
Ipv6Addr::new(0xff30 | scope, 0, 0, 0, 0, 0, 0, 0),
46+
32,
47+
)
48+
}
49+
50+
/// IPv6 Source-Specific Multicast (SSM) subnets, one per scope field value.
51+
///
52+
/// RFC 4607 §1 specifies "ff3x::/32 for each scope x", meaning one /32
53+
/// block per scope (ff30::/32, ff31::/32, ..., ff3f::/32).
54+
///
55+
/// These blocks cannot be represented by one CIDR: the scope nibble precedes
56+
/// the 16 zero bits that complete each /32. In particular, ff3e:1:: is outside
57+
/// ff3e::/32 even though it is inside the broader ff30::/12 prefix.
58+
pub const IPV6_SSM_SUBNETS: [Ipv6Net; 16] = [
59+
ipv6_ssm_subnet(0x0),
60+
ipv6_ssm_subnet(0x1),
61+
ipv6_ssm_subnet(0x2),
62+
ipv6_ssm_subnet(0x3),
63+
ipv6_ssm_subnet(0x4),
64+
ipv6_ssm_subnet(0x5),
65+
ipv6_ssm_subnet(0x6),
66+
ipv6_ssm_subnet(0x7),
67+
ipv6_ssm_subnet(0x8),
68+
ipv6_ssm_subnet(0x9),
69+
ipv6_ssm_subnet(0xa),
70+
ipv6_ssm_subnet(0xb),
71+
ipv6_ssm_subnet(0xc),
72+
ipv6_ssm_subnet(0xd),
73+
ipv6_ssm_subnet(0xe),
74+
ipv6_ssm_subnet(0xf),
75+
];
76+
77+
/// Check if an IP is in the SSM (Source-Specific Multicast) range.
78+
///
79+
/// SSM ranges per RFC 4607 §1:
80+
/// - IPv4: 232.0.0.0/8
81+
/// - IPv6: ff3x::/32 (all SSM scopes)
82+
///
83+
/// The IPv6 check matches the exact per-scope /32 blocks, not ff30::/12.
84+
/// A /12 match would also classify RFC 3306 unicast-prefix-based addresses
85+
/// with a nonzero network prefix as SSM.
86+
pub fn is_ssm_address(ip: IpAddr) -> bool {
87+
match ip {
88+
IpAddr::V4(addr) => IPV4_SSM_SUBNET.contains(addr),
89+
IpAddr::V6(addr) => {
90+
IPV6_SSM_SUBNETS.iter().any(|subnet| subnet.contains(addr))
91+
}
92+
}
93+
}
4594

4695
/// IPv4 multicast address range (224.0.0.0/4) per RFC 5771.
4796
pub const IPV4_MULTICAST_RANGE: Ipv4Net =
@@ -111,7 +160,10 @@ mod tests {
111160
#[test]
112161
fn constants_match_canonical_values() {
113162
assert_eq!(IPV4_SSM_SUBNET, canonical::IPV4_SSM_SUBNET);
114-
assert_eq!(IPV6_SSM_SUBNET, canonical::IPV6_SSM_SUBNET);
163+
// TODO: Compare IPV4_SSM_RESERVED_SUBNET, IPV6_SSM_SUBNETS, and
164+
// is_ssm_address against their canonical originals once the Omicron
165+
// change replacing the ff30::/12 IPV6_SSM_SUBNET with per-scope /32
166+
// blocks lands on main and the pinned revision picks it up.
115167
assert_eq!(IPV4_MULTICAST_RANGE, canonical::IPV4_MULTICAST_RANGE);
116168
assert_eq!(
117169
IPV4_LINK_LOCAL_MULTICAST_SUBNET,

0 commit comments

Comments
 (0)