1use std::sync::LazyLock;
16
17use mac_address::mac_address_by_name;
18use nix::ifaddrs::getifaddrs;
19
20const ENV_MAC_ADDRESS: &str = "KUMO_MAC_ADDRESS";
25
26const ENV_MAC_INTERFACE: &str = "KUMO_MAC_INTERFACE";
29
30const VIRTUAL_INTERFACE_NAMES: &[&str] = &["lo", "lo0"];
35
36const VIRTUAL_INTERFACE_PREFIXES: &[&str] = &[
41 "br-", "cali", "cni", "docker", "dummy", "flannel", "gre", "ifb", "ip6tnl", "kube", "nlmon",
42 "sit", "tap", "tun", "veth", "virbr", "vmbr", "vnet", "wg", "zt",
43];
44
45static MAC: LazyLock<[u8; 6]> = LazyLock::new(resolve);
46
47pub fn get_mac_address() -> &'static [u8; 6] {
51 &MAC
52}
53
54fn resolve() -> [u8; 6] {
57 if let Some(mac) = from_env_literal() {
58 tracing::info!("using node MAC {} from {ENV_MAC_ADDRESS}", format_mac(&mac));
59 return mac;
60 }
61
62 if let Some((mac, name)) = from_env_interface() {
63 tracing::info!(
64 "using node MAC {} from interface {name} named by {ENV_MAC_INTERFACE}",
65 format_mac(&mac)
66 );
67 return mac;
68 }
69
70 if let Some((mac, name)) = first_physical_interface() {
71 tracing::info!("using node MAC {} from interface {name}", format_mac(&mac));
72 return mac;
73 }
74
75 let mac = from_host_id();
76 tracing::warn!(
77 "no usable network interface MAC found; derived node MAC {} from gethostid()",
78 format_mac(&mac)
79 );
80 mac
81}
82
83fn from_env_literal() -> Option<[u8; 6]> {
84 let value = std::env::var(ENV_MAC_ADDRESS).ok()?;
85 let value = value.trim();
86 if value.is_empty() {
87 return None;
88 }
89 match parse_mac(value) {
90 Some(mac) if mac.iter().all(|b| *b == 0) => {
91 tracing::error!("{ENV_MAC_ADDRESS}=`{value}` is a zero MAC address; ignoring it");
92 None
93 }
94 Some(mac) => Some(mac),
95 None => {
96 tracing::error!("{ENV_MAC_ADDRESS}=`{value}` is not a valid MAC address; ignoring it");
97 None
98 }
99 }
100}
101
102fn from_env_interface() -> Option<([u8; 6], String)> {
103 let name = std::env::var(ENV_MAC_INTERFACE).ok()?;
104 let name = name.trim().to_string();
105 if name.is_empty() {
106 return None;
107 }
108 match mac_address_by_name(&name) {
109 Ok(Some(addr)) => {
110 let bytes = addr.bytes();
111 if bytes.iter().all(|b| *b == 0) {
112 tracing::error!(
113 "interface {name} from {ENV_MAC_INTERFACE} has a zero MAC address; ignoring it"
114 );
115 None
116 } else {
117 Some((bytes, name))
118 }
119 }
120 Ok(None) => {
121 tracing::error!("interface {name} from {ENV_MAC_INTERFACE} was not found; ignoring it");
122 None
123 }
124 Err(err) => {
125 tracing::error!(
126 "failed to read MAC for interface {name} from {ENV_MAC_INTERFACE}: {err:#}; \
127 ignoring it"
128 );
129 None
130 }
131 }
132}
133
134fn first_physical_interface() -> Option<([u8; 6], String)> {
143 let mut first_usable = None;
144 for iface in getifaddrs().ok()? {
145 let Some(bytes) = iface.address.and_then(|a| a.as_link_addr()?.addr()) else {
146 continue;
147 };
148 if bytes.iter().all(|b| *b == 0) {
149 continue;
150 }
151 let name = iface.interface_name;
152 if first_usable.is_none() {
153 first_usable = Some((bytes, name.clone()));
154 }
155 if is_virtual_interface(&name) {
156 continue;
157 }
158 return Some((bytes, name));
159 }
160 first_usable
161}
162
163fn is_virtual_interface(name: &str) -> bool {
164 VIRTUAL_INTERFACE_NAMES.contains(&name)
165 || VIRTUAL_INTERFACE_PREFIXES
166 .iter()
167 .any(|prefix| name.starts_with(prefix))
168}
169
170fn from_host_id() -> [u8; 6] {
174 let host_id = unsafe { libc::gethostid() }.to_le_bytes();
175 [
176 host_id[0], host_id[1], host_id[2], host_id[3], host_id[4], host_id[5],
177 ]
178}
179
180fn parse_mac(input: &str) -> Option<[u8; 6]> {
185 let groups: Vec<&str> = if input.contains(':') {
186 input.split(':').collect()
187 } else if input.contains('-') {
188 input.split('-').collect()
189 } else {
190 if input.len() != 12 {
191 return None;
192 }
193 return parse_hex_bytes(input);
194 };
195 if groups.len() != 6 || groups.iter().any(|g| g.len() != 2) {
196 return None;
197 }
198 parse_hex_bytes(&groups.concat())
199}
200
201fn parse_hex_bytes(hex: &str) -> Option<[u8; 6]> {
202 if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
203 return None;
204 }
205 let mut out = [0u8; 6];
206 for (i, byte) in out.iter_mut().enumerate() {
207 *byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).ok()?;
208 }
209 Some(out)
210}
211
212fn format_mac(mac: &[u8; 6]) -> String {
213 format!(
214 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
215 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
216 )
217}
218
219#[cfg(test)]
220mod test {
221 use super::*;
222
223 #[test]
224 fn parse_colon_separated() {
225 assert_eq!(
226 parse_mac("02:1a:2b:3c:4d:5e"),
227 Some([0x02, 0x1a, 0x2b, 0x3c, 0x4d, 0x5e])
228 );
229 }
230
231 #[test]
232 fn parse_hyphen_separated() {
233 assert_eq!(
234 parse_mac("02-1A-2B-3C-4D-5E"),
235 Some([0x02, 0x1a, 0x2b, 0x3c, 0x4d, 0x5e])
236 );
237 }
238
239 #[test]
240 fn parse_bare_hex() {
241 assert_eq!(
242 parse_mac("021a2b3c4d5e"),
243 Some([0x02, 0x1a, 0x2b, 0x3c, 0x4d, 0x5e])
244 );
245 }
246
247 #[test]
248 fn parse_rejects_bad_input() {
249 assert_eq!(parse_mac("nope"), None);
250 assert_eq!(parse_mac("02:1a:2b:3c:4d"), None);
251 assert_eq!(parse_mac("02:1a:2b:3c:4d:5e:6f"), None);
252 assert_eq!(parse_mac("gg:1a:2b:3c:4d:5e"), None);
253 }
254
255 #[test]
256 fn parse_rejects_mixed_separators() {
257 assert_eq!(parse_mac("02:1a-2b:3c-4d:5e"), None);
258 assert_eq!(parse_mac("021a2b-3c4d5e"), None);
259 }
260
261 #[test]
262 fn virtual_interfaces_match() {
263 assert!(is_virtual_interface("lo"));
264 assert!(is_virtual_interface("lo0"));
265 assert!(is_virtual_interface("docker0"));
266 assert!(is_virtual_interface("veth1234"));
267 assert!(is_virtual_interface("br-abcdef"));
268 assert!(is_virtual_interface("dummy0"));
269 assert!(is_virtual_interface("gre0"));
270 assert!(is_virtual_interface("sit0"));
271 assert!(!is_virtual_interface("lom0"));
272 assert!(!is_virtual_interface("eth0"));
273 assert!(!is_virtual_interface("ens5"));
274 assert!(!is_virtual_interface("bond0"));
275 }
276}