From dcca22ecdd61d2535d8e04f9b90c510d99cb653a Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Fri, 2 Oct 2026 03:31:59 +0000 Subject: [PATCH] Show an Ethernet peer address as a MAC whatever its bytes decode as MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A transport address printed its bytes as text whenever they were valid UTF-8, and as a colon-separated MAC only when they were not. A MAC is six arbitrary bytes, so some Ethernet peers showed up in `fipsctl show links` and in log lines as garbled text such as "2|ѬZd" instead of 32:7c:d1:ac:5a:64. An address built from a MAC now records that it is one, and always displays as a MAC. The Ethernet receive path, the beacon buffer and configured-peer resolution build their addresses that way. Equality and hashing still compare the bytes only, so lookups and comparisons behave as before, and every other transport's addresses display as they did. --- src/node/mod.rs | 2 +- src/transport/ethernet/mod.rs | 2 +- src/transport/ethernet/neighbor.rs | 13 ++++- src/transport/mod.rs | 25 +++++++- src/transport/types.rs | 93 +++++++++++++++++++++--------- 5 files changed, 104 insertions(+), 31 deletions(-) diff --git a/src/node/mod.rs b/src/node/mod.rs index 571c035f..c53d83cc 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -1328,7 +1328,7 @@ impl Node { NodeError::NoTransportForType(format!("invalid MAC in '{}': {}", addr_str, e)) })?; - Ok((transport_id, TransportAddr::from_bytes(&mac))) + Ok((transport_id, TransportAddr::from_mac(mac))) } #[cfg(not(any(target_os = "linux", target_os = "macos")))] { diff --git a/src/transport/ethernet/mod.rs b/src/transport/ethernet/mod.rs index a1531c65..056125f4 100644 --- a/src/transport/ethernet/mod.rs +++ b/src/transport/ethernet/mod.rs @@ -465,7 +465,7 @@ async fn ethernet_receive_loop( } }; let bytes = data.len(); - let addr = TransportAddr::from_bytes(&src_mac); + let addr = TransportAddr::from_mac(src_mac); let packet = ReceivedPacket::new(transport_id, addr, data); trace!( diff --git a/src/transport/ethernet/neighbor.rs b/src/transport/ethernet/neighbor.rs index 1c543bf0..9bb73d8d 100644 --- a/src/transport/ethernet/neighbor.rs +++ b/src/transport/ethernet/neighbor.rs @@ -144,7 +144,7 @@ impl NeighborBuffer { /// Build the buffered peer record for one beacon. fn peer(&self, src_mac: [u8; 6], pubkey: XOnlyPublicKey) -> DiscoveredPeer { - let addr = TransportAddr::from_bytes(&src_mac); + let addr = TransportAddr::from_mac(src_mac); DiscoveredPeer::with_hint(self.transport_id, addr, pubkey) } } @@ -234,6 +234,17 @@ mod tests { assert!(peers.is_empty()); } + #[test] + fn a_discovered_peer_whose_mac_bytes_are_valid_utf8_still_displays_as_a_mac() { + // "2|\u{46c}Zd": six bytes that decode as UTF-8, seen on a veth MAC. + let mac = [0x32, 0x7c, 0xd1, 0xac, 0x5a, 0x64]; + assert!(core::str::from_utf8(&mac).is_ok()); + let buffer = NeighborBuffer::new(TransportId::new(1)); + assert!(buffer.add_peer(mac, test_pubkey())); + let peers = buffer.take(); + assert_eq!(peers[0].addr.to_string(), "32:7c:d1:ac:5a:64"); + } + #[test] fn test_neighbor_buffer_dedup() { let buffer = NeighborBuffer::new(TransportId::new(1)); diff --git a/src/transport/mod.rs b/src/transport/mod.rs index bc839500..79f55fe8 100644 --- a/src/transport/mod.rs +++ b/src/transport/mod.rs @@ -1182,12 +1182,33 @@ mod tests { #[test] fn test_transport_addr_mac_display() { - // Raw 6-byte MACs (as Ethernet stores via from_bytes) display in - // standard colon-separated notation, not bare hex. + // Raw 6-byte non-UTF-8 values from from_bytes display in standard + // colon-separated notation, not bare hex. let mac = TransportAddr::from_bytes(&[0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]); assert_eq!(format!("{}", mac), "aa:bb:cc:dd:ee:ff"); } + #[test] + fn a_mac_address_whose_bytes_are_valid_utf8_displays_as_a_mac() { + let bytes = [0x32, 0x7c, 0xd1, 0xac, 0x5a, 0x64]; + assert!(core::str::from_utf8(&bytes).is_ok()); + let mac = TransportAddr::from_mac(bytes); + assert_eq!(mac.to_string(), "32:7c:d1:ac:5a:64"); + assert_eq!(format!("{:?}", mac), "TransportAddr(32:7c:d1:ac:5a:64)"); + assert_eq!(mac.as_bytes(), &bytes); + } + + #[test] + fn a_mac_address_equals_and_hashes_as_its_bytes() { + use std::hash::BuildHasher; + let bytes = [0x32, 0x7c, 0xd1, 0xac, 0x5a, 0x64]; + let mac = TransportAddr::from_mac(bytes); + let raw = TransportAddr::from_bytes(&bytes); + assert_eq!(mac, raw); + let hasher = std::collections::hash_map::RandomState::new(); + assert_eq!(hasher.hash_one(&mac), hasher.hash_one(&raw)); + } + #[test] fn test_transport_addr_non_mac_binary_is_bare_hex() { // Non-6-byte non-UTF-8 payloads stay bare hex (no separators). diff --git a/src/transport/types.rs b/src/transport/types.rs index 4e95f4ce..2dac9fd3 100644 --- a/src/transport/types.rs +++ b/src/transport/types.rs @@ -90,75 +90,116 @@ impl fmt::Display for LinkDirection { /// /// Each transport type interprets this differently: /// - UDP/TCP: "host:port" (IP address or DNS hostname) -/// - Ethernet: MAC address (6 bytes) -#[derive(Clone, PartialEq, Eq, Hash)] -pub struct TransportAddr(Vec); +/// - Ethernet: MAC address (6 bytes), built with [`TransportAddr::from_mac`] +/// so it always displays as a MAC, whatever its bytes decode as +#[derive(Clone)] +pub struct TransportAddr { + bytes: Vec, + /// Set only by [`TransportAddr::from_mac`]: the bytes are a MAC address + /// and always display as one. Not part of equality or hashing. + mac: bool, +} + +impl PartialEq for TransportAddr { + fn eq(&self, other: &Self) -> bool { + self.bytes == other.bytes + } +} + +impl Eq for TransportAddr {} + +impl core::hash::Hash for TransportAddr { + fn hash(&self, state: &mut H) { + self.bytes.hash(state); + } +} impl TransportAddr { /// Create a transport address from raw bytes. pub fn new(bytes: Vec) -> Self { - Self(bytes) + Self { bytes, mac: false } } /// Create a transport address from a byte slice. pub fn from_bytes(bytes: &[u8]) -> Self { - Self(bytes.to_vec()) + Self::new(bytes.to_vec()) + } + + /// Create an Ethernet transport address from a MAC address. + pub fn from_mac(mac: [u8; 6]) -> Self { + Self { + bytes: mac.to_vec(), + mac: true, + } } /// Create a transport address from a string. pub fn from_string(s: &str) -> Self { - Self(s.as_bytes().to_vec()) + Self::new(s.as_bytes().to_vec()) } /// Get the raw bytes. pub fn as_bytes(&self) -> &[u8] { - &self.0 + &self.bytes } /// Try to interpret as a UTF-8 string. pub fn as_str(&self) -> Option<&str> { - core::str::from_utf8(&self.0).ok() + core::str::from_utf8(&self.bytes).ok() } /// Get the length in bytes. pub fn len(&self) -> usize { - self.0.len() + self.bytes.len() } /// Check if empty. pub fn is_empty(&self) -> bool { - self.0.is_empty() + self.bytes.is_empty() + } +} + +impl TransportAddr { + /// Write the bytes as a colon-separated MAC address. + fn write_mac(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (i, byte) in self.bytes.iter().enumerate() { + if i > 0 { + write!(f, ":")?; + } + write!(f, "{:02x}", byte)?; + } + Ok(()) } } impl fmt::Debug for TransportAddr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.mac { + write!(f, "TransportAddr(")?; + self.write_mac(f)?; + return write!(f, ")"); + } match self.as_str() { Some(s) => write!(f, "TransportAddr(\"{}\")", s), - None => write!(f, "TransportAddr({:?})", self.0), + None => write!(f, "TransportAddr({:?})", self.bytes), } } } impl fmt::Display for TransportAddr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Best-effort display as string if valid UTF-8. Otherwise render a - // 6-byte payload as a colon-separated MAC (standard Unix notation, - // matching BLE addrs, `ip link`/`ip neigh`, and packet logs), and - // any other non-UTF-8 byte string as bare hex. + // An Ethernet address is a MAC whatever its bytes decode as. Any + // other address displays as a string if it is valid UTF-8; otherwise + // a 6-byte payload is rendered as a colon-separated MAC and any other + // byte string as bare hex. + if self.mac { + return self.write_mac(f); + } match self.as_str() { Some(s) => write!(f, "{}", s), - None if self.0.len() == 6 => { - for (i, byte) in self.0.iter().enumerate() { - if i > 0 { - write!(f, ":")?; - } - write!(f, "{:02x}", byte)?; - } - Ok(()) - } + None if self.bytes.len() == 6 => self.write_mac(f), None => { - for byte in &self.0 { + for byte in &self.bytes { write!(f, "{:02x}", byte)?; } Ok(()) @@ -175,7 +216,7 @@ impl From<&str> for TransportAddr { impl From for TransportAddr { fn from(s: String) -> Self { - Self(s.into_bytes()) + Self::new(s.into_bytes()) } }