1010//! - The mechanistic mapper for proposal filtering
1111//! - The gateway server for defense-in-depth validation on approval
1212
13+ use ipnet:: { IpNet , Ipv4Net , Ipv6Net } ;
1314use std:: net:: { IpAddr , Ipv4Addr , Ipv6Addr } ;
1415
1516/// Check if a hostname is a known cloud metadata hostname that resolves to an
@@ -80,9 +81,9 @@ pub fn is_always_blocked_ip(ip: IpAddr) -> bool {
8081///
8182/// Used at policy load time and server-side approval to reject entries that
8283/// would be silently blocked at runtime by [`is_always_blocked_ip`].
83- pub fn is_always_blocked_net ( net : ipnet :: IpNet ) -> bool {
84+ pub fn is_always_blocked_net ( net : IpNet ) -> bool {
8485 match net {
85- ipnet :: IpNet :: V4 ( v4net) => {
86+ IpNet :: V4 ( v4net) => {
8687 let network = v4net. network ( ) ;
8788 let broadcast = v4net. broadcast ( ) ;
8889
@@ -107,7 +108,7 @@ pub fn is_always_blocked_net(net: ipnet::IpNet) -> bool {
107108
108109 false
109110 }
110- ipnet :: IpNet :: V6 ( v6net) => {
111+ IpNet :: V6 ( v6net) => {
111112 // For IPv6, check the network address itself and representative
112113 // addresses within the range.
113114 let network = v6net. network ( ) ;
@@ -155,6 +156,43 @@ pub fn is_always_blocked_net(net: ipnet::IpNet) -> bool {
155156 }
156157}
157158
159+ const RFC1918_10_NET : Ipv4Net = Ipv4Net :: new_assert ( Ipv4Addr :: new ( 10 , 0 , 0 , 0 ) , 8 ) ;
160+ const RFC1918_172_NET : Ipv4Net = Ipv4Net :: new_assert ( Ipv4Addr :: new ( 172 , 16 , 0 , 0 ) , 12 ) ;
161+ const RFC1918_192_NET : Ipv4Net = Ipv4Net :: new_assert ( Ipv4Addr :: new ( 192 , 168 , 0 , 0 ) , 16 ) ;
162+ const CGNAT_NET : Ipv4Net = Ipv4Net :: new_assert ( Ipv4Addr :: new ( 100 , 64 , 0 , 0 ) , 10 ) ;
163+ const IETF_PROTOCOL_ASSIGNMENTS_NET : Ipv4Net = Ipv4Net :: new_assert ( Ipv4Addr :: new ( 192 , 0 , 0 , 0 ) , 24 ) ;
164+ const TEST_NET_1 : Ipv4Net = Ipv4Net :: new_assert ( Ipv4Addr :: new ( 192 , 0 , 2 , 0 ) , 24 ) ;
165+ const BENCHMARKING_NET : Ipv4Net = Ipv4Net :: new_assert ( Ipv4Addr :: new ( 198 , 18 , 0 , 0 ) , 15 ) ;
166+ const TEST_NET_2 : Ipv4Net = Ipv4Net :: new_assert ( Ipv4Addr :: new ( 198 , 51 , 100 , 0 ) , 24 ) ;
167+ const TEST_NET_3 : Ipv4Net = Ipv4Net :: new_assert ( Ipv4Addr :: new ( 203 , 0 , 113 , 0 ) , 24 ) ;
168+ const LIMITED_BROADCAST_NET : Ipv4Net = Ipv4Net :: new_assert ( Ipv4Addr :: BROADCAST , 32 ) ;
169+ const IPV6_ULA_NET : Ipv6Net = Ipv6Net :: new_assert ( Ipv6Addr :: new ( 0xfc00 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ) , 7 ) ;
170+
171+ const NON_HARD_INTERNAL_V4_NETS : [ Ipv4Net ; 10 ] = [
172+ RFC1918_10_NET ,
173+ RFC1918_172_NET ,
174+ RFC1918_192_NET ,
175+ CGNAT_NET ,
176+ IETF_PROTOCOL_ASSIGNMENTS_NET ,
177+ TEST_NET_1 ,
178+ BENCHMARKING_NET ,
179+ TEST_NET_2 ,
180+ TEST_NET_3 ,
181+ LIMITED_BROADCAST_NET ,
182+ ] ;
183+
184+ fn ipv4_nets_intersect ( left : Ipv4Net , right : Ipv4Net ) -> bool {
185+ left. network ( ) <= right. broadcast ( ) && right. network ( ) <= left. broadcast ( )
186+ }
187+
188+ fn ipv6_nets_intersect ( left : Ipv6Net , right : Ipv6Net ) -> bool {
189+ left. network ( ) <= right. broadcast ( ) && right. network ( ) <= left. broadcast ( )
190+ }
191+
192+ fn ipv4_net_to_mapped_ipv6 ( net : Ipv4Net ) -> Ipv6Net {
193+ Ipv6Net :: new_assert ( net. network ( ) . to_ipv6_mapped ( ) , 96_u8 + net. prefix_len ( ) )
194+ }
195+
158196/// Check if an IP address is internal (loopback, private RFC 1918, link-local,
159197/// or unspecified).
160198///
@@ -188,6 +226,26 @@ pub fn is_internal_ip(ip: IpAddr) -> bool {
188226 }
189227}
190228
229+ /// Check if a CIDR network intersects any address range classified by
230+ /// [`is_internal_ip`].
231+ pub fn is_internal_net ( net : IpNet ) -> bool {
232+ if is_always_blocked_net ( net) {
233+ return true ;
234+ }
235+
236+ match net {
237+ IpNet :: V4 ( net) => NON_HARD_INTERNAL_V4_NETS
238+ . iter ( )
239+ . any ( |internal| ipv4_nets_intersect ( net, * internal) ) ,
240+ IpNet :: V6 ( net) => {
241+ ipv6_nets_intersect ( net, IPV6_ULA_NET )
242+ || NON_HARD_INTERNAL_V4_NETS
243+ . iter ( )
244+ . any ( |internal| ipv6_nets_intersect ( net, ipv4_net_to_mapped_ipv6 ( * internal) ) )
245+ }
246+ }
247+ }
248+
191249/// IPv4 internal address check covering RFC 1918, CGNAT (RFC 6598), and other
192250/// special-use ranges that should never be reachable from sandbox egress.
193251fn is_internal_v4 ( v4 : Ipv4Addr ) -> bool {
@@ -382,90 +440,90 @@ mod tests {
382440
383441 #[ test]
384442 fn test_always_blocked_net_loopback_v4 ( ) {
385- let net: ipnet :: IpNet = "127.0.0.0/8" . parse ( ) . unwrap ( ) ;
443+ let net: IpNet = "127.0.0.0/8" . parse ( ) . unwrap ( ) ;
386444 assert ! ( is_always_blocked_net( net) ) ;
387445 }
388446
389447 #[ test]
390448 fn test_always_blocked_net_link_local_v4 ( ) {
391- let net: ipnet :: IpNet = "169.254.0.0/16" . parse ( ) . unwrap ( ) ;
449+ let net: IpNet = "169.254.0.0/16" . parse ( ) . unwrap ( ) ;
392450 assert ! ( is_always_blocked_net( net) ) ;
393451 }
394452
395453 #[ test]
396454 fn test_always_blocked_net_unspecified_v4 ( ) {
397- let net: ipnet :: IpNet = "0.0.0.0/32" . parse ( ) . unwrap ( ) ;
455+ let net: IpNet = "0.0.0.0/32" . parse ( ) . unwrap ( ) ;
398456 assert ! ( is_always_blocked_net( net) ) ;
399457 }
400458
401459 #[ test]
402460 fn test_always_blocked_net_loopback_v6 ( ) {
403- let net: ipnet :: IpNet = "::1/128" . parse ( ) . unwrap ( ) ;
461+ let net: IpNet = "::1/128" . parse ( ) . unwrap ( ) ;
404462 assert ! ( is_always_blocked_net( net) ) ;
405463 }
406464
407465 #[ test]
408466 fn test_always_blocked_net_link_local_v6 ( ) {
409- let net: ipnet :: IpNet = "fe80::/10" . parse ( ) . unwrap ( ) ;
467+ let net: IpNet = "fe80::/10" . parse ( ) . unwrap ( ) ;
410468 assert ! ( is_always_blocked_net( net) ) ;
411469 }
412470
413471 #[ test]
414472 fn test_always_blocked_net_ipv4_mapped_v6_loopback ( ) {
415- let net: ipnet :: IpNet = "::ffff:127.0.0.1/128" . parse ( ) . unwrap ( ) ;
473+ let net: IpNet = "::ffff:127.0.0.1/128" . parse ( ) . unwrap ( ) ;
416474 assert ! ( is_always_blocked_net( net) ) ;
417475 }
418476
419477 #[ test]
420478 fn test_always_blocked_net_allows_rfc1918 ( ) {
421- let net10: ipnet :: IpNet = "10.0.0.0/8" . parse ( ) . unwrap ( ) ;
422- let net172: ipnet :: IpNet = "172.16.0.0/12" . parse ( ) . unwrap ( ) ;
423- let net192: ipnet :: IpNet = "192.168.0.0/16" . parse ( ) . unwrap ( ) ;
479+ let net10: IpNet = "10.0.0.0/8" . parse ( ) . unwrap ( ) ;
480+ let net172: IpNet = "172.16.0.0/12" . parse ( ) . unwrap ( ) ;
481+ let net192: IpNet = "192.168.0.0/16" . parse ( ) . unwrap ( ) ;
424482 assert ! ( !is_always_blocked_net( net10) ) ;
425483 assert ! ( !is_always_blocked_net( net172) ) ;
426484 assert ! ( !is_always_blocked_net( net192) ) ;
427485 }
428486
429487 #[ test]
430488 fn test_always_blocked_net_allows_public ( ) {
431- let net: ipnet :: IpNet = "8.8.8.0/24" . parse ( ) . unwrap ( ) ;
489+ let net: IpNet = "8.8.8.0/24" . parse ( ) . unwrap ( ) ;
432490 assert ! ( !is_always_blocked_net( net) ) ;
433491 }
434492
435493 #[ test]
436494 fn test_always_blocked_net_single_ip_loopback ( ) {
437- let net: ipnet :: IpNet = "127.0.0.1/32" . parse ( ) . unwrap ( ) ;
495+ let net: IpNet = "127.0.0.1/32" . parse ( ) . unwrap ( ) ;
438496 assert ! ( is_always_blocked_net( net) ) ;
439497 }
440498
441499 #[ test]
442500 fn test_always_blocked_net_single_ip_metadata ( ) {
443- let net: ipnet :: IpNet = "169.254.169.254/32" . parse ( ) . unwrap ( ) ;
501+ let net: IpNet = "169.254.169.254/32" . parse ( ) . unwrap ( ) ;
444502 assert ! ( is_always_blocked_net( net) ) ;
445503 }
446504
447505 #[ test]
448506 fn test_always_blocked_net_broad_cidr_containing_blocked ( ) {
449507 // 0.0.0.0/0 contains everything including unspecified, loopback, link-local
450- let net: ipnet :: IpNet = "0.0.0.0/0" . parse ( ) . unwrap ( ) ;
508+ let net: IpNet = "0.0.0.0/0" . parse ( ) . unwrap ( ) ;
451509 assert ! ( is_always_blocked_net( net) ) ;
452510 }
453511
454512 #[ test]
455513 fn test_always_blocked_net_v6_broad_containing_loopback ( ) {
456- let net: ipnet :: IpNet = "::/0" . parse ( ) . unwrap ( ) ;
514+ let net: IpNet = "::/0" . parse ( ) . unwrap ( ) ;
457515 assert ! ( is_always_blocked_net( net) ) ;
458516 }
459517
460518 #[ test]
461519 fn test_always_blocked_net_v6_ipv4_mapped_loopback_single ( ) {
462- let net: ipnet :: IpNet = "::ffff:127.0.0.1/128" . parse ( ) . unwrap ( ) ;
520+ let net: IpNet = "::ffff:127.0.0.1/128" . parse ( ) . unwrap ( ) ;
463521 assert ! ( is_always_blocked_net( net) ) ;
464522 }
465523
466524 #[ test]
467525 fn test_always_blocked_net_v6_ipv4_mapped_link_local_single ( ) {
468- let net: ipnet :: IpNet = "::ffff:169.254.0.1/128" . parse ( ) . unwrap ( ) ;
526+ let net: IpNet = "::ffff:169.254.0.1/128" . parse ( ) . unwrap ( ) ;
469527 assert ! ( is_always_blocked_net( net) ) ;
470528 }
471529
@@ -474,25 +532,82 @@ mod tests {
474532 // ::ffff:168.0.0.0/103 has a public network address (168.0.0.0) but
475533 // the range covers 168.0.0.0–169.255.255.255, which includes the
476534 // link-local block 169.254.0.0/16.
477- let net: ipnet :: IpNet = "::ffff:168.0.0.0/103" . parse ( ) . unwrap ( ) ;
535+ let net: IpNet = "::ffff:168.0.0.0/103" . parse ( ) . unwrap ( ) ;
478536 assert ! ( is_always_blocked_net( net) ) ;
479537 }
480538
481539 #[ test]
482540 fn test_always_blocked_net_v6_ipv4_mapped_broad_spans_loopback ( ) {
483541 // ::ffff:64.0.0.0/98 has a public network address (64.0.0.0) but the
484542 // range covers 64.0.0.0–127.255.255.255, which includes loopback.
485- let net: ipnet :: IpNet = "::ffff:64.0.0.0/98" . parse ( ) . unwrap ( ) ;
543+ let net: IpNet = "::ffff:64.0.0.0/98" . parse ( ) . unwrap ( ) ;
486544 assert ! ( is_always_blocked_net( net) ) ;
487545 }
488546
489547 #[ test]
490548 fn test_always_blocked_net_v6_ipv4_mapped_allows_public ( ) {
491549 // ::ffff:8.8.8.8/128 is a public address — should not be blocked.
492- let net: ipnet :: IpNet = "::ffff:8.8.8.8/128" . parse ( ) . unwrap ( ) ;
550+ let net: IpNet = "::ffff:8.8.8.8/128" . parse ( ) . unwrap ( ) ;
493551 assert ! ( !is_always_blocked_net( net) ) ;
494552 }
495553
554+ // -- is_internal_net --
555+
556+ #[ test]
557+ fn test_internal_net_rfc1918_contained_and_supernet ( ) {
558+ for cidr in [ "10.1.0.0/16" , "8.0.0.0/5" ] {
559+ assert ! ( is_internal_net( cidr. parse( ) . unwrap( ) ) , "{cidr}" ) ;
560+ }
561+ }
562+
563+ #[ test]
564+ fn test_internal_net_cgnat_contained_and_supernet ( ) {
565+ for cidr in [ "100.64.0.0/10" , "100.0.0.0/9" ] {
566+ assert ! ( is_internal_net( cidr. parse( ) . unwrap( ) ) , "{cidr}" ) ;
567+ }
568+ }
569+
570+ #[ test]
571+ fn test_internal_net_ipv4_special_use_ranges ( ) {
572+ for cidr in [
573+ "192.0.0.0/24" ,
574+ "192.0.2.0/24" ,
575+ "198.18.0.0/15" ,
576+ "198.51.100.0/24" ,
577+ "203.0.113.0/24" ,
578+ "255.255.255.255/32" ,
579+ ] {
580+ assert ! ( is_internal_net( cidr. parse( ) . unwrap( ) ) , "{cidr}" ) ;
581+ }
582+ }
583+
584+ #[ test]
585+ fn test_internal_net_ipv6_ula ( ) {
586+ for cidr in [ "fc00::/7" , "fd00::/8" ] {
587+ assert ! ( is_internal_net( cidr. parse( ) . unwrap( ) ) , "{cidr}" ) ;
588+ }
589+ }
590+
591+ #[ test]
592+ fn test_internal_net_ipv4_mapped_cgnat_supernet ( ) {
593+ let net: IpNet = "::ffff:100.0.0.0/105" . parse ( ) . unwrap ( ) ;
594+ assert ! ( is_internal_net( net) ) ;
595+ }
596+
597+ #[ test]
598+ fn test_internal_net_always_blocked ( ) {
599+ for cidr in [ "127.0.0.0/8" , "fe80::/10" ] {
600+ assert ! ( is_internal_net( cidr. parse( ) . unwrap( ) ) , "{cidr}" ) ;
601+ }
602+ }
603+
604+ #[ test]
605+ fn test_internal_net_allows_large_public_ranges ( ) {
606+ for cidr in [ "8.0.0.0/8" , "::ffff:8.0.0.0/104" ] {
607+ assert ! ( !is_internal_net( cidr. parse( ) . unwrap( ) ) , "{cidr}" ) ;
608+ }
609+ }
610+
496611 // -- is_internal_ip --
497612
498613 #[ test]
0 commit comments