Skip to content

[feature] RFC-compliant multicast MAC derivation #57

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [0.1.3] - 2025-07-18

* Adds `MulticastMac` trait for deriving multicast MAC addresses from IP addresses
* Implements RFC 1112 (IPv4) and RFC 2464 (IPv6) multicast MAC derivation
* Optional `macaddr` feature for `MacAddr6` integration

## [0.1.2] - 2025-05-25

* Bumps Rust min-version to 1.84 for direct `is_unique_local` call on IPv6
Expand All @@ -19,6 +25,7 @@

Initial release.

[0.1.3]: https://github.com/oxidecomputer/oxnet/releases/oxnet-0.1.3
[0.1.2]: https://github.com/oxidecomputer/oxnet/releases/oxnet-0.1.2
[0.1.1]: https://github.com/oxidecomputer/oxnet/releases/oxnet-0.1.1
[0.1.0]: https://github.com/oxidecomputer/oxnet/releases/oxnet-0.1.0
9 changes: 8 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "oxnet"
version = "0.1.2"
version = "0.1.3"
edition = "2021"
rust-version = "1.84.0"
license = "MIT OR Apache-2.0"
Expand All @@ -14,6 +14,7 @@ categories = ["network-programming", "web-programming"]
[features]
default = ["serde", "schemars", "ipnetwork"]
ipnetwork = ["dep:ipnetwork"]
macaddr = ["dep:macaddr"]
schemars = ["dep:schemars", "dep:serde_json"]
serde = ["dep:serde"]
std = []
Expand All @@ -23,6 +24,7 @@ schemars = {version = "0.8.22", optional = true }
serde = { version = "1.0.219", optional = true }
serde_json = { version = "1.0.140", optional = true }
ipnetwork = { version = "0.21.1", optional = true }
macaddr = { version = "1.0.1", optional = true }

[dev-dependencies]
expectorate = "1.2.0"
Expand Down
10 changes: 5 additions & 5 deletions src/ipnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ impl std::fmt::Display for IpNetParseError {
match self {
IpNetParseError::InvalidAddr(e) => e.fmt(f),
IpNetParseError::PrefixValue(e) => {
write!(f, "invalid prefix value: {}", e)
write!(f, "invalid prefix value: {e}")
}
IpNetParseError::NoPrefix => write!(f, "missing '/' character"),
IpNetParseError::InvalidPrefix(e) => e.fmt(f),
Expand Down Expand Up @@ -238,8 +238,8 @@ impl From<Ipv6Net> for IpNet {
impl std::fmt::Display for IpNet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IpNet::V4(inner) => write!(f, "{}", inner),
IpNet::V6(inner) => write!(f, "{}", inner),
IpNet::V4(inner) => write!(f, "{inner}"),
IpNet::V6(inner) => write!(f, "{inner}"),
}
}
}
Expand Down Expand Up @@ -519,7 +519,7 @@ impl serde::Serialize for Ipv4Net {
where
S: serde::Serializer,
{
serializer.serialize_str(&format!("{}", self))
serializer.serialize_str(&format!("{self}"))
}
}

