From 1cd76fcd951577b997833ab81b963383a29c734b Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Sun, 20 Sep 2026 17:15:57 +0200 Subject: [PATCH] test(06-08): add failing transition address regressions - Cover private and public IPv4 embeddings across NAT64 and 6to4 - Exercise unsafe transition literals through the production dial control - Require classifier-owned IPv4-mapped normalization --- fetchguard/fetch_test.go | 30 ++++++++++++++++++++++++++++++ fetchguard/ip_test.go | 39 ++++++++++++++++++++++++++++++++------- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/fetchguard/fetch_test.go b/fetchguard/fetch_test.go index d6b75dd..f50a21a 100644 --- a/fetchguard/fetch_test.go +++ b/fetchguard/fetch_test.go @@ -95,6 +95,36 @@ func TestFetchPrivateIPBlockedInBothModes(t *testing.T) { }) } +func TestDialControlRejectsUnsafeIPv6Transitions(t *testing.T) { + tests := []struct { + name string + ip string + }{ + {name: "nat64 well-known loopback", ip: "64:ff9b::7f00:1"}, + {name: "nat64 well-known rfc1918", ip: "64:ff9b::a00:1"}, + {name: "nat64 well-known metadata", ip: "64:ff9b::a9fe:a9fe"}, + {name: "nat64 local-use loopback", ip: "64:ff9b:1:7f00:0:100::"}, + {name: "nat64 local-use rfc1918", ip: "64:ff9b:1:a00:0:100::"}, + {name: "nat64 local-use metadata", ip: "64:ff9b:1:a9fe:a9:fe00::"}, + {name: "6to4 loopback", ip: "2002:7f00:1::"}, + {name: "6to4 rfc1918", ip: "2002:a00:1::"}, + {name: "6to4 metadata", ip: "2002:a9fe:a9fe::"}, + } + + control := dialControl(Policy{Mode: PublicOnlyMode}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := control("tcp6", "["+tt.ip+"]:443", nil) + if !errors.Is(err, errPrivateIP) { + t.Fatalf("dialControl(%s) error = %v, want errPrivateIP", tt.ip, err) + } + if got := mapTransportError(err).Reason; got != ReasonPrivateIP { + t.Fatalf("mapTransportError(%s) reason = %q, want %q", tt.ip, got, ReasonPrivateIP) + } + }) + } +} + func TestFetchDoesNotFollowRedirect(t *testing.T) { var followed atomic.Bool mux := http.NewServeMux() diff --git a/fetchguard/ip_test.go b/fetchguard/ip_test.go index 4c5d270..d43a13d 100644 --- a/fetchguard/ip_test.go +++ b/fetchguard/ip_test.go @@ -10,9 +10,6 @@ func TestIsReservedOrPrivate(t *testing.T) { name string ip string want bool - // unmap documents that IPv4-mapped IPv6 literals are the CALLER's - // responsibility to Unmap() before classification (fetch.go dial hook). - unmap bool }{ {name: "loopback v4", ip: "127.0.0.1", want: true}, {name: "rfc1918 10/8", ip: "10.1.2.3", want: true}, @@ -29,7 +26,7 @@ func TestIsReservedOrPrivate(t *testing.T) { {name: "link-local v6", ip: "fe80::1", want: true}, {name: "unique-local v6", ip: "fc00::1", want: true}, {name: "public v6", ip: "2606:4700:4700::1111", want: false}, - {name: "v4-mapped metadata after Unmap", ip: "::ffff:169.254.169.254", want: true, unmap: true}, + {name: "v4-mapped metadata", ip: "::ffff:169.254.169.254", want: true}, {name: "multicast v4", ip: "224.0.0.1", want: true}, {name: "unspecified v4", ip: "0.0.0.0", want: true}, {name: "just below 172.16.0.0/12", ip: "172.15.255.255", want: false}, @@ -42,9 +39,6 @@ func TestIsReservedOrPrivate(t *testing.T) { if err != nil { t.Fatalf("ParseAddr(%q): %v", tt.ip, err) } - if tt.unmap { - addr = addr.Unmap() - } got := isReservedOrPrivate(addr) if got != tt.want { t.Fatalf("isReservedOrPrivate(%s) = %v, want %v", addr, got, tt.want) @@ -52,3 +46,34 @@ func TestIsReservedOrPrivate(t *testing.T) { }) } } + +func TestIsReservedOrPrivateIPv6Transitions(t *testing.T) { + tests := []struct { + name string + ip string + want bool + }{ + {name: "nat64 well-known loopback", ip: "64:ff9b::7f00:1", want: true}, + {name: "nat64 well-known rfc1918", ip: "64:ff9b::a00:1", want: true}, + {name: "nat64 well-known metadata", ip: "64:ff9b::a9fe:a9fe", want: true}, + {name: "nat64 well-known public", ip: "64:ff9b::808:808", want: false}, + {name: "nat64 local-use loopback", ip: "64:ff9b:1:7f00:0:100::", want: true}, + {name: "nat64 local-use rfc1918", ip: "64:ff9b:1:a00:0:100::", want: true}, + {name: "nat64 local-use metadata", ip: "64:ff9b:1:a9fe:a9:fe00::", want: true}, + {name: "nat64 local-use public", ip: "64:ff9b:1:808:8:800::", want: false}, + {name: "nat64 local-use non-zero u octet", ip: "64:ff9b:1:7f00:100:100::", want: true}, + {name: "6to4 loopback", ip: "2002:7f00:1::", want: true}, + {name: "6to4 rfc1918", ip: "2002:a00:1::", want: true}, + {name: "6to4 metadata", ip: "2002:a9fe:a9fe::", want: true}, + {name: "6to4 public", ip: "2002:808:808::", want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + addr := netip.MustParseAddr(tt.ip) + if got := isReservedOrPrivate(addr); got != tt.want { + t.Fatalf("isReservedOrPrivate(%s) = %v, want %v", addr, got, tt.want) + } + }) + } +}