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