//! Detect the `fips0` TUN interface's ULA (fd00::/8) IPv6 address. //! //! The `fips` daemon configures the TUN device with an address derived //! from the node's identity key. We need that address to bind a //! peer-facing listener that is only reachable from the FIPS overlay — //! WAN IPv6 addresses never carry ULA prefixes, so binding specifically //! to the fips0 address keeps the peer surface off the public internet. //! //! We read `/proc/net/if_inet6` rather than shelling out to `ip` so //! this can run under the `archipelago` service user without extra //! capabilities. #![allow(dead_code)] use std::net::Ipv6Addr; /// Interface name the FIPS daemon creates (matches upstream default in /// `/etc/fips/fips.yaml: tun.name`). pub const FIPS_IFACE: &str = "fips0"; /// Return the first ULA (fd00::/8) address assigned to `fips0`, if any. /// /// - `None` if the interface is missing, has no address, or only has /// link-local addresses. /// - Link-local (`fe80::/10`) and non-ULA addresses are ignored — we /// only want the mesh-routable ULA that `.fips` DNS resolves to. pub fn fips0_ula() -> Option { addresses_on(FIPS_IFACE).into_iter().find(|a| is_ula(a)) } /// List every IPv6 address bound to a given interface from /// `/proc/net/if_inet6`. Returns empty on any parse failure. pub fn addresses_on(iface: &str) -> Vec { let contents = match std::fs::read_to_string("/proc/net/if_inet6") { Ok(s) => s, Err(_) => return Vec::new(), }; contents .lines() .filter_map(|line| parse_line(line, iface)) .collect() } /// `fd00::/8` test — covers the full ULA range. pub fn is_ula(addr: &Ipv6Addr) -> bool { (addr.octets()[0] & 0xFE) == 0xFC } fn parse_line(line: &str, iface: &str) -> Option { // /proc/net/if_inet6 format (whitespace-separated): // <32 hex chars addr> // e.g. "fdd8...cd85 6f 80 00 80 fips0" let mut parts = line.split_whitespace(); let hex = parts.next()?; let _idx = parts.next()?; let _prefix = parts.next()?; let _scope = parts.next()?; let _flags = parts.next()?; let name = parts.next()?; if name != iface { return None; } if hex.len() != 32 { return None; } let mut octets = [0u8; 16]; for i in 0..16 { octets[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).ok()?; } Some(Ipv6Addr::from(octets)) } #[cfg(test)] mod tests { use super::*; #[test] fn parse_line_extracts_address() { let line = "fdd83d5aabe08c0ee67f75fcf0d4cd85 6f 80 00 80 fips0"; let addr = parse_line(line, "fips0").unwrap(); assert_eq!( addr, "fdd8:3d5a:abe0:8c0e:e67f:75fc:f0d4:cd85" .parse::() .unwrap() ); } #[test] fn parse_line_rejects_other_iface() { let line = "fdd83d5aabe08c0ee67f75fcf0d4cd85 6f 80 00 80 eth0"; assert!(parse_line(line, "fips0").is_none()); } #[test] fn parse_line_ignores_malformed() { assert!(parse_line("garbage", "fips0").is_none()); assert!(parse_line("shorthex 6f 80 00 80 fips0", "fips0").is_none()); } #[test] fn ula_classifier_matches_fd_range() { assert!(is_ula(&"fd00::1".parse().unwrap())); assert!(is_ula(&"fdff::".parse().unwrap())); assert!(is_ula(&"fc00::1".parse().unwrap())); assert!(!is_ula(&"fe80::1".parse().unwrap())); // link-local assert!(!is_ula(&"2001:db8::1".parse().unwrap())); // global assert!(!is_ula(&"::1".parse().unwrap())); // loopback } }