Skip to content

Commit 82fdb86

Browse files
committed
fix: cache resolvers via OnceLock, blocklist takes precedence, use TEST-NET IPs in tests
Signed-off-by: danbugs <danilochiarlone@gmail.com>
1 parent 02dd490 commit 82fdb86

1 file changed

Lines changed: 33 additions & 42 deletions

File tree

host/src/lib.rs

Lines changed: 33 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -304,52 +304,47 @@ impl BlockList {
304304
}
305305
}
306306

307-
/// Parse `/etc/resolv.conf` and return the set of nameserver IPs.
308-
///
309-
/// On non-Unix platforms this returns an empty set (DNS goes through the
310-
/// system resolver and never needs a raw port-53 exemption).
311-
fn parse_resolv_conf() -> HashSet<IpAddr> {
312-
#[cfg(unix)]
313-
{
314-
let mut resolvers = HashSet::new();
315-
if let Ok(contents) = std::fs::read_to_string("/etc/resolv.conf") {
316-
for line in contents.lines() {
317-
let line = line.trim();
318-
if let Some(rest) = line.strip_prefix("nameserver") {
319-
if let Some(ip_str) = rest.split_whitespace().next() {
320-
if let Ok(ip) = ip_str.parse::<IpAddr>() {
321-
resolvers.insert(ip);
307+
fn dns_resolvers() -> &'static HashSet<IpAddr> {
308+
static RESOLVERS: std::sync::OnceLock<HashSet<IpAddr>> = std::sync::OnceLock::new();
309+
RESOLVERS.get_or_init(|| {
310+
#[cfg(unix)]
311+
{
312+
let mut set = HashSet::new();
313+
if let Ok(contents) = std::fs::read_to_string("/etc/resolv.conf") {
314+
for line in contents.lines() {
315+
let line = line.trim();
316+
if let Some(rest) = line.strip_prefix("nameserver") {
317+
if let Some(ip_str) = rest.split_whitespace().next() {
318+
if let Ok(ip) = ip_str.parse::<IpAddr>() {
319+
set.insert(ip);
320+
}
322321
}
323322
}
324323
}
325324
}
325+
set
326326
}
327-
resolvers
328-
}
329-
#[cfg(not(unix))]
330-
{
331-
HashSet::new()
332-
}
327+
#[cfg(not(unix))]
328+
{
329+
HashSet::new()
330+
}
331+
})
333332
}
334333

335334
impl NetworkPolicy {
336335
fn check(&self, addr: &std::net::SocketAddr) -> Result<()> {
337336
match self {
338337
NetworkPolicy::AllowAll => Ok(()),
339338
NetworkPolicy::AllowList(al) => {
340-
if (addr.port() == 53 && parse_resolv_conf().contains(&addr.ip()))
341-
|| al.is_allowed(&addr.ip())
339+
if al.is_allowed(&addr.ip())
340+
|| (addr.port() == 53 && dns_resolvers().contains(&addr.ip()))
342341
{
343342
Ok(())
344343
} else {
345344
Err(anyhow!("network policy denies connection to {}", addr))
346345
}
347346
}
348347
NetworkPolicy::BlockList(bl) => {
349-
// Allow DNS (port 53) only to configured resolver IPs.
350-
if addr.port() == 53 && parse_resolv_conf().contains(&addr.ip()) {
351-
return Ok(());
352-
}
353348
if bl.is_blocked(&addr.ip()) {
354349
Err(anyhow!("network policy denies connection to {}", addr))
355350
} else {
@@ -2656,11 +2651,10 @@ mod tests {
26562651

26572652
#[test]
26582653
fn test_port53_arbitrary_ip_blocked() {
2659-
// Only 1.1.1.1 is in the allowlist — 8.8.8.8:53 must be denied
2660-
// because 8.8.8.8 is (almost certainly) not in /etc/resolv.conf.
2661-
let al = AllowList::from_hosts(&["1.1.1.1"]).unwrap();
2654+
// RFC5737 TEST-NET-2 address — guaranteed not in /etc/resolv.conf.
2655+
let al = AllowList::from_hosts(&["198.51.100.1"]).unwrap();
26622656
let policy = NetworkPolicy::AllowList(al);
2663-
let addr: std::net::SocketAddr = "8.8.8.8:53".parse().unwrap();
2657+
let addr: std::net::SocketAddr = "198.51.100.99:53".parse().unwrap();
26642658
assert!(
26652659
policy.check(&addr).is_err(),
26662660
"port 53 to a non-resolver IP must be denied"
@@ -2670,16 +2664,14 @@ mod tests {
26702664
#[cfg(unix)]
26712665
#[test]
26722666
fn test_port53_real_resolver_allowed() {
2673-
// Grab the first resolver from /etc/resolv.conf and verify port
2674-
// 53 to that IP is allowed even when the IP is not in the
2675-
// AllowList itself.
2676-
let resolvers = parse_resolv_conf();
2667+
let resolvers = dns_resolvers();
26772668
if resolvers.is_empty() {
26782669
eprintln!("skipping: no resolvers found in /etc/resolv.conf");
26792670
return;
26802671
}
26812672
let resolver_ip = *resolvers.iter().next().unwrap();
2682-
let al = AllowList::from_hosts(&["1.1.1.1"]).unwrap();
2673+
// Allowlist uses a TEST-NET IP that won't match the resolver.
2674+
let al = AllowList::from_hosts(&["198.51.100.1"]).unwrap();
26832675
let policy = NetworkPolicy::AllowList(al);
26842676
let addr = std::net::SocketAddr::new(resolver_ip, 53);
26852677
assert!(
@@ -2691,8 +2683,7 @@ mod tests {
26912683

26922684
#[test]
26932685
fn test_port53_blocklist_enforced() {
2694-
// Even on port 53, a blocklisted IP must be denied — unless it
2695-
// happens to be a real resolver. Use a bogus IP to be safe.
2686+
// RFC5737 TEST-NET-1 address — blocklist always wins.
26962687
let bl = BlockList::from_hosts(&["192.0.2.1"]).unwrap();
26972688
let policy = NetworkPolicy::BlockList(bl);
26982689
let addr: std::net::SocketAddr = "192.0.2.1:53".parse().unwrap();
@@ -2735,13 +2726,13 @@ mod tests {
27352726

27362727
#[test]
27372728
fn network_policy_blocklist_denies_blocked_ip_on_port53() {
2738-
// A blocked IP must not be exempted just because the port is 53.
2739-
let bl = BlockList::from_hosts(&["1.2.3.4"]).unwrap();
2729+
// RFC5737 TEST-NET-3 — blocklist always wins over DNS exemption.
2730+
let bl = BlockList::from_hosts(&["203.0.113.1"]).unwrap();
27402731
let policy = NetworkPolicy::BlockList(bl);
2741-
let addr: std::net::SocketAddr = "1.2.3.4:53".parse().unwrap();
2732+
let addr: std::net::SocketAddr = "203.0.113.1:53".parse().unwrap();
27422733
assert!(
27432734
policy.check(&addr).is_err(),
2744-
"blocked IP 1.2.3.4 must be denied even on port 53"
2735+
"blocked IP must be denied even on port 53"
27452736
);
27462737
}
27472738

0 commit comments

Comments
 (0)