Skip to content

Commit a605ab1

Browse files
[feature] RFC-compliant multicast MAC derivation
Implements multicast MAC address derivation from IP addresses following RFC standards: - IPv4: RFC 1112 Section 6.4 (01:00:5e prefix + 23 bits) - IPv6: RFC 2464 Section 7 (33:33 prefix + 32 bits) Features: - Core trait returns [u8; 6] for dependency-light usage - Optional macaddr feature provides MacAddr6 integration - Support for IpAddr, Ipv4Addr, Ipv6Addr, and IpNetwork types - Comprehensive tests with RFC compliance verification We need this in both dendrite (testing) and omicron. It felt proper to place this here over omicron common (for example).
1 parent d76a75a commit a605ab1

File tree

5 files changed

+179
-2
lines changed

5 files changed

+179
-2
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## [0.2.0] - 2025-07-18
4+
5+
* Adds `MulticastMac` trait for deriving multicast MAC addresses from IP addresses
6+
* Implements RFC 1112 (IPv4) and RFC 2464 (IPv6) multicast MAC derivation
7+
* Optional `macaddr` feature for `MacAddr6` integration
8+
39
## [0.1.2] - 2025-05-25
410

511
* Bumps Rust min-version to 1.84 for direct `is_unique_local` call on IPv6

Cargo.lock

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "oxnet"
3-
version = "0.1.2"
3+
version = "0.2.0"
44
edition = "2021"
55
rust-version = "1.84.0"
66
license = "MIT OR Apache-2.0"
@@ -14,6 +14,7 @@ categories = ["network-programming", "web-programming"]
1414
[features]
1515
default = ["serde", "schemars", "ipnetwork"]
1616
ipnetwork = ["dep:ipnetwork"]
17+
macaddr = ["dep:macaddr"]
1718
schemars = ["dep:schemars", "dep:serde_json"]
1819
serde = ["dep:serde"]
1920
std = []
@@ -23,6 +24,7 @@ schemars = {version = "0.8.22", optional = true }
2324
serde = { version = "1.0.219", optional = true }
2425
serde_json = { version = "1.0.140", optional = true }
2526
ipnetwork = { version = "0.21.1", optional = true }
27+
macaddr = { version = "1.0", optional = true }
2628

2729
[dev-dependencies]
2830
expectorate = "1.2.0"

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
#![doc = include_str!("../README.md")]
66

77
mod ipnet;
8+
mod multicast;
89
#[cfg(feature = "schemars")]
910
mod schema_util;
1011

1112
pub use ipnet::{
1213
IpNet, IpNetParseError, IpNetPrefixError, Ipv4Net, Ipv6Net, IPV4_NET_WIDTH_MAX,
1314
IPV6_NET_WIDTH_MAX,
1415
};
16+
pub use multicast::MulticastMac;

