From 0340b12734b15d75b2afd792c1fed0dbd00f3943 Mon Sep 17 00:00:00 2001 From: arm64v8a <48624112+arm64v8a@users.noreply.github.com> Date: Tue, 11 Oct 2022 14:29:20 +0800 Subject: [PATCH] Fix(freebsd): ReadUDPMsg nil pointer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ksco.he Co-authored-by: 秋のかえで Chore: bump google.golang.org/grpc from 1.49.0 to 1.50.0 (#2046) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> fix: socks4/4a client handshake (#1971) Feat: add transport original name to listen unix (#2048) Feat: add bind to device to Windows and Darwin (#1972) fix: Replace "math/rand" with "crypto/rand" in padding generation(#2032) Chore: github.com/lucas-clemente/quic-go from 0.29.0 to 0.29.1 (#2010) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> feat: Replace default Health Ping URL to HTTPS (#1991) Chore: update dependencies (#1995) Refactor: replace netaddr package with netipx (#1994) Fix: remove v2ctl from debian/rules (#1954) * Remove v2ctl from debian/rules It seems to be left over from https://github.com/v2fly/v2ray-core/pull/488 * Chore: use Go v1.19 to build debian package Co-authored-by: Loyalsoldier <10487845+Loyalsoldier@users.noreply.github.com> Chore: bump google.golang.org/grpc from 1.48.0 to 1.49.0 (#1935) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Chore: bump github.com/google/go-cmp from 0.5.8 to 0.5.9 (#1959) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Chore: bump github.com/jhump/protoreflect from 1.12.0 to 1.13.0 (#1982) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> feat: Implement Match and MatchAny for all MatcherGroup, IndexMatcher [common/strmatcher] Implement Match and MatchAny for all MatcherGroup and IndexMatcher --- app/observatory/burst/config.pb.go | 2 +- app/observatory/burst/config.proto | 4 +- app/observatory/burst/healthping.go | 5 +- app/router/condition_geoip.go | 20 ++- common/crypto/auth.go | 5 +- .../strmatcher/benchmark_indexmatcher_test.go | 58 +++++++ common/strmatcher/benchmark_matchers_test.go | 149 ++++++++++++++++ common/strmatcher/benchmark_test.go | 161 ------------------ common/strmatcher/indexmatcher_linear.go | 71 ++++++-- common/strmatcher/indexmatcher_mph.go | 33 +++- common/strmatcher/indexmatcher_mph_test.go | 94 ++++++++++ .../strmatcher/matchergroup_ac_automation.go | 18 +- common/strmatcher/matchergroup_domain.go | 150 ++++++++-------- common/strmatcher/matchergroup_domain_test.go | 4 +- common/strmatcher/matchergroup_full.go | 16 +- common/strmatcher/matchergroup_full_test.go | 4 +- common/strmatcher/matchergroup_mph.go | 15 +- common/strmatcher/matchergroup_simple.go | 7 +- common/strmatcher/matchergroup_substr.go | 24 ++- common/strmatcher/matchers.go | 111 +++++++++++- go.mod | 8 +- go.sum | 18 +- proxy/socks/client.go | 17 +- proxy/socks/protocol.go | 43 +++++ release/debian/rules | 2 - transport/internet/sockopt_darwin.go | 28 +++ transport/internet/sockopt_windows.go | 18 +- transport/internet/tcp_hub.go | 5 + transport/internet/udp/hub_freebsd.go | 18 +- 29 files changed, 766 insertions(+), 342 deletions(-) create mode 100644 common/strmatcher/benchmark_indexmatcher_test.go create mode 100644 common/strmatcher/benchmark_matchers_test.go delete mode 100644 common/strmatcher/benchmark_test.go create mode 100644 common/strmatcher/indexmatcher_mph_test.go diff --git a/app/observatory/burst/config.pb.go b/app/observatory/burst/config.pb.go index 21768852496..e6c988cdf1d 100644 --- a/app/observatory/burst/config.pb.go +++ b/app/observatory/burst/config.pb.go @@ -77,7 +77,7 @@ type HealthPingConfig struct { unknownFields protoimpl.UnknownFields // destination url, need 204 for success return - // default http://www.google.com/gen_204 + // default https://connectivitycheck.gstatic.com/generate_204 Destination string `protobuf:"bytes,1,opt,name=destination,proto3" json:"destination,omitempty"` // connectivity check url Connectivity string `protobuf:"bytes,2,opt,name=connectivity,proto3" json:"connectivity,omitempty"` diff --git a/app/observatory/burst/config.proto b/app/observatory/burst/config.proto index 6448ffcf039..64bbcd1551a 100644 --- a/app/observatory/burst/config.proto +++ b/app/observatory/burst/config.proto @@ -21,7 +21,7 @@ message Config { message HealthPingConfig { // destination url, need 204 for success return - // default http://www.google.com/gen_204 + // default https://connectivitycheck.gstatic.com/generate_204 string destination = 1; // connectivity check url string connectivity = 2; @@ -31,4 +31,4 @@ message HealthPingConfig { int32 samplingCount = 4; // ping timeout, int64 values of time.Duration int64 timeout = 5; -} \ No newline at end of file +} diff --git a/app/observatory/burst/healthping.go b/app/observatory/burst/healthping.go index f4a0596e60f..67bec690455 100644 --- a/app/observatory/burst/healthping.go +++ b/app/observatory/burst/healthping.go @@ -43,7 +43,10 @@ func NewHealthPing(ctx context.Context, config *HealthPingConfig) *HealthPing { } } if settings.Destination == "" { - settings.Destination = "http://www.google.com/gen_204" + // Destination URL, need 204 for success return default to chromium + // https://github.com/chromium/chromium/blob/main/components/safety_check/url_constants.cc#L10 + // https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/safety_check/url_constants.cc#10 + settings.Destination = "https://connectivitycheck.gstatic.com/generate_204" } if settings.Interval == 0 { settings.Interval = time.Duration(1) * time.Minute diff --git a/app/router/condition_geoip.go b/app/router/condition_geoip.go index 762caa79db0..78746ebbf0b 100644 --- a/app/router/condition_geoip.go +++ b/app/router/condition_geoip.go @@ -1,7 +1,9 @@ package router import ( - "inet.af/netaddr" + "net/netip" + + "go4.org/netipx" "github.com/v2fly/v2ray-core/v5/app/router/routercommon" "github.com/v2fly/v2ray-core/v5/common/net" @@ -10,18 +12,20 @@ import ( type GeoIPMatcher struct { countryCode string reverseMatch bool - ip4 *netaddr.IPSet - ip6 *netaddr.IPSet + ip4 *netipx.IPSet + ip6 *netipx.IPSet } func (m *GeoIPMatcher) Init(cidrs []*routercommon.CIDR) error { - var builder4, builder6 netaddr.IPSetBuilder + var builder4, builder6 netipx.IPSetBuilder for _, cidr := range cidrs { - netaddrIP, ok := netaddr.FromStdIP(net.IP(cidr.GetIp())) + netaddrIP, ok := netip.AddrFromSlice(cidr.GetIp()) if !ok { return newError("invalid IP address ", cidr) } - ipPrefix := netaddr.IPPrefixFrom(netaddrIP, uint8(cidr.GetPrefix())) + netaddrIP = netaddrIP.Unmap() + ipPrefix := netip.PrefixFrom(netaddrIP, int(cidr.GetPrefix())) + switch { case netaddrIP.Is4(): builder4.AddPrefix(ipPrefix) @@ -48,7 +52,7 @@ func (m *GeoIPMatcher) SetReverseMatch(isReverseMatch bool) { } func (m *GeoIPMatcher) match4(ip net.IP) bool { - nip, ok := netaddr.FromStdIP(ip) + nip, ok := netipx.FromStdIP(ip) if !ok { return false } @@ -56,7 +60,7 @@ func (m *GeoIPMatcher) match4(ip net.IP) bool { } func (m *GeoIPMatcher) match6(ip net.IP) bool { - nip, ok := netaddr.FromStdIP(ip) + nip, ok := netipx.FromStdIP(ip) if !ok { return false } diff --git a/common/crypto/auth.go b/common/crypto/auth.go index 6e6c3e92db6..2a04ae5c386 100644 --- a/common/crypto/auth.go +++ b/common/crypto/auth.go @@ -2,8 +2,8 @@ package crypto import ( "crypto/cipher" + "crypto/rand" "io" - "math/rand" "github.com/v2fly/v2ray-core/v5/common" "github.com/v2fly/v2ray-core/v5/common/buf" @@ -270,7 +270,8 @@ func (w *AuthenticationWriter) seal(b []byte) (*buf.Buffer, error) { return nil, err } if paddingSize > 0 { - // With size of the chunk and padding length encrypted, the content of padding doesn't matter much. + // These paddings will send in clear text. + // To avoid leakage of PRNG internal state, a cryptographically secure PRNG should be used. paddingBytes := eb.Extend(paddingSize) common.Must2(rand.Read(paddingBytes)) } diff --git a/common/strmatcher/benchmark_indexmatcher_test.go b/common/strmatcher/benchmark_indexmatcher_test.go new file mode 100644 index 00000000000..8ca1a223bbc --- /dev/null +++ b/common/strmatcher/benchmark_indexmatcher_test.go @@ -0,0 +1,58 @@ +package strmatcher_test + +import ( + "testing" + + . "github.com/v2fly/v2ray-core/v5/common/strmatcher" +) + +func BenchmarkLinearIndexMatcher(b *testing.B) { + benchmarkIndexMatcher(b, func() IndexMatcher { + return NewLinearIndexMatcher() + }) +} + +func BenchmarkMphIndexMatcher(b *testing.B) { + benchmarkIndexMatcher(b, func() IndexMatcher { + return NewMphIndexMatcher() + }) +} + +func benchmarkIndexMatcher(b *testing.B, ctor func() IndexMatcher) { + b.Run("Match", func(b *testing.B) { + b.Run("Domain------------", func(b *testing.B) { + benchmarkMatch(b, ctor(), map[Type]bool{Domain: true}) + }) + b.Run("Domain+Full-------", func(b *testing.B) { + benchmarkMatch(b, ctor(), map[Type]bool{Domain: true, Full: true}) + }) + b.Run("Domain+Full+Substr", func(b *testing.B) { + benchmarkMatch(b, ctor(), map[Type]bool{Domain: true, Full: true, Substr: true}) + }) + b.Run("All-Fail----------", func(b *testing.B) { + benchmarkMatch(b, ctor(), map[Type]bool{Domain: false, Full: false, Substr: false}) + }) + }) + b.Run("Match/Dotless", func(b *testing.B) { // Dotless domain matcher automatically inserted in DNS app when "localhost" DNS is used. + b.Run("All-Succ", func(b *testing.B) { + benchmarkMatch(b, ctor(), map[Type]bool{Domain: true, Full: true, Substr: true, Regex: true}) + }) + b.Run("All-Fail", func(b *testing.B) { + benchmarkMatch(b, ctor(), map[Type]bool{Domain: false, Full: false, Substr: false, Regex: false}) + }) + }) + b.Run("MatchAny", func(b *testing.B) { + b.Run("First-Full--", func(b *testing.B) { + benchmarkMatchAny(b, ctor(), map[Type]bool{Full: true, Domain: true, Substr: true}) + }) + b.Run("First-Domain", func(b *testing.B) { + benchmarkMatchAny(b, ctor(), map[Type]bool{Full: false, Domain: true, Substr: true}) + }) + b.Run("First-Substr", func(b *testing.B) { + benchmarkMatchAny(b, ctor(), map[Type]bool{Full: false, Domain: false, Substr: true}) + }) + b.Run("All-Fail----", func(b *testing.B) { + benchmarkMatchAny(b, ctor(), map[Type]bool{Full: false, Domain: false, Substr: false}) + }) + }) +} diff --git a/common/strmatcher/benchmark_matchers_test.go b/common/strmatcher/benchmark_matchers_test.go new file mode 100644 index 00000000000..ac739b185dc --- /dev/null +++ b/common/strmatcher/benchmark_matchers_test.go @@ -0,0 +1,149 @@ +package strmatcher_test + +import ( + "strconv" + "testing" + + "github.com/v2fly/v2ray-core/v5/common" + . "github.com/v2fly/v2ray-core/v5/common/strmatcher" +) + +func BenchmarkFullMatcher(b *testing.B) { + b.Run("SimpleMatcherGroup------", func(b *testing.B) { + benchmarkMatcherType(b, Full, func() MatcherGroup { + return new(SimpleMatcherGroup) + }) + }) + b.Run("FullMatcherGroup--------", func(b *testing.B) { + benchmarkMatcherType(b, Full, func() MatcherGroup { + return NewFullMatcherGroup() + }) + }) + b.Run("ACAutomationMatcherGroup", func(b *testing.B) { + benchmarkMatcherType(b, Full, func() MatcherGroup { + return NewACAutomatonMatcherGroup() + }) + }) + b.Run("MphMatcherGroup---------", func(b *testing.B) { + benchmarkMatcherType(b, Full, func() MatcherGroup { + return NewMphMatcherGroup() + }) + }) +} + +func BenchmarkDomainMatcher(b *testing.B) { + b.Run("SimpleMatcherGroup------", func(b *testing.B) { + benchmarkMatcherType(b, Domain, func() MatcherGroup { + return new(SimpleMatcherGroup) + }) + }) + b.Run("DomainMatcherGroup------", func(b *testing.B) { + benchmarkMatcherType(b, Domain, func() MatcherGroup { + return NewDomainMatcherGroup() + }) + }) + b.Run("ACAutomationMatcherGroup", func(b *testing.B) { + benchmarkMatcherType(b, Domain, func() MatcherGroup { + return NewACAutomatonMatcherGroup() + }) + }) + b.Run("MphMatcherGroup---------", func(b *testing.B) { + benchmarkMatcherType(b, Domain, func() MatcherGroup { + return NewMphMatcherGroup() + }) + }) +} + +func BenchmarkSubstrMatcher(b *testing.B) { + b.Run("SimpleMatcherGroup------", func(b *testing.B) { + benchmarkMatcherType(b, Substr, func() MatcherGroup { + return new(SimpleMatcherGroup) + }) + }) + b.Run("SubstrMatcherGroup------", func(b *testing.B) { + benchmarkMatcherType(b, Substr, func() MatcherGroup { + return new(SubstrMatcherGroup) + }) + }) + b.Run("ACAutomationMatcherGroup", func(b *testing.B) { + benchmarkMatcherType(b, Substr, func() MatcherGroup { + return NewACAutomatonMatcherGroup() + }) + }) +} + +// Utility functions for benchmark + +func benchmarkMatcherType(b *testing.B, t Type, ctor func() MatcherGroup) { + b.Run("Match", func(b *testing.B) { + b.Run("Succ", func(b *testing.B) { + benchmarkMatch(b, ctor(), map[Type]bool{t: true}) + }) + b.Run("Fail", func(b *testing.B) { + benchmarkMatch(b, ctor(), map[Type]bool{t: false}) + }) + }) + b.Run("MatchAny", func(b *testing.B) { + b.Run("Succ", func(b *testing.B) { + benchmarkMatchAny(b, ctor(), map[Type]bool{t: true}) + }) + b.Run("Fail", func(b *testing.B) { + benchmarkMatchAny(b, ctor(), map[Type]bool{t: false}) + }) + }) +} + +func benchmarkMatch(b *testing.B, g MatcherGroup, enabledTypes map[Type]bool) { + prepareMatchers(g, enabledTypes) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = g.Match("0.v2fly.org") + } +} + +func benchmarkMatchAny(b *testing.B, g MatcherGroup, enabledTypes map[Type]bool) { + prepareMatchers(g, enabledTypes) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = g.MatchAny("0.v2fly.org") + } +} + +func prepareMatchers(g MatcherGroup, enabledTypes map[Type]bool) { + for matcherType, hasMatch := range enabledTypes { + switch matcherType { + case Domain: + if hasMatch { + AddMatcherToGroup(g, DomainMatcher("v2fly.org"), 0) + } + for i := 1; i < 1024; i++ { + AddMatcherToGroup(g, DomainMatcher(strconv.Itoa(i)+".v2fly.org"), uint32(i)) + } + case Full: + if hasMatch { + AddMatcherToGroup(g, FullMatcher("0.v2fly.org"), 0) + } + for i := 1; i < 64; i++ { + AddMatcherToGroup(g, FullMatcher(strconv.Itoa(i)+".v2fly.org"), uint32(i)) + } + case Substr: + if hasMatch { + AddMatcherToGroup(g, SubstrMatcher("v2fly.org"), 0) + } + for i := 1; i < 4; i++ { + AddMatcherToGroup(g, SubstrMatcher(strconv.Itoa(i)+".v2fly.org"), uint32(i)) + } + case Regex: + matcher, err := Regex.New("^[^.]*$") // Dotless domain matcher automatically inserted in DNS app when "localhost" DNS is used. + common.Must(err) + AddMatcherToGroup(g, matcher, 0) + } + } + if g, ok := g.(buildable); ok { + common.Must(g.Build()) + } +} + +type buildable interface { + Build() error +} diff --git a/common/strmatcher/benchmark_test.go b/common/strmatcher/benchmark_test.go deleted file mode 100644 index 0d1ffefb936..00000000000 --- a/common/strmatcher/benchmark_test.go +++ /dev/null @@ -1,161 +0,0 @@ -package strmatcher_test - -import ( - "strconv" - "testing" - - "github.com/v2fly/v2ray-core/v5/common" - . "github.com/v2fly/v2ray-core/v5/common/strmatcher" -) - -// Benchmark Domain Matcher Groups - -func BenchmarkSimpleMatcherGroupForDomain(b *testing.B) { - g := new(SimpleMatcherGroup) - - for i := 1; i <= 1024; i++ { - AddMatcherToGroup(g, DomainMatcher(strconv.Itoa(i)+".v2fly.org"), uint32(i)) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = g.Match("0.v2fly.org") - } -} - -func BenchmarkDomainMatcherGroup(b *testing.B) { - g := new(DomainMatcherGroup) - - for i := 1; i <= 1024; i++ { - AddMatcherToGroup(g, DomainMatcher(strconv.Itoa(i)+".v2fly.org"), uint32(i)) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = g.Match("0.v2fly.org") - } -} - -func BenchmarkACAutomatonMatcherGroupForDomain(b *testing.B) { - ac := NewACAutomatonMatcherGroup() - for i := 1; i <= 1024; i++ { - AddMatcherToGroup(ac, DomainMatcher(strconv.Itoa(i)+".v2fly.org"), uint32(i)) - } - ac.Build() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = ac.MatchAny("0.v2fly.org") - } -} - -func BenchmarkMphMatcherGroupForDomain(b *testing.B) { - mph := NewMphMatcherGroup() - for i := 1; i <= 1024; i++ { - AddMatcherToGroup(mph, DomainMatcher(strconv.Itoa(i)+".v2fly.org"), uint32(i)) - } - mph.Build() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = mph.MatchAny("0.v2fly.org") - } -} - -// Benchmark Full Matcher Groups - -func BenchmarkSimpleMatcherGroupForFull(b *testing.B) { - g := new(SimpleMatcherGroup) - - for i := 1; i <= 1024; i++ { - AddMatcherToGroup(g, FullMatcher(strconv.Itoa(i)+".v2fly.org"), uint32(i)) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = g.Match("0.v2fly.org") - } -} - -func BenchmarkFullMatcherGroup(b *testing.B) { - g := new(FullMatcherGroup) - - for i := 1; i <= 1024; i++ { - AddMatcherToGroup(g, FullMatcher(strconv.Itoa(i)+".v2fly.org"), uint32(i)) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = g.Match("0.v2fly.org") - } -} - -func BenchmarkACAutomatonMatcherGroupForFull(b *testing.B) { - ac := NewACAutomatonMatcherGroup() - for i := 1; i <= 1024; i++ { - AddMatcherToGroup(ac, FullMatcher(strconv.Itoa(i)+".v2fly.org"), uint32(i)) - } - ac.Build() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = ac.MatchAny("0.v2fly.org") - } -} - -func BenchmarkMphMatcherGroupFull(b *testing.B) { - mph := NewMphMatcherGroup() - for i := 1; i <= 1024; i++ { - AddMatcherToGroup(mph, FullMatcher(strconv.Itoa(i)+".v2fly.org"), uint32(i)) - } - mph.Build() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = mph.MatchAny("0.v2fly.org") - } -} - -// Benchmark Substr Matcher Groups - -func BenchmarkSimpleMatcherGroupForSubstr(b *testing.B) { - g := new(SimpleMatcherGroup) - - for i := 1; i <= 1024; i++ { - AddMatcherToGroup(g, SubstrMatcher(strconv.Itoa(i)+".v2fly.org"), uint32(i)) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = g.Match("0.v2fly.org") - } -} - -func BenchmarkACAutomatonMatcherGroupForSubstr(b *testing.B) { - ac := NewACAutomatonMatcherGroup() - for i := 1; i <= 1024; i++ { - AddMatcherToGroup(ac, SubstrMatcher(strconv.Itoa(i)+".v2fly.org"), uint32(i)) - } - ac.Build() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = ac.MatchAny("0.v2fly.org") - } -} - -// Benchmark Index Matchers - -func BenchmarkLinearIndexMatcher(b *testing.B) { - g := new(LinearIndexMatcher) - for i := 1; i <= 1024; i++ { - m, err := Domain.New(strconv.Itoa(i) + ".v2fly.org") - common.Must(err) - g.Add(m) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = g.Match("0.v2fly.org") - } -} diff --git a/common/strmatcher/indexmatcher_linear.go b/common/strmatcher/indexmatcher_linear.go index da599416e6c..dcdc1d33666 100644 --- a/common/strmatcher/indexmatcher_linear.go +++ b/common/strmatcher/indexmatcher_linear.go @@ -1,13 +1,12 @@ package strmatcher // LinearIndexMatcher is an implementation of IndexMatcher. -// Empty initialization works. type LinearIndexMatcher struct { - count uint32 - fullMatcher FullMatcherGroup - domainMatcher DomainMatcherGroup - substrMatcher SubstrMatcherGroup - otherMatchers SimpleMatcherGroup + count uint32 + full *FullMatcherGroup + domain *DomainMatcherGroup + substr *SubstrMatcherGroup + regex *SimpleMatcherGroup } func NewLinearIndexMatcher() *LinearIndexMatcher { @@ -21,13 +20,25 @@ func (g *LinearIndexMatcher) Add(matcher Matcher) uint32 { switch matcher := matcher.(type) { case FullMatcher: - g.fullMatcher.AddFullMatcher(matcher, index) + if g.full == nil { + g.full = NewFullMatcherGroup() + } + g.full.AddFullMatcher(matcher, index) case DomainMatcher: - g.domainMatcher.AddDomainMatcher(matcher, index) + if g.domain == nil { + g.domain = NewDomainMatcherGroup() + } + g.domain.AddDomainMatcher(matcher, index) case SubstrMatcher: - g.substrMatcher.AddSubstrMatcher(matcher, index) + if g.substr == nil { + g.substr = new(SubstrMatcherGroup) + } + g.substr.AddSubstrMatcher(matcher, index) default: - g.otherMatchers.AddMatcher(matcher, index) + if g.regex == nil { + g.regex = new(SimpleMatcherGroup) + } + g.regex.AddMatcher(matcher, index) } return index @@ -40,17 +51,43 @@ func (*LinearIndexMatcher) Build() error { // Match implements IndexMatcher.Match. func (g *LinearIndexMatcher) Match(input string) []uint32 { - result := []uint32{} - result = append(result, g.fullMatcher.Match(input)...) - result = append(result, g.domainMatcher.Match(input)...) - result = append(result, g.substrMatcher.Match(input)...) - result = append(result, g.otherMatchers.Match(input)...) - return result + // Allocate capacity to prevent matches escaping to heap + result := make([][]uint32, 0, 5) + if g.full != nil { + if matches := g.full.Match(input); len(matches) > 0 { + result = append(result, matches) + } + } + if g.domain != nil { + if matches := g.domain.Match(input); len(matches) > 0 { + result = append(result, matches) + } + } + if g.substr != nil { + if matches := g.substr.Match(input); len(matches) > 0 { + result = append(result, matches) + } + } + if g.regex != nil { + if matches := g.regex.Match(input); len(matches) > 0 { + result = append(result, matches) + } + } + return CompositeMatches(result) } // MatchAny implements IndexMatcher.MatchAny. func (g *LinearIndexMatcher) MatchAny(input string) bool { - return len(g.Match(input)) > 0 + if g.full != nil && g.full.MatchAny(input) { + return true + } + if g.domain != nil && g.domain.MatchAny(input) { + return true + } + if g.substr != nil && g.substr.MatchAny(input) { + return true + } + return g.regex != nil && g.regex.MatchAny(input) } // Size implements IndexMatcher.Size. diff --git a/common/strmatcher/indexmatcher_mph.go b/common/strmatcher/indexmatcher_mph.go index 79614806506..cb4c11c628e 100644 --- a/common/strmatcher/indexmatcher_mph.go +++ b/common/strmatcher/indexmatcher_mph.go @@ -8,15 +8,11 @@ type MphIndexMatcher struct { count uint32 mph *MphMatcherGroup ac *ACAutomatonMatcherGroup - regex SimpleMatcherGroup + regex *SimpleMatcherGroup } func NewMphIndexMatcher() *MphIndexMatcher { - return &MphIndexMatcher{ - mph: nil, - ac: nil, - regex: SimpleMatcherGroup{}, - } + return new(MphIndexMatcher) } // Add implements IndexMatcher.Add. @@ -41,6 +37,9 @@ func (g *MphIndexMatcher) Add(matcher Matcher) uint32 { } g.ac.AddSubstrMatcher(matcher, index) case *RegexMatcher: + if g.regex == nil { + g.regex = &SimpleMatcherGroup{} + } g.regex.AddMatcher(matcher, index) } @@ -59,8 +58,24 @@ func (g *MphIndexMatcher) Build() error { } // Match implements IndexMatcher.Match. -func (*MphIndexMatcher) Match(string) []uint32 { - return nil +func (g *MphIndexMatcher) Match(input string) []uint32 { + result := make([][]uint32, 0, 5) + if g.mph != nil { + if matches := g.mph.Match(input); len(matches) > 0 { + result = append(result, matches) + } + } + if g.ac != nil { + if matches := g.ac.Match(input); len(matches) > 0 { + result = append(result, matches) + } + } + if g.regex != nil { + if matches := g.regex.Match(input); len(matches) > 0 { + result = append(result, matches) + } + } + return CompositeMatches(result) } // MatchAny implements IndexMatcher.MatchAny. @@ -71,7 +86,7 @@ func (g *MphIndexMatcher) MatchAny(input string) bool { if g.ac != nil && g.ac.MatchAny(input) { return true } - return g.regex.MatchAny(input) + return g.regex != nil && g.regex.MatchAny(input) } // Size implements IndexMatcher.Size. diff --git a/common/strmatcher/indexmatcher_mph_test.go b/common/strmatcher/indexmatcher_mph_test.go new file mode 100644 index 00000000000..87c0790ca6b --- /dev/null +++ b/common/strmatcher/indexmatcher_mph_test.go @@ -0,0 +1,94 @@ +package strmatcher_test + +import ( + "reflect" + "testing" + + "github.com/v2fly/v2ray-core/v5/common" + . "github.com/v2fly/v2ray-core/v5/common/strmatcher" +) + +func TestMphIndexMatcher(t *testing.T) { + rules := []struct { + Type Type + Domain string + }{ + { + Type: Regex, + Domain: "apis\\.us$", + }, + { + Type: Substr, + Domain: "apis", + }, + { + Type: Domain, + Domain: "googleapis.com", + }, + { + Type: Domain, + Domain: "com", + }, + { + Type: Full, + Domain: "www.baidu.com", + }, + { + Type: Substr, + Domain: "apis", + }, + { + Type: Domain, + Domain: "googleapis.com", + }, + { + Type: Full, + Domain: "fonts.googleapis.com", + }, + { + Type: Full, + Domain: "www.baidu.com", + }, + { + Type: Domain, + Domain: "example.com", + }, + } + cases := []struct { + Input string + Output []uint32 + }{ + { + Input: "www.baidu.com", + Output: []uint32{5, 9, 4}, + }, + { + Input: "fonts.googleapis.com", + Output: []uint32{8, 3, 7, 4, 2, 6}, + }, + { + Input: "example.googleapis.com", + Output: []uint32{3, 7, 4, 2, 6}, + }, + { + Input: "testapis.us", + Output: []uint32{2, 6, 1}, + }, + { + Input: "example.com", + Output: []uint32{10, 4}, + }, + } + matcherGroup := NewMphIndexMatcher() + for _, rule := range rules { + matcher, err := rule.Type.New(rule.Domain) + common.Must(err) + matcherGroup.Add(matcher) + } + matcherGroup.Build() + for _, test := range cases { + if m := matcherGroup.Match(test.Input); !reflect.DeepEqual(m, test.Output) { + t.Error("unexpected output: ", m, " for test case ", test) + } + } +} diff --git a/common/strmatcher/matchergroup_ac_automation.go b/common/strmatcher/matchergroup_ac_automation.go index 1f8e2de1ed2..dae021dafce 100644 --- a/common/strmatcher/matchergroup_ac_automation.go +++ b/common/strmatcher/matchergroup_ac_automation.go @@ -127,8 +127,8 @@ func (ac *ACAutomatonMatcherGroup) Build() error { // Match implements MatcherGroup.Match. func (ac *ACAutomatonMatcherGroup) Match(input string) []uint32 { - var suffixMatches [][]uint32 - var substrMatches [][]uint32 + suffixMatches := make([][]uint32, 0, 5) + substrMatches := make([][]uint32, 0, 5) fullMatch := true // fullMatch indicates no fail edge traversed so far. node := &ac.nodes[0] // start from root node. // 1. the match string is all through trie edge. FULL MATCH or DOMAIN @@ -177,18 +177,10 @@ func (ac *ACAutomatonMatcherGroup) Match(input string) []uint32 { suffixMatches = append(suffixMatches, values[Full]) } } - switch matches := append(substrMatches, suffixMatches...); len(matches) { // nolint: gocritic - case 0: - return nil - case 1: - return matches[0] - default: - result := []uint32{} - for i := len(matches) - 1; i >= 0; i-- { - result = append(result, matches[i]...) - } - return result + if len(substrMatches) == 0 { + return CompositeMatchesReverse(suffixMatches) } + return CompositeMatchesReverse(append(substrMatches, suffixMatches...)) } // MatchAny implements MatcherGroup.MatchAny. diff --git a/common/strmatcher/matchergroup_domain.go b/common/strmatcher/matchergroup_domain.go index bf8db2d6cf2..29334214985 100644 --- a/common/strmatcher/matchergroup_domain.go +++ b/common/strmatcher/matchergroup_domain.go @@ -1,101 +1,109 @@ package strmatcher -import "strings" - -func breakDomain(domain string) []string { - return strings.Split(domain, ".") -} - -type node struct { - values []uint32 - sub map[string]*node +type trieNode struct { + values []uint32 + children map[string]*trieNode } // DomainMatcherGroup is an implementation of MatcherGroup. // It uses trie to optimize both memory consumption and lookup speed. Trie node is domain label based. type DomainMatcherGroup struct { - root *node + root *trieNode } -// AddDomainMatcher implements MatcherGroupForDomain.AddDomainMatcher. -func (g *DomainMatcherGroup) AddDomainMatcher(matcher DomainMatcher, value uint32) { - if g.root == nil { - g.root = new(node) +func NewDomainMatcherGroup() *DomainMatcherGroup { + return &DomainMatcherGroup{ + root: new(trieNode), } +} - current := g.root - parts := breakDomain(matcher.Pattern()) - for i := len(parts) - 1; i >= 0; i-- { - part := parts[i] - if current.sub == nil { - current.sub = make(map[string]*node) +// AddDomainMatcher implements MatcherGroupForDomain.AddDomainMatcher. +func (g *DomainMatcherGroup) AddDomainMatcher(matcher DomainMatcher, value uint32) { + node := g.root + pattern := matcher.Pattern() + for i := len(pattern); i > 0; { + var part string + for j := i - 1; ; j-- { + if pattern[j] == '.' { + part = pattern[j+1 : i] + i = j + break + } + if j == 0 { + part = pattern[j:i] + i = j + break + } } - next := current.sub[part] + if node.children == nil { + node.children = make(map[string]*trieNode) + } + next := node.children[part] if next == nil { - next = new(node) - current.sub[part] = next + next = new(trieNode) + node.children[part] = next } - current = next + node = next } - current.values = append(current.values, value) + node.values = append(node.values, value) } // Match implements MatcherGroup.Match. -func (g *DomainMatcherGroup) Match(domain string) []uint32 { - if domain == "" { - return nil - } - - current := g.root - if current == nil { - return nil - } - - nextPart := func(idx int) int { - for i := idx - 1; i >= 0; i-- { - if domain[i] == '.' { - return i +func (g *DomainMatcherGroup) Match(input string) []uint32 { + matches := make([][]uint32, 0, 5) + node := g.root + for i := len(input); i > 0; { + for j := i - 1; ; j-- { + if input[j] == '.' { // Domain label found + node = node.children[input[j+1:i]] + i = j + break + } + if j == 0 { // The last part of domain label + node = node.children[input[j:i]] + i = j + break } } - return -1 - } - - matches := [][]uint32{} - idx := len(domain) - for { - if idx == -1 || current.sub == nil { - break - } - - nidx := nextPart(idx) - part := domain[nidx+1 : idx] - next := current.sub[part] - if next == nil { + if node == nil { // No more match if no trie edge transition break } - current = next - idx = nidx - if len(current.values) > 0 { - matches = append(matches, current.values) + if len(node.values) > 0 { // Found matched matchers + matches = append(matches, node.values) } - } - switch len(matches) { - case 0: - return nil - case 1: - return matches[0] - default: - result := []uint32{} - for idx := range matches { - // Insert reversely, the subdomain that matches further ranks higher - result = append(result, matches[len(matches)-1-idx]...) + if node.children == nil { // No more match if leaf node reached + break } - return result } + return CompositeMatchesReverse(matches) } // MatchAny implements MatcherGroup.MatchAny. -func (g *DomainMatcherGroup) MatchAny(domain string) bool { - return len(g.Match(domain)) > 0 +func (g *DomainMatcherGroup) MatchAny(input string) bool { + node := g.root + for i := len(input); i > 0; { + for j := i - 1; ; j-- { + if input[j] == '.' { + node = node.children[input[j+1:i]] + i = j + break + } + if j == 0 { + node = node.children[input[j:i]] + i = j + break + } + } + if node == nil { + return false + } + if len(node.values) > 0 { + return true + } + if node.children == nil { + return false + } + } + return false } diff --git a/common/strmatcher/matchergroup_domain_test.go b/common/strmatcher/matchergroup_domain_test.go index 73934dd3316..e419a86f5b5 100644 --- a/common/strmatcher/matchergroup_domain_test.go +++ b/common/strmatcher/matchergroup_domain_test.go @@ -82,7 +82,7 @@ func TestDomainMatcherGroup(t *testing.T) { Result: []uint32{4, 6}, }, } - g := new(DomainMatcherGroup) + g := NewDomainMatcherGroup() for _, pattern := range patterns { AddMatcherToGroup(g, DomainMatcher(pattern.Pattern), pattern.Value) } @@ -95,7 +95,7 @@ func TestDomainMatcherGroup(t *testing.T) { } func TestEmptyDomainMatcherGroup(t *testing.T) { - g := new(DomainMatcherGroup) + g := NewDomainMatcherGroup() r := g.Match("v2fly.org") if len(r) != 0 { t.Error("Expect [], but ", r) diff --git a/common/strmatcher/matchergroup_full.go b/common/strmatcher/matchergroup_full.go index 7947729450e..85057b36f60 100644 --- a/common/strmatcher/matchergroup_full.go +++ b/common/strmatcher/matchergroup_full.go @@ -6,25 +6,25 @@ type FullMatcherGroup struct { matchers map[string][]uint32 } -// AddFullMatcher implements MatcherGroupForFull.AddFullMatcher. -func (g *FullMatcherGroup) AddFullMatcher(matcher FullMatcher, value uint32) { - if g.matchers == nil { - g.matchers = make(map[string][]uint32) +func NewFullMatcherGroup() *FullMatcherGroup { + return &FullMatcherGroup{ + matchers: make(map[string][]uint32), } +} +// AddFullMatcher implements MatcherGroupForFull.AddFullMatcher. +func (g *FullMatcherGroup) AddFullMatcher(matcher FullMatcher, value uint32) { domain := matcher.Pattern() g.matchers[domain] = append(g.matchers[domain], value) } // Match implements MatcherGroup.Match. func (g *FullMatcherGroup) Match(input string) []uint32 { - if g.matchers == nil { - return nil - } return g.matchers[input] } // MatchAny implements MatcherGroup.Any. func (g *FullMatcherGroup) MatchAny(input string) bool { - return len(g.Match(input)) > 0 + _, found := g.matchers[input] + return found } diff --git a/common/strmatcher/matchergroup_full_test.go b/common/strmatcher/matchergroup_full_test.go index 645cb349c64..7d733579f92 100644 --- a/common/strmatcher/matchergroup_full_test.go +++ b/common/strmatcher/matchergroup_full_test.go @@ -50,7 +50,7 @@ func TestFullMatcherGroup(t *testing.T) { Result: []uint32{4, 6}, }, } - g := new(FullMatcherGroup) + g := NewFullMatcherGroup() for _, pattern := range patterns { AddMatcherToGroup(g, FullMatcher(pattern.Pattern), pattern.Value) } @@ -63,7 +63,7 @@ func TestFullMatcherGroup(t *testing.T) { } func TestEmptyFullMatcherGroup(t *testing.T) { - g := new(FullMatcherGroup) + g := NewFullMatcherGroup() r := g.Match("v2fly.org") if len(r) != 0 { t.Error("Expect [], but ", r) diff --git a/common/strmatcher/matchergroup_mph.go b/common/strmatcher/matchergroup_mph.go index d842e4486f4..f5afa5dc4c3 100644 --- a/common/strmatcher/matchergroup_mph.go +++ b/common/strmatcher/matchergroup_mph.go @@ -152,7 +152,7 @@ func (g *MphMatcherGroup) Lookup(rollingHash uint32, input string) uint32 { // Match implements MatcherGroup.Match. func (g *MphMatcherGroup) Match(input string) []uint32 { - matches := [][]uint32{} + matches := make([][]uint32, 0, 5) hash := uint32(0) for i := len(input) - 1; i >= 0; i-- { hash = hash*PrimeRK + uint32(input[i]) @@ -165,18 +165,7 @@ func (g *MphMatcherGroup) Match(input string) []uint32 { if mphIdx := g.Lookup(hash, input); mphIdx != 0 { matches = append(matches, g.values[mphIdx]) } - switch len(matches) { - case 0: - return nil - case 1: - return matches[0] - default: - result := []uint32{} - for i := len(matches) - 1; i >= 0; i-- { - result = append(result, matches[i]...) - } - return result - } + return CompositeMatchesReverse(matches) } // MatchAny implements MatcherGroup.MatchAny. diff --git a/common/strmatcher/matchergroup_simple.go b/common/strmatcher/matchergroup_simple.go index 7077a274cbf..fa6f0eb2a34 100644 --- a/common/strmatcher/matchergroup_simple.go +++ b/common/strmatcher/matchergroup_simple.go @@ -32,5 +32,10 @@ func (g *SimpleMatcherGroup) Match(input string) []uint32 { // MatchAny implements MatcherGroup.MatchAny. func (g *SimpleMatcherGroup) MatchAny(input string) bool { - return len(g.Match(input)) > 0 + for _, e := range g.matchers { + if e.matcher.Match(input) { + return true + } + } + return false } diff --git a/common/strmatcher/matchergroup_substr.go b/common/strmatcher/matchergroup_substr.go index 1eb3d96927e..ccaa0c9ff48 100644 --- a/common/strmatcher/matchergroup_substr.go +++ b/common/strmatcher/matchergroup_substr.go @@ -20,16 +20,30 @@ func (g *SubstrMatcherGroup) AddSubstrMatcher(matcher SubstrMatcher, value uint3 // Match implements MatcherGroup.Match. func (g *SubstrMatcherGroup) Match(input string) []uint32 { - result := []uint32{} + var result []uint32 for i, pattern := range g.patterns { for j := strings.LastIndex(input, pattern); j != -1; j = strings.LastIndex(input[:j], pattern) { result = append(result, uint32(j)<<16|uint32(i)&0xffff) // uint32: position (higher 16 bit) | patternIdx (lower 16 bit) } } - // Sort the match results in dictionary order, so that: - // 1. Pattern matched at smaller position (meaning matched further) takes precedence. - // 2. When patterns matched at same position, pattern with smaller index (meaning inserted early) takes precedence. - sort.Slice(result, func(i, j int) bool { return result[i] < result[j] }) + // sort.Slice will trigger allocation no matter what input is. See https://github.com/golang/go/issues/17332 + // We optimize the sorting by length to prevent memory allocation as possible. + switch len(result) { + case 0: + return nil + case 1: + // No need to sort + case 2: + // Do a simple swap if unsorted + if result[0] > result[1] { + result[0], result[1] = result[1], result[0] + } + default: + // Sort the match results in dictionary order, so that: + // 1. Pattern matched at smaller position (meaning matched further) takes precedence. + // 2. When patterns matched at same position, pattern with smaller index (meaning inserted early) takes precedence. + sort.Slice(result, func(i, j int) bool { return result[i] < result[j] }) + } for i, entry := range result { result[i] = g.values[entry&0xffff] // Get pattern value from its index (the lower 16 bit) } diff --git a/common/strmatcher/matchers.go b/common/strmatcher/matchers.go index c5d6fcdfcc3..f13756fa499 100644 --- a/common/strmatcher/matchers.go +++ b/common/strmatcher/matchers.go @@ -4,6 +4,7 @@ import ( "errors" "regexp" "strings" + "unicode/utf8" ) // FullMatcher is an implementation of Matcher. @@ -96,6 +97,10 @@ func (t Type) New(pattern string) (Matcher, error) { case Substr: return SubstrMatcher(pattern), nil case Domain: + pattern, err := ToDomain(pattern) + if err != nil { + return nil, err + } return DomainMatcher(pattern), nil case Regex: // 1. regex matching is case-sensitive regex, err := regexp.Compile(pattern) @@ -104,10 +109,73 @@ func (t Type) New(pattern string) (Matcher, error) { } return &RegexMatcher{pattern: regex}, nil default: - panic("Unknown type") + return nil, errors.New("unknown matcher type") } } +// NewDomainPattern creates a new Matcher based on the given domain pattern. +// It works like `Type.New`, but will do validation and conversion to ensure it's a valid domain pattern. +func (t Type) NewDomainPattern(pattern string) (Matcher, error) { + switch t { + case Full: + pattern, err := ToDomain(pattern) + if err != nil { + return nil, err + } + return FullMatcher(pattern), nil + case Substr: + pattern, err := ToDomain(pattern) + if err != nil { + return nil, err + } + return SubstrMatcher(pattern), nil + case Domain: + pattern, err := ToDomain(pattern) + if err != nil { + return nil, err + } + return DomainMatcher(pattern), nil + case Regex: // Regex's charset not in LDH subset + regex, err := regexp.Compile(pattern) + if err != nil { + return nil, err + } + return &RegexMatcher{pattern: regex}, nil + default: + return nil, errors.New("unknown matcher type") + } +} + +// ToDomain converts input pattern to a domain string, and return error if such a conversion cannot be made. +// 1. Conforms to Letter-Digit-Hyphen (LDH) subset (https://tools.ietf.org/html/rfc952): +// * Letters A to Z (no distinction between uppercase and lowercase, we convert to lowers) +// * Digits 0 to 9 +// * Hyphens(-) and Periods(.) +// 2. Non-ASCII characters not supported for now. +// * May support Internationalized domain name to Punycode if needed in the future. +func ToDomain(pattern string) (string, error) { + builder := strings.Builder{} + builder.Grow(len(pattern)) + for i := 0; i < len(pattern); i++ { + c := pattern[i] + if c >= utf8.RuneSelf { + return "", errors.New("non-ASCII characters not supported for now") + } + switch { + case 'A' <= c && c <= 'Z': + c += 'a' - 'A' + case 'a' <= c && c <= 'z': + case '0' <= c && c <= '9': + case c == '-': + case c == '.': + default: + return "", errors.New("pattern string does not conform to Letter-Digit-Hyphen (LDH) subset") + } + builder.WriteByte(c) + } + return builder.String(), nil +} + // MatcherGroupForAll is an interface indicating a MatcherGroup could accept all types of matchers. type MatcherGroupForAll interface { AddMatcher(matcher Matcher, value uint32) @@ -137,6 +205,10 @@ type MatcherGroupForRegex interface { // It returns error if the MatcherGroup does not accept the provided Matcher's type. // This function is provided to help writing code to test a MatcherGroup. func AddMatcherToGroup(g MatcherGroup, matcher Matcher, value uint32) error { + if g, ok := g.(IndexMatcher); ok { + g.Add(matcher) + return nil + } if g, ok := g.(MatcherGroupForAll); ok { g.AddMatcher(matcher, value) return nil @@ -165,3 +237,40 @@ func AddMatcherToGroup(g MatcherGroup, matcher Matcher, value uint32) error { } return errors.New("cannot add matcher to matcher group") } + +// CompositeMatches flattens the matches slice to produce a single matched indices slice. +// It is designed to avoid new memory allocation as possible. +func CompositeMatches(matches [][]uint32) []uint32 { + switch len(matches) { + case 0: + return nil + case 1: + return matches[0] + default: + result := make([]uint32, 0, 5) + for i := 0; i < len(matches); i++ { + result = append(result, matches[i]...) + } + return result + } +} + +// CompositeMatches flattens the matches slice to produce a single matched indices slice. +// It is designed that: +// 1. All matchers are concatenated in reverse order, so the matcher that matches further ranks higher. +// 2. Indices in the same matcher keeps their original order. +// 3. Avoid new memory allocation as possible. +func CompositeMatchesReverse(matches [][]uint32) []uint32 { + switch len(matches) { + case 0: + return nil + case 1: + return matches[0] + default: + result := make([]uint32, 0, 5) + for i := len(matches) - 1; i >= 0; i-- { + result = append(result, matches[i]...) + } + return result + } +} diff --git a/go.mod b/go.mod index d35313d5033..ec93741c5c8 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432 go.starlark.net v0.0.0-20220302181546-5411bad688d1 + go4.org/netipx v0.0.0-20220925034521-797b0c90d8ab golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e golang.org/x/sync v0.0.0-20210220032951-036812b2e83c @@ -28,7 +29,6 @@ require ( google.golang.org/grpc v1.48.0 google.golang.org/protobuf v1.28.1 h12.io/socks v1.0.3 - inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 ) require ( @@ -55,12 +55,10 @@ require ( github.com/rogpeppe/go-internal v1.8.0 // indirect github.com/secure-io/siv-go v0.0.0-20180922214919-5ff40651e2c4 // indirect github.com/xtaci/smux v1.5.15 // indirect - go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect - go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect - golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/text v0.3.7 // indirect - golang.org/x/tools v0.1.10 // indirect + golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/go.sum b/go.sum index af406ffef95..c464c7ff8e3 100644 --- a/go.sum +++ b/go.sum @@ -63,7 +63,6 @@ github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fp github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165 h1:BS21ZUJ/B5X2UVUbczfmdWH7GapPWAhxcMsDnjJTU1E= github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -338,11 +337,8 @@ go.starlark.net v0.0.0-20220302181546-5411bad688d1/go.mod h1:t3mmBBPzAVvK0L0n1dr go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE= -go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA= -go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= -go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4= -go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= +go4.org/netipx v0.0.0-20220925034521-797b0c90d8ab h1:+yW1yrZ09EYNu1spCUOHBBNRbrLnfmutwyhbhCv3b6Q= +go4.org/netipx v0.0.0-20220925034521-797b0c90d8ab/go.mod h1:tgPU4N2u9RByaTN3NC2p9xOzyFpte4jYwsIIRF7XlSc= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -377,8 +373,8 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -482,8 +478,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= -golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f h1:OKYpQQVE3DKSc3r3zHVzq46vq5YH7x8xpR3/k9ixmUg= +golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -567,6 +563,4 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 h1:acCzuUSQ79tGsM/O50VRFySfMm19IoMKL+sZztZkCxw= -inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6/go.mod h1:y3MGhcFMlh0KZPMuXXow8mpjxxAk3yoDNsp4cQz54i8= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/proxy/socks/client.go b/proxy/socks/client.go index 1db813dfad8..2ac4ea17ba2 100644 --- a/proxy/socks/client.go +++ b/proxy/socks/client.go @@ -146,10 +146,21 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter if err := conn.SetDeadline(time.Now().Add(p.Timeouts.Handshake)); err != nil { newError("failed to set deadline for handshake").Base(err).WriteToLog(session.ExportIDToError(ctx)) } - udpRequest, err := ClientHandshake(request, conn, conn) - if err != nil { - return newError("failed to establish connection to server").AtWarning().Base(err) + + var udpRequest *protocol.RequestHeader + var err error + if request.Version == socks4Version { + err = ClientHandshake4(request, conn, conn) + if err != nil { + return newError("failed to establish connection to server").AtWarning().Base(err) + } + } else { + udpRequest, err = ClientHandshake(request, conn, conn) + if err != nil { + return newError("failed to establish connection to server").AtWarning().Base(err) + } } + if udpRequest != nil { if udpRequest.Address == net.AnyIP || udpRequest.Address == net.AnyIPv6 { udpRequest.Address = dest.Address diff --git a/proxy/socks/protocol.go b/proxy/socks/protocol.go index 24e449d1a73..092a8c69756 100644 --- a/proxy/socks/protocol.go +++ b/proxy/socks/protocol.go @@ -557,3 +557,46 @@ func ClientHandshake(request *protocol.RequestHeader, reader io.Reader, writer i return nil, nil } + +func ClientHandshake4(request *protocol.RequestHeader, reader io.Reader, writer io.Writer) error { + b := buf.New() + defer b.Release() + + common.Must2(b.Write([]byte{socks4Version, cmdTCPConnect})) + portBytes := b.Extend(2) + binary.BigEndian.PutUint16(portBytes, request.Port.Value()) + switch request.Address.Family() { + case net.AddressFamilyIPv4: + common.Must2(b.Write(request.Address.IP())) + case net.AddressFamilyDomain: + common.Must2(b.Write([]byte{0x00, 0x00, 0x00, 0x01})) + case net.AddressFamilyIPv6: + return newError("ipv6 is not supported in socks4") + default: + panic("Unknown family type.") + } + if request.User != nil { + account := request.User.Account.(*Account) + common.Must2(b.WriteString(account.Username)) + } + common.Must(b.WriteByte(0x00)) + if request.Address.Family() == net.AddressFamilyDomain { + common.Must2(b.WriteString(request.Address.Domain())) + common.Must(b.WriteByte(0x00)) + } + if err := buf.WriteAllBytes(writer, b.Bytes()); err != nil { + return err + } + + b.Clear() + if _, err := b.ReadFullFrom(reader, 8); err != nil { + return err + } + if b.Byte(0) != 0x00 { + return newError("unexpected version of the reply code: ", b.Byte(0)) + } + if b.Byte(1) != socks4RequestGranted { + return newError("server rejects request: ", b.Byte(1)) + } + return nil +} diff --git a/release/debian/rules b/release/debian/rules index 5064961f389..412d60df5a5 100755 --- a/release/debian/rules +++ b/release/debian/rules @@ -14,8 +14,6 @@ execute_after_dh_auto_configure: override_dh_auto_build: DH_GOPKG="github.com/v2fly/v2ray-core/v5/main" dh_auto_build -- -ldflags "-s -w" cd $(BUILDDIR); mv bin/main bin/v2ray - DH_GOPKG="github.com/v2fly/v2ray-core/v5/infra/control/main" dh_auto_build -- -ldflags "-s -w" -tags confonly - cd $(BUILDDIR); mv bin/main bin/v2ctl override_dh_auto_install: dh_auto_install -- --no-source diff --git a/transport/internet/sockopt_darwin.go b/transport/internet/sockopt_darwin.go index 200b6abf8ac..18868405e0f 100644 --- a/transport/internet/sockopt_darwin.go +++ b/transport/internet/sockopt_darwin.go @@ -1,6 +1,8 @@ package internet import ( + "net" + "golang.org/x/sys/unix" ) @@ -42,6 +44,19 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf } } + if config.BindToDevice != "" { + iface, err := net.InterfaceByName(config.BindToDevice) + if err != nil { + return newError("failed to get interface ", config.BindToDevice).Base(err) + } + if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index); err != nil { + return newError("failed to set IP_BOUND_IF", err) + } + if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index); err != nil { + return newError("failed to set IPV6_BOUND_IF", err) + } + } + if config.TxBufSize != 0 { if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_SNDBUF, int(config.TxBufSize)); err != nil { return newError("failed to set SO_SNDBUF").Base(err) @@ -86,6 +101,19 @@ func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) } } + if config.BindToDevice != "" { + iface, err := net.InterfaceByName(config.BindToDevice) + if err != nil { + return newError("failed to get interface ", config.BindToDevice).Base(err) + } + if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index); err != nil { + return newError("failed to set IP_BOUND_IF", err) + } + if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index); err != nil { + return newError("failed to set IPV6_BOUND_IF", err) + } + } + if config.TxBufSize != 0 { if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_SNDBUF, int(config.TxBufSize)); err != nil { return newError("failed to set SO_SNDBUF/SO_SNDBUFFORCE").Base(err) diff --git a/transport/internet/sockopt_windows.go b/transport/internet/sockopt_windows.go index eca97278ab2..6a4ec03fd3f 100644 --- a/transport/internet/sockopt_windows.go +++ b/transport/internet/sockopt_windows.go @@ -1,13 +1,16 @@ package internet import ( + "net" "syscall" "golang.org/x/sys/windows" ) const ( - TCP_FASTOPEN = 15 // nolint: revive,stylecheck + TCP_FASTOPEN = 15 // nolint: revive,stylecheck + IP_UNICAST_IF = 31 // nolint: revive,stylecheck + IPV6_UNICAST_IF = 31 // nolint: revive,stylecheck ) func setTFO(fd syscall.Handle, settings SocketConfig_TCPFastOpenState) error { @@ -36,6 +39,19 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf } } + if config.BindToDevice != "" { + iface, err := net.InterfaceByName(config.BindToDevice) + if err != nil { + return newError("failed to get interface ", config.BindToDevice).Base(err) + } + if err := windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, IP_UNICAST_IF, iface.Index); err != nil { + return newError("failed to set IP_UNICAST_IF", err) + } + if err := windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, IPV6_UNICAST_IF, iface.Index); err != nil { + return newError("failed to set IPV6_UNICAST_IF", err) + } + } + if config.TxBufSize != 0 { if err := windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_SNDBUF, int(config.TxBufSize)); err != nil { return newError("failed to set SO_SNDBUF").Base(err) diff --git a/transport/internet/tcp_hub.go b/transport/internet/tcp_hub.go index 177b5688775..3cc25e5bf3e 100644 --- a/transport/internet/tcp_hub.go +++ b/transport/internet/tcp_hub.go @@ -36,6 +36,11 @@ func ListenUnix(ctx context.Context, address net.Address, settings *MemoryStream } protocol := settings.ProtocolName + + if originalProtocolName := getOriginalMessageName(settings); originalProtocolName != "" { + protocol = originalProtocolName + } + listenFunc := transportListenerCache[protocol] if listenFunc == nil { return nil, newError(protocol, " unix istener not registered.").AtError() diff --git a/transport/internet/udp/hub_freebsd.go b/transport/internet/udp/hub_freebsd.go index f5f853c34fc..87924bf3fff 100644 --- a/transport/internet/udp/hub_freebsd.go +++ b/transport/internet/udp/hub_freebsd.go @@ -8,6 +8,7 @@ import ( "encoding/gob" "io" + "github.com/v2fly/v2ray-core/v5/common/errors" "github.com/v2fly/v2ray-core/v5/common/net" "github.com/v2fly/v2ray-core/v5/transport/internet" ) @@ -28,11 +29,24 @@ func RetrieveOriginalDest(oob []byte) net.Destination { // ReadUDPMsg stores laddr, caddr for later use func ReadUDPMsg(conn *net.UDPConn, payload []byte, oob []byte) (int, int, int, *net.UDPAddr, error) { nBytes, addr, err := conn.ReadFromUDP(payload) + if err != nil { + return nBytes, 0, 0, addr, err + } + var buf bytes.Buffer enc := gob.NewEncoder(&buf) - enc.Encode(conn.LocalAddr().(*net.UDPAddr)) + localAddr, ok := conn.LocalAddr().(*net.UDPAddr) + if !ok { + return 0, 0, 0, nil, errors.New("invalid local address") + } + if addr == nil { + return 0, 0, 0, nil, errors.New("invalid remote address") + } + enc.Encode(localAddr) enc.Encode(addr) + var reader io.Reader = &buf noob, _ := reader.Read(oob) - return nBytes, noob, 0, addr, err + + return nBytes, noob, 0, addr, nil }