Skip to content

Commit cd92f68

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 8277a34 commit cd92f68

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
@@ -299,52 +299,47 @@ impl BlockList {
299299
}
300300
}
301301

302-
/// Parse `/etc/resolv.conf` and return the set of nameserver IPs.
303-
///
304-
/// On non-Unix platforms this returns an empty set (DNS goes through the
305-
/// system resolver and never needs a raw port-53 exemption).
306-
fn parse_resolv_conf() -> HashSet<IpAddr> {
307-
#[cfg(unix)]
308-
{
309-
let mut resolvers = HashSet::new();
310-
if let Ok(contents) = std::fs::read_to_string("/etc/resolv.conf") {
311-
for line in contents.lines() {
312-
let line = line.trim();
313-
if let Some(rest) = line.strip_prefix("nameserver") {
314-
if let Some(ip_str) = rest.split_whitespace().next() {
315-
if let Ok(ip) = ip_str.parse::<IpAddr>() {
316-
resolvers.insert(ip);
302+
fn dns_resolvers() -> &'static HashSet<IpAddr> {
303+
static RESOLVERS: std::sync::OnceLock<HashSet<IpAddr>> = std::sync::OnceLock::new();
304+
RESOLVERS.get_or_init(|| {
305+
#[cfg(unix)]
306+
{
307+
let mut set = HashSet::new();
308+
if let Ok(contents) = std::fs::read_to_string("/etc/resolv.conf") {
309+
for line in contents.lines() {
310+
let line = line.trim();
311+
if let Some(rest) = line.strip_prefix("nameserver") {
312+
if let Some(ip_str) = rest.split_whitespace().next() {
313+
if let Ok(ip) = ip_str.parse::<IpAddr>() {
314+
set.insert(ip);
315+
}
317316
}
318317
}
319318
}
320319
}
320+
set
321321
}
322-
resolvers
323-
}
324-
#[cfg(not(unix))]
325-
{
326-
HashSet::new()
327-
}
322+
#[cfg(not(unix))]
323+
{
324+
HashSet::new()
325+
}
326+
})
328327
}
329328

330329
impl NetworkPolicy {
331330
fn check(&self, addr: &std::net::SocketAddr) -> Result<()> {
332331
match self {
333332
NetworkPolicy::AllowAll => Ok(()),
334333
NetworkPolicy::AllowList(al) => {
335-
if (addr.port() == 53 && parse_resolv_conf().contains(&addr.ip()))
336-
|| al.is_allowed(&addr.ip())
334+
if al.is_allowed(&addr.ip())
335+
|| (addr.port() == 53 && dns_resolvers().contains(&addr.ip()))
337336
{
338337
Ok(())
339338
} else {
340339
Err(anyhow!("network policy denies connection to {}", addr))
341340
}
342341
}
343342
NetworkPolicy::BlockList(bl) => {
344-
// Allow DNS (port 53) only to configured resolver IPs.
345-
if addr.port() == 53 && parse_resolv_conf().contains(&addr.ip()) {
346-
return Ok(());
347-
}
348343
if bl.is_blocked(&addr.ip()) {
349344
Err(anyhow!("network policy denies connection to {}", addr))
350345
} else {
@@ -2651,11 +2646,10 @@ mod tests {
26512646

26522647
#[test]
26532648
fn test_port53_arbitrary_ip_blocked() {
2654-
// Only 1.1.1.1 is in the allowlist — 8.8.8.8:53 must be denied
2655-
// because 8.8.8.8 is (almost certainly) not in /etc/resolv.conf.
2656-
let al = AllowList::from_hosts(&["1.1.1.1"]).unwrap();
2649+
// RFC5737 TEST-NET-2 address — guaranteed not in /etc/resolv.conf.
2650+
let al = AllowList::from_hosts(&["198.51.100.1"]).unwrap();
26572651
let policy = NetworkPolicy::AllowList(al);
2658-
let addr: std::net::SocketAddr = "8.8.8.8:53".parse().unwrap();
2652+
let addr: std::net::SocketAddr = "198.51.100.99:53".parse().unwrap();
26592653
assert!(
26602654
policy.check(&addr).is_err(),
26612655
"port 53 to a non-resolver IP must be denied"
@@ -2665,16 +2659,14 @@ mod tests {
26652659
#[cfg(unix)]
26662660
#[test]
26672661
fn test_port53_real_resolver_allowed() {
2668-
// Grab the first resolver from /etc/resolv.conf and verify port
2669-
// 53 to that IP is allowed even when the IP is not in the
2670-
// AllowList itself.
2671-
let resolvers = parse_resolv_conf();
2662+
let resolvers = dns_resolvers();
26722663
if resolvers.is_empty() {
26732664
eprintln!("skipping: no resolvers found in /etc/resolv.conf");
26742665
return;
26752666
}
26762667
let resolver_ip = *resolvers.iter().next().unwrap();
2677-
let al = AllowList::from_hosts(&["1.1.1.1"]).unwrap();
2668+
// Allowlist uses a TEST-NET IP that won't match the resolver.
2669+
let al = AllowList::from_hosts(&["198.51.100.1"]).unwrap();
26782670
let policy = NetworkPolicy::AllowList(al);
26792671
let addr = std::net::SocketAddr::new(resolver_ip, 53);
26802672
assert!(
@@ -2686,8 +2678,7 @@ mod tests {
26862678

26872679
#[test]
26882680
fn test_port53_blocklist_enforced() {
2689-
// Even on port 53, a blocklisted IP must be denied — unless it
2690-
// happens to be a real resolver. Use a bogus IP to be safe.
2681+
// RFC5737 TEST-NET-1 address — blocklist always wins.
26912682
let bl = BlockList::from_hosts(&["192.0.2.1"]).unwrap();
26922683
let policy = NetworkPolicy::BlockList(bl);
26932684
let addr: std::net::SocketAddr = "192.0.2.1:53".parse().unwrap();
@@ -2730,13 +2721,13 @@ mod tests {
27302721

27312722
#[test]
27322723
fn network_policy_blocklist_denies_blocked_ip_on_port53() {
2733-
// A blocked IP must not be exempted just because the port is 53.
2734-
let bl = BlockList::from_hosts(&["1.2.3.4"]).unwrap();
2724+
// RFC5737 TEST-NET-3 — blocklist always wins over DNS exemption.
2725+
let bl = BlockList::from_hosts(&["203.0.113.1"]).unwrap();
27352726
let policy = NetworkPolicy::BlockList(bl);
2736-
let addr: std::net::SocketAddr = "1.2.3.4:53".parse().unwrap();
2727+
let addr: std::net::SocketAddr = "203.0.113.1:53".parse().unwrap();
27372728
assert!(
27382729
policy.check(&addr).is_err(),
2739-
"blocked IP 1.2.3.4 must be denied even on port 53"
2730+
"blocked IP must be denied even on port 53"
27402731
);
27412732
}
27422733

0 commit comments

Comments
 (0)