Show an Ethernet peer address as a MAC whatever its bytes decode as

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.
This commit is contained in:
Johnathan Corgan
2026-10-02 03:31:59 +00:00
parent 21054eeb74
commit dcca22ecdd
5 changed files with 104 additions and 31 deletions
+1 -1
View File
@@ -1328,7 +1328,7 @@ impl Node {
NodeError::NoTransportForType(format!("invalid MAC in '{}': {}", addr_str, e)) 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")))] #[cfg(not(any(target_os = "linux", target_os = "macos")))]
{ {
+1 -1
View File
@@ -465,7 +465,7 @@ async fn ethernet_receive_loop(
} }
}; };
let bytes = data.len(); 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); let packet = ReceivedPacket::new(transport_id, addr, data);
trace!( trace!(
+12 -1
View File
@@ -144,7 +144,7 @@ impl NeighborBuffer {
/// Build the buffered peer record for one beacon. /// Build the buffered peer record for one beacon.
fn peer(&self, src_mac: [u8; 6], pubkey: XOnlyPublicKey) -> DiscoveredPeer { 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) DiscoveredPeer::with_hint(self.transport_id, addr, pubkey)
} }
} }
@@ -234,6 +234,17 @@ mod tests {
assert!(peers.is_empty()); 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] #[test]
fn test_neighbor_buffer_dedup() { fn test_neighbor_buffer_dedup() {
let buffer = NeighborBuffer::new(TransportId::new(1)); let buffer = NeighborBuffer::new(TransportId::new(1));
+23 -2
View File
@@ -1182,12 +1182,33 @@ mod tests {
#[test] #[test]
fn test_transport_addr_mac_display() { fn test_transport_addr_mac_display() {
// Raw 6-byte MACs (as Ethernet stores via from_bytes) display in // Raw 6-byte non-UTF-8 values from from_bytes display in standard
// standard colon-separated notation, not bare hex. // colon-separated notation, not bare hex.
let mac = TransportAddr::from_bytes(&[0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]); let mac = TransportAddr::from_bytes(&[0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
assert_eq!(format!("{}", mac), "aa:bb:cc:dd:ee:ff"); 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] #[test]
fn test_transport_addr_non_mac_binary_is_bare_hex() { fn test_transport_addr_non_mac_binary_is_bare_hex() {
// Non-6-byte non-UTF-8 payloads stay bare hex (no separators). // Non-6-byte non-UTF-8 payloads stay bare hex (no separators).
+67 -26
View File
@@ -90,75 +90,116 @@ impl fmt::Display for LinkDirection {
/// ///
/// Each transport type interprets this differently: /// Each transport type interprets this differently:
/// - UDP/TCP: "host:port" (IP address or DNS hostname) /// - UDP/TCP: "host:port" (IP address or DNS hostname)
/// - Ethernet: MAC address (6 bytes) /// - Ethernet: MAC address (6 bytes), built with [`TransportAddr::from_mac`]
#[derive(Clone, PartialEq, Eq, Hash)] /// so it always displays as a MAC, whatever its bytes decode as
pub struct TransportAddr(Vec<u8>); #[derive(Clone)]
pub struct TransportAddr {
bytes: Vec<u8>,
/// 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<H: core::hash::Hasher>(&self, state: &mut H) {
self.bytes.hash(state);
}
}
impl TransportAddr { impl TransportAddr {
/// Create a transport address from raw bytes. /// Create a transport address from raw bytes.
pub fn new(bytes: Vec<u8>) -> Self { pub fn new(bytes: Vec<u8>) -> Self {
Self(bytes) Self { bytes, mac: false }
} }
/// Create a transport address from a byte slice. /// Create a transport address from a byte slice.
pub fn from_bytes(bytes: &[u8]) -> Self { 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. /// Create a transport address from a string.
pub fn from_string(s: &str) -> Self { pub fn from_string(s: &str) -> Self {
Self(s.as_bytes().to_vec()) Self::new(s.as_bytes().to_vec())
} }
/// Get the raw bytes. /// Get the raw bytes.
pub fn as_bytes(&self) -> &[u8] { pub fn as_bytes(&self) -> &[u8] {
&self.0 &self.bytes
} }
/// Try to interpret as a UTF-8 string. /// Try to interpret as a UTF-8 string.
pub fn as_str(&self) -> Option<&str> { 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. /// Get the length in bytes.
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.0.len() self.bytes.len()
} }
/// Check if empty. /// Check if empty.
pub fn is_empty(&self) -> bool { 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 { impl fmt::Debug for TransportAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 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() { match self.as_str() {
Some(s) => write!(f, "TransportAddr(\"{}\")", s), Some(s) => write!(f, "TransportAddr(\"{}\")", s),
None => write!(f, "TransportAddr({:?})", self.0), None => write!(f, "TransportAddr({:?})", self.bytes),
} }
} }
} }
impl fmt::Display for TransportAddr { impl fmt::Display for TransportAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Best-effort display as string if valid UTF-8. Otherwise render a // An Ethernet address is a MAC whatever its bytes decode as. Any
// 6-byte payload as a colon-separated MAC (standard Unix notation, // other address displays as a string if it is valid UTF-8; otherwise
// matching BLE addrs, `ip link`/`ip neigh`, and packet logs), and // a 6-byte payload is rendered as a colon-separated MAC and any other
// any other non-UTF-8 byte string as bare hex. // byte string as bare hex.
if self.mac {
return self.write_mac(f);
}
match self.as_str() { match self.as_str() {
Some(s) => write!(f, "{}", s), Some(s) => write!(f, "{}", s),
None if self.0.len() == 6 => { None if self.bytes.len() == 6 => self.write_mac(f),
for (i, byte) in self.0.iter().enumerate() {
if i > 0 {
write!(f, ":")?;
}
write!(f, "{:02x}", byte)?;
}
Ok(())
}
None => { None => {
for byte in &self.0 { for byte in &self.bytes {
write!(f, "{:02x}", byte)?; write!(f, "{:02x}", byte)?;
} }
Ok(()) Ok(())
@@ -175,7 +216,7 @@ impl From<&str> for TransportAddr {
impl From<String> for TransportAddr { impl From<String> for TransportAddr {
fn from(s: String) -> Self { fn from(s: String) -> Self {
Self(s.into_bytes()) Self::new(s.into_bytes())
} }
} }