Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions ciphers/aead_2022_cipher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package ciphers

import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"fmt"
"sync"
"time"
)

type CipherConf2022 struct {
KeyLen int
SaltLen int
NonceLen int
TagLen int
NewCipher func(key []byte) (cipher.AEAD, error)
NewBlockCipher func(key []byte) (cipher.Block, error)
}

const (
// Timestamp tolerance
TimestampTolerance = 30 * time.Second

// Salt storage duration
SaltStorageDuration = 60 * time.Second
)

var (
Aead2022CiphersConf = map[string]*CipherConf2022{
"2022-blake3-aes-256-gcm": {KeyLen: 32, SaltLen: 32, NonceLen: 12, TagLen: 16, NewCipher: NewGcm, NewBlockCipher: aes.NewCipher},
"2022-blake3-aes-128-gcm": {KeyLen: 16, SaltLen: 16, NonceLen: 12, TagLen: 16, NewCipher: NewGcm, NewBlockCipher: aes.NewCipher},
}
)

// ValidateBase64PSK validates that the PSK is a valid base64 string with correct length
func ValidateBase64PSK(pskBase64 string, expectedKeyLen int) ([]byte, error) {
if pskBase64 == "" {
return nil, fmt.Errorf("PSK cannot be empty for SIP022 methods")
}

psk, err := base64.StdEncoding.DecodeString(pskBase64)
if err != nil {
return nil, fmt.Errorf("PSK must be valid base64 for SIP022 methods: %w", err)
}

if len(psk) != expectedKeyLen {
return nil, fmt.Errorf("PSK length must be %d bytes for this method, got %d", expectedKeyLen, len(psk))
}

return psk, nil
}

// SlidingWindowFilter implements a sliding window filter for packet ID replay protection
type SlidingWindowFilter struct {
window []uint64
windowSize uint64
latest uint64
initialized bool
mutex sync.Mutex
}

// NewSlidingWindowFilter creates a new sliding window filter
func NewSlidingWindowFilter(windowSize int) *SlidingWindowFilter {
if windowSize <= 0 {
windowSize = 1024
}
wordCount := (windowSize + 63) / 64
return &SlidingWindowFilter{
window: make([]uint64, wordCount),
windowSize: uint64(windowSize),
}
}
Comment on lines +64 to +73

Copilot AI Feb 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential bug in sliding window implementation: on line 65, the window is allocated with size 'windowSize', but the implementation appears to treat the window as an array of 64-bit words where each bit represents a packet ID. Line 108 calculates 'wordIndex := index / 64', suggesting the window should have size 'windowSize / 64' (or '(windowSize + 63) / 64' to round up). The current implementation may cause array index out of bounds when index >= 64 * len(f.window).

Copilot uses AI. Check for mistakes.

// CheckAndUpdate checks if the packet ID is valid and updates the window
func (f *SlidingWindowFilter) CheckAndUpdate(packetID uint64) bool {
f.mutex.Lock()
defer f.mutex.Unlock()
if !f.initialized {
f.initialized = true
f.latest = packetID
f.setBit(0)
return true
}

if packetID > f.latest {
shift := packetID - f.latest
f.shiftWindow(shift)
f.latest = packetID
f.setBit(0)
return true
}

distance := f.latest - packetID
if distance >= f.windowSize {
return false
}
if f.getBit(distance) {
return false
}
f.setBit(distance)
return true
}

func (f *SlidingWindowFilter) getBit(index uint64) bool {
wordIndex := index / 64
bitIndex := index % 64
return f.window[wordIndex]&(uint64(1)<<bitIndex) != 0
}

func (f *SlidingWindowFilter) setBit(index uint64) {
wordIndex := index / 64
bitIndex := index % 64
f.window[wordIndex] |= uint64(1) << bitIndex
}

func (f *SlidingWindowFilter) setBitInWindow(window []uint64, index uint64) {
wordIndex := index / 64
bitIndex := index % 64
window[wordIndex] |= uint64(1) << bitIndex
}

