diff --git a/fetchguard/fetch_test.go b/fetchguard/fetch_test.go index 1e743b2..300b846 100644 --- a/fetchguard/fetch_test.go +++ b/fetchguard/fetch_test.go @@ -306,10 +306,43 @@ func withTestLoopback(srv *httptest.Server, p Policy) Policy { } func TestDialControlRejectsZonedAndSpecialUse(t *testing.T) { - ctl := dialControl(Policy{}) - for _, a := range []string{"[fe80::1%eth0]:443", "198.18.0.1:443"} { - if err := ctl("tcp", a, nil); !errors.Is(err, errPrivateIP) { - t.Errorf("%s: got %v", a, err) - } + probes := []string{"[fe80::1%eth0]:443", "198.18.0.1:443", "192.0.0.1:443", "240.0.0.1:443"} + ctl := dialControl(Policy{Mode: PublicOnlyMode}) + for _, a := range probes { + t.Run(a, func(t *testing.T) { + err := ctl("tcp", a, nil) + if !errors.Is(err, errPrivateIP) { + t.Fatalf("dialControl(%s) = %v, want errPrivateIP", a, err) + } + if got := mapTransportError(err).Reason; got != ReasonPrivateIP { + t.Fatalf("reason = %q, want %q", got, ReasonPrivateIP) + } + }) + } + if err := ctl("tcp", "8.8.8.8:443", nil); err != nil { + t.Fatalf("public address rejected: %v", err) + } + if err := ctl("tcp", "[2606:4700:4700::1111]:443", nil); err != nil { + t.Fatalf("public v6 rejected: %v", err) + } + if err := ctl("tcp", "no-port", nil); err == nil { + t.Fatal("address without port accepted") + } + if err := ctl("tcp", "example.com:443", nil); err == nil || errors.Is(err, errPrivateIP) { + t.Fatalf("hostname dial target: err = %v, want unparseable error", err) + } + if err := dialControl(Policy{skipReservedCheck: true})("tcp", "127.0.0.1:1", nil); err != nil { + t.Fatalf("skipReservedCheck: %v", err) + } +} + +func TestFetchPublicOnlyMapsSpecialUseToPrivateIP(t *testing.T) { + for _, u := range []string{"https://[fe80::1%25eth0]/", "https://198.18.0.1/", "https://192.0.0.1/", "https://240.0.0.1/"} { + t.Run(u, func(t *testing.T) { + _, err := Fetch(t.Context(), u, Policy{Mode: PublicOnlyMode, Timeout: 2 * time.Second, MaxBytes: 1024}, nil) + if got := reasonFrom(t, err); got != ReasonPrivateIP { + t.Fatalf("reason = %q, want %s (err %v)", got, ReasonPrivateIP, err) + } + }) } } diff --git a/fetchguard/ip_test.go b/fetchguard/ip_test.go index 832ed59..d77e555 100644 --- a/fetchguard/ip_test.go +++ b/fetchguard/ip_test.go @@ -90,3 +90,89 @@ func TestIsReservedOrPrivateSpecialUseSmoke(t *testing.T) { } } } + +func prefixBounds(p netip.Prefix) (first, last netip.Addr) { + first = p.Masked().Addr() + b := first.AsSlice() + for bit := p.Bits(); bit < len(b)*8; bit++ { + b[bit/8] |= 1 << (7 - bit%8) + } + last, _ = netip.AddrFromSlice(b) + return first, last +} + +func TestIsReservedOrPrivateIANABoundaries(t *testing.T) { + var all []netip.Prefix + all = append(all, privateV4...) + all = append(all, privateV6...) + inAny := func(a netip.Addr) bool { + for _, p := range all { + if p.Contains(a) { + return true + } + } + return false + } + for _, p := range all { + t.Run(p.String(), func(t *testing.T) { + first, last := prefixBounds(p) + interior := first.Next() + for _, a := range []netip.Addr{first, last, interior} { + if !p.Contains(a) { + t.Fatalf("test bug: %s not in %s", a, p) + } + if !isReservedOrPrivate(a) { + t.Errorf("%s (in %s) classified public", a, p) + } + } + for _, a := range []netip.Addr{first.Prev(), last.Next()} { + if !a.IsValid() || inAny(a) || a.IsMulticast() || a.IsUnspecified() { + continue + } + if _, ok := embeddedTransitionIPv4(a); ok { + continue + } + if isReservedOrPrivate(a) { + t.Errorf("neighbour %s of %s classified non-public", a, p) + } + } + }) + } + + explicit := []struct { + ip string + want bool + }{ + {"198.17.255.255", false}, {"198.18.0.0", true}, {"198.19.255.255", true}, {"198.20.0.0", false}, + {"191.255.255.255", false}, {"192.0.0.0", true}, {"192.0.1.0", false}, + {"239.255.255.255", true}, {"240.0.0.0", true}, {"255.255.255.255", true}, + {"100.63.255.255", false}, {"100.64.0.0", true}, {"100.127.255.255", true}, {"100.128.0.0", false}, + {"172.15.255.255", false}, {"172.32.0.0", false}, + {"2001:200::1", false}, {"2001:db8::", true}, {"2001:db9::", false}, + {"3fff:fff:ffff:ffff:ffff:ffff:ffff:ffff", true}, {"3fff:1000::", false}, + {"8.8.8.8", false}, {"1.1.1.1", false}, {"2606:4700:4700::1111", false}, + {"::ffff:127.0.0.1", true}, {"::ffff:10.0.0.1", true}, {"::ffff:198.18.0.1", true}, {"::ffff:8.8.8.8", false}, + } + for _, tc := range explicit { + if got := isReservedOrPrivate(netip.MustParseAddr(tc.ip)); got != tc.want { + t.Errorf("isReservedOrPrivate(%s) = %v, want %v", tc.ip, got, tc.want) + } + } + if !isReservedOrPrivate(netip.Addr{}) { + t.Error("invalid addr must fail closed") + } +} + +func TestIsReservedOrPrivateIgnoresZone(t *testing.T) { + for _, s := range []string{"fe80::1%eth0", "fe80::1%1", "fe80::1", "fc00::1%eth0"} { + if !isReservedOrPrivate(netip.MustParseAddr(s)) { + t.Errorf("%s should be non-public", s) + } + } + for _, s := range []string{"2606:4700:4700::1111%eth0", "2606:4700:4700::1111%2"} { + a := netip.MustParseAddr(s) + if isReservedOrPrivate(a) != isReservedOrPrivate(a.WithZone("")) || isReservedOrPrivate(a) { + t.Errorf("%s should classify as its unzoned public form", s) + } + } +} diff --git a/surf/bodylimit_test.go b/surf/bodylimit_test.go index e822137..df65d5f 100644 --- a/surf/bodylimit_test.go +++ b/surf/bodylimit_test.go @@ -267,3 +267,17 @@ func TestBodyLimitBoundsBodyConsumingMiddleware(t *testing.T) { } }) } + +func TestBodyLimitInvalidParamFailsBoot(t *testing.T) { + for _, param := range []string{"abc", "0", "-5", ""} { + t.Run(param, func(t *testing.T) { + cfg := writeHTTPConfig(t, "body_limits:\n default_bytes: 4\n upload_bytes: 8\n") + _, err := Assemble(backpack.New(cfg), []party.Plugin{bodyProbePlugin{ + id: "golem15.probe", use: []string{"body.limit:" + param}, + }}) + if err == nil || !strings.Contains(err.Error(), "body.limit") { + t.Fatalf("err = %v", err) + } + }) + } +} diff --git a/surf/limiter_test.go b/surf/limiter_test.go index 2ad6e01..a22c56d 100644 --- a/surf/limiter_test.go +++ b/surf/limiter_test.go @@ -471,3 +471,22 @@ func TestMiddlewareFailsClosed(t *testing.T) { }) } } + +func TestRegisterBucketRejectsNilLimiterEmptyNameAndDuplicate(t *testing.T) { + key := func(*http.Request) string { return "k" } + good := Bucket{Max: 1, Decay: time.Minute, Key: key} + var nilLim *FixedWindowLimiter + if err := nilLim.RegisterBucket("p", "n", good); err == nil { + t.Fatal("nil limiter accepted") + } + l := NewFixedWindowLimiter(NewMemoryStore(time.Minute), nil) + if err := l.RegisterBucket("p", "", good); err == nil { + t.Fatal("empty name accepted") + } + if err := l.RegisterBucket("p", "n", good); err != nil { + t.Fatal(err) + } + if err := l.RegisterBucket("q", "n", good); err == nil || !strings.Contains(err.Error(), "p") { + t.Fatalf("duplicate: %v", err) + } +}