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 4b365e1..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,12 +335,31 @@ fn dns_resolvers() -> &'static HashSet { } } } - set } - #[cfg(not(unix))] + #[cfg(windows)] { - 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 }) } @@ -2572,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" ); } @@ -2658,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();