src/multicast.rs

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Copyright 2025 Oxide Computer Company
2+
3+
//! Multicast MAC address derivation from IP addresses.
4+
//!
5+
//! This module provides functions for deriving multicast MAC addresses from
6+
//! IPv4 and IPv6 multicast IP addresses according to RFC standards:
7+
//!
8+
//! - IPv4: [RFC 1112 Section 6.4][rfc1112]
9+
//! - IPv6: [RFC 2464 Section 7][rfc2464]
10+
//!
11+
//! [rfc1112]: https://datatracker.ietf.org/doc/html/rfc1112#section-6.4
12+
//! [rfc2464]: https://datatracker.ietf.org/doc/html/rfc2464#section-7
13+
14+
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
15+
16+
/// Trait for deriving multicast MAC addresses from IP addresses.
17+
pub trait MulticastMac {
18+
/// Derive the multicast MAC address from this IP address as a byte array.
19+
///
20+
/// For IPv4 addresses, follows [RFC 1112 Section 6.4][rfc1112]: places the low-order
21+
/// 23 bits of the IP address into the low-order 23 bits of the Ethernet
22+
/// address 01-00-5E-00-00-00.
23+
///
24+
/// For IPv6 addresses, follows [RFC 2464 Section 7][rfc2464]: places the low-order
25+
/// 32 bits of the IP address into the low-order 32 bits of the Ethernet
26+
/// address 33-33-00-00-00-00.
27+
///
28+
/// [rfc1112]: https://datatracker.ietf.org/doc/html/rfc1112#section-6.4
29+
/// [rfc2464]: https://datatracker.ietf.org/doc/html/rfc2464#section-7
30+
fn derive_multicast_mac(&self) -> [u8; 6];
31+
32+
/// Derive the multicast MAC address from this IP address as a `macaddr::MacAddr6`.
33+
///
34+
/// This is a convenience method that converts the byte array result to the
35+
/// `macaddr` crate's `MacAddr6` type.
36+
#[cfg(feature = "macaddr")]
37+
fn derive_multicast_mac_addr(&self) -> macaddr::MacAddr6 {
38+
macaddr::MacAddr6::from(self.derive_multicast_mac())
39+
}
40+
}
41+
42+
impl MulticastMac for IpAddr {
43+
fn derive_multicast_mac(&self) -> [u8; 6] {
44+
match self {
45+
IpAddr::V4(ipv4) => ipv4.derive_multicast_mac(),
46+
IpAddr::V6(ipv6) => ipv6.derive_multicast_mac(),
47+
}
48+
}
49+
}
50+
51+
impl MulticastMac for Ipv4Addr {
52+
fn derive_multicast_mac(&self) -> [u8; 6] {
53+
let octets = self.octets();
54+
// Take the last 23 bits of the IPv4 address (mask the high bit of the second octet)
55+
[
56+
0x01,
57+
0x00,
58+
0x5e,
59+
octets[1] & 0x7f, // Clear the high bit to get only 23 bits total
60+
octets[2],
61+
octets[3],
62+
]
63+
}
64+
}
65+
66+
impl MulticastMac for Ipv6Addr {
67+
fn derive_multicast_mac(&self) -> [u8; 6] {
68+
let octets = self.octets();
69+
// Take the last 4 bytes (32 bits) of the IPv6 address
70+
[0x33, 0x33, octets[12], octets[13], octets[14], octets[15]]
71+
}
72+
}
73+
74+
#[cfg(feature = "ipnetwork")]
75+
impl MulticastMac for ipnetwork::IpNetwork {
76+
fn derive_multicast_mac(&self) -> [u8; 6] {
77+
self.ip().derive_multicast_mac()
78+
}
79+
}
80+
81+
#[cfg(test)]
82+
mod tests {
83+
use super::*;
84+
85+
#[test]
86+
fn test_derive_multicast_mac_ipv4() {
87+
let ipv4_addr = Ipv4Addr::new(224, 1, 1, 1);
88+
let mac = ipv4_addr.derive_multicast_mac();
89+
let expected = [0x01, 0x00, 0x5e, 0x01, 0x01, 0x01];
90+
assert_eq!(mac, expected);
91+
92+
// Test edge case with high bit set in second octet
93+
let ipv4_addr = Ipv4Addr::new(224, 129, 1, 1); // 0x81 in second octet
94+
let mac = ipv4_addr.derive_multicast_mac();
95+
let expected = [0x01, 0x00, 0x5e, 0x01, 0x01, 0x01]; // High bit masked off
96+
assert_eq!(mac, expected);
97+
}
98+
99+
#[test]
100+
fn test_derive_multicast_mac_ipv6() {
101+
let ipv6_addr = Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 0x0001);
102+
let mac = ipv6_addr.derive_multicast_mac();
103+
let expected = [0x33, 0x33, 0x00, 0x00, 0x00, 0x01];
104+
assert_eq!(mac, expected);
105+
}
106+
107+
#[test]
108+
fn test_derive_multicast_mac_generic() {
109+
// Test IPv4
110+
let ipv4_addr = IpAddr::V4(Ipv4Addr::new(224, 1, 1, 1));
111+
let mac = ipv4_addr.derive_multicast_mac();
112+
let expected = [0x01, 0x00, 0x5e, 0x01, 0x01, 0x01];
113+
assert_eq!(mac, expected);
114+
115+
// Test IPv6
116+
let ipv6_addr = IpAddr::V6(Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 0x0001));
117+
let mac = ipv6_addr.derive_multicast_mac();
118+
let expected = [0x33, 0x33, 0x00, 0x00, 0x00, 0x01];
119+
assert_eq!(mac, expected);
120+
}
121+
122+
#[cfg(feature = "ipnetwork")]
123+
#[test]
124+
fn test_derive_multicast_mac_ipnetwork() {
125+
use ipnetwork::IpNetwork;
126+
use std::str::FromStr;
127+
128+
// Test IPv4 network
129+
let ipv4_net = IpNetwork::from_str("224.1.1.1/32").unwrap();
130+
let mac = ipv4_net.derive_multicast_mac();
131+
let expected = [0x01, 0x00, 0x5e, 0x01, 0x01, 0x01];
132+
assert_eq!(mac, expected);
133+
134+
// Test IPv6 network
135+
let ipv6_net = IpNetwork::from_str("ff02::1/128").unwrap();
136+
let mac = ipv6_net.derive_multicast_mac();
137+
let expected = [0x33, 0x33, 0x00, 0x00, 0x00, 0x01];
138+
assert_eq!(mac, expected);
139+
}
140+
141+
#[cfg(feature = "macaddr")]
142+
#[test]
143+
fn test_derive_multicast_mac_addr() {
144+
let ipv4_addr = Ipv4Addr::new(224, 1, 1, 1);
145+
let mac_addr = ipv4_addr.derive_multicast_mac_addr();
146+
let expected = macaddr::MacAddr6::new(0x01, 0x00, 0x5e, 0x01, 0x01, 0x01);
147+
assert_eq!(mac_addr, expected);
148+
149+
let ipv6_addr = Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 0x0001);
150+
let mac_addr = ipv6_addr.derive_multicast_mac_addr();
151+
let expected = macaddr::MacAddr6::new(0x33, 0x33, 0x00, 0x00, 0x00, 0x01);
152+
assert_eq!(mac_addr, expected);
153+
154+
// Test with IpAddr enum
155+
let ip = IpAddr::V4(Ipv4Addr::new(224, 1, 1, 1));
156+
let mac_addr = ip.derive_multicast_mac_addr();
157+
let expected = macaddr::MacAddr6::new(0x01, 0x00, 0x5e, 0x01, 0x01, 0x01);
158+
assert_eq!(mac_addr, expected);
159+
}
160+
}

0 commit comments

Comments
 (0)