From 5f86668cebaa153c989979544f57b3dc5139fbc0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Nov 2021 11:28:39 +0800 Subject: [PATCH 01/11] Chore: bump google.golang.org/grpc from 1.40.0 to 1.42.0 (#1366) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.40.0 to 1.42.0. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.40.0...v1.42.0) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index aa5f99b53..ec31436cb 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sys v0.0.0-20210903071746-97244b99971b - google.golang.org/grpc v1.40.0 + google.golang.org/grpc v1.42.0 google.golang.org/protobuf v1.27.1 gopkg.in/yaml.v2 v2.4.0 h12.io/socks v1.0.3 diff --git a/go.sum b/go.sum index b0eb4d519..64c49dbec 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,7 @@ github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBT github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -44,7 +45,10 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -68,7 +72,7 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF 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= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= @@ -582,8 +586,8 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0 h1:XT2/MFpuPFsEX2fWh3YQtHkZ+WYZFQRfaUgLZYj/p6A= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From 5dedf7d9519764912cd58f710105f924cbbf3f37 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Nov 2021 13:11:06 +0800 Subject: [PATCH 02/11] Chore: bump github.com/jhump/protoreflect from 1.9.0 to 1.10.1 (#1323) Bumps [github.com/jhump/protoreflect](https://github.com/jhump/protoreflect) from 1.9.0 to 1.10.1. - [Release notes](https://github.com/jhump/protoreflect/releases) - [Commits](https://github.com/jhump/protoreflect/compare/v1.9.0...v1.10.1) --- updated-dependencies: - dependency-name: github.com/jhump/protoreflect dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ec31436cb..d6376f62d 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/golang/protobuf v1.5.2 github.com/google/go-cmp v0.5.6 github.com/gorilla/websocket v1.4.2 - github.com/jhump/protoreflect v1.9.0 + github.com/jhump/protoreflect v1.10.1 github.com/lucas-clemente/quic-go v0.24.0 github.com/miekg/dns v1.1.43 github.com/pelletier/go-toml v1.9.4 diff --git a/go.sum b/go.sum index 64c49dbec..1e73c0354 100644 --- a/go.sum +++ b/go.sum @@ -187,8 +187,8 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= -github.com/jhump/protoreflect v1.9.0 h1:npqHz788dryJiR/l6K/RUQAyh2SwV91+d1dnh4RjO9w= -github.com/jhump/protoreflect v1.9.0/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg= +github.com/jhump/protoreflect v1.10.1 h1:iH+UZfsbRE6vpyZH7asAjTPWJf7RJbpZ9j/N3lDlKs0= +github.com/jhump/protoreflect v1.10.1/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= From 80d92381af379078bea9a3c415c2bee1b16bf07b Mon Sep 17 00:00:00 2001 From: ayanamist Date: Fri, 5 Nov 2021 13:22:30 +0800 Subject: [PATCH 03/11] Add PATCH TRACE http method to sniffer (#1314) --- common/protocol/http/sniff.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/protocol/http/sniff.go b/common/protocol/http/sniff.go index b96c947eb..af2781c05 100644 --- a/common/protocol/http/sniff.go +++ b/common/protocol/http/sniff.go @@ -37,7 +37,8 @@ func (h *SniffHeader) Domain() string { } var ( - methods = [...]string{"get", "post", "head", "put", "delete", "options", "connect"} + // refer to https://pkg.go.dev/net/http@master#pkg-constants + methods = [...]string{"get", "post", "head", "put", "delete", "options", "connect", "patch", "trace"} errNotHTTPMethod = errors.New("not an HTTP method") ) From ed9641dad1ab971e2977e26f2fd4545eb6524e45 Mon Sep 17 00:00:00 2001 From: Ye Zhihao Date: Fri, 5 Nov 2021 13:24:46 +0800 Subject: [PATCH 04/11] Refactor strmatcher.MphMatcherGroup (#1364) * Refactor strmatcher.MphMatcherGroup * Add test for empty mph matcher group --- common/strmatcher/matchergroup_mph.go | 334 +++++++++------------ common/strmatcher/matchergroup_mph_test.go | 104 +++++++ 2 files changed, 246 insertions(+), 192 deletions(-) diff --git a/common/strmatcher/matchergroup_mph.go b/common/strmatcher/matchergroup_mph.go index 0ec1146e8..d842e4486 100644 --- a/common/strmatcher/matchergroup_mph.go +++ b/common/strmatcher/matchergroup_mph.go @@ -10,134 +10,187 @@ import ( // PrimeRK is the prime base used in Rabin-Karp algorithm. const PrimeRK = 16777619 -// calculate the rolling murmurHash of given string -func RollingHash(s string) uint32 { - h := uint32(0) - for i := len(s) - 1; i >= 0; i-- { - h = h*PrimeRK + uint32(s[i]) +// RollingHash calculates the rolling murmurHash of given string based on a provided suffix hash. +func RollingHash(hash uint32, input string) uint32 { + for i := len(input) - 1; i >= 0; i-- { + hash = hash*PrimeRK + uint32(input[i]) } - return h + return hash +} + +// MemHash is the hash function used by go map, it utilizes available hardware instructions(behaves +// as aeshash if aes instruction is available). +// With different seed, each MemHash performs as distinct hash functions. +func MemHash(seed uint32, input string) uint32 { + return uint32(strhash(unsafe.Pointer(&input), uintptr(seed))) // nosemgrep +} + +const ( + mphMatchTypeCount = 2 // Full and Domain +) + +type mphRuleInfo struct { + rollingHash uint32 + matchers [mphMatchTypeCount][]uint32 } // MphMatcherGroup is an implementation of MatcherGroup. // It implements Rabin-Karp algorithm and minimal perfect hash table for Full and Domain matcher. type MphMatcherGroup struct { - rules []string - level0 []uint32 - level0Mask int - level1 []uint32 - level1Mask int - ruleMap *map[string]uint32 + rules []string // RuleIdx -> pattern string, index 0 reserved for failed lookup + values [][]uint32 // RuleIdx -> registered matcher values for the pattern (Full Matcher takes precedence) + level0 []uint32 // RollingHash & Mask -> seed for Memhash + level0Mask uint32 // Mask restricting RollingHash to 0 ~ len(level0) + level1 []uint32 // Memhash & Mask -> stored index for rules + level1Mask uint32 // Mask for restricting Memhash to 0 ~ len(level1) + ruleInfos *map[string]mphRuleInfo } func NewMphMatcherGroup() *MphMatcherGroup { return &MphMatcherGroup{ - rules: nil, + rules: []string{""}, + values: [][]uint32{nil}, level0: nil, level0Mask: 0, level1: nil, level1Mask: 0, - ruleMap: &map[string]uint32{}, + ruleInfos: &map[string]mphRuleInfo{}, // Only used for building, destroyed after build complete } } // AddFullMatcher implements MatcherGroupForFull. -func (g *MphMatcherGroup) AddFullMatcher(matcher FullMatcher, _ uint32) { +func (g *MphMatcherGroup) AddFullMatcher(matcher FullMatcher, value uint32) { pattern := strings.ToLower(matcher.Pattern()) - (*g.ruleMap)[pattern] = RollingHash(pattern) + g.addPattern(0, "", pattern, matcher.Type(), value) } // AddDomainMatcher implements MatcherGroupForDomain. -func (g *MphMatcherGroup) AddDomainMatcher(matcher DomainMatcher, _ uint32) { +func (g *MphMatcherGroup) AddDomainMatcher(matcher DomainMatcher, value uint32) { pattern := strings.ToLower(matcher.Pattern()) - h := RollingHash(pattern) - (*g.ruleMap)[pattern] = h - (*g.ruleMap)["."+pattern] = h*PrimeRK + uint32('.') + hash := g.addPattern(0, "", pattern, matcher.Type(), value) // For full domain match + g.addPattern(hash, pattern, ".", matcher.Type(), value) // For partial domain match +} + +func (g *MphMatcherGroup) addPattern(suffixHash uint32, suffixPattern string, pattern string, matcherType Type, value uint32) uint32 { + fullPattern := pattern + suffixPattern + info, found := (*g.ruleInfos)[fullPattern] + if !found { + info = mphRuleInfo{rollingHash: RollingHash(suffixHash, pattern)} + g.rules = append(g.rules, fullPattern) + g.values = append(g.values, nil) + } + info.matchers[matcherType] = append(info.matchers[matcherType], value) + (*g.ruleInfos)[fullPattern] = info + return info.rollingHash } // Build builds a minimal perfect hash table for insert rules. -func (g *MphMatcherGroup) Build() { - keyLen := len(*g.ruleMap) - if keyLen == 0 { - keyLen = 1 - (*g.ruleMap)["empty___"] = RollingHash("empty___") - } - g.level0 = make([]uint32, nextPow2(keyLen/4)) - g.level0Mask = len(g.level0) - 1 - g.level1 = make([]uint32, nextPow2(keyLen)) - g.level1Mask = len(g.level1) - 1 - sparseBuckets := make([][]int, len(g.level0)) - var ruleIdx int - for rule, hash := range *g.ruleMap { - n := int(hash) & g.level0Mask - g.rules = append(g.rules, rule) - sparseBuckets[n] = append(sparseBuckets[n], ruleIdx) - ruleIdx++ - } - g.ruleMap = nil - var buckets []indexBucket - for n, vals := range sparseBuckets { - if len(vals) > 0 { - buckets = append(buckets, indexBucket{n, vals}) - } - } - sort.Sort(bySize(buckets)) +// Algorithm used: Hash, displace, and compress. See http://cmph.sourceforge.net/papers/esa09.pdf +func (g *MphMatcherGroup) Build() error { + ruleCount := len(*g.ruleInfos) + g.level0 = make([]uint32, nextPow2(ruleCount/4)) + g.level0Mask = uint32(len(g.level0) - 1) + g.level1 = make([]uint32, nextPow2(ruleCount)) + g.level1Mask = uint32(len(g.level1) - 1) - occ := make([]bool, len(g.level1)) - var tmpOcc []int - for _, bucket := range buckets { + // Create buckets based on all rule's rolling hash + buckets := make([][]uint32, len(g.level0)) + for ruleIdx := 1; ruleIdx < len(g.rules); ruleIdx++ { // Traverse rules starting from index 1 (0 reserved for failed lookup) + ruleInfo := (*g.ruleInfos)[g.rules[ruleIdx]] + bucketIdx := ruleInfo.rollingHash & g.level0Mask + buckets[bucketIdx] = append(buckets[bucketIdx], uint32(ruleIdx)) + g.values[ruleIdx] = append(ruleInfo.matchers[Full], ruleInfo.matchers[Domain]...) // nolint:gocritic + } + g.ruleInfos = nil // Set ruleInfos nil to release memory + + // Sort buckets in descending order with respect to each bucket's size + bucketIdxs := make([]int, len(buckets)) + for bucketIdx := range buckets { + bucketIdxs[bucketIdx] = bucketIdx + } + sort.Slice(bucketIdxs, func(i, j int) bool { return len(buckets[bucketIdxs[i]]) > len(buckets[bucketIdxs[j]]) }) + + // Exercise Hash, Displace, and Compress algorithm to construct minimal perfect hash table + occupied := make([]bool, len(g.level1)) // Whether a second-level hash has been already used + hashedBucket := make([]uint32, 0, 4) // Second-level hashes for each rule in a specific bucket + for _, bucketIdx := range bucketIdxs { + bucket := buckets[bucketIdx] + hashedBucket = hashedBucket[:0] seed := uint32(0) - for { - findSeed := true - tmpOcc = tmpOcc[:0] - for _, i := range bucket.vals { - n := int(strhashFallback(unsafe.Pointer(&g.rules[i]), uintptr(seed))) & g.level1Mask // nosemgrep - if occ[n] { - for _, n := range tmpOcc { - occ[n] = false + for len(hashedBucket) != len(bucket) { + for _, ruleIdx := range bucket { + memHash := MemHash(seed, g.rules[ruleIdx]) & g.level1Mask + if occupied[memHash] { // Collision occurred with this seed + for _, hash := range hashedBucket { // Revert all values in this hashed bucket + occupied[hash] = false + g.level1[hash] = 0 } - seed++ - findSeed = false + hashedBucket = hashedBucket[:0] + seed++ // Try next seed break } - occ[n] = true - tmpOcc = append(tmpOcc, n) - g.level1[n] = uint32(i) - } - if findSeed { - g.level0[bucket.n] = seed - break + occupied[memHash] = true + g.level1[memHash] = ruleIdx // The final value in the hash table + hashedBucket = append(hashedBucket, memHash) } } + g.level0[bucketIdx] = seed // Displacement value for this bucket } -} - -// Lookup searches for s in t and returns its index and whether it was found. -func (g *MphMatcherGroup) Lookup(h uint32, s string) bool { - i0 := int(h) & g.level0Mask - seed := g.level0[i0] - i1 := int(strhashFallback(unsafe.Pointer(&s), uintptr(seed))) & g.level1Mask // nosemgrep - n := g.level1[i1] - return s == g.rules[int(n)] -} - -// Match implements MatcherGroup.Match. -func (*MphMatcherGroup) Match(_ string) []uint32 { return nil } -// MatchAny implements MatcherGroup.MatchAny. -func (g *MphMatcherGroup) MatchAny(pattern string) bool { +// Lookup searches for input in minimal perfect hash table and returns its index. 0 indicates not found. +func (g *MphMatcherGroup) Lookup(rollingHash uint32, input string) uint32 { + i0 := rollingHash & g.level0Mask + seed := g.level0[i0] + i1 := MemHash(seed, input) & g.level1Mask + if n := g.level1[i1]; g.rules[n] == input { + return n + } + return 0 +} + +// Match implements MatcherGroup.Match. +func (g *MphMatcherGroup) Match(input string) []uint32 { + matches := [][]uint32{} hash := uint32(0) - for i := len(pattern) - 1; i >= 0; i-- { - hash = hash*PrimeRK + uint32(pattern[i]) - if pattern[i] == '.' { - if g.Lookup(hash, pattern[i:]) { + for i := len(input) - 1; i >= 0; i-- { + hash = hash*PrimeRK + uint32(input[i]) + if input[i] == '.' { + if mphIdx := g.Lookup(hash, input[i:]); mphIdx != 0 { + matches = append(matches, g.values[mphIdx]) + } + } + } + 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 + } +} + +// MatchAny implements MatcherGroup.MatchAny. +func (g *MphMatcherGroup) MatchAny(input string) bool { + hash := uint32(0) + for i := len(input) - 1; i >= 0; i-- { + hash = hash*PrimeRK + uint32(input[i]) + if input[i] == '.' { + if g.Lookup(hash, input[i:]) != 0 { return true } } } - return g.Lookup(hash, pattern) + return g.Lookup(hash, input) != 0 } func nextPow2(v int) int { @@ -149,109 +202,6 @@ func nextPow2(v int) int { return int(n) } -type indexBucket struct { - n int - vals []int -} - -type bySize []indexBucket - -func (s bySize) Len() int { return len(s) } -func (s bySize) Less(i, j int) bool { return len(s[i].vals) > len(s[j].vals) } -func (s bySize) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -type stringStruct struct { - str unsafe.Pointer - len int -} - -func strhashFallback(a unsafe.Pointer, h uintptr) uintptr { - x := (*stringStruct)(a) - return memhashFallback(x.str, h, uintptr(x.len)) -} - -const ( - // Constants for multiplication: four random odd 64-bit numbers. - m1 = 16877499708836156737 - m2 = 2820277070424839065 - m3 = 9497967016996688599 - m4 = 15839092249703872147 -) - -var hashkey = [4]uintptr{1, 1, 1, 1} - -func memhashFallback(p unsafe.Pointer, seed, s uintptr) uintptr { - h := uint64(seed + s*hashkey[0]) -tail: - switch { - case s == 0: - case s < 4: - h ^= uint64(*(*byte)(p)) - h ^= uint64(*(*byte)(add(p, s>>1))) << 8 - h ^= uint64(*(*byte)(add(p, s-1))) << 16 - h = rotl31(h*m1) * m2 - case s <= 8: - h ^= uint64(readUnaligned32(p)) - h ^= uint64(readUnaligned32(add(p, s-4))) << 32 - h = rotl31(h*m1) * m2 - case s <= 16: - h ^= readUnaligned64(p) - h = rotl31(h*m1) * m2 - h ^= readUnaligned64(add(p, s-8)) - h = rotl31(h*m1) * m2 - case s <= 32: - h ^= readUnaligned64(p) - h = rotl31(h*m1) * m2 - h ^= readUnaligned64(add(p, 8)) - h = rotl31(h*m1) * m2 - h ^= readUnaligned64(add(p, s-16)) - h = rotl31(h*m1) * m2 - h ^= readUnaligned64(add(p, s-8)) - h = rotl31(h*m1) * m2 - default: - v1 := h - v2 := uint64(seed * hashkey[1]) - v3 := uint64(seed * hashkey[2]) - v4 := uint64(seed * hashkey[3]) - for s >= 32 { - v1 ^= readUnaligned64(p) - v1 = rotl31(v1*m1) * m2 - p = add(p, 8) - v2 ^= readUnaligned64(p) - v2 = rotl31(v2*m2) * m3 - p = add(p, 8) - v3 ^= readUnaligned64(p) - v3 = rotl31(v3*m3) * m4 - p = add(p, 8) - v4 ^= readUnaligned64(p) - v4 = rotl31(v4*m4) * m1 - p = add(p, 8) - s -= 32 - } - h = v1 ^ v2 ^ v3 ^ v4 - goto tail - } - - h ^= h >> 29 - h *= m3 - h ^= h >> 32 - return uintptr(h) -} - -func add(p unsafe.Pointer, x uintptr) unsafe.Pointer { - return unsafe.Pointer(uintptr(p) + x) // nosemgrep -} - -func readUnaligned32(p unsafe.Pointer) uint32 { - q := (*[4]byte)(p) - return uint32(q[0]) | uint32(q[1])<<8 | uint32(q[2])<<16 | uint32(q[3])<<24 -} - -func rotl31(x uint64) uint64 { - return (x << 31) | (x >> (64 - 31)) -} - -func readUnaligned64(p unsafe.Pointer) uint64 { - q := (*[8]byte)(p) - return uint64(q[0]) | uint64(q[1])<<8 | uint64(q[2])<<16 | uint64(q[3])<<24 | uint64(q[4])<<32 | uint64(q[5])<<40 | uint64(q[6])<<48 | uint64(q[7])<<56 -} +//go:noescape +//go:linkname strhash runtime.strhash +func strhash(p unsafe.Pointer, h uintptr) uintptr diff --git a/common/strmatcher/matchergroup_mph_test.go b/common/strmatcher/matchergroup_mph_test.go index 88b569036..b876227e6 100644 --- a/common/strmatcher/matchergroup_mph_test.go +++ b/common/strmatcher/matchergroup_mph_test.go @@ -1,6 +1,7 @@ package strmatcher_test import ( + "reflect" "testing" "github.com/v2fly/v2ray-core/v4/common" @@ -172,3 +173,106 @@ func TestMphMatcherGroup(t *testing.T) { } } } + +// See https://github.com/v2fly/v2ray-core/issues/92#issuecomment-673238489 +func TestMphMatcherGroupAsIndexMatcher(t *testing.T) { + rules := []struct { + Type Type + Domain string + }{ + // Regex not supported by MphMatcherGroup + // { + // Type: Regex, + // Domain: "apis\\.us$", + // }, + // Substr not supported by MphMatcherGroup + // { + // Type: Substr, + // Domain: "apis", + // }, + { + Type: Domain, + Domain: "googleapis.com", + }, + { + Type: Domain, + Domain: "com", + }, + { + Type: Full, + Domain: "www.baidu.com", + }, + // Substr not supported by MphMatcherGroup, We add another matcher to preserve index + { + Type: Domain, // Substr, + Domain: "example.com", // "apis", + }, + { + Type: Domain, + Domain: "googleapis.com", + }, + { + Type: Full, + Domain: "fonts.googleapis.com", + }, + { + Type: Full, + Domain: "www.baidu.com", + }, + { // This matcher (index 10) is swapped with matcher (index 6) to test that full matcher takes high priority. + Type: Full, + Domain: "example.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,*/ }, + Output: nil, + }, + { + Input: "example.com", + Output: []uint32{10, 6, 11, 4}, + }, + } + matcherGroup := NewMphMatcherGroup() + for i, rule := range rules { + matcher, err := rule.Type.New(rule.Domain) + common.Must(err) + common.Must(AddMatcherToGroup(matcherGroup, matcher, uint32(i+3))) + } + 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) + } + } +} + +func TestEmptyMphMatcherGroup(t *testing.T) { + g := NewMphMatcherGroup() + g.Build() + r := g.Match("v2fly.org") + if len(r) != 0 { + t.Error("Expect [], but ", r) + } +} From c25e33c4232e9a2c1dc4d49c37d1b240dce82bb0 Mon Sep 17 00:00:00 2001 From: dyhkwong <50692134+dyhkwong@users.noreply.github.com> Date: Sat, 6 Nov 2021 15:48:06 +0800 Subject: [PATCH 05/11] Fix `test` command does not load default `config.json` (#1368) --- main/commands/test.go | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/main/commands/test.go b/main/commands/test.go index 0fbbc058a..c71981ba9 100644 --- a/main/commands/test.go +++ b/main/commands/test.go @@ -2,9 +2,7 @@ package commands import ( "fmt" - "log" - core "github.com/v2fly/v2ray-core/v4" "github.com/v2fly/v2ray-core/v4/main/commands/base" ) @@ -51,24 +49,9 @@ Use "{{.Exec}} help format-loader" for more information about format. func executeTest(cmd *base.Command, args []string) { setConfigFlags(cmd) cmd.Flag.Parse(args) - - extension, err := core.GetLoaderExtensions(*configFormat) - if err != nil { - base.Fatalf(err.Error()) - } - - if len(configDirs) > 0 { - dirReader := readConfDir - if *configDirRecursively { - dirReader = readConfDirRecursively - } - for _, d := range configDirs { - log.Println("Using confdir from arg:", d) - configFiles = append(configFiles, dirReader(d, extension)...) - } - } printVersion() - _, err = startV2Ray() + configFiles = getConfigFilePath() + _, err := startV2Ray() if err != nil { base.Fatalf("Test failed: %s", err) } From 77b88171d6bd837b76a5ad6e6b23689391530ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E8=81=96=E9=BB=8E=20=28Zhu=20Sheng=20Li=29?= Date: Sat, 6 Nov 2021 22:09:26 +0800 Subject: [PATCH 06/11] fix: readRequest API changed since go1.17 (#1370) fixed: #1265 --- transport/internet/headers/http/http.go | 4 ++-- transport/internet/headers/http/linkedreadRequest.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/transport/internet/headers/http/http.go b/transport/internet/headers/http/http.go index c73f7a63e..54611889b 100644 --- a/transport/internet/headers/http/http.go +++ b/transport/internet/headers/http/http.go @@ -90,7 +90,7 @@ func (h *HeaderReader) Read(reader io.Reader) (*buf.Buffer, error) { buffer.Clear() copy(buffer.Extend(lenEnding), leftover) - if _, err := readRequest(bufio.NewReader(bytes.NewReader(headerBuf.Bytes())), false); err != io.ErrUnexpectedEOF { + if _, err := readRequest(bufio.NewReader(bytes.NewReader(headerBuf.Bytes()))); err != io.ErrUnexpectedEOF { return nil, err } } @@ -110,7 +110,7 @@ func (h *HeaderReader) Read(reader io.Reader) (*buf.Buffer, error) { } // Parse the request - if req, err := readRequest(bufio.NewReader(bytes.NewReader(headerBuf.Bytes())), false); err != nil { + if req, err := readRequest(bufio.NewReader(bytes.NewReader(headerBuf.Bytes()))); err != nil { return nil, err } else { // nolint: golint h.req = req diff --git a/transport/internet/headers/http/linkedreadRequest.go b/transport/internet/headers/http/linkedreadRequest.go index 35154b871..457733125 100644 --- a/transport/internet/headers/http/linkedreadRequest.go +++ b/transport/internet/headers/http/linkedreadRequest.go @@ -9,4 +9,4 @@ import ( ) //go:linkname readRequest net/http.readRequest -func readRequest(b *bufio.Reader, deleteHostHeader bool) (req *http.Request, err error) +func readRequest(b *bufio.Reader) (req *http.Request, err error) From 1490ce645d3a59c35ce1f48344f3806d352dcc75 Mon Sep 17 00:00:00 2001 From: kslr Date: Sat, 6 Nov 2021 22:18:26 +0800 Subject: [PATCH 07/11] put code_of_conduct in the right place --- .github/CODE_OF_CONDUCT.md | 124 +++++++++++++++++++++++++++++------ CODE_OF_CONDUCT.md | 128 ------------------------------------- 2 files changed, 103 insertions(+), 149 deletions(-) delete mode 100644 CODE_OF_CONDUCT.md diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index ea7e1ce81..6630814e4 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -2,45 +2,127 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment include: +Examples of behavior that contributes to a positive environment for our +community include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at love@v2ray.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +conduct@v2fly.org. +All complaints will be reviewed and investigated promptly and fairly. -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 6630814e4..000000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,128 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, religion, or sexual identity -and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the - overall community - -Examples of unacceptable behavior include: - -* The use of sexualized language or imagery, and sexual attention or - advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email - address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -conduct@v2fly.org. -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series -of actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or -permanent ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within -the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.0, available at -https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. - -Community Impact Guidelines were inspired by [Mozilla's code of conduct -enforcement ladder](https://github.com/mozilla/diversity). - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. From dbc26775c90f9dc1b0b134c662a7ff60759cffed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 2 Nov 2021 19:36:55 +0800 Subject: [PATCH 08/11] Add quic sniffer --- app/dispatcher/default.go | 31 ++-- app/dispatcher/sniffer.go | 15 +- common/buf/buffer.go | 34 +++- common/protocol/quic/sniff.go | 210 ++++++++++++++++++++++++ common/protocol/quic/sniff_test.go | 18 ++ go.mod | 2 +- infra/conf/cfgcommon/sniffer/sniffer.go | 2 + 7 files changed, 285 insertions(+), 27 deletions(-) create mode 100644 common/protocol/quic/sniff.go create mode 100644 common/protocol/quic/sniff_test.go diff --git a/app/dispatcher/default.go b/app/dispatcher/default.go index 0e4807126..074114b66 100644 --- a/app/dispatcher/default.go +++ b/app/dispatcher/default.go @@ -174,6 +174,9 @@ func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *tran } func shouldOverride(result SniffResult, domainOverride []string) bool { + if result.Domain() == "" { + return false + } protocolString := result.Protocol() if resComp, ok := result.(SnifferResultComposite); ok { protocolString = resComp.ProtocolForDomainResult() @@ -207,32 +210,16 @@ func (d *DefaultDispatcher) Dispatch(ctx context.Context, destination net.Destin content = new(session.Content) ctx = session.ContextWithContent(ctx, content) } - sniffingRequest := content.SniffingRequest - switch { - case !sniffingRequest.Enabled: + if !sniffingRequest.Enabled { go d.routedDispatch(ctx, outbound, destination) - - case destination.Network != net.Network_TCP: - // Only metadata sniff will be used for non tcp connection - result, err := sniffer(ctx, nil, true) - if err == nil { - content.Protocol = result.Protocol() - if shouldOverride(result, sniffingRequest.OverrideDestinationForProtocol) { - domain := result.Domain() - newError("sniffed domain: ", domain).WriteToLog(session.ExportIDToError(ctx)) - destination.Address = net.ParseAddress(domain) - ob.Target = destination - } - } - go d.routedDispatch(ctx, outbound, destination) - default: + } else { go func() { cReader := &cachedReader{ reader: outbound.Reader.(*pipe.Reader), } outbound.Reader = cReader - result, err := sniffer(ctx, cReader, sniffingRequest.MetadataOnly) + result, err := sniffer(ctx, cReader, sniffingRequest.MetadataOnly, destination.Network) if err == nil { content.Protocol = result.Protocol() } @@ -245,10 +232,11 @@ func (d *DefaultDispatcher) Dispatch(ctx context.Context, destination net.Destin d.routedDispatch(ctx, outbound, destination) }() } + return inbound, nil } -func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool) (SniffResult, error) { +func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool, network net.Network) (SniffResult, error) { payload := buf.New() defer payload.Release() @@ -274,7 +262,7 @@ func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool) (Sni cReader.Cache(payload) if !payload.IsEmpty() { - result, err := sniffer.Sniff(ctx, payload.Bytes()) + result, err := sniffer.Sniff(ctx, payload.Bytes(), network) if err != common.ErrNoClue { return result, err } @@ -293,6 +281,7 @@ func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool) (Sni } return contentResult, contentErr } + func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.Link, destination net.Destination) { var handler outbound.Handler diff --git a/app/dispatcher/sniffer.go b/app/dispatcher/sniffer.go index 101e1e46c..8fe61049a 100644 --- a/app/dispatcher/sniffer.go +++ b/app/dispatcher/sniffer.go @@ -4,8 +4,10 @@ import ( "context" "github.com/v2fly/v2ray-core/v4/common" + "github.com/v2fly/v2ray-core/v4/common/net" "github.com/v2fly/v2ray-core/v4/common/protocol/bittorrent" "github.com/v2fly/v2ray-core/v4/common/protocol/http" + "github.com/v2fly/v2ray-core/v4/common/protocol/quic" "github.com/v2fly/v2ray-core/v4/common/protocol/tls" ) @@ -22,6 +24,7 @@ type protocolSnifferWithMetadata struct { // for both TCP and UDP connections // It will not be shown as a traffic type for routing unless there is no other successful sniffing. metadataSniffer bool + network net.Network } type Sniffer struct { @@ -31,9 +34,10 @@ type Sniffer struct { func NewSniffer(ctx context.Context) *Sniffer { ret := &Sniffer{ sniffer: []protocolSnifferWithMetadata{ - {func(c context.Context, b []byte) (SniffResult, error) { return http.SniffHTTP(b) }, false}, - {func(c context.Context, b []byte) (SniffResult, error) { return tls.SniffTLS(b) }, false}, - {func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffBittorrent(b) }, false}, + {func(c context.Context, b []byte) (SniffResult, error) { return http.SniffHTTP(b) }, false, net.Network_TCP}, + {func(c context.Context, b []byte) (SniffResult, error) { return tls.SniffTLS(b) }, false, net.Network_TCP}, + {func(c context.Context, b []byte) (SniffResult, error) { return quic.SniffQUIC(b) }, false, net.Network_UDP}, + {func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffBittorrent(b) }, false, net.Network_UDP}, }, } if sniffer, err := newFakeDNSSniffer(ctx); err == nil { @@ -49,13 +53,16 @@ func NewSniffer(ctx context.Context) *Sniffer { var errUnknownContent = newError("unknown content") -func (s *Sniffer) Sniff(c context.Context, payload []byte) (SniffResult, error) { +func (s *Sniffer) Sniff(c context.Context, payload []byte, network net.Network) (SniffResult, error) { var pendingSniffer []protocolSnifferWithMetadata for _, si := range s.sniffer { s := si.protocolSniffer if si.metadataSniffer { continue } + if si.network != network { + continue + } result, err := s(c, payload) if err == common.ErrNoClue { pendingSniffer = append(pendingSniffer, si) diff --git a/common/buf/buffer.go b/common/buf/buffer.go index fa1de3cfe..38cec4655 100644 --- a/common/buf/buffer.go +++ b/common/buf/buffer.go @@ -20,6 +20,7 @@ type Buffer struct { v []byte start int32 end int32 + out bool } // New creates a Buffer with 0 length and 2K capacity. @@ -29,6 +30,15 @@ func New() *Buffer { } } +// As creates a Buffer with an existed bytearray +func As(data []byte) *Buffer { + return &Buffer{ + v: data, + end: int32(len(data)), + out: true, + } +} + // StackNew creates a new Buffer object on stack. // This method is for buffers that is released in the same function. func StackNew() Buffer { @@ -39,7 +49,7 @@ func StackNew() Buffer { // Release recycles the buffer into an internal buffer pool. func (b *Buffer) Release() { - if b == nil || b.v == nil { + if b == nil || b.v == nil || b.out { return } @@ -173,6 +183,28 @@ func (b *Buffer) WriteString(s string) (int, error) { return b.Write([]byte(s)) } +// ReadByte implements io.ByteReader +func (b *Buffer) ReadByte() (byte, error) { + if b.start == b.end { + return 0, io.EOF + } + + nb := b.v[b.start] + b.start++ + return nb, nil +} + +// ReadBytes implements bufio.Reader.ReadBytes +func (b *Buffer) ReadBytes(length int32) ([]byte, error) { + if b.end-b.start < length { + return nil, io.EOF + } + + nb := b.v[b.start : b.start+length] + b.start += length + return nb, nil +} + // Read implements io.Reader.Read(). func (b *Buffer) Read(data []byte) (int, error) { if b.Len() == 0 { diff --git a/common/protocol/quic/sniff.go b/common/protocol/quic/sniff.go new file mode 100644 index 000000000..58f306dc2 --- /dev/null +++ b/common/protocol/quic/sniff.go @@ -0,0 +1,210 @@ +package quic + +import ( + "crypto" + "crypto/aes" + "crypto/tls" + "encoding/binary" + "io" + + "github.com/v2fly/v2ray-core/v4/common/errors" + + "github.com/lucas-clemente/quic-go/quicvarint" + "github.com/marten-seemann/qtls-go1-17" + "golang.org/x/crypto/hkdf" + + "github.com/v2fly/v2ray-core/v4/common" + "github.com/v2fly/v2ray-core/v4/common/buf" + ptls "github.com/v2fly/v2ray-core/v4/common/protocol/tls" +) + +type SniffHeader struct { + domain string +} + +func (s SniffHeader) Protocol() string { + return "quic" +} + +func (s SniffHeader) Domain() string { + return s.domain +} + +const ( + versionDraft29 uint32 = 0xff00001d + version1 uint32 = 0x1 +) + +var ( + quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99} + quicSalt = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a} + initialSuite = &qtls.CipherSuiteTLS13{ + ID: tls.TLS_AES_128_GCM_SHA256, + KeyLen: 16, + AEAD: qtls.AEADAESGCMTLS13, + Hash: crypto.SHA256, + } + errNotQuic = errors.New("not quic") + errNotQuicInitial = errors.New("not initial packet") +) + +func SniffQUIC(b []byte) (*SniffHeader, error) { + buffer := buf.As(b) + typeByte, err := buffer.ReadByte() + if err != nil { + return nil, errNotQuic + } + isLongHeader := typeByte&0x80 > 0 + if !isLongHeader || typeByte&0x40 == 0 { + return nil, errNotQuicInitial + } + + vb, err := buffer.ReadBytes(4) + if err != nil { + return nil, errNotQuic + } + + versionNumber := binary.BigEndian.Uint32(vb) + + if versionNumber != 0 && typeByte&0x40 == 0 { + return nil, errNotQuic + } else if versionNumber != versionDraft29 && versionNumber != version1 { + return nil, errNotQuic + } + + if (typeByte&0x30)>>4 != 0x0 { + return nil, errNotQuicInitial + } + + var destConnID []byte + if l, err := buffer.ReadByte(); err != nil { + return nil, errNotQuic + } else if destConnID, err = buffer.ReadBytes(int32(l)); err != nil { + return nil, errNotQuic + } + + if l, err := buffer.ReadByte(); err != nil { + return nil, errNotQuic + } else if common.Error2(buffer.ReadBytes(int32(l))) != nil { + return nil, errNotQuic + } + + tokenLen, err := quicvarint.Read(buffer) + if err != nil || tokenLen > uint64(len(b)) { + return nil, errNotQuic + } + + if _, err = buffer.ReadBytes(int32(tokenLen)); err != nil { + return nil, errNotQuic + } + + packetLen, err := quicvarint.Read(buffer) + if err != nil { + return nil, errNotQuic + } + + hdrLen := len(b) - int(buffer.Len()) + + origPNBytes := make([]byte, 4) + copy(origPNBytes, b[hdrLen:hdrLen+4]) + + var salt []byte + if versionNumber == version1 { + salt = quicSalt + } else { + salt = quicSaltOld + } + initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, salt) + secret := hkdfExpandLabel(crypto.SHA256, initialSecret, []byte{}, "client in", crypto.SHA256.Size()) + hpKey := hkdfExpandLabel(initialSuite.Hash, secret, []byte{}, "quic hp", initialSuite.KeyLen) + block, err := aes.NewCipher(hpKey) + if err != nil { + return nil, err + } + + cache := buf.New() + defer cache.Release() + + mask := cache.Extend(int32(block.BlockSize())) + block.Encrypt(mask, b[hdrLen+4:hdrLen+4+16]) + b[0] ^= mask[0] & 0xf + for i := range b[hdrLen : hdrLen+4] { + b[hdrLen+i] ^= mask[i+1] + } + packetNumberLength := b[0]&0x3 + 1 + if packetNumberLength != 1 { + return nil, errNotQuicInitial + } + var packetNumber uint32 + { + n, err := buffer.ReadByte() + if err != nil { + return nil, err + } + packetNumber = uint32(n) + } + + if packetNumber != 0 { + return nil, errNotQuicInitial + } + + extHdrLen := hdrLen + int(packetNumberLength) + copy(b[extHdrLen:hdrLen+4], origPNBytes[packetNumberLength:]) + data := b[extHdrLen : int(packetLen)+hdrLen] + + key := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic key", 16) + iv := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12) + cipher := qtls.AEADAESGCMTLS13(key, iv) + nonce := cache.Extend(int32(cipher.NonceSize())) + binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(packetNumber)) + decrypted, err := cipher.Open(b[extHdrLen:extHdrLen], nonce, data, b[:extHdrLen]) + if err != nil { + return nil, err + } + buffer = buf.As(decrypted) + frameType, err := buffer.ReadByte() + if err != nil { + return nil, io.ErrUnexpectedEOF + } + if frameType != 0x6 { + // not crypto frame + return &SniffHeader{domain: ""}, nil + } + if common.Error2(quicvarint.Read(buffer)) != nil { + return nil, io.ErrUnexpectedEOF + } + dataLen, err := quicvarint.Read(buffer) + if err != nil { + return nil, io.ErrUnexpectedEOF + } + if dataLen > uint64(buffer.Len()) { + return nil, io.ErrUnexpectedEOF + } + frameData, err := buffer.ReadBytes(int32(dataLen)) + common.Must(err) + tlsHdr := &ptls.SniffHeader{} + err = ptls.ReadClientHello(frameData, tlsHdr) + if err != nil { + return nil, err + } + + return &SniffHeader{domain: tlsHdr.Domain()}, nil +} + +func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte { + b := make([]byte, 3, 3+6+len(label)+1+len(context)) + binary.BigEndian.PutUint16(b, uint16(length)) + b[2] = uint8(6 + len(label)) + b = append(b, []byte("tls13 ")...) + b = append(b, []byte(label)...) + b = b[:3+6+len(label)+1] + b[3+6+len(label)] = uint8(len(context)) + b = append(b, context...) + + out := make([]byte, length) + n, err := hkdf.Expand(hash.New, secret, b).Read(out) + if err != nil || n != length { + panic("quic: HKDF-Expand-Label invocation failed unexpectedly") + } + return out +} diff --git a/common/protocol/quic/sniff_test.go b/common/protocol/quic/sniff_test.go new file mode 100644 index 000000000..f33cc4394 --- /dev/null +++ b/common/protocol/quic/sniff_test.go @@ -0,0 +1,18 @@ +package quic_test + +import ( + "encoding/hex" + "testing" + + "github.com/v2fly/v2ray-core/v4/common" + "github.com/v2fly/v2ray-core/v4/common/protocol/quic" +) + +func TestSniffQUIC(t *testing.T) { + pkt, err := hex.DecodeString("cd0000000108f1fb7bcc78aa5e7203a8f86400421531fe825b19541876db6c55c38890cd73149d267a084afee6087304095417a3033df6a81bbb71d8512e7a3e16df1e277cae5df3182cb214b8fe982ba3fdffbaa9ffec474547d55945f0fddbeadfb0b5243890b2fa3da45169e2bd34ec04b2e29382f48d612b28432a559757504d158e9e505407a77dd34f4b60b8d3b555ee85aacd6648686802f4de25e7216b19e54c5f78e8a5963380c742d861306db4c16e4f7fc94957aa50b9578a0b61f1e406b2ad5f0cd3cd271c4d99476409797b0c3cb3efec256118912d4b7e4fd79d9cb9016b6e5eaa4f5e57b637b217755daf8968a4092bed0ed5413f5d04904b3a61e4064f9211b2629e5b52a89c7b19f37a713e41e27743ea6dfa736dfa1bb0a4b2bc8c8dc632c6ce963493a20c550e6fdb2475213665e9a85cfc394da9cec0cf41f0c8abed3fc83be5245b2b5aa5e825d29349f721d30774ef5bf965b540f3d8d98febe20956b1fc8fa047e10e7d2f921c9c6622389e02322e80621a1cf5264e245b7276966eb02932584e3f7038bd36aa908766ad3fb98344025dec18670d6db43a1c5daac00937fce7b7c7d61ff4e6efd01a2bdee0ee183108b926393df4f3d74bbcbb015f240e7e346b7d01c41111a401225ce3b095ab4623a5836169bf9599eeca79d1d2e9b2202b5960a09211e978058d6fc0484eff3e91ce4649a5e3ba15b906d334cf66e28d9ff575406e1ae1ac2febafd72870b6f5d58fc5fb949cb1f40feb7c1d9ce5e71b") + common.Must(err) + quicHdr, err := quic.SniffQUIC(pkt) + if err != nil || quicHdr.Domain() != "www.google.com" { + t.Error("failed") + } +} diff --git a/go.mod b/go.mod index d6376f62d..d31b97bab 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/gorilla/websocket v1.4.2 github.com/jhump/protoreflect v1.10.1 github.com/lucas-clemente/quic-go v0.24.0 + github.com/marten-seemann/qtls-go1-17 v0.1.0 github.com/miekg/dns v1.1.43 github.com/pelletier/go-toml v1.9.4 github.com/pires/go-proxyproto v0.6.1 @@ -44,7 +45,6 @@ require ( github.com/leodido/go-urn v1.2.1 // indirect github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect github.com/marten-seemann/qtls-go1-16 v0.1.4 // indirect - github.com/marten-seemann/qtls-go1-17 v0.1.0 // indirect github.com/nxadm/tail v1.4.8 // indirect github.com/onsi/ginkgo v1.16.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/infra/conf/cfgcommon/sniffer/sniffer.go b/infra/conf/cfgcommon/sniffer/sniffer.go index 84654cc54..7f4557da0 100644 --- a/infra/conf/cfgcommon/sniffer/sniffer.go +++ b/infra/conf/cfgcommon/sniffer/sniffer.go @@ -25,6 +25,8 @@ func (c *SniffingConfig) Build() (*proxyman.SniffingConfig, error) { p = append(p, "http") case "tls", "https", "ssl": p = append(p, "tls") + case "quic": + p = append(p, "quic") case "fakedns": p = append(p, "fakedns") case "fakedns+others": From d781cc0cfd8622ad405064d1a7167b00aa5c3c46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 3 Nov 2021 09:25:48 +0800 Subject: [PATCH 09/11] Add uTP sniffer --- app/dispatcher/sniffer.go | 3 +- common/protocol/bittorrent/bittorrent.go | 60 ++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/app/dispatcher/sniffer.go b/app/dispatcher/sniffer.go index 8fe61049a..72ba883cc 100644 --- a/app/dispatcher/sniffer.go +++ b/app/dispatcher/sniffer.go @@ -37,7 +37,8 @@ func NewSniffer(ctx context.Context) *Sniffer { {func(c context.Context, b []byte) (SniffResult, error) { return http.SniffHTTP(b) }, false, net.Network_TCP}, {func(c context.Context, b []byte) (SniffResult, error) { return tls.SniffTLS(b) }, false, net.Network_TCP}, {func(c context.Context, b []byte) (SniffResult, error) { return quic.SniffQUIC(b) }, false, net.Network_UDP}, - {func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffBittorrent(b) }, false, net.Network_UDP}, + {func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffBittorrent(b) }, false, net.Network_TCP}, + {func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffUTP(b) }, false, net.Network_UDP}, }, } if sniffer, err := newFakeDNSSniffer(ctx); err == nil { diff --git a/common/protocol/bittorrent/bittorrent.go b/common/protocol/bittorrent/bittorrent.go index 248c10f67..b3c8e4ed4 100644 --- a/common/protocol/bittorrent/bittorrent.go +++ b/common/protocol/bittorrent/bittorrent.go @@ -1,7 +1,12 @@ package bittorrent import ( + "encoding/binary" "errors" + "math" + "time" + + "github.com/v2fly/v2ray-core/v4/common/buf" "github.com/v2fly/v2ray-core/v4/common" ) @@ -29,3 +34,58 @@ func SniffBittorrent(b []byte) (*SniffHeader, error) { return nil, errNotBittorrent } + +func SniffUTP(b []byte) (*SniffHeader, error) { + if len(b) < 20 { + return nil, common.ErrNoClue + } + + buffer := buf.As(b) + + var typeAndVersion uint8 + + if binary.Read(buffer, binary.BigEndian, &typeAndVersion) != nil { + return nil, common.ErrNoClue + } else if b[0]>>4&0xF > 4 || b[0]&0xF != 1 { + return nil, errNotBittorrent + } + + var extension uint8 + + if binary.Read(buffer, binary.BigEndian, &extension) != nil { + return nil, common.ErrNoClue + } else if extension != 0 && extension != 1 { + return nil, errNotBittorrent + } + + for extension != 0 { + if extension != 1 { + return nil, errNotBittorrent + } + if binary.Read(buffer, binary.BigEndian, &extension) != nil { + return nil, common.ErrNoClue + } + + var length uint8 + if err := binary.Read(buffer, binary.BigEndian, &length); err != nil { + return nil, common.ErrNoClue + } + if common.Error2(buffer.ReadBytes(int32(length))) != nil { + return nil, common.ErrNoClue + } + } + + if common.Error2(buffer.ReadBytes(2)) != nil { + return nil, common.ErrNoClue + } + + var timestamp uint32 + if err := binary.Read(buffer, binary.BigEndian, ×tamp); err != nil { + return nil, common.ErrNoClue + } + if math.Abs(float64(time.Now().UnixMicro()-int64(timestamp))) > float64(24*time.Hour) { + return nil, errNotBittorrent + } + + return &SniffHeader{}, nil +} From 802780e9e117c5e81935a02f029e0f941b5f2ff3 Mon Sep 17 00:00:00 2001 From: Shelikhoo Date: Sun, 7 Nov 2021 22:34:39 +0000 Subject: [PATCH 10/11] rename buf pool membership status to unmanaged --- common/buf/buffer.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/common/buf/buffer.go b/common/buf/buffer.go index 38cec4655..2db468c49 100644 --- a/common/buf/buffer.go +++ b/common/buf/buffer.go @@ -17,10 +17,10 @@ var pool = bytespool.GetPool(Size) // the buffer into an internal buffer pool, in order to recreate a buffer more // quickly. type Buffer struct { - v []byte - start int32 - end int32 - out bool + v []byte + start int32 + end int32 + unmanaged bool } // New creates a Buffer with 0 length and 2K capacity. @@ -33,9 +33,9 @@ func New() *Buffer { // As creates a Buffer with an existed bytearray func As(data []byte) *Buffer { return &Buffer{ - v: data, - end: int32(len(data)), - out: true, + v: data, + end: int32(len(data)), + unmanaged: true, } } @@ -49,7 +49,7 @@ func StackNew() Buffer { // Release recycles the buffer into an internal buffer pool. func (b *Buffer) Release() { - if b == nil || b.v == nil || b.out { + if b == nil || b.v == nil || b.unmanaged { return } From 65174fa487d582cb23c764b083122438eebfa8c6 Mon Sep 17 00:00:00 2001 From: Shelikhoo Date: Sun, 7 Nov 2021 22:45:36 +0000 Subject: [PATCH 11/11] rename buf type adaptor into FromBytes --- common/buf/buffer.go | 4 ++-- common/protocol/bittorrent/bittorrent.go | 2 +- common/protocol/quic/sniff.go | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/common/buf/buffer.go b/common/buf/buffer.go index 2db468c49..fe03eb9bb 100644 --- a/common/buf/buffer.go +++ b/common/buf/buffer.go @@ -30,8 +30,8 @@ func New() *Buffer { } } -// As creates a Buffer with an existed bytearray -func As(data []byte) *Buffer { +// FromBytes creates a Buffer with an existed bytearray +func FromBytes(data []byte) *Buffer { return &Buffer{ v: data, end: int32(len(data)), diff --git a/common/protocol/bittorrent/bittorrent.go b/common/protocol/bittorrent/bittorrent.go index b3c8e4ed4..84b151166 100644 --- a/common/protocol/bittorrent/bittorrent.go +++ b/common/protocol/bittorrent/bittorrent.go @@ -40,7 +40,7 @@ func SniffUTP(b []byte) (*SniffHeader, error) { return nil, common.ErrNoClue } - buffer := buf.As(b) + buffer := buf.FromBytes(b) var typeAndVersion uint8 diff --git a/common/protocol/quic/sniff.go b/common/protocol/quic/sniff.go index 58f306dc2..db42e7a28 100644 --- a/common/protocol/quic/sniff.go +++ b/common/protocol/quic/sniff.go @@ -49,7 +49,7 @@ var ( ) func SniffQUIC(b []byte) (*SniffHeader, error) { - buffer := buf.As(b) + buffer := buf.FromBytes(b) typeByte, err := buffer.ReadByte() if err != nil { return nil, errNotQuic @@ -161,7 +161,7 @@ func SniffQUIC(b []byte) (*SniffHeader, error) { if err != nil { return nil, err } - buffer = buf.As(decrypted) + buffer = buf.FromBytes(decrypted) frameType, err := buffer.ReadByte() if err != nil { return nil, io.ErrUnexpectedEOF