@@ -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
335334impl 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