|
22 | 22 | //! [RFC 7346]: https://www.rfc-editor.org/rfc/rfc7346 |
23 | 23 |
|
24 | 24 | use oxnet::{Ipv4Net, Ipv6Net}; |
25 | | -use std::net::{Ipv4Addr, Ipv6Addr}; |
| 25 | +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; |
26 | 26 |
|
27 | 27 | // TODO: Consolidate these constants and the `omicron_common::address` |
28 | 28 | // originals into `oxnet`, the cycle-free leaf crate that maghemite, dendrite, |
29 | 29 | // and omicron already share, so the duplication can be removed. |
30 | 30 |
|
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. |
32 | 32 | pub const IPV4_SSM_SUBNET: Ipv4Net = |
33 | 33 | Ipv4Net::new_unchecked(Ipv4Addr::new(232, 0, 0, 0), 8); |
34 | 34 |
|
35 | | -/// IPv6 Source-Specific Multicast (SSM) subnet. |
| 35 | +/// Reserved IPv4 SSM subnet (232.0.0.0/24). |
36 | 36 | /// |
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 | +} |
45 | 94 |
|
46 | 95 | /// IPv4 multicast address range (224.0.0.0/4) per RFC 5771. |
47 | 96 | pub const IPV4_MULTICAST_RANGE: Ipv4Net = |
@@ -111,7 +160,10 @@ mod tests { |
111 | 160 | #[test] |
112 | 161 | fn constants_match_canonical_values() { |
113 | 162 | 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. |
115 | 167 | assert_eq!(IPV4_MULTICAST_RANGE, canonical::IPV4_MULTICAST_RANGE); |
116 | 168 | assert_eq!( |
117 | 169 | IPV4_LINK_LOCAL_MULTICAST_SUBNET, |
|
0 commit comments