From 440a1922b2c6da12b94f725a8433d50ed0a7163c Mon Sep 17 00:00:00 2001 From: Michal Treter Date: Wed, 10 Jun 2026 13:14:55 +0200 Subject: [PATCH] Match rule ranges against the same address family Guard addrIsInRuleRange so an IPv4-mapped IPv6 range (e.g. "::ffff:0:0/96") no longer collapses to "0.0.0.0/0" and match every IPv4 address. Add a test covering this case along with basic match and port matching. Co-authored-by: Cursor --- pkg/smokescreen/smokescreen.go | 15 ++++++++-- pkg/smokescreen/smokescreen_test.go | 43 +++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/pkg/smokescreen/smokescreen.go b/pkg/smokescreen/smokescreen.go index fc04fbe3..ebb0667e 100644 --- a/pkg/smokescreen/smokescreen.go +++ b/pkg/smokescreen/smokescreen.go @@ -67,6 +67,7 @@ const ( LogMitmReqMethod = "mitm_req_method" LogMitmReqHeaders = "mitm_req_headers" ) + type ipType int type ACLDecision struct { @@ -182,13 +183,23 @@ const roleHeader = "X-Smokescreen-Role" const traceHeader = "X-Smokescreen-Trace-ID" func addrIsInRuleRange(ranges []RuleRange, addr *net.TCPAddr) bool { + addrIsV4 := addr.IP.To4() != nil for _, rng := range ranges { // If the range specifies a port and the port doesn't match, // then this range doesn't match if rng.Port != 0 && addr.Port != rng.Port { continue } - + // Match only within the same address family. The range's declared + // family is determined from its parsed mask length rather than To4(), + // because IPv4-mapped IPv6 ranges (e.g. "::ffff:0:0/96") would otherwise + // collapse to "0.0.0.0/0" via net.IPNet.Contains -> To4() and match every + // IPv4 address. IPv4-mapped destinations are converted to IPv4 elsewhere, + // so an IPv6-written range should never match an IPv4 address. + rangeIsV4 := len(rng.Net.Mask) == net.IPv4len + if rangeIsV4 != addrIsV4 { + continue + } if rng.Net.Contains(addr.IP) { return true } @@ -655,7 +666,7 @@ func rejectResponse(pctx *goproxy.ProxyCtx, err error) *http.Response { resp.ProtoMajor = pctx.Req.ProtoMajor resp.ProtoMinor = pctx.Req.ProtoMinor resp.Header.Set(errorHeader, msg) - + // Add Retry-After header for tunnel limit errors if _, ok := err.(tunnelLimitError); ok { resp.Header.Set("Retry-After", "1") diff --git a/pkg/smokescreen/smokescreen_test.go b/pkg/smokescreen/smokescreen_test.go index 802b7a17..ec95ffcd 100644 --- a/pkg/smokescreen/smokescreen_test.go +++ b/pkg/smokescreen/smokescreen_test.go @@ -2200,3 +2200,46 @@ func TestMaxConcurrentConnectTunnels(t *testing.T) { } }) } + +func TestAddrIsInRuleRange(t *testing.T) { + mustParseCIDR := func(s string) net.IPNet { + _, n, err := net.ParseCIDR(s) + require.NoError(t, err) + return *n + } + + tests := []struct { + name string + ranges []RuleRange + addr *net.TCPAddr + expected bool + }{ + { + name: "IPv4 within range", + ranges: []RuleRange{{Net: mustParseCIDR("10.0.0.0/8")}}, + addr: &net.TCPAddr{IP: net.ParseIP("10.1.2.3"), Port: 80}, + expected: true, + }, + { + name: "non-matching port", + ranges: []RuleRange{{Net: mustParseCIDR("10.0.0.0/8"), Port: 443}}, + addr: &net.TCPAddr{IP: net.ParseIP("10.1.2.3"), Port: 80}, + expected: false, + }, + { + // The address-family guard: an IPv4-mapped IPv6 range collapses to + // "0.0.0.0/0" via net.IPNet.Contains, so without the guard this would + // wrongly match every IPv4 address. + name: "IPv4-mapped IPv6 range does not match IPv4 address", + ranges: []RuleRange{{Net: mustParseCIDR("::ffff:0:0/96")}}, + addr: &net.TCPAddr{IP: net.ParseIP("8.8.8.8"), Port: 80}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, addrIsInRuleRange(tt.ranges, tt.addr)) + }) + } +}