diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bd4fb8..85fb38b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 2cfea4e..8d85db0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -155,6 +155,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +[[package]] +name = "macaddr" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baee0bbc17ce759db233beb01648088061bf678383130602a298e6998eedb2d8" + [[package]] name = "memchr" version = "2.7.2" @@ -178,10 +184,11 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "oxnet" -version = "0.1.2" +version = "0.1.3" dependencies = [ "expectorate", "ipnetwork", + "macaddr", "regress", "schemars", "serde", diff --git a/Cargo.toml b/Cargo.toml index a431fd8..214ecfa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" @@ -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 = [] @@ -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" diff --git a/src/ipnet.rs b/src/ipnet.rs index 5f3059c..689b13a 100644 --- a/src/ipnet.rs +++ b/src/ipnet.rs @@ -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), @@ -238,8 +238,8 @@ impl From 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}"), } } } @@ -519,7 +519,7 @@ impl serde::Serialize for Ipv4Net { where S: serde::Serializer, { - serializer.serialize_str(&format!("{}", self)) + serializer.serialize_str(&format!("{self}")) } } @@ -771,7 +771,7 @@ impl serde::Serialize for Ipv6Net { where S: serde::Serializer, { - serializer.serialize_str(&format!("{}", self)) + serializer.serialize_str(&format!("{self}")) } } diff --git a/src/lib.rs b/src/lib.rs index ac6150e..1b2d24c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ #![doc = include_str!("../README.md")] mod ipnet; +mod multicast; #[cfg(feature = "schemars")] mod schema_util; @@ -12,3 +13,4 @@ pub use ipnet::{ IpNet, IpNetParseError, IpNetPrefixError, Ipv4Net, Ipv6Net, IPV4_NET_WIDTH_MAX, IPV6_NET_WIDTH_MAX, }; +pub use multicast::MulticastMac; diff --git a/src/multicast.rs b/src/multicast.rs new file mode 100644 index 0000000..efeb258 --- /dev/null +++ b/src/multicast.rs @@ -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); + } +}