Files
summercms/fetchguard/ip.go
Jakub Zych 99fa932d43 fix(06-08): classify IPv6 transition addresses
- Decode embedded IPv4 from NAT64 well-known and local-use prefixes
- Reapply private and reserved IPv4 policy to 6to4 destinations
- Fail closed on malformed RFC 6052 local-use encodings
2026-09-20 18:14:12 +02:00

79 lines
2.3 KiB
Go

package fetchguard
import "net/netip"
// privateV4 is a literal port of ManualCoverUrlFetcher.php PRIVATE_V4_CIDRS.
var privateV4 = []netip.Prefix{
netip.MustParsePrefix("127.0.0.0/8"),
netip.MustParsePrefix("10.0.0.0/8"),
netip.MustParsePrefix("172.16.0.0/12"),
netip.MustParsePrefix("192.168.0.0/16"),
netip.MustParsePrefix("169.254.0.0/16"),
netip.MustParsePrefix("100.64.0.0/10"),
netip.MustParsePrefix("0.0.0.0/8"),
}
// privateV6 is a literal port of PRIVATE_V6_PREFIXES. PHP lists bare "::1"
// as a prefix-less loopback literal; it is expressed here as ::1/128 so
// Prefix.Contains works uniformly with the CIDR entries.
var privateV6 = []netip.Prefix{
netip.MustParsePrefix("::1/128"),
netip.MustParsePrefix("fe80::/10"),
netip.MustParsePrefix("fc00::/7"),
}
var (
nat64WellKnownPrefix = netip.MustParsePrefix("64:ff9b::/96")
nat64LocalUsePrefix = netip.MustParsePrefix("64:ff9b:1::/48")
sixToFourPrefix = netip.MustParsePrefix("2002::/16")
)
// isReservedOrPrivate classifies addr against the PHP private/loopback/
// reserved/CGNAT table, including IPv4 embedded in supported IPv6 transition
// formats.
func isReservedOrPrivate(addr netip.Addr) bool {
if !addr.IsValid() {
return true
}
addr = addr.Unmap()
if addr.IsMulticast() || addr.IsUnspecified() {
return true
}
if embedded, ok := embeddedTransitionIPv4(addr); ok {
return isReservedOrPrivate(embedded)
}
table := privateV4
if !addr.Is4() {
table = privateV6
}
for _, prefix := range table {
if prefix.Contains(addr) {
return true
}
}
return false
}
// embeddedTransitionIPv4 extracts IPv4 from the transition formats supported
// by fetchguard. A recognized but malformed RFC 6052 /48 address returns an
// invalid address with ok=true so the classifier fails closed.
func embeddedTransitionIPv4(addr netip.Addr) (netip.Addr, bool) {
if !addr.Is6() {
return netip.Addr{}, false
}
b := addr.As16()
switch {
case nat64WellKnownPrefix.Contains(addr):
return netip.AddrFrom4([4]byte{b[12], b[13], b[14], b[15]}), true
case nat64LocalUsePrefix.Contains(addr):
if b[8] != 0 {
return netip.Addr{}, true
}
return netip.AddrFrom4([4]byte{b[6], b[7], b[9], b[10]}), true
case sixToFourPrefix.Contains(addr):
return netip.AddrFrom4([4]byte{b[2], b[3], b[4], b[5]}), true
default:
return netip.Addr{}, false
}
}