func (f *SlidingWindowFilter) shiftWindow(shift uint64) {
if shift >= f.windowSize {
// Clear all bits in-place
for i := range f.window {
f.window[i] = 0
}
return
}

// Optimized in-place shift to avoid allocation
wordShift := int(shift / 64)
bitShift := shift % 64

// Shift right by wordShift positions
if wordShift > 0 {
for i := len(f.window) - 1; i >= wordShift; i-- {
f.window[i] = f.window[i-wordShift]
}
for i := 0; i < wordShift; i++ {
f.window[i] = 0
}
}

// Handle remaining bit shift
if bitShift > 0 {
for i := len(f.window) - 1; i > 0; i-- {
f.window[i] = (f.window[i] >> bitShift) | (f.window[i-1] << (64 - bitShift))
}
f.window[0] = f.window[0] >> bitShift
}
}
46 changes: 46 additions & 0 deletions ciphers/aead_2022_cipher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package ciphers

import "testing"

func TestSlidingWindowFilter_BasicAndDuplicate(t *testing.T) {
f := NewSlidingWindowFilter(64)

if !f.CheckAndUpdate(100) {
t.Fatalf("first packet should pass")
}
if f.CheckAndUpdate(100) {
t.Fatalf("duplicate packet should be rejected")
}
if !f.CheckAndUpdate(101) {
t.Fatalf("next packet should pass")
}
if !f.CheckAndUpdate(99) {
t.Fatalf("out-of-order but in-window packet should pass")
}
if f.CheckAndUpdate(99) {
t.Fatalf("duplicate out-of-order packet should be rejected")
}
}

func TestSlidingWindowFilter_ShiftAndTooOld(t *testing.T) {
f := NewSlidingWindowFilter(8)

if !f.CheckAndUpdate(1) {
t.Fatalf("packet 1 should pass")
}
if !f.CheckAndUpdate(2) {
t.Fatalf("packet 2 should pass")
}
if !f.CheckAndUpdate(20) {
t.Fatalf("packet 20 should pass")
}
if f.CheckAndUpdate(2) {
t.Fatalf("packet 2 should be too old after large shift")
}
if !f.CheckAndUpdate(19) {
t.Fatalf("packet 19 should pass within current window")
}
if f.CheckAndUpdate(19) {
t.Fatalf("duplicate packet 19 should be rejected")
}
}
9 changes: 5 additions & 4 deletions ciphers/aead_cipher.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,8 @@ var (
"aes-256-gcm": {KeyLen: 32, SaltLen: 32, NonceLen: 12, TagLen: 16, NewCipher: NewGcm},
"aes-128-gcm": {KeyLen: 16, SaltLen: 16, NonceLen: 12, TagLen: 16, NewCipher: NewGcm},
}
ZeroNonce [MaxNonceSize]byte
ShadowsocksReusedInfo = []byte("ss-subkey")
JuicityReusedInfo = []byte("juicity-reused-info")
ZeroNonce [MaxNonceSize]byte
JuicityReusedInfo = []byte("juicity-reused-info")
)

