Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 82 additions & 9 deletions host/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,22 +304,47 @@ impl BlockList {
}
}

fn dns_resolvers() -> &'static HashSet<IpAddr> {
static RESOLVERS: std::sync::OnceLock<HashSet<IpAddr>> = 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::<IpAddr>() {
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 {
Expand Down Expand Up @@ -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"
);
Comment on lines +2653 to +2661
}

#[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();
Expand Down Expand Up @@ -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"
);
Comment on lines 2727 to +2736
}

#[test]
Expand Down
Loading