From cf07b542d0cc9578a98ce9fbbdcf1a84531ab97d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Sun, 18 Jan 2026 09:57:48 +0000 Subject: [PATCH 1/4] Remove dependency and optimize vmess replay filter --- common/antireplay/antireplay_test.go | 33 ++++++++++++ common/antireplay/bloomring.go | 78 +++++++++++++++++++++++++--- common/antireplay/mapfilter.go | 46 ++++++++++++++++ common/antireplay/replayfilter.go | 58 --------------------- go.mod | 5 +- go.sum | 11 ---- proxy/vmess/aead/authid.go | 2 +- 7 files changed, 153 insertions(+), 80 deletions(-) create mode 100644 common/antireplay/antireplay_test.go create mode 100644 common/antireplay/mapfilter.go delete mode 100644 common/antireplay/replayfilter.go diff --git a/common/antireplay/antireplay_test.go b/common/antireplay/antireplay_test.go new file mode 100644 index 000000000000..336450abbb49 --- /dev/null +++ b/common/antireplay/antireplay_test.go @@ -0,0 +1,33 @@ +package antireplay + +import ( + "bufio" + "crypto/rand" + "testing" +) + +func BenchmarkMapFilter(b *testing.B) { + filter := NewMapFilter(120) + sample := make([]byte, 16) + reader := bufio.NewReader(rand.Reader) + reader.Read(sample) + b.ResetTimer() + for range b.N { + reader.Read(sample) + filter.Check(sample) + } +} + +func TestMapFilter(t *testing.T) { + filter := NewMapFilter(120) + sample := make([]byte, 16) + rand.Read(sample) + filter.Check(sample) + if filter.Check(sample) { + t.Error("Unexpected true negative") + } + sample[0]++ + if !filter.Check(sample) { + t.Error("Unexpected false positive") + } +} diff --git a/common/antireplay/bloomring.go b/common/antireplay/bloomring.go index 50fbbffec67d..50a093818d4c 100644 --- a/common/antireplay/bloomring.go +++ b/common/antireplay/bloomring.go @@ -1,14 +1,15 @@ package antireplay import ( + "hash/fnv" "sync" - ss_bloomring "github.com/v2fly/ss-bloomring" + "github.com/riobard/go-bloom" ) type BloomRing struct { - *ss_bloomring.BloomRing - lock *sync.Mutex + bloom *bloomRing + lock *sync.Mutex } func (b BloomRing) Interval() int64 { @@ -18,10 +19,10 @@ func (b BloomRing) Interval() int64 { func (b BloomRing) Check(sum []byte) bool { b.lock.Lock() defer b.lock.Unlock() - if b.Test(sum) { + if b.bloom.Test(sum) { return false } - b.Add(sum) + b.bloom.Add(sum) return true } @@ -32,5 +33,70 @@ func NewBloomRing() BloomRing { DefaultSFFPR = 1e-6 DefaultSFSlot = 10 ) - return BloomRing{ss_bloomring.NewBloomRing(DefaultSFSlot, DefaultSFCapacity, DefaultSFFPR), &sync.Mutex{}} + return BloomRing{newBloomRing(DefaultSFSlot, DefaultSFCapacity, DefaultSFFPR), &sync.Mutex{}} +} + +// simply use Double FNV here as our Bloom Filter hash +func doubleFNV(b []byte) (uint64, uint64) { + hx := fnv.New64() + hx.Write(b) + x := hx.Sum64() + hy := fnv.New64a() + hy.Write(b) + y := hy.Sum64() + return x, y +} + +type bloomRing struct { + slotCapacity int + slotPosition int + slotCount int + entryCounter int + slots []bloom.Filter + mutex sync.RWMutex +} + +func newBloomRing(slot, capacity int, falsePositiveRate float64) *bloomRing { + // Calculate entries for each slot + r := &bloomRing{ + slotCapacity: capacity / slot, + slotCount: slot, + slots: make([]bloom.Filter, slot), + } + for i := 0; i < slot; i++ { + r.slots[i] = bloom.New(r.slotCapacity, falsePositiveRate, doubleFNV) + } + return r +} + +func (r *bloomRing) Add(b []byte) { + if r == nil { + return + } + r.mutex.Lock() + defer r.mutex.Unlock() + slot := r.slots[r.slotPosition] + if r.entryCounter > r.slotCapacity { + // Move to next slot and reset + r.slotPosition = (r.slotPosition + 1) % r.slotCount + slot = r.slots[r.slotPosition] + slot.Reset() + r.entryCounter = 0 + } + r.entryCounter++ + slot.Add(b) +} + +func (r *bloomRing) Test(b []byte) bool { + if r == nil { + return false + } + r.mutex.RLock() + defer r.mutex.RUnlock() + for _, s := range r.slots { + if s.Test(b) { + return true + } + } + return false } diff --git a/common/antireplay/mapfilter.go b/common/antireplay/mapfilter.go new file mode 100644 index 000000000000..0cef93ea00c2 --- /dev/null +++ b/common/antireplay/mapfilter.go @@ -0,0 +1,46 @@ +package antireplay + +import ( + "sync" + "time" +) + +// ReplayFilter checks for replay attacks. +type ReplayFilter struct { + lock sync.Mutex + pool map[string]struct{} + interval int64 + lastClean int64 +} + +// NewMapFilter create a new filter with specifying the expiration time interval in seconds. +func NewMapFilter(interval int64) *ReplayFilter { + filter := &ReplayFilter{ + pool: make(map[string]struct{}), + interval: interval, + } + return filter +} + +// Interval in second for expiration time for duplicate records. +func (filter *ReplayFilter) Interval() int64 { + return filter.interval +} + +// Check determines if there are duplicate records. +func (filter *ReplayFilter) Check(sum []byte) bool { + filter.lock.Lock() + defer filter.lock.Unlock() + + now := time.Now().Unix() + + elapsed := now - filter.lastClean + if elapsed >= filter.Interval() { + filter.pool = make(map[string]struct{}) + filter.lastClean = now + } + + _, exists := filter.pool[string(sum)] + filter.pool[string(sum)] = struct{}{} + return !exists +} diff --git a/common/antireplay/replayfilter.go b/common/antireplay/replayfilter.go deleted file mode 100644 index ffd7d68de369..000000000000 --- a/common/antireplay/replayfilter.go +++ /dev/null @@ -1,58 +0,0 @@ -package antireplay - -import ( - "sync" - "time" - - cuckoo "github.com/seiflotfy/cuckoofilter" -) - -const replayFilterCapacity = 100000 - -// ReplayFilter checks for replay attacks. -type ReplayFilter struct { - lock sync.Mutex - poolA *cuckoo.Filter - poolB *cuckoo.Filter - poolSwap bool - lastSwap int64 - interval int64 -} - -// NewReplayFilter create a new filter with specifying the expiration time interval in seconds. -func NewReplayFilter(interval int64) *ReplayFilter { - filter := &ReplayFilter{} - filter.interval = interval - return filter -} - -// Interval in second for expiration time for duplicate records. -func (filter *ReplayFilter) Interval() int64 { - return filter.interval -} - -// Check determines if there are duplicate records. -func (filter *ReplayFilter) Check(sum []byte) bool { - filter.lock.Lock() - defer filter.lock.Unlock() - - now := time.Now().Unix() - if filter.lastSwap == 0 { - filter.lastSwap = now - filter.poolA = cuckoo.NewFilter(replayFilterCapacity) - filter.poolB = cuckoo.NewFilter(replayFilterCapacity) - } - - elapsed := now - filter.lastSwap - if elapsed >= filter.Interval() { - if filter.poolSwap { - filter.poolA.Reset() - } else { - filter.poolB.Reset() - } - filter.poolSwap = !filter.poolSwap - filter.lastSwap = now - } - - return filter.poolA.InsertUnique(sum) && filter.poolB.InsertUnique(sum) -} diff --git a/go.mod b/go.mod index f3457b21bf17..397d55ff7e47 100644 --- a/go.mod +++ b/go.mod @@ -13,11 +13,10 @@ require ( github.com/pelletier/go-toml v1.9.5 github.com/pires/go-proxyproto v0.8.1 github.com/refraction-networking/utls v1.8.2 + github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 github.com/sagernet/sing v0.5.1 github.com/sagernet/sing-shadowsocks v0.2.7 - github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 github.com/stretchr/testify v1.11.1 - github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e github.com/vishvananda/netlink v1.3.1 github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba @@ -38,7 +37,6 @@ require ( require ( github.com/andybalholm/brotli v1.0.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165 // indirect github.com/google/btree v1.1.2 // indirect github.com/juju/ratelimit v1.0.2 // indirect github.com/klauspost/compress v1.17.4 // indirect @@ -46,7 +44,6 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect - github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/vishvananda/netns v0.0.5 // indirect golang.org/x/mod v0.31.0 // indirect golang.org/x/text v0.33.0 // indirect diff --git a/go.sum b/go.sum index 26bdf1a2240c..5d60389a8813 100644 --- a/go.sum +++ b/go.sum @@ -5,11 +5,8 @@ github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178/go.mod h1:N1WIj github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ= github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -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/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -62,14 +59,8 @@ github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y= github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8= github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE= -github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4= -github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI= -github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= @@ -154,8 +145,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gvisor.dev/gvisor v0.0.0-20260109181451-4be7c433dae2 h1:fr6L00yGG2RP5NMea6njWpdC+bm+cMdFClrSpaicp1c= diff --git a/proxy/vmess/aead/authid.go b/proxy/vmess/aead/authid.go index 7309010aa511..38007e245502 100644 --- a/proxy/vmess/aead/authid.go +++ b/proxy/vmess/aead/authid.go @@ -68,7 +68,7 @@ func (aidd *AuthIDDecoder) Decode(data [16]byte) (int64, uint32, int32, []byte) } func NewAuthIDDecoderHolder() *AuthIDDecoderHolder { - return &AuthIDDecoderHolder{make(map[string]*AuthIDDecoderItem), antireplay.NewReplayFilter(120)} + return &AuthIDDecoderHolder{make(map[string]*AuthIDDecoderItem), antireplay.NewMapFilter(120)} } type AuthIDDecoderHolder struct { From 804f4991862e159f2f9fc1fe4522de3022aad9a2 Mon Sep 17 00:00:00 2001 From: Fangliding Date: Tue, 27 Jan 2026 22:15:33 +0800 Subject: [PATCH 2/4] Remove bloom filter --- common/antireplay/bloomring.go | 102 --------------------------------- go.mod | 1 - go.sum | 2 - infra/conf/shadowsocks.go | 8 --- proxy/shadowsocks/config.go | 23 +------- proxy/shadowsocks/protocol.go | 10 ---- proxy/shadowsocks/validator.go | 1 - 7 files changed, 2 insertions(+), 145 deletions(-) delete mode 100644 common/antireplay/bloomring.go diff --git a/common/antireplay/bloomring.go b/common/antireplay/bloomring.go deleted file mode 100644 index 50a093818d4c..000000000000 --- a/common/antireplay/bloomring.go +++ /dev/null @@ -1,102 +0,0 @@ -package antireplay - -import ( - "hash/fnv" - "sync" - - "github.com/riobard/go-bloom" -) - -type BloomRing struct { - bloom *bloomRing - lock *sync.Mutex -} - -func (b BloomRing) Interval() int64 { - return 9999999 -} - -func (b BloomRing) Check(sum []byte) bool { - b.lock.Lock() - defer b.lock.Unlock() - if b.bloom.Test(sum) { - return false - } - b.bloom.Add(sum) - return true -} - -func NewBloomRing() BloomRing { - const ( - DefaultSFCapacity = 1e6 - // FalsePositiveRate - DefaultSFFPR = 1e-6 - DefaultSFSlot = 10 - ) - return BloomRing{newBloomRing(DefaultSFSlot, DefaultSFCapacity, DefaultSFFPR), &sync.Mutex{}} -} - -// simply use Double FNV here as our Bloom Filter hash -func doubleFNV(b []byte) (uint64, uint64) { - hx := fnv.New64() - hx.Write(b) - x := hx.Sum64() - hy := fnv.New64a() - hy.Write(b) - y := hy.Sum64() - return x, y -} - -type bloomRing struct { - slotCapacity int - slotPosition int - slotCount int - entryCounter int - slots []bloom.Filter - mutex sync.RWMutex -} - -func newBloomRing(slot, capacity int, falsePositiveRate float64) *bloomRing { - // Calculate entries for each slot - r := &bloomRing{ - slotCapacity: capacity / slot, - slotCount: slot, - slots: make([]bloom.Filter, slot), - } - for i := 0; i < slot; i++ { - r.slots[i] = bloom.New(r.slotCapacity, falsePositiveRate, doubleFNV) - } - return r -} - -func (r *bloomRing) Add(b []byte) { - if r == nil { - return - } - r.mutex.Lock() - defer r.mutex.Unlock() - slot := r.slots[r.slotPosition] - if r.entryCounter > r.slotCapacity { - // Move to next slot and reset - r.slotPosition = (r.slotPosition + 1) % r.slotCount - slot = r.slots[r.slotPosition] - slot.Reset() - r.entryCounter = 0 - } - r.entryCounter++ - slot.Add(b) -} - -func (r *bloomRing) Test(b []byte) bool { - if r == nil { - return false - } - r.mutex.RLock() - defer r.mutex.RUnlock() - for _, s := range r.slots { - if s.Test(b) { - return true - } - } - return false -} diff --git a/go.mod b/go.mod index 397d55ff7e47..203472391442 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/pelletier/go-toml v1.9.5 github.com/pires/go-proxyproto v0.8.1 github.com/refraction-networking/utls v1.8.2 - github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 github.com/sagernet/sing v0.5.1 github.com/sagernet/sing-shadowsocks v0.2.7 github.com/stretchr/testify v1.11.1 diff --git a/go.sum b/go.sum index 5d60389a8813..8e072848c121 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,6 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo= github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= -github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= -github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y= diff --git a/infra/conf/shadowsocks.go b/infra/conf/shadowsocks.go index 3163798d7d64..bf4d4fd985b5 100644 --- a/infra/conf/shadowsocks.go +++ b/infra/conf/shadowsocks.go @@ -46,7 +46,6 @@ type ShadowsocksServerConfig struct { Email string `json:"email"` Users []*ShadowsocksUserConfig `json:"clients"` NetworkList *NetworkList `json:"network"` - IVCheck bool `json:"ivCheck"` } func (v *ShadowsocksServerConfig) Build() (proto.Message, error) { @@ -64,7 +63,6 @@ func (v *ShadowsocksServerConfig) Build() (proto.Message, error) { account := &shadowsocks.Account{ Password: user.Password, CipherType: cipherFromString(user.Cipher), - IvCheck: v.IVCheck, } if account.Password == "" { return nil, errors.New("Shadowsocks password is not specified.") @@ -83,7 +81,6 @@ func (v *ShadowsocksServerConfig) Build() (proto.Message, error) { account := &shadowsocks.Account{ Password: v.Password, CipherType: cipherFromString(v.Cipher), - IvCheck: v.IVCheck, } if account.Password == "" { return nil, errors.New("Shadowsocks password is not specified.") @@ -168,7 +165,6 @@ type ShadowsocksServerTarget struct { Email string `json:"email"` Cipher string `json:"method"` Password string `json:"password"` - IVCheck bool `json:"ivCheck"` UoT bool `json:"uot"` UoTVersion int `json:"uotVersion"` } @@ -180,7 +176,6 @@ type ShadowsocksClientConfig struct { Email string `json:"email"` Cipher string `json:"method"` Password string `json:"password"` - IVCheck bool `json:"ivCheck"` UoT bool `json:"uot"` UoTVersion int `json:"uotVersion"` Servers []*ShadowsocksServerTarget `json:"servers"` @@ -198,7 +193,6 @@ func (v *ShadowsocksClientConfig) Build() (proto.Message, error) { Email: v.Email, Cipher: v.Cipher, Password: v.Password, - IVCheck: v.IVCheck, UoT: v.UoT, UoTVersion: v.UoTVersion, }, @@ -254,8 +248,6 @@ func (v *ShadowsocksClientConfig) Build() (proto.Message, error) { return nil, errors.New("unknown cipher method: ", server.Cipher) } - account.IvCheck = server.IVCheck - ss := &protocol.ServerEndpoint{ Address: server.Address.Build(), Port: uint32(server.Port), diff --git a/proxy/shadowsocks/config.go b/proxy/shadowsocks/config.go index 39c397fa5af4..dc8bff0b5761 100644 --- a/proxy/shadowsocks/config.go +++ b/proxy/shadowsocks/config.go @@ -5,11 +5,11 @@ import ( "crypto/cipher" "crypto/md5" "crypto/sha1" - "google.golang.org/protobuf/proto" "io" + "google.golang.org/protobuf/proto" + "github.com/xtls/xray-core/common" - "github.com/xtls/xray-core/common/antireplay" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/crypto" "github.com/xtls/xray-core/common/errors" @@ -24,8 +24,6 @@ type MemoryAccount struct { CipherType CipherType Key []byte Password string - - replayFilter antireplay.GeneralizedReplayFilter } var ErrIVNotUnique = errors.New("IV is not unique") @@ -42,18 +40,7 @@ func (a *MemoryAccount) ToProto() proto.Message { return &Account{ CipherType: a.CipherType, Password: a.Password, - IvCheck: a.replayFilter != nil, - } -} - -func (a *MemoryAccount) CheckIV(iv []byte) error { - if a.replayFilter == nil { - return nil - } - if a.replayFilter.Check(iv) { - return nil } - return ErrIVNotUnique } func createAesGcm(key []byte) cipher.AEAD { @@ -116,12 +103,6 @@ func (a *Account) AsAccount() (protocol.Account, error) { CipherType: a.CipherType, Key: passwordToCipherKey([]byte(a.Password), Cipher.KeySize()), Password: a.Password, - replayFilter: func() antireplay.GeneralizedReplayFilter { - if a.IvCheck { - return antireplay.NewBloomRing() - } - return nil - }(), }, nil } diff --git a/proxy/shadowsocks/protocol.go b/proxy/shadowsocks/protocol.go index c992f6198f05..88006cded544 100644 --- a/proxy/shadowsocks/protocol.go +++ b/proxy/shadowsocks/protocol.go @@ -139,9 +139,6 @@ func WriteTCPRequest(request *protocol.RequestHeader, writer io.Writer) (buf.Wri if account.Cipher.IVSize() > 0 { iv = make([]byte, account.Cipher.IVSize()) common.Must2(rand.Read(iv)) - if ivError := account.CheckIV(iv); ivError != nil { - return nil, errors.New("failed to mark outgoing iv").Base(ivError) - } if err := buf.WriteAllBytes(writer, iv, nil); err != nil { return nil, errors.New("failed to write IV") } @@ -188,10 +185,6 @@ func ReadTCPResponse(user *protocol.MemoryUser, reader io.Reader) (buf.Reader, e } } - if ivError := account.CheckIV(iv); ivError != nil { - return nil, drain.WithError(drainer, reader, errors.New("failed iv check").Base(ivError)) - } - return account.Cipher.NewDecryptionReader(account.Key, iv, reader) } @@ -203,9 +196,6 @@ func WriteTCPResponse(request *protocol.RequestHeader, writer io.Writer) (buf.Wr if account.Cipher.IVSize() > 0 { iv = make([]byte, account.Cipher.IVSize()) common.Must2(rand.Read(iv)) - if ivError := account.CheckIV(iv); ivError != nil { - return nil, errors.New("failed to mark outgoing iv").Base(ivError) - } if err := buf.WriteAllBytes(writer, iv, nil); err != nil { return nil, errors.New("failed to write IV.").Base(err) } diff --git a/proxy/shadowsocks/validator.go b/proxy/shadowsocks/validator.go index 84e59dc1cb31..2c48334f41ee 100644 --- a/proxy/shadowsocks/validator.go +++ b/proxy/shadowsocks/validator.go @@ -140,7 +140,6 @@ func (v *Validator) Get(bs []byte, command protocol.RequestCommand) (u *protocol if matchErr == nil { u = user - err = account.CheckIV(iv) return } } else { From 429ecbbd7b302497aeeac7e4fca19ec4390ab424 Mon Sep 17 00:00:00 2001 From: Fangliding Date: Tue, 27 Jan 2026 22:31:37 +0800 Subject: [PATCH 3/4] Generic --- common/antireplay/antireplay.go | 6 ---- common/antireplay/antireplay_test.go | 14 ++++----- common/antireplay/mapfilter.go | 43 ++++++++++++++-------------- proxy/vmess/aead/authid.go | 6 ++-- 4 files changed, 31 insertions(+), 38 deletions(-) delete mode 100644 common/antireplay/antireplay.go diff --git a/common/antireplay/antireplay.go b/common/antireplay/antireplay.go deleted file mode 100644 index 4d1a93196233..000000000000 --- a/common/antireplay/antireplay.go +++ /dev/null @@ -1,6 +0,0 @@ -package antireplay - -type GeneralizedReplayFilter interface { - Interval() int64 - Check(sum []byte) bool -} diff --git a/common/antireplay/antireplay_test.go b/common/antireplay/antireplay_test.go index 336450abbb49..57e97184f167 100644 --- a/common/antireplay/antireplay_test.go +++ b/common/antireplay/antireplay_test.go @@ -7,21 +7,21 @@ import ( ) func BenchmarkMapFilter(b *testing.B) { - filter := NewMapFilter(120) - sample := make([]byte, 16) + filter := NewMapFilter[[16]byte](120) + var sample [16]byte reader := bufio.NewReader(rand.Reader) - reader.Read(sample) + reader.Read(sample[:]) b.ResetTimer() for range b.N { - reader.Read(sample) + reader.Read(sample[:]) filter.Check(sample) } } func TestMapFilter(t *testing.T) { - filter := NewMapFilter(120) - sample := make([]byte, 16) - rand.Read(sample) + filter := NewMapFilter[[16]byte](120) + var sample [16]byte + rand.Read(sample[:]) filter.Check(sample) if filter.Check(sample) { t.Error("Unexpected true negative") diff --git a/common/antireplay/mapfilter.go b/common/antireplay/mapfilter.go index 0cef93ea00c2..2db31213acc7 100644 --- a/common/antireplay/mapfilter.go +++ b/common/antireplay/mapfilter.go @@ -6,41 +6,40 @@ import ( ) // ReplayFilter checks for replay attacks. -type ReplayFilter struct { +type ReplayFilter[T comparable] struct { lock sync.Mutex - pool map[string]struct{} - interval int64 - lastClean int64 + poolA map[T]struct{} + poolB map[T]struct{} + interval time.Duration + lastClean time.Time } // NewMapFilter create a new filter with specifying the expiration time interval in seconds. -func NewMapFilter(interval int64) *ReplayFilter { - filter := &ReplayFilter{ - pool: make(map[string]struct{}), - interval: interval, +func NewMapFilter[T comparable](interval int64) *ReplayFilter[T] { + filter := &ReplayFilter[T]{ + poolA: make(map[T]struct{}), + poolB: make(map[T]struct{}), + interval: time.Duration(interval) * time.Second, } return filter } -// Interval in second for expiration time for duplicate records. -func (filter *ReplayFilter) Interval() int64 { - return filter.interval -} - // Check determines if there are duplicate records. -func (filter *ReplayFilter) Check(sum []byte) bool { +func (filter *ReplayFilter[T]) Check(sum T) bool { filter.lock.Lock() defer filter.lock.Unlock() - now := time.Now().Unix() - - elapsed := now - filter.lastClean - if elapsed >= filter.Interval() { - filter.pool = make(map[string]struct{}) + now := time.Now() + if now.Sub(filter.lastClean) >= filter.interval { + filter.poolB = filter.poolA + filter.poolA = make(map[T]struct{}) filter.lastClean = now } - _, exists := filter.pool[string(sum)] - filter.pool[string(sum)] = struct{}{} - return !exists + _, existsA := filter.poolA[sum] + _, existsB := filter.poolB[sum] + if !existsA && !existsB { + filter.poolA[sum] = struct{}{} + } + return !(existsA || existsB) } diff --git a/proxy/vmess/aead/authid.go b/proxy/vmess/aead/authid.go index 38007e245502..478bb19ce50a 100644 --- a/proxy/vmess/aead/authid.go +++ b/proxy/vmess/aead/authid.go @@ -68,12 +68,12 @@ func (aidd *AuthIDDecoder) Decode(data [16]byte) (int64, uint32, int32, []byte) } func NewAuthIDDecoderHolder() *AuthIDDecoderHolder { - return &AuthIDDecoderHolder{make(map[string]*AuthIDDecoderItem), antireplay.NewMapFilter(120)} + return &AuthIDDecoderHolder{make(map[string]*AuthIDDecoderItem), antireplay.NewMapFilter[[16]byte](120)} } type AuthIDDecoderHolder struct { decoders map[string]*AuthIDDecoderItem - filter *antireplay.ReplayFilter + filter *antireplay.ReplayFilter[[16]byte] } type AuthIDDecoderItem struct { @@ -111,7 +111,7 @@ func (a *AuthIDDecoderHolder) Match(authID [16]byte) (interface{}, error) { return nil, ErrInvalidTime } - if !a.filter.Check(authID[:]) { + if !a.filter.Check(authID) { return nil, ErrReplay } From 9d67a92a8a0574bda73fa31ebc4809a39d4b0452 Mon Sep 17 00:00:00 2001 From: Fangliding Date: Wed, 28 Jan 2026 01:14:50 +0800 Subject: [PATCH 4/4] clean --- common/antireplay/mapfilter.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/common/antireplay/mapfilter.go b/common/antireplay/mapfilter.go index 2db31213acc7..8cfef953a1fd 100644 --- a/common/antireplay/mapfilter.go +++ b/common/antireplay/mapfilter.go @@ -17,9 +17,10 @@ type ReplayFilter[T comparable] struct { // NewMapFilter create a new filter with specifying the expiration time interval in seconds. func NewMapFilter[T comparable](interval int64) *ReplayFilter[T] { filter := &ReplayFilter[T]{ - poolA: make(map[T]struct{}), - poolB: make(map[T]struct{}), - interval: time.Duration(interval) * time.Second, + poolA: make(map[T]struct{}), + poolB: make(map[T]struct{}), + interval: time.Duration(interval) * time.Second, + lastClean: time.Now(), } return filter }