diff --git a/host/src/lib.rs b/host/src/lib.rs index 2761587..ef10d2f 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -304,22 +304,47 @@ impl BlockList { } } +fn dns_resolvers() -> &'static HashSet { + static RESOLVERS: std::sync::OnceLock> = std::sync::OnceLock::new(); + RESOLVERS.get_or_init(|| { + #[cfg(unix)] + { + let mut set = HashSet::new(); + if let Ok(contents) = std::fs::read_to_string("/etc/resolv.conf") { + for line in contents.lines() { + let line = line.trim(); + if let Some(rest) = line.strip_prefix("nameserver") { + if let Some(ip_str) = rest.split_whitespace().next() { + if let Ok(ip) = ip_str.parse::() { + set.insert(ip); + } + } + } + } + } + set + } + #[cfg(not(unix))] + { + HashSet::new() + } + }) +} + impl NetworkPolicy { fn check(&self, addr: &std::net::SocketAddr) -> Result<()> { match self { NetworkPolicy::AllowAll => Ok(()), NetworkPolicy::AllowList(al) => { - if addr.port() == 53 || al.is_allowed(&addr.ip()) { + if al.is_allowed(&addr.ip()) + || (addr.port() == 53 && dns_resolvers().contains(&addr.ip())) + { Ok(()) } else { Err(anyhow!("network policy denies connection to {}", addr)) } } NetworkPolicy::BlockList(bl) => { - // DNS (port 53) is always allowed so the guest can resolve names. - if addr.port() == 53 { - return Ok(()); - } if bl.is_blocked(&addr.ip()) { Err(anyhow!("network policy denies connection to {}", addr)) } else { @@ -2624,6 +2649,50 @@ mod tests { assert!(err.to_string().contains("network policy denies"), "{err}"); } + #[test] + fn test_port53_arbitrary_ip_blocked() { + // RFC5737 TEST-NET-2 address — guaranteed not in /etc/resolv.conf. + let al = AllowList::from_hosts(&["198.51.100.1"]).unwrap(); + let policy = NetworkPolicy::AllowList(al); + let addr: std::net::SocketAddr = "198.51.100.99:53".parse().unwrap(); + assert!( + policy.check(&addr).is_err(), + "port 53 to a non-resolver IP must be denied" + ); + } + + #[cfg(unix)] + #[test] + fn test_port53_real_resolver_allowed() { + let resolvers = dns_resolvers(); + if resolvers.is_empty() { + eprintln!("skipping: no resolvers found in /etc/resolv.conf"); + return; + } + let resolver_ip = *resolvers.iter().next().unwrap(); + // Allowlist uses a TEST-NET IP that won't match the resolver. + let al = AllowList::from_hosts(&["198.51.100.1"]).unwrap(); + let policy = NetworkPolicy::AllowList(al); + let addr = std::net::SocketAddr::new(resolver_ip, 53); + assert!( + policy.check(&addr).is_ok(), + "port 53 to a configured resolver ({}) must be allowed", + resolver_ip + ); + } + + #[test] + fn test_port53_blocklist_enforced() { + // RFC5737 TEST-NET-1 address — blocklist always wins. + let bl = BlockList::from_hosts(&["192.0.2.1"]).unwrap(); + let policy = NetworkPolicy::BlockList(bl); + let addr: std::net::SocketAddr = "192.0.2.1:53".parse().unwrap(); + assert!( + policy.check(&addr).is_err(), + "blocklisted IP must be denied even on port 53" + ); + } + #[test] fn allowlist_resolves_hostnames() { let al = AllowList::from_hosts(&["localhost"]).unwrap(); @@ -2656,11 +2725,15 @@ mod tests { } #[test] - fn network_policy_blocklist_allows_dns() { - let bl = BlockList::from_hosts(&["1.2.3.4"]).unwrap(); + fn network_policy_blocklist_denies_blocked_ip_on_port53() { + // RFC5737 TEST-NET-3 — blocklist always wins over DNS exemption. + let bl = BlockList::from_hosts(&["203.0.113.1"]).unwrap(); let policy = NetworkPolicy::BlockList(bl); - let addr: std::net::SocketAddr = "1.2.3.4:53".parse().unwrap(); - assert!(policy.check(&addr).is_ok()); + let addr: std::net::SocketAddr = "203.0.113.1:53".parse().unwrap(); + assert!( + policy.check(&addr).is_err(), + "blocked IP must be denied even on port 53" + ); } #[test]