@@ -40,6 +40,38 @@ pub struct PeerResponse {
4040 pub reachable : bool ,
4141}
4242
43+ /// Extract an IPv4 address embedded in an IPv6 literal across the transition
44+ /// formats that carry one: IPv4-mapped (`::ffff:a.b.c.d`), IPv4-compatible
45+ /// (`::a.b.c.d`), 6to4 (`2002:WWXX:YYZZ::/16`), and the NAT64 well-known prefix
46+ /// (`64:ff9b::/96`). Returns `None` for native IPv6. Callers fold the result
47+ /// back through the v4 range checks so loopback/private addresses smuggled in
48+ /// via any of these encodings are rejected, not just the mapped/compatible pair.
49+ ///
50+ /// INCOMPLETE-NAT64 CONTRACT: non-well-known NAT64 prefixes (e.g. the RFC 8215
51+ /// local-use `64:ff9b:1::/48`) are deliberately **not** decoded here — their v4
52+ /// sits at a prefix-length-dependent offset (RFC 6052 §2.2) — so they return
53+ /// `None`. Any caller that needs them blocked must reject the wider
54+ /// `64:ff9b::/32` itself; `is_public_http_url` does this in its native-v6 arm.
55+ fn embedded_ipv4 ( v6 : std:: net:: Ipv6Addr ) -> Option < std:: net:: Ipv4Addr > {
56+ if let Some ( v4) = v6. to_ipv4_mapped ( ) . or_else ( || v6. to_ipv4 ( ) ) {
57+ return Some ( v4) ;
58+ }
59+ let s = v6. segments ( ) ;
60+ // 6to4 (2002::/16) embeds W.X.Y.Z in the next two segments.
61+ if s[ 0 ] == 0x2002 {
62+ let [ a, b] = s[ 1 ] . to_be_bytes ( ) ;
63+ let [ c, d] = s[ 2 ] . to_be_bytes ( ) ;
64+ return Some ( std:: net:: Ipv4Addr :: new ( a, b, c, d) ) ;
65+ }
66+ // NAT64 well-known prefix (64:ff9b::/96) embeds the IPv4 in the low 32 bits.
67+ if s[ 0 ] == 0x0064 && s[ 1 ] == 0xff9b && s[ 2 ..6 ] == [ 0 , 0 , 0 , 0 ] {
68+ let [ a, b] = s[ 6 ] . to_be_bytes ( ) ;
69+ let [ c, d] = s[ 7 ] . to_be_bytes ( ) ;
70+ return Some ( std:: net:: Ipv4Addr :: new ( a, b, c, d) ) ;
71+ }
72+ None
73+ }
74+
4375/// Whether a peer `http_url` is a public http(s) endpoint safe to register.
4476/// Rejects non-http(s) schemes, loopback/unspecified/private/link-local IPs,
4577/// and `localhost` / `.local` / `.internal` hostnames. Used at announce time
@@ -78,15 +110,12 @@ pub fn is_public_http_url(raw: &str) -> bool {
78110 if ip. is_loopback ( ) || ip. is_unspecified ( ) {
79111 return false ;
80112 }
81- // Fold IPv4-mapped/compatible IPv6 (`::ffff:127.0.0.1`, `::127.0.0.1`)
82- // down to IPv4 so the v4 range checks catch loopback/private addresses
83- // smuggled in via an IPv6 literal, then re-check loopback/unspecified.
113+ // Fold any IPv6 literal that embeds an IPv4 address (mapped, compatible,
114+ // 6to4, NAT64) down to that IPv4 so the v4 range checks catch
115+ // loopback/private addresses smuggled in via an IPv6 encoding, then
116+ // re-check loopback/unspecified.
84117 let ip = match ip {
85- std:: net:: IpAddr :: V6 ( v6) => v6
86- . to_ipv4_mapped ( )
87- . or_else ( || v6. to_ipv4 ( ) )
88- . map ( std:: net:: IpAddr :: V4 )
89- . unwrap_or ( ip) ,
118+ std:: net:: IpAddr :: V6 ( v6) => embedded_ipv4 ( v6) . map ( std:: net:: IpAddr :: V4 ) . unwrap_or ( ip) ,
90119 v4 => v4,
91120 } ;
92121 if ip. is_loopback ( ) || ip. is_unspecified ( ) {
@@ -95,8 +124,14 @@ pub fn is_public_http_url(raw: &str) -> bool {
95124 match ip {
96125 std:: net:: IpAddr :: V4 ( v4) => {
97126 let o = v4. octets ( ) ;
98- // RFC1918 private, link-local, or CGNAT (100.64.0.0/10).
99- if v4. is_private ( ) || v4. is_link_local ( ) || ( o[ 0 ] == 100 && ( o[ 1 ] & 0xc0 ) == 64 ) {
127+ // RFC1918 private, link-local, CGNAT (100.64.0.0/10), or the
128+ // RFC1122 "this host" block 0.0.0.0/8 (never a valid destination;
129+ // 0.0.0.0 itself is already caught by the is_unspecified check).
130+ if v4. is_private ( )
131+ || v4. is_link_local ( )
132+ || ( o[ 0 ] == 100 && ( o[ 1 ] & 0xc0 ) == 64 )
133+ || o[ 0 ] == 0
134+ {
100135 return false ;
101136 }
102137 }
@@ -106,6 +141,16 @@ pub fn is_public_http_url(raw: &str) -> bool {
106141 if ( s[ 0 ] & 0xfe00 ) == 0xfc00 || ( s[ 0 ] & 0xffc0 ) == 0xfe80 {
107142 return false ;
108143 }
144+ // Any NAT64 address (64:ff9b::/32) that is not the cleanly
145+ // decodable well-known /96 — e.g. the RFC 8215 local-use
146+ // 64:ff9b:1::/48 — carries a translated target whose embedded v4
147+ // sits at a prefix-length-dependent offset (RFC 6052 §2.2).
148+ // Rather than risk a wrong decode across every prefix length we
149+ // reject the whole NAT64 space here. The well-known /96 was
150+ // already folded to its v4 above and never reaches this arm.
151+ if s[ 0 ] == 0x0064 && s[ 1 ] == 0xff9b {
152+ return false ;
153+ }
109154 }
110155 }
111156 }
@@ -425,6 +470,11 @@ mod tests {
425470 "http://127.0.0.1:5432/" ,
426471 "http://localhost:22/" ,
427472 "http://0.0.0.0:7545" ,
473+ // RFC1122 "this host" block 0.0.0.0/8 beyond 0.0.0.0 itself,
474+ // including the upper boundary (1.0.0.0 is public, tested below).
475+ "http://0.0.0.1:7545" ,
476+ "http://0.1.2.3/" ,
477+ "http://0.255.255.255/" ,
428478 "http://10.0.0.5:7545" ,
429479 "http://192.168.1.10/" ,
430480 "http://172.16.0.1:7545" ,
@@ -445,15 +495,46 @@ mod tests {
445495 "http://[::ffff:10.0.0.1]/" ,
446496 "http://[::ffff:192.168.1.1]:7545" ,
447497 "http://[::127.0.0.1]/" ,
498+ // 6to4 (2002::/16) embedding loopback/private v4.
499+ "http://[2002:7f00:1::]:7545" ,
500+ "http://[2002:a00:1::]/" ,
501+ "http://[2002:c0a8:101::]/" ,
502+ // NAT64 well-known prefix (64:ff9b::/96) embedding loopback/private v4.
503+ "http://[64:ff9b::7f00:1]/" ,
504+ "http://[64:ff9b::a00:1]:7545" ,
505+ // NAT64 local-use prefix (64:ff9b:1::/48, RFC 8215) — rejected
506+ // outright rather than decoded, so the whole NAT64 space is closed.
507+ "http://[64:ff9b:1::7f00:1]/" ,
508+ "http://[64:ff9b:1::a00:1]:7545" ,
509+ "http://[64:ff9b:1:a00::]/" ,
510+ // Local-use NAT64 wrapping a *public* v4 (203.0.113.10) is still
511+ // rejected: the block is prefix-based, not payload-based.
512+ "http://[64:ff9b:1::cb00:710a]/" ,
448513 ] {
449514 assert ! ( !is_public_http_url( bad) , "{bad:?} must be rejected" ) ;
450515 }
451516 }
452517
518+ #[ test]
519+ fn accepts_6to4_and_nat64_wrapping_public_v4 ( ) {
520+ // Fold-and-recheck stays consistent with the mapped/compatible handling:
521+ // a transition address that wraps a public v4 is allowed, only the
522+ // private/loopback-wrapping forms above are rejected. 203.0.113.10.
523+ assert ! ( is_public_http_url( "http://[2002:cb00:710a::]/" ) ) ;
524+ assert ! ( is_public_http_url( "http://[64:ff9b::cb00:710a]/" ) ) ;
525+ }
526+
453527 #[ test]
454528 fn accepts_public_outside_cgnat_range ( ) {
455529 // 100.0.0.0/10 boundary: .63 and .128 are public, only 100.64-127 is CGNAT.
456530 assert ! ( is_public_http_url( "http://100.63.255.255:7545" ) ) ;
457531 assert ! ( is_public_http_url( "http://100.128.0.1/" ) ) ;
458532 }
533+
534+ #[ test]
535+ fn accepts_first_address_above_this_host_block ( ) {
536+ // 1.0.0.0 is the first address above the rejected 0.0.0.0/8 block;
537+ // pins the `o[0] == 0` check against an off-by-one into 1.x.
538+ assert ! ( is_public_http_url( "http://1.0.0.0/" ) ) ;
539+ }
459540}
0 commit comments