func NewGcm(key []byte) (cipher.AEAD, error) {
Expand All @@ -46,7 +45,9 @@ func NewGcm(key []byte) (cipher.AEAD, error) {
return cipher.NewGCM(block)
}

// Verify is used for legacy compatibility
func (conf *CipherConf) Verify(buf []byte, masterKey []byte, salt []byte, cipherText []byte, subKey *[]byte) ([]byte, bool) {
var shadowsocksReusedInfo = []byte("ss-subkey")
var sk []byte
if subKey != nil && len(*subKey) == conf.KeyLen {
sk = *subKey
Expand All @@ -57,7 +58,7 @@ func (conf *CipherConf) Verify(buf []byte, masterKey []byte, salt []byte, cipher
sha1.New,
masterKey,
salt,
ShadowsocksReusedInfo,
shadowsocksReusedInfo,
)
io.ReadFull(kdf, sk)
if subKey != nil && cap(*subKey) >= conf.KeyLen {
Expand Down
6 changes: 2 additions & 4 deletions dialer/shadowsocks/shadowsocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,13 @@ import (
"github.com/daeuniverse/outbound/dialer"
"github.com/daeuniverse/outbound/netproxy"
"github.com/daeuniverse/outbound/protocol"
"github.com/daeuniverse/outbound/protocol/shadowsocks"
"github.com/daeuniverse/outbound/transport/mux"
"github.com/daeuniverse/outbound/transport/simpleobfs"
"github.com/daeuniverse/outbound/transport/tls"
"github.com/daeuniverse/outbound/transport/ws"
)

func init() {
// Use random salt by default to decrease the boot time
shadowsocks.DefaultSaltGeneratorType = shadowsocks.RandomSaltGeneratorType

dialer.FromLinkRegister("shadowsocks", NewShadowsocksFromLink)
dialer.FromLinkRegister("ss", NewShadowsocksFromLink)
}
Expand Down Expand Up @@ -119,6 +115,8 @@ func (s *Shadowsocks) Dialer(option *dialer.ExtraOption, nextDialer netproxy.Dia
switch s.Cipher {
case "aes-256-gcm", "aes-128-gcm", "chacha20-poly1305", "chacha20-ietf-poly1305":
nextDialerName = "shadowsocks"
case "2022-blake3-aes-256-gcm", "2022-blake3-aes-128-gcm":
nextDialerName = "shadowsocks_2022"
case "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "aes-128-ctr", "aes-192-ctr", "aes-256-ctr", "aes-128-ofb", "aes-192-ofb", "aes-256-ofb", "des-cfb", "bf-cfb", "cast5-cfb", "rc4-md5", "rc4-md5-6", "chacha20", "chacha20-ietf", "salsa20", "camellia-128-cfb", "camellia-192-cfb", "camellia-256-cfb", "idea-cfb", "rc2-cfb", "seed-cfb", "rc4", "none", "plain":
nextDialerName = "shadowsocks_stream"
default:
Expand Down
9 changes: 8 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,18 @@ require (
github.com/json-iterator/go v1.1.12
github.com/mzz2017/disk-bloom v1.0.1
github.com/refraction-networking/utls v1.6.4
github.com/samber/oops v1.19.4
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.11.1
gitlab.com/yawning/chacha20.git v0.0.0-20230427033715-7877545b1b37
golang.org/x/crypto v0.33.0
golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3
golang.org/x/net v0.34.0
golang.org/x/sys v0.30.0
google.golang.org/grpc v1.57.0
google.golang.org/protobuf v1.36.1
lukechampine.com/blake3 v1.4.1
)

require (
Expand All @@ -40,11 +42,16 @@ require (
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/pprof v0.0.0-20250208200701-d0013a598941 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/oklog/ulid/v2 v2.1.1 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/samber/lo v1.52.0 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/mod v0.23.0 // indirect
golang.org/x/sync v0.11.0 // indirect
Expand Down
19 changes: 17 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,32 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mzz2017/disk-bloom v1.0.1 h1:rEF9MiXd9qMW3ibRpqcerLXULoTgRlM21yqqJl1B90M=
github.com/mzz2017/disk-bloom v1.0.1/go.mod h1:JLHETtUu44Z6iBmsqzkOtFlRvXSlKnxjwiBRDapizDI=
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/refraction-networking/utls v1.6.4 h1:aeynTroaYn7y+mFtqv8D0bQ4bw0y9nJHneGxJ7lvRDM=
github.com/refraction-networking/utls v1.6.4/go.mod h1:2VL2xfiqgFAZtJKeUTlf+PSYFs3Eu7km0gCtXJ3m8zs=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/samber/oops v1.19.4 h1:NMzXd3JtdJ4IM2dJgJ4/W8V2bljeCACKYRfPg9vWjeg=
github.com/samber/oops v1.19.4/go.mod h1:Hsm/sKPxtCfPh0w/cE3xVoRfSiE1joDRiStPAsmG9bo=
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
Expand All @@ -73,10 +82,14 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
gitlab.com/yawning/chacha20.git v0.0.0-20230427033715-7877545b1b37 h1:ZrWBE3u/o9cHU2mySXf1687MaK09JOeZt1A+fHnCjmU=
gitlab.com/yawning/chacha20.git v0.0.0-20230427033715-7877545b1b37/go.mod h1:3x6b94nWCP/a2XB/joOPMiGYUBvqbLfeY/BkHLeDs6s=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
Expand Down Expand Up @@ -123,3 +136,5 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
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=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
Loading