From ac978df9caa7afad609185460276da321628419e Mon Sep 17 00:00:00 2001 From: danbugs Date: Fri, 15 May 2026 13:38:21 +0000 Subject: [PATCH 1/2] fix: populate dns_resolvers() on Windows via ipconfig Signed-off-by: danbugs --- host/src/lib.rs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/host/src/lib.rs b/host/src/lib.rs index 4b365e1..89326d9 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -324,7 +324,32 @@ fn dns_resolvers() -> &'static HashSet { } set } - #[cfg(not(unix))] + #[cfg(windows)] + { + let mut set = HashSet::new(); + if let Ok(output) = std::process::Command::new("ipconfig").arg("/all").output() { + let text = String::from_utf8_lossy(&output.stdout); + let mut in_dns_block = false; + for line in text.lines() { + let trimmed = line.trim(); + if let Some(rest) = trimmed.strip_prefix("DNS Servers") { + in_dns_block = true; + let value = rest.trim_start_matches(['.', ' ', ':']); + if let Ok(ip) = value.parse::() { + set.insert(ip); + } + } else if in_dns_block { + if let Ok(ip) = trimmed.parse::() { + set.insert(ip); + } else { + in_dns_block = false; + } + } + } + } + set + } + #[cfg(not(any(unix, windows)))] { HashSet::new() } From bf35761aa9f6fb309afc30ca4279992f94a98278 Mon Sep 17 00:00:00 2001 From: danbugs Date: Fri, 15 May 2026 14:00:32 +0000 Subject: [PATCH 2/2] fix: include well-known public DNS in AllowList port-53 exemption The AllowList DNS exemption only covered the host's resolvers from /etc/resolv.conf, but the guest hardcodes 8.8.8.8/8.8.4.4 in its initrd. On any host with different resolvers, AllowList silently broke DNS resolution. Add Google (8.8.8.8, 8.8.4.4) and Cloudflare (1.1.1.1, 1.0.0.1) to the exempted resolver set. Port 53 to arbitrary IPs is still blocked, preserving the anti-exfiltration property. Signed-off-by: danbugs --- host/Cargo.lock | 2 +- host/Cargo.toml | 2 +- host/src/lib.rs | 75 +++++++++++++++++++++++++++++++++---------------- 3 files changed, 53 insertions(+), 26 deletions(-) diff --git a/host/Cargo.lock b/host/Cargo.lock index bde1944..541109c 100644 --- a/host/Cargo.lock +++ b/host/Cargo.lock @@ -569,7 +569,7 @@ dependencies = [ [[package]] name = "hyperlight-unikraft-host" -version = "0.3.0" +version = "0.4.0" dependencies = [ "anyhow", "base64", diff --git a/host/Cargo.toml b/host/Cargo.toml index 4b5b9c7..835e77a 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hyperlight-unikraft-host" -version = "0.3.0" +version = "0.4.0" edition = "2021" description = "Embedded Hyperlight host for running Unikraft unikernels" license = "MIT OR Apache-2.0" diff --git a/host/src/lib.rs b/host/src/lib.rs index 89326d9..e424872 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -304,12 +304,25 @@ impl BlockList { } } +/// DNS resolver IPs that the AllowList exempts on port 53. +/// +/// Includes the host's configured resolvers (from `/etc/resolv.conf` on +/// Unix, `ipconfig /all` on Windows) **plus** well-known public DNS +/// servers (Google, Cloudflare) that the guest may hardcode in its own +/// `/etc/resolv.conf`. fn dns_resolvers() -> &'static HashSet { static RESOLVERS: std::sync::OnceLock> = std::sync::OnceLock::new(); RESOLVERS.get_or_init(|| { + let mut set = HashSet::new(); + // Well-known public DNS that the guest's initrd may hardcode. + for ip in [ + "8.8.8.8", "8.8.4.4", // Google + "1.1.1.1", "1.0.0.1", // Cloudflare + ] { + set.insert(ip.parse::().unwrap()); + } #[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(); @@ -322,11 +335,9 @@ fn dns_resolvers() -> &'static HashSet { } } } - set } #[cfg(windows)] { - let mut set = HashSet::new(); if let Ok(output) = std::process::Command::new("ipconfig").arg("/all").output() { let text = String::from_utf8_lossy(&output.stdout); let mut in_dns_block = false; @@ -347,12 +358,8 @@ fn dns_resolvers() -> &'static HashSet { } } } - set - } - #[cfg(not(any(unix, windows)))] - { - HashSet::new() } + set }) } @@ -2597,34 +2604,43 @@ mod tests { } #[test] - fn test_port53_arbitrary_ip_blocked() { - // RFC5737 TEST-NET-2 address — guaranteed not in /etc/resolv.conf. + fn allowlist_permits_dns_to_well_known_resolvers() { + // AllowList exempts port 53 to well-known public DNS and host + // resolvers so the guest's hardcoded nameservers (8.8.8.8 etc.) + // work even when the host uses different resolvers. + let al = AllowList::from_hosts(&["198.51.100.1"]).unwrap(); + let policy = NetworkPolicy::AllowList(al); + for ip in ["8.8.8.8:53", "8.8.4.4:53", "1.1.1.1:53", "1.0.0.1:53"] { + let addr: std::net::SocketAddr = ip.parse().unwrap(); + assert!( + policy.check(&addr).is_ok(), + "AllowList must permit DNS (port 53) to well-known resolver {}", + ip + ); + } + } + + #[test] + fn allowlist_denies_port53_to_arbitrary_ip() { + // Port 53 is NOT a blanket bypass — only known DNS resolvers + // are exempted. RFC5737 TEST-NET addresses are never resolvers. 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" + "port 53 to an unknown IP must still 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. + fn allowlist_denies_non_dns_to_unlisted_ip() { let al = AllowList::from_hosts(&["198.51.100.1"]).unwrap(); let policy = NetworkPolicy::AllowList(al); - let addr = std::net::SocketAddr::new(resolver_ip, 53); + let addr: std::net::SocketAddr = "198.51.100.99:443".parse().unwrap(); assert!( - policy.check(&addr).is_ok(), - "port 53 to a configured resolver ({}) must be allowed", - resolver_ip + policy.check(&addr).is_err(), + "AllowList must deny non-DNS traffic to unlisted IPs" ); } @@ -2683,6 +2699,17 @@ mod tests { ); } + #[test] + fn blocklist_permits_dns_to_non_blocked_ip() { + let bl = BlockList::from_hosts(&["203.0.113.1"]).unwrap(); + let policy = NetworkPolicy::BlockList(bl); + let addr: std::net::SocketAddr = "8.8.8.8:53".parse().unwrap(); + assert!( + policy.check(&addr).is_ok(), + "BlockList must allow DNS to non-blocked IPs" + ); + } + #[test] fn blocklist_resolves_hostnames() { let bl = BlockList::from_hosts(&["localhost"]).unwrap();