fix(outbound): ignore IPv6 zone identifiers when classifying addresses

This commit is contained in:
Julien Neuhart
2026-09-13 10:15:07 +02:00
parent 430ed38cc5
commit 8e850b1c93
2 changed files with 41 additions and 1 deletions

View File

@@ -109,11 +109,18 @@ var nonPublicIPv4Prefixes = []netip.Prefix{
// the prefixes themselves are deprecated or translation-only. See // the prefixes themselves are deprecated or translation-only. See
// [nonPublicIPv6Prefixes] and [nonPublicIPv4Prefixes] for the full lists // [nonPublicIPv6Prefixes] and [nonPublicIPv4Prefixes] for the full lists
// and rationale. // and rationale.
//
// An IPv6 zone identifier is ignored, so [::%1] classifies the same as [::].
func IsPublicIP(addr netip.Addr) bool { func IsPublicIP(addr netip.Addr) bool {
if !addr.IsValid() { if !addr.IsValid() {
return false return false
} }
addr = addr.Unmap() // A zone does not change where a non-link-local address routes, but
// [netip.Prefix.Contains] never matches a zoned address and
// [netip.Addr.IsUnspecified] compares the zone too. Keeping it would let
// http://[::%251]/ or any zoned address in the prefixes below pass as
// public.
addr = addr.WithZone("").Unmap()
switch { switch {
case addr.IsLoopback(), case addr.IsLoopback(),
addr.IsPrivate(), addr.IsPrivate(),

View File

@@ -103,6 +103,23 @@ func TestIsPublicIP(t *testing.T) {
// Discard prefix (RFC 6666). // Discard prefix (RFC 6666).
{"100::1", false}, {"100::1", false},
// A zone identifier must not change the classification.
{"::%1", false},
{"::%lo", false},
{"::1%1", false},
{"fe80::1%eth0", false},
{"fc00::1%1", false},
{"::ffff:127.0.0.1%1", false},
{"fec0::1%eth0", false},
{"2002:a9fe:a9fe::%1", false},
{"2001:0:abcd:ef12:3456:7890:a9fe:a9fe%1", false},
{"64:ff9b::a9fe:a9fe%1", false},
{"64:ff9b:1::a9fe:a9fe%1", false},
{"::a9fe:a9fe%1", false},
{"2001:db8::1%1", false},
{"100::1%1", false},
{"2606:4700:4700::1111%1", true},
} { } {
t.Run(tc.addr, func(t *testing.T) { t.Run(tc.addr, func(t *testing.T) {
addr, err := netip.ParseAddr(tc.addr) addr, err := netip.ParseAddr(tc.addr)
@@ -304,6 +321,22 @@ func TestFilterOutboundURL(t *testing.T) {
expectErr: true, expectErr: true,
expectIs: ErrFiltered, expectIs: ErrFiltered,
}, },
{
scenario: "zoned unspecified address blocked with deny-private-ips",
rawURL: "http://[::%251]:9999/",
deny: defaultDeny,
opts: []DecideOption{WithDenyPrivateIPs(true)},
expectErr: true,
expectIs: ErrFiltered,
},
{
scenario: "zoned site-local address blocked with deny-private-ips",
rawURL: "http://[fec0:1234::3%25eth0]:8080/",
deny: defaultDeny,
opts: []DecideOption{WithDenyPrivateIPs(true)},
expectErr: true,
expectIs: ErrFiltered,
},
} { } {
t.Run(tc.scenario, func(t *testing.T) { t.Run(tc.scenario, func(t *testing.T) {
if tc.stub != nil { if tc.stub != nil {