From 02dd490fcf592e5be0063e05eeb5237a0df78fd3 Mon Sep 17 00:00:00 2001 From: danbugs Date: Fri, 15 May 2026 05:21:51 +0000 Subject: [PATCH 1/2] fix: scope DNS port-53 exemption to configured resolver IPs Signed-off-by: danbugs --- host/src/lib.rs | 92 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 5 deletions(-) diff --git a/host/src/lib.rs b/host/src/lib.rs index 2761587..fcb1598 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -304,20 +304,50 @@ impl BlockList { } } +/// Parse `/etc/resolv.conf` and return the set of nameserver IPs. +/// +/// On non-Unix platforms this returns an empty set (DNS goes through the +/// system resolver and never needs a raw port-53 exemption). +fn parse_resolv_conf() -> HashSet { + #[cfg(unix)] + { + let mut resolvers = 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::() { + resolvers.insert(ip); + } + } + } + } + } + resolvers + } + #[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 (addr.port() == 53 && parse_resolv_conf().contains(&addr.ip())) + || al.is_allowed(&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 { + // Allow DNS (port 53) only to configured resolver IPs. + if addr.port() == 53 && parse_resolv_conf().contains(&addr.ip()) { return Ok(()); } if bl.is_blocked(&addr.ip()) { @@ -2624,6 +2654,54 @@ mod tests { assert!(err.to_string().contains("network policy denies"), "{err}"); } + #[test] + fn test_port53_arbitrary_ip_blocked() { + // Only 1.1.1.1 is in the allowlist — 8.8.8.8:53 must be denied + // because 8.8.8.8 is (almost certainly) not in /etc/resolv.conf. + let al = AllowList::from_hosts(&["1.1.1.1"]).unwrap(); + let policy = NetworkPolicy::AllowList(al); + let addr: std::net::SocketAddr = "8.8.8.8: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() { + // Grab the first resolver from /etc/resolv.conf and verify port + // 53 to that IP is allowed even when the IP is not in the + // AllowList itself. + let resolvers = parse_resolv_conf(); + if resolvers.is_empty() { + eprintln!("skipping: no resolvers found in /etc/resolv.conf"); + return; + } + let resolver_ip = *resolvers.iter().next().unwrap(); + let al = AllowList::from_hosts(&["1.1.1.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() { + // Even on port 53, a blocklisted IP must be denied — unless it + // happens to be a real resolver. Use a bogus IP to be safe. + 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 +2734,15 @@ mod tests { } #[test] - fn network_policy_blocklist_allows_dns() { + fn network_policy_blocklist_denies_blocked_ip_on_port53() { + // A blocked IP must not be exempted just because the port is 53. let bl = BlockList::from_hosts(&["1.2.3.4"]).unwrap(); let policy = NetworkPolicy::BlockList(bl); let addr: std::net::SocketAddr = "1.2.3.4:53".parse().unwrap(); - assert!(policy.check(&addr).is_ok()); + assert!( + policy.check(&addr).is_err(), + "blocked IP 1.2.3.4 must be denied even on port 53" + ); } #[test] From 82fdb869f9bdd447963747bf6c737e60c31c8401 Mon Sep 17 00:00:00 2001 From: danbugs Date: Fri, 15 May 2026 06:00:34 +0000 Subject: [PATCH 2/2] fix: cache resolvers via OnceLock, blocklist takes precedence, use TEST-NET IPs in tests Signed-off-by: danbugs --- host/src/lib.rs | 75 ++++++++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 42 deletions(-) diff --git a/host/src/lib.rs b/host/src/lib.rs index fcb1598..ef10d2f 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -304,32 +304,31 @@ impl BlockList { } } -/// Parse `/etc/resolv.conf` and return the set of nameserver IPs. -/// -/// On non-Unix platforms this returns an empty set (DNS goes through the -/// system resolver and never needs a raw port-53 exemption). -fn parse_resolv_conf() -> HashSet { - #[cfg(unix)] - { - let mut resolvers = 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::() { - resolvers.insert(ip); +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 } - resolvers - } - #[cfg(not(unix))] - { - HashSet::new() - } + #[cfg(not(unix))] + { + HashSet::new() + } + }) } impl NetworkPolicy { @@ -337,8 +336,8 @@ impl NetworkPolicy { match self { NetworkPolicy::AllowAll => Ok(()), NetworkPolicy::AllowList(al) => { - if (addr.port() == 53 && parse_resolv_conf().contains(&addr.ip())) - || al.is_allowed(&addr.ip()) + if al.is_allowed(&addr.ip()) + || (addr.port() == 53 && dns_resolvers().contains(&addr.ip())) { Ok(()) } else { @@ -346,10 +345,6 @@ impl NetworkPolicy { } } NetworkPolicy::BlockList(bl) => { - // Allow DNS (port 53) only to configured resolver IPs. - if addr.port() == 53 && parse_resolv_conf().contains(&addr.ip()) { - return Ok(()); - } if bl.is_blocked(&addr.ip()) { Err(anyhow!("network policy denies connection to {}", addr)) } else { @@ -2656,11 +2651,10 @@ mod tests { #[test] fn test_port53_arbitrary_ip_blocked() { - // Only 1.1.1.1 is in the allowlist — 8.8.8.8:53 must be denied - // because 8.8.8.8 is (almost certainly) not in /etc/resolv.conf. - let al = AllowList::from_hosts(&["1.1.1.1"]).unwrap(); + // 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 = "8.8.8.8:53".parse().unwrap(); + 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" @@ -2670,16 +2664,14 @@ mod tests { #[cfg(unix)] #[test] fn test_port53_real_resolver_allowed() { - // Grab the first resolver from /etc/resolv.conf and verify port - // 53 to that IP is allowed even when the IP is not in the - // AllowList itself. - let resolvers = parse_resolv_conf(); + 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(); - let al = AllowList::from_hosts(&["1.1.1.1"]).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!( @@ -2691,8 +2683,7 @@ mod tests { #[test] fn test_port53_blocklist_enforced() { - // Even on port 53, a blocklisted IP must be denied — unless it - // happens to be a real resolver. Use a bogus IP to be safe. + // 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(); @@ -2735,13 +2726,13 @@ mod tests { #[test] fn network_policy_blocklist_denies_blocked_ip_on_port53() { - // A blocked IP must not be exempted just because the port is 53. - let bl = BlockList::from_hosts(&["1.2.3.4"]).unwrap(); + // 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(); + let addr: std::net::SocketAddr = "203.0.113.1:53".parse().unwrap(); assert!( policy.check(&addr).is_err(), - "blocked IP 1.2.3.4 must be denied even on port 53" + "blocked IP must be denied even on port 53" ); }