Files
archy/core/archipelago/src/fips/iface.rs
T

110 lines
3.6 KiB
Rust

//! 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 `<npub>.fips` DNS resolves to.
pub fn fips0_ula() -> Option<Ipv6Addr> {
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<Ipv6Addr> {
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<Ipv6Addr> {
// /proc/net/if_inet6 format (whitespace-separated):
// <32 hex chars addr> <idx> <prefixlen> <scope> <flags> <devname>
// 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::<Ipv6Addr>()
.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
}
}