Expand Down Expand Up @@ -771,7 +771,7 @@ impl serde::Serialize for Ipv6Net {
where
S: serde::Serializer,
{
serializer.serialize_str(&format!("{}", self))
serializer.serialize_str(&format!("{self}"))
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
#![doc = include_str!("../README.md")]

mod ipnet;
mod multicast;
#[cfg(feature = "schemars")]
mod schema_util;

pub use ipnet::{
IpNet, IpNetParseError, IpNetPrefixError, Ipv4Net, Ipv6Net, IPV4_NET_WIDTH_MAX,
IPV6_NET_WIDTH_MAX,
};
pub use multicast::MulticastMac;
179 changes: 179 additions & 0 deletions src/multicast.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// Copyright 2025 Oxide Computer Company

//! Multicast MAC address derivation from IP addresses.

use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

use crate::IpNet;

/// Trait for deriving multicast MAC addresses from IP addresses.
pub trait MulticastMac {
/// Derive the multicast MAC address from this IP address as a byte array.
///
/// For IPv4 addresses, follows [RFC 1112 Section 6.4][rfc1112]: places the low-order
/// 23 bits of the IP address into the low-order 23 bits of the Ethernet
/// address 01-00-5E-00-00-00.
///
/// For IPv6 addresses, follows [RFC 2464 Section 7][rfc2464]: places the low-order
/// 32 bits of the IP address into the low-order 32 bits of the Ethernet
/// address 33-33-00-00-00-00.
///
/// [rfc1112]: https://datatracker.ietf.org/doc/html/rfc1112#section-6.4
/// [rfc2464]: https://datatracker.ietf.org/doc/html/rfc2464#section-7
fn derive_multicast_mac(&self) -> [u8; 6];

/// Derive the multicast MAC address from this IP address as a `macaddr::MacAddr6`.
///
/// This is a convenience method that converts the byte array result to the
/// `macaddr` crate's `MacAddr6` type.
#[cfg(feature = "macaddr")]
fn derive_multicast_mac_addr(&self) -> macaddr::MacAddr6 {
macaddr::MacAddr6::from(self.derive_multicast_mac())
}
}

impl MulticastMac for IpAddr {
fn derive_multicast_mac(&self) -> [u8; 6] {
match self {
IpAddr::V4(ipv4) => ipv4.derive_multicast_mac(),
IpAddr::V6(ipv6) => ipv6.derive_multicast_mac(),
}
}
}

impl MulticastMac for Ipv4Addr {
fn derive_multicast_mac(&self) -> [u8; 6] {
let octets = self.octets();
// Take the last 23 bits of the IPv4 address (mask the high bit of the second octet)
[
0x01,
0x00,
0x5e,
octets[1] & 0x7f, // Clear the high bit to get only 23 bits total
octets[2],
octets[3],
]
}
}

impl MulticastMac for Ipv6Addr {
fn derive_multicast_mac(&self) -> [u8; 6] {
let octets = self.octets();
// Take the last 4 bytes (32 bits) of the IPv6 address
[0x33, 0x33, octets[12], octets[13], octets[14], octets[15]]
}
}

impl MulticastMac for IpNet {
fn derive_multicast_mac(&self) -> [u8; 6] {
match self {
IpNet::V4(ipv4_net) => ipv4_net.addr().derive_multicast_mac(),
IpNet::V6(ipv6_net) => ipv6_net.addr().derive_multicast_mac(),
}
}
}

#[cfg(feature = "ipnetwork")]
impl MulticastMac for ipnetwork::IpNetwork {
fn derive_multicast_mac(&self) -> [u8; 6] {
self.ip().derive_multicast_mac()
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_derive_multicast_mac_ipv4() {
let ipv4_addr = Ipv4Addr::new(224, 1, 1, 1);
let mac = ipv4_addr.derive_multicast_mac();
let expected = [0x01, 0x00, 0x5e, 0x01, 0x01, 0x01];
assert_eq!(mac, expected);

// Test edge case with high bit set in second octet
let ipv4_addr = Ipv4Addr::new(224, 129, 1, 1); // 0x81 in second octet
let mac = ipv4_addr.derive_multicast_mac();
let expected = [0x01, 0x00, 0x5e, 0x01, 0x01, 0x01]; // High bit masked off
assert_eq!(mac, expected);
}

#[test]
fn test_derive_multicast_mac_ipv6() {
let ipv6_addr = Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 0x0001);
let mac = ipv6_addr.derive_multicast_mac();
let expected = [0x33, 0x33, 0x00, 0x00, 0x00, 0x01];
assert_eq!(mac, expected);
}

#[test]
fn test_derive_multicast_mac_generic() {
// Test IPv4
let ipv4_addr = IpAddr::V4(Ipv4Addr::new(224, 1, 1, 1));
let mac = ipv4_addr.derive_multicast_mac();
let expected = [0x01, 0x00, 0x5e, 0x01, 0x01, 0x01];
assert_eq!(mac, expected);

// Test IPv6
let ipv6_addr = IpAddr::V6(Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 0x0001));
let mac = ipv6_addr.derive_multicast_mac();
let expected = [0x33, 0x33, 0x00, 0x00, 0x00, 0x01];
assert_eq!(mac, expected);
}

#[cfg(feature = "ipnetwork")]
#[test]
fn test_derive_multicast_mac_ipnetwork() {
use ipnetwork::IpNetwork;
use std::str::FromStr;

// Test IPv4 network
let ipv4_net = IpNetwork::from_str("224.1.1.1/32").unwrap();
let mac = ipv4_net.derive_multicast_mac();
let expected = [0x01, 0x00, 0x5e, 0x01, 0x01, 0x01];
assert_eq!(mac, expected);

// Test IPv6 network
let ipv6_net = IpNetwork::from_str("ff02::1/128").unwrap();
let mac = ipv6_net.derive_multicast_mac();
let expected = [0x33, 0x33, 0x00, 0x00, 0x00, 0x01];
assert_eq!(mac, expected);
}

#[test]
fn test_derive_multicast_mac_ipnet() {
use std::str::FromStr;

// Test IPv4 network
let ipv4_net = IpNet::from_str("224.1.1.1/32").unwrap();
let mac = ipv4_net.derive_multicast_mac();
let expected = [0x01, 0x00, 0x5e, 0x01, 0x01, 0x01];
assert_eq!(mac, expected);

// Test IPv6 network
let ipv6_net = IpNet::from_str("ff02::1/128").unwrap();
let mac = ipv6_net.derive_multicast_mac();
let expected = [0x33, 0x33, 0x00, 0x00, 0x00, 0x01];
assert_eq!(mac, expected);
}

#[cfg(feature = "macaddr")]
#[test]
fn test_derive_multicast_mac_addr() {
let ipv4_addr = Ipv4Addr::new(224, 1, 1, 1);
let mac_addr = ipv4_addr.derive_multicast_mac_addr();
let expected = macaddr::MacAddr6::new(0x01, 0x00, 0x5e, 0x01, 0x01, 0x01);
assert_eq!(mac_addr, expected);

let ipv6_addr = Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 0x0001);
let mac_addr = ipv6_addr.derive_multicast_mac_addr();
let expected = macaddr::MacAddr6::new(0x33, 0x33, 0x00, 0x00, 0x00, 0x01);
assert_eq!(mac_addr, expected);

// Test with IpAddr enum
let ip = IpAddr::V4(Ipv4Addr::new(224, 1, 1, 1));
let mac_addr = ip.derive_multicast_mac_addr();
let expected = macaddr::MacAddr6::new(0x01, 0x00, 0x5e, 0x01, 0x01, 0x01);
assert_eq!(mac_addr, expected);
}
}
Loading