From 58235c074c3b830a37bca0575c0a6db540d1071e Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 14:32:59 +0300 Subject: [PATCH 01/25] initial shadowsocks files --- shadowsocks/consts.go | 1 + shadowsocks/dialer.go | 1 + shadowsocks/dialer_test.go | 1 + shadowsocks/server.go | 1 + shadowsocks/server_test.go | 1 + 5 files changed, 5 insertions(+) create mode 100644 shadowsocks/consts.go create mode 100644 shadowsocks/dialer.go create mode 100644 shadowsocks/dialer_test.go create mode 100644 shadowsocks/server.go create mode 100644 shadowsocks/server_test.go diff --git a/shadowsocks/consts.go b/shadowsocks/consts.go new file mode 100644 index 0000000..4b35346 --- /dev/null +++ b/shadowsocks/consts.go @@ -0,0 +1 @@ +package shadowsocks diff --git a/shadowsocks/dialer.go b/shadowsocks/dialer.go new file mode 100644 index 0000000..4b35346 --- /dev/null +++ b/shadowsocks/dialer.go @@ -0,0 +1 @@ +package shadowsocks diff --git a/shadowsocks/dialer_test.go b/shadowsocks/dialer_test.go new file mode 100644 index 0000000..1df463b --- /dev/null +++ b/shadowsocks/dialer_test.go @@ -0,0 +1 @@ +package shadowsocks_test diff --git a/shadowsocks/server.go b/shadowsocks/server.go new file mode 100644 index 0000000..4b35346 --- /dev/null +++ b/shadowsocks/server.go @@ -0,0 +1 @@ +package shadowsocks diff --git a/shadowsocks/server_test.go b/shadowsocks/server_test.go new file mode 100644 index 0000000..1df463b --- /dev/null +++ b/shadowsocks/server_test.go @@ -0,0 +1 @@ +package shadowsocks_test From e53b0af11a9370d11e4f29ccb72248e63bf8fb3b Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 15:06:47 +0300 Subject: [PATCH 02/25] dialer --- shadowsocks/config.go | 57 ++++++++++++++++++ shadowsocks/config_test.go | 120 +++++++++++++++++++++++++++++++++++++ shadowsocks/dialer.go | 94 +++++++++++++++++++++++++++++ 3 files changed, 271 insertions(+) create mode 100644 shadowsocks/config.go create mode 100644 shadowsocks/config_test.go diff --git a/shadowsocks/config.go b/shadowsocks/config.go new file mode 100644 index 0000000..bd90c35 --- /dev/null +++ b/shadowsocks/config.go @@ -0,0 +1,57 @@ +package shadowsocks + +import ( + "encoding/base64" + "fmt" +) + +const ( + Method2022Blake3AES128GCM = "2022-blake3-aes-128-gcm" + Method2022Blake3AES256GCM = "2022-blake3-aes-256-gcm" + Method2022Blake3ChaCha20Poly1305 = "2022-blake3-chacha20-poly1305" +) + +// Config holds the configuration for a Shadowsocks proxy. +type Config struct { + Method string + PSK string + Plugin string + Tag string +} + +// Validate checks if the Config is valid and returns an error if not. +func (c *Config) Validate() error { + if c == nil { + return fmt.Errorf("nil config") + } + + switch c.Method { + case Method2022Blake3AES128GCM, + Method2022Blake3AES256GCM, + Method2022Blake3ChaCha20Poly1305: + default: + return fmt.Errorf("invalid method: %s", c.Method) + } + + if c.PSK == "" { + return fmt.Errorf("missing PSK") + } + + rawKey, err := base64.StdEncoding.DecodeString(c.PSK) + if err != nil { + return fmt.Errorf("invalid PSK: must be standard base64: %w", err) + } + + switch c.Method { + case Method2022Blake3AES128GCM: + if len(rawKey) != 16 { + return fmt.Errorf("invalid PSK length for %s: got %d, want 16", c.Method, len(rawKey)) + } + case Method2022Blake3AES256GCM, Method2022Blake3ChaCha20Poly1305: + if len(rawKey) != 32 { + return fmt.Errorf("invalid PSK length for %s: got %d, want 32", c.Method, len(rawKey)) + } + } + + return nil +} diff --git a/shadowsocks/config_test.go b/shadowsocks/config_test.go new file mode 100644 index 0000000..7e962a0 --- /dev/null +++ b/shadowsocks/config_test.go @@ -0,0 +1,120 @@ +package shadowsocks_test + +import ( + "encoding/base64" + "strings" + "testing" + + "github.com/33TU/socks/shadowsocks" +) + +func TestConfig_Validate(t *testing.T) { + t.Parallel() + + psk16 := base64.StdEncoding.EncodeToString(make([]byte, 16)) + psk32 := base64.StdEncoding.EncodeToString(make([]byte, 32)) + psk15 := base64.StdEncoding.EncodeToString(make([]byte, 15)) + psk31 := base64.StdEncoding.EncodeToString(make([]byte, 31)) + + tests := []struct { + name string + cfg *shadowsocks.Config + wantErr string + }{ + { + name: "nil config", + cfg: nil, + wantErr: "nil config", + }, + { + name: "valid aes128", + cfg: &shadowsocks.Config{ + Method: shadowsocks.Method2022Blake3AES128GCM, + PSK: psk16, + }, + }, + { + name: "valid aes256", + cfg: &shadowsocks.Config{ + Method: shadowsocks.Method2022Blake3AES256GCM, + PSK: psk32, + }, + }, + { + name: "valid chacha20", + cfg: &shadowsocks.Config{ + Method: shadowsocks.Method2022Blake3ChaCha20Poly1305, + PSK: psk32, + }, + }, + { + name: "invalid method", + cfg: &shadowsocks.Config{ + Method: "invalid-method", + PSK: psk32, + }, + wantErr: "invalid method", + }, + { + name: "missing psk", + cfg: &shadowsocks.Config{ + Method: shadowsocks.Method2022Blake3AES128GCM, + }, + wantErr: "missing PSK", + }, + { + name: "invalid base64", + cfg: &shadowsocks.Config{ + Method: shadowsocks.Method2022Blake3AES128GCM, + PSK: "!!!not-base64!!!", + }, + wantErr: "invalid PSK: must be standard base64", + }, + { + name: "invalid aes128 key length", + cfg: &shadowsocks.Config{ + Method: shadowsocks.Method2022Blake3AES128GCM, + PSK: psk15, + }, + wantErr: "invalid PSK length", + }, + { + name: "invalid aes256 key length", + cfg: &shadowsocks.Config{ + Method: shadowsocks.Method2022Blake3AES256GCM, + PSK: psk31, + }, + wantErr: "invalid PSK length", + }, + { + name: "invalid chacha20 key length", + cfg: &shadowsocks.Config{ + Method: shadowsocks.Method2022Blake3ChaCha20Poly1305, + PSK: psk31, + }, + wantErr: "invalid PSK length", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := tt.cfg.Validate() + if tt.wantErr == "" { + if err != nil { + t.Fatalf("Validate() error = %v", err) + } + return + } + + if err == nil { + t.Fatalf("expected error containing %q, got nil", tt.wantErr) + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("expected error containing %q, got %q", tt.wantErr, err.Error()) + } + }) + } +} diff --git a/shadowsocks/dialer.go b/shadowsocks/dialer.go index 4b35346..086c889 100644 --- a/shadowsocks/dialer.go +++ b/shadowsocks/dialer.go @@ -1 +1,95 @@ package shadowsocks + +import ( + "fmt" + "net" + "net/url" + + socksnet "github.com/33TU/socks/net" +) + +// Dialer implements a Shadowsocks proxy dialer. +type Dialer struct { + ProxyAddr string + Config *Config + Dialer socksnet.Dialer +} + +// NewDialer creates a new Shadowsocks dialer instance. +func NewDialer(proxyAddr string, cfg *Config, dialer socksnet.Dialer) *Dialer { + if dialer == nil { + dialer = socksnet.DefaultDialer + } + + return &Dialer{ + ProxyAddr: proxyAddr, + Config: cfg, + Dialer: dialer, + } +} + +// NewDialerFromURL creates a new Dialer from a URL of the form +// ss://method:psk@host:port[/?plugin=...][#tag] +// +// For AEAD-2022, userinfo must be plain method:psk and not legacy base64-wrapped userinfo. +func NewDialerFromURL(u *url.URL, dialer socksnet.Dialer) (*Dialer, error) { + if u == nil { + return nil, fmt.Errorf("nil proxy URL") + } + + switch u.Scheme { + case "ss": + default: + return nil, fmt.Errorf("invalid scheme: %s", u.Scheme) + } + + host := u.Hostname() + if host == "" { + return nil, fmt.Errorf("missing host in proxy URL") + } + + port := u.Port() + if port == "" { + return nil, fmt.Errorf("missing port in proxy URL") + } + + proxyAddr := net.JoinHostPort(host, port) + + if u.User == nil { + return nil, fmt.Errorf("missing method/psk in proxy URL") + } + + method := u.User.Username() + if method == "" { + return nil, fmt.Errorf("missing method in proxy URL") + } + + psk, hasPassword := u.User.Password() + if !hasPassword { + return nil, fmt.Errorf("missing PSK in proxy URL") + } + + cfg := &Config{ + Method: method, + PSK: psk, + Plugin: u.Query().Get("plugin"), + Tag: u.Fragment, + } + if err := cfg.Validate(); err != nil { + return nil, fmt.Errorf("invalid proxy URL: %w", err) + } + + return NewDialer(proxyAddr, cfg, dialer), nil +} + +// NewDialerFromURLString creates a new Dialer from a URL string of the form +// ss://method:psk@host:port[/?plugin=...][#tag] +// +// For AEAD-2022, userinfo must be plain method:psk and not legacy base64-wrapped userinfo. +func NewDialerFromURLString(rawURL string, dialer socksnet.Dialer) (*Dialer, error) { + u, err := url.Parse(rawURL) + if err != nil { + return nil, fmt.Errorf("invalid proxy URL: %w", err) + } + return NewDialerFromURL(u, dialer) +} From 2b9e51c6bcbfcb65dba21a664b79992db1b04283 Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 15:21:56 +0300 Subject: [PATCH 03/25] dialer tests & refactor --- shadowsocks/config.go | 6 -- shadowsocks/consts.go | 7 ++ shadowsocks/dialer_test.go | 201 +++++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 6 deletions(-) diff --git a/shadowsocks/config.go b/shadowsocks/config.go index bd90c35..eeeed1a 100644 --- a/shadowsocks/config.go +++ b/shadowsocks/config.go @@ -5,12 +5,6 @@ import ( "fmt" ) -const ( - Method2022Blake3AES128GCM = "2022-blake3-aes-128-gcm" - Method2022Blake3AES256GCM = "2022-blake3-aes-256-gcm" - Method2022Blake3ChaCha20Poly1305 = "2022-blake3-chacha20-poly1305" -) - // Config holds the configuration for a Shadowsocks proxy. type Config struct { Method string diff --git a/shadowsocks/consts.go b/shadowsocks/consts.go index 4b35346..3c86b47 100644 --- a/shadowsocks/consts.go +++ b/shadowsocks/consts.go @@ -1 +1,8 @@ package shadowsocks + +// Encryption method constants for Shadowsocks AEAD-2022. +const ( + Method2022Blake3AES128GCM = "2022-blake3-aes-128-gcm" + Method2022Blake3AES256GCM = "2022-blake3-aes-256-gcm" + Method2022Blake3ChaCha20Poly1305 = "2022-blake3-chacha20-poly1305" +) diff --git a/shadowsocks/dialer_test.go b/shadowsocks/dialer_test.go index 1df463b..f4ee080 100644 --- a/shadowsocks/dialer_test.go +++ b/shadowsocks/dialer_test.go @@ -1 +1,202 @@ package shadowsocks_test + +import ( + "bytes" + "encoding/base64" + "net/url" + "strings" + "testing" + + socksnet "github.com/33TU/socks/net" + "github.com/33TU/socks/shadowsocks" +) + +func mustBase64Key(n int) string { + return base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{0x42}, n)) +} + +func TestNewDialerFromURL(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + rawURL string + wantAddr string + wantMethod string + wantPSK string + wantPlugin string + wantTag string + wantErr string + }{ + { + name: "valid aes-256-gcm", + rawURL: "ss://2022-blake3-aes-256-gcm:" + url.QueryEscape(mustBase64Key(32)) + "@127.0.0.1:8388", + wantAddr: "127.0.0.1:8388", + wantMethod: shadowsocks.Method2022Blake3AES256GCM, + wantPSK: mustBase64Key(32), + }, + { + name: "valid aes-128-gcm with plugin and tag", + rawURL: "ss://2022-blake3-aes-128-gcm:" + url.QueryEscape(mustBase64Key(16)) + "@proxy.example.com:443/?plugin=" + url.QueryEscape("v2ray-plugin;server;host=example.com") + "#edge", + wantAddr: "proxy.example.com:443", + wantMethod: shadowsocks.Method2022Blake3AES128GCM, + wantPSK: mustBase64Key(16), + wantPlugin: "v2ray-plugin;server;host=example.com", + wantTag: "edge", + }, + { + name: "valid chacha20-poly1305", + rawURL: "ss://2022-blake3-chacha20-poly1305:" + url.QueryEscape(mustBase64Key(32)) + "@example.com:8443#demo", + wantAddr: "example.com:8443", + wantMethod: shadowsocks.Method2022Blake3ChaCha20Poly1305, + wantPSK: mustBase64Key(32), + wantTag: "demo", + }, + { + name: "nil url", + wantErr: "nil proxy URL", + }, + { + name: "invalid scheme", + rawURL: "http://127.0.0.1:8388", + wantErr: "invalid scheme", + }, + { + name: "missing host", + rawURL: "ss://2022-blake3-aes-256-gcm:" + url.QueryEscape(mustBase64Key(32)) + "@:8388", + wantErr: "missing host", + }, + { + name: "missing port", + rawURL: "ss://2022-blake3-aes-256-gcm:" + url.QueryEscape(mustBase64Key(32)) + "@127.0.0.1", + wantErr: "missing port", + }, + { + name: "missing userinfo", + rawURL: "ss://127.0.0.1:8388", + wantErr: "missing method/psk", + }, + { + name: "missing method", + rawURL: "ss://:" + url.QueryEscape(mustBase64Key(32)) + "@127.0.0.1:8388", + wantErr: "missing method", + }, + { + name: "missing psk", + rawURL: "ss://2022-blake3-aes-256-gcm@127.0.0.1:8388", + wantErr: "missing PSK", + }, + { + name: "invalid method", + rawURL: "ss://aes-256-gcm:" + url.QueryEscape(mustBase64Key(32)) + "@127.0.0.1:8388", + wantErr: "invalid proxy URL", + }, + { + name: "invalid psk length for aes-128", + rawURL: "ss://2022-blake3-aes-128-gcm:" + url.QueryEscape(mustBase64Key(32)) + "@127.0.0.1:8388", + wantErr: "invalid proxy URL", + }, + { + name: "invalid psk base64", + rawURL: "ss://2022-blake3-aes-256-gcm:not-base64!!!@127.0.0.1:8388", + wantErr: "invalid proxy URL", + }, + { + name: "ipv6 host", + rawURL: "ss://2022-blake3-aes-256-gcm:" + url.QueryEscape(mustBase64Key(32)) + "@[2001:db8::1]:8388", + wantAddr: "[2001:db8::1]:8388", + wantMethod: shadowsocks.Method2022Blake3AES256GCM, + wantPSK: mustBase64Key(32), + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + if tt.wantErr == "nil proxy URL" { + d, err := shadowsocks.NewDialerFromURL(nil, nil) + if d != nil { + t.Fatal("expected nil dialer") + } + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("expected error containing %q, got %v", tt.wantErr, err) + } + return + } + + u, err := url.Parse(tt.rawURL) + if err != nil { + t.Fatalf("url.Parse(%q): %v", tt.rawURL, err) + } + + d, err := shadowsocks.NewDialerFromURL(u, nil) + if tt.wantErr != "" { + if err == nil { + t.Fatalf("expected error containing %q, got nil", tt.wantErr) + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("expected error containing %q, got %q", tt.wantErr, err.Error()) + } + return + } + if err != nil { + t.Fatalf("NewDialerFromURL() error = %v", err) + } + if d == nil { + t.Fatal("NewDialerFromURL() returned nil dialer") + } + if d.ProxyAddr != tt.wantAddr { + t.Fatalf("ProxyAddr = %q, want %q", d.ProxyAddr, tt.wantAddr) + } + if d.Config == nil { + t.Fatal("Config is nil") + } + if d.Config.Method != tt.wantMethod { + t.Fatalf("Config.Method = %q, want %q", d.Config.Method, tt.wantMethod) + } + if d.Config.PSK != tt.wantPSK { + t.Fatalf("Config.PSK = %q, want %q", d.Config.PSK, tt.wantPSK) + } + if d.Config.Plugin != tt.wantPlugin { + t.Fatalf("Config.Plugin = %q, want %q", d.Config.Plugin, tt.wantPlugin) + } + if d.Config.Tag != tt.wantTag { + t.Fatalf("Config.Tag = %q, want %q", d.Config.Tag, tt.wantTag) + } + if d.Dialer == nil { + t.Fatal("Dialer is nil, want default dialer") + } + }) + } +} + +func TestNewDialerFromURLString(t *testing.T) { + t.Parallel() + + rawURL := "ss://2022-blake3-aes-256-gcm:" + url.QueryEscape(mustBase64Key(32)) + "@127.0.0.1:8388/?plugin=" + url.QueryEscape("simple-obfs") + "#local" + + d, err := shadowsocks.NewDialerFromURLString(rawURL, socksnet.DefaultDialer) + if err != nil { + t.Fatalf("NewDialerFromURLString() error = %v", err) + } + if d == nil { + t.Fatal("NewDialerFromURLString() returned nil dialer") + } + if d.ProxyAddr != "127.0.0.1:8388" { + t.Fatalf("ProxyAddr = %q, want %q", d.ProxyAddr, "127.0.0.1:8388") + } + if d.Config == nil { + t.Fatal("Config is nil") + } + if d.Config.Method != shadowsocks.Method2022Blake3AES256GCM { + t.Fatalf("Config.Method = %q, want %q", d.Config.Method, shadowsocks.Method2022Blake3AES256GCM) + } + if d.Config.Plugin != "simple-obfs" { + t.Fatalf("Config.Plugin = %q, want %q", d.Config.Plugin, "simple-obfs") + } + if d.Config.Tag != "local" { + t.Fatalf("Config.Tag = %q, want %q", d.Config.Tag, "local") + } +} From ec145e2098d634db98c2a19ad10bba71e630837e Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 15:54:56 +0300 Subject: [PATCH 04/25] shadowsocks key and method --- go.mod | 7 + go.sum | 8 ++ shadowsocks/key.go | 78 +++++++++++ shadowsocks/key_test.go | 259 +++++++++++++++++++++++++++++++++++++ shadowsocks/method.go | 146 +++++++++++++++++++++ shadowsocks/method_test.go | 231 +++++++++++++++++++++++++++++++++ 6 files changed, 729 insertions(+) create mode 100644 shadowsocks/key.go create mode 100644 shadowsocks/key_test.go create mode 100644 shadowsocks/method.go create mode 100644 shadowsocks/method_test.go diff --git a/go.mod b/go.mod index 46cbe21..67f4512 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,10 @@ module github.com/33TU/socks go 1.25.1 require golang.org/x/sync v0.20.0 + +require ( + github.com/klauspost/cpuid/v2 v2.0.12 // indirect + github.com/zeebo/blake3 v0.2.4 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/sys v0.43.0 // indirect +) diff --git a/go.sum b/go.sum index 733d716..1d6683a 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,10 @@ +github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= +github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/shadowsocks/key.go b/shadowsocks/key.go new file mode 100644 index 0000000..d1e95e8 --- /dev/null +++ b/shadowsocks/key.go @@ -0,0 +1,78 @@ +package shadowsocks + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + + "github.com/zeebo/blake3" +) + +// blake3SessionSubkeyContext is the context string used for deriving session subkeys with Blake3 in Shadowsocks 2022 methods. +const blake3SessionSubkeyContext = "shadowsocks 2022 session subkey" + +// DecodePSKTo decodes and validates a base64 PSK for the given method into dst. +// The returned slice may reuse dst's backing array. +func DecodePSKTo(dst []byte, method Method, s string) ([]byte, error) { + if err := method.Validate(); err != nil { + return nil, err + } + if s == "" { + return nil, fmt.Errorf("missing PSK") + } + + key, err := base64.StdEncoding.AppendDecode(dst[:0], []byte(s)) + if err != nil { + return nil, fmt.Errorf("invalid PSK: %w", err) + } + if len(key) != method.KeySize { + return nil, fmt.Errorf("invalid PSK length: got %d, want %d", len(key), method.KeySize) + } + + return key, nil +} + +// FillSalt fills dst with a random salt for the given method. +func FillSalt(dst []byte, method Method) error { + if err := method.Validate(); err != nil { + return err + } + if len(dst) != method.SaltSize { + return fmt.Errorf("invalid salt length: got %d, want %d", len(dst), method.SaltSize) + } + + if _, err := rand.Read(dst); err != nil { + return fmt.Errorf("generate salt: %w", err) + } + + return nil +} + +// DeriveSubkeyTo derives a session subkey into dst. +func DeriveSubkeyTo(dst []byte, method Method, key, salt []byte) error { + if err := method.Validate(); err != nil { + return err + } + if len(key) != method.KeySize { + return fmt.Errorf("invalid key length: got %d, want %d", len(key), method.KeySize) + } + if len(salt) != method.SaltSize { + return fmt.Errorf("invalid salt length: got %d, want %d", len(salt), method.SaltSize) + } + if len(dst) != method.KeySize { + return fmt.Errorf("invalid subkey length: got %d, want %d", len(dst), method.KeySize) + } + + h := blake3.NewDeriveKey(blake3SessionSubkeyContext) + if _, err := h.Write(key); err != nil { + return fmt.Errorf("derive subkey: %w", err) + } + if _, err := h.Write(salt); err != nil { + return fmt.Errorf("derive subkey: %w", err) + } + + sum := h.Sum(nil) + copy(dst, sum[:method.KeySize]) + + return nil +} diff --git a/shadowsocks/key_test.go b/shadowsocks/key_test.go new file mode 100644 index 0000000..1a7c2bc --- /dev/null +++ b/shadowsocks/key_test.go @@ -0,0 +1,259 @@ +package shadowsocks_test + +import ( + "bytes" + "encoding/base64" + "strings" + "testing" + + "github.com/33TU/socks/shadowsocks" +) + +func TestDecodePSKTo(t *testing.T) { + t.Parallel() + + method, err := shadowsocks.ParseMethod(shadowsocks.Method2022Blake3AES128GCM) + if err != nil { + t.Fatalf("ParseMethod() error = %v", err) + } + + validRaw := make([]byte, method.KeySize) + for i := range validRaw { + validRaw[i] = byte(i + 1) + } + + validPSK := base64.StdEncoding.EncodeToString(validRaw) + shortPSK := base64.StdEncoding.EncodeToString(make([]byte, method.KeySize-1)) + + tests := []struct { + name string + method shadowsocks.Method + psk string + want []byte + wantErr string + }{ + { + name: "valid", + method: method, + psk: validPSK, + want: validRaw, + }, + { + name: "missing psk", + method: method, + psk: "", + wantErr: "missing PSK", + }, + { + name: "invalid base64", + method: method, + psk: "!!!", + wantErr: "invalid PSK", + }, + { + name: "invalid key length", + method: method, + psk: shortPSK, + wantErr: "invalid PSK length", + }, + { + name: "invalid method", + method: shadowsocks.Method{ + Kind: shadowsocks.MethodKindUnknown, + }, + psk: validPSK, + wantErr: "invalid method kind", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := shadowsocks.DecodePSKTo(nil, tt.method, tt.psk) + if tt.wantErr != "" { + if err == nil { + t.Fatalf("expected error containing %q, got nil", tt.wantErr) + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("expected error containing %q, got %q", tt.wantErr, err.Error()) + } + return + } + + if err != nil { + t.Fatalf("DecodePSKTo() error = %v", err) + } + if !bytes.Equal(got, tt.want) { + t.Fatalf("DecodePSKTo() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDecodePSKTo_ReuseDst(t *testing.T) { + t.Parallel() + + method, err := shadowsocks.ParseMethod(shadowsocks.Method2022Blake3AES128GCM) + if err != nil { + t.Fatalf("ParseMethod() error = %v", err) + } + + raw := make([]byte, method.KeySize) + for i := range raw { + raw[i] = byte(i + 1) + } + + psk := base64.StdEncoding.EncodeToString(raw) + dst := make([]byte, 0, 64) + + got, err := shadowsocks.DecodePSKTo(dst, method, psk) + if err != nil { + t.Fatalf("DecodePSKTo() error = %v", err) + } + + if !bytes.Equal(got, raw) { + t.Fatalf("DecodePSKTo() = %v, want %v", got, raw) + } +} + +func TestFillSalt(t *testing.T) { + t.Parallel() + + method, err := shadowsocks.ParseMethod(shadowsocks.Method2022Blake3AES256GCM) + if err != nil { + t.Fatalf("ParseMethod() error = %v", err) + } + + t.Run("valid", func(t *testing.T) { + t.Parallel() + + salt := make([]byte, method.SaltSize) + if err := shadowsocks.FillSalt(salt, method); err != nil { + t.Fatalf("FillSalt() error = %v", err) + } + + if len(salt) != method.SaltSize { + t.Fatalf("len(salt) = %d, want %d", len(salt), method.SaltSize) + } + + if bytes.Equal(salt, make([]byte, method.SaltSize)) { + t.Fatal("salt is all zeros, expected random data") + } + }) + + t.Run("invalid salt length", func(t *testing.T) { + t.Parallel() + + salt := make([]byte, method.SaltSize-1) + err := shadowsocks.FillSalt(salt, method) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid salt length") { + t.Fatalf("expected invalid salt length error, got %q", err.Error()) + } + }) + + t.Run("invalid method", func(t *testing.T) { + t.Parallel() + + err := shadowsocks.FillSalt(make([]byte, 16), shadowsocks.Method{}) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid method kind") { + t.Fatalf("expected invalid method kind error, got %q", err.Error()) + } + }) +} + +func TestDeriveSubkeyTo(t *testing.T) { + t.Parallel() + + method, err := shadowsocks.ParseMethod(shadowsocks.Method2022Blake3AES256GCM) + if err != nil { + t.Fatalf("ParseMethod() error = %v", err) + } + + key := make([]byte, method.KeySize) + salt := make([]byte, method.SaltSize) + for i := range key { + key[i] = byte(i + 1) + } + for i := range salt { + salt[i] = byte(i + 101) + } + + t.Run("valid deterministic", func(t *testing.T) { + t.Parallel() + + dst1 := make([]byte, method.KeySize) + dst2 := make([]byte, method.KeySize) + + if err := shadowsocks.DeriveSubkeyTo(dst1, method, key, salt); err != nil { + t.Fatalf("DeriveSubkeyTo() error = %v", err) + } + if err := shadowsocks.DeriveSubkeyTo(dst2, method, key, salt); err != nil { + t.Fatalf("DeriveSubkeyTo() error = %v", err) + } + + if !bytes.Equal(dst1, dst2) { + t.Fatal("derived subkeys differ for same key and salt") + } + if bytes.Equal(dst1, make([]byte, method.KeySize)) { + t.Fatal("derived subkey is all zeros") + } + }) + + t.Run("invalid key length", func(t *testing.T) { + t.Parallel() + + dst := make([]byte, method.KeySize) + err := shadowsocks.DeriveSubkeyTo(dst, method, key[:len(key)-1], salt) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid key length") { + t.Fatalf("expected invalid key length error, got %q", err.Error()) + } + }) + + t.Run("invalid salt length", func(t *testing.T) { + t.Parallel() + + dst := make([]byte, method.KeySize) + err := shadowsocks.DeriveSubkeyTo(dst, method, key, salt[:len(salt)-1]) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid salt length") { + t.Fatalf("expected invalid salt length error, got %q", err.Error()) + } + }) + + t.Run("invalid subkey length", func(t *testing.T) { + t.Parallel() + + dst := make([]byte, method.KeySize-1) + err := shadowsocks.DeriveSubkeyTo(dst, method, key, salt) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid subkey length") { + t.Fatalf("expected invalid subkey length error, got %q", err.Error()) + } + }) + + t.Run("invalid method", func(t *testing.T) { + t.Parallel() + + err := shadowsocks.DeriveSubkeyTo(make([]byte, 16), shadowsocks.Method{}, make([]byte, 16), make([]byte, 16)) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid method kind") { + t.Fatalf("expected invalid method kind error, got %q", err.Error()) + } + }) +} diff --git a/shadowsocks/method.go b/shadowsocks/method.go new file mode 100644 index 0000000..4bf7338 --- /dev/null +++ b/shadowsocks/method.go @@ -0,0 +1,146 @@ +package shadowsocks + +import ( + "crypto/aes" + "crypto/cipher" + "fmt" + + "golang.org/x/crypto/chacha20poly1305" +) + +const ( + aeadNonceSize = 12 + aeadTagSize = 16 +) + +// MethodKind identifies the AEAD cipher family used by a Shadowsocks method. +type MethodKind uint8 + +const ( + MethodKindUnknown MethodKind = iota + MethodKindAESGCM + MethodKindChaCha20Poly1305 +) + +// Method describes a supported Shadowsocks method and its cryptographic parameters. +type Method struct { + Kind MethodKind + KeySize int + SaltSize int + NonceSize int + TagSize int +} + +var ( + method2022Blake3AES128GCM = Method{ + Kind: MethodKindAESGCM, + KeySize: 16, + SaltSize: 16, + NonceSize: aeadNonceSize, + TagSize: aeadTagSize, + } + + method2022Blake3AES256GCM = Method{ + Kind: MethodKindAESGCM, + KeySize: 32, + SaltSize: 32, + NonceSize: aeadNonceSize, + TagSize: aeadTagSize, + } + + method2022Blake3ChaCha20Poly1305 = Method{ + Kind: MethodKindChaCha20Poly1305, + KeySize: 32, + SaltSize: 32, + NonceSize: aeadNonceSize, + TagSize: aeadTagSize, + } +) + +// ParseMethod parses a Shadowsocks method name into its method definition. +func ParseMethod(name string) (Method, error) { + switch name { + case Method2022Blake3AES128GCM: + return method2022Blake3AES128GCM, nil + case Method2022Blake3AES256GCM: + return method2022Blake3AES256GCM, nil + case Method2022Blake3ChaCha20Poly1305: + return method2022Blake3ChaCha20Poly1305, nil + default: + return Method{}, fmt.Errorf("invalid method: %s", name) + } +} + +// IsSupportedMethod reports whether name is a supported Shadowsocks method. +func IsSupportedMethod(name string) bool { + _, err := ParseMethod(name) + return err == nil +} + +// Validate checks whether the method definition is internally valid. +func (m Method) Validate() error { + switch m.Kind { + case MethodKindAESGCM, MethodKindChaCha20Poly1305: + default: + return fmt.Errorf("invalid method kind: %d", m.Kind) + } + + if m.KeySize <= 0 { + return fmt.Errorf("invalid key size: %d", m.KeySize) + } + if m.SaltSize <= 0 { + return fmt.Errorf("invalid salt size: %d", m.SaltSize) + } + if m.NonceSize <= 0 { + return fmt.Errorf("invalid nonce size: %d", m.NonceSize) + } + if m.TagSize <= 0 { + return fmt.Errorf("invalid tag size: %d", m.TagSize) + } + + return nil +} + +// NewAEAD constructs a new AEAD instance for the method using key. +func (m Method) NewAEAD(key []byte) (cipher.AEAD, error) { + if err := m.Validate(); err != nil { + return nil, err + } + if len(key) != m.KeySize { + return nil, fmt.Errorf("invalid key length: got %d, want %d", len(key), m.KeySize) + } + + var aead cipher.AEAD + + switch m.Kind { + case MethodKindAESGCM: + block, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("create AES cipher: %w", err) + } + + aead, err = cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("create AES-GCM: %w", err) + } + + case MethodKindChaCha20Poly1305: + var err error + aead, err = chacha20poly1305.New(key) + if err != nil { + return nil, fmt.Errorf("create ChaCha20-Poly1305: %w", err) + } + + default: + return nil, fmt.Errorf("unsupported method kind: %d", m.Kind) + } + + if aead.NonceSize() != m.NonceSize { + return nil, fmt.Errorf("unexpected nonce size: got %d, want %d", aead.NonceSize(), m.NonceSize) + } + if aead.Overhead() != m.TagSize { + return nil, fmt.Errorf("unexpected tag size: got %d, want %d", aead.Overhead(), m.TagSize) + } + + return aead, nil +} diff --git a/shadowsocks/method_test.go b/shadowsocks/method_test.go new file mode 100644 index 0000000..9755985 --- /dev/null +++ b/shadowsocks/method_test.go @@ -0,0 +1,231 @@ +package shadowsocks_test + +import ( + "testing" + + "github.com/33TU/socks/shadowsocks" +) + +func TestParseMethod(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + methodName string + wantKind shadowsocks.MethodKind + wantKeyLen int + wantErr bool + }{ + { + name: "aes128", + methodName: shadowsocks.Method2022Blake3AES128GCM, + wantKind: shadowsocks.MethodKindAESGCM, + wantKeyLen: 16, + }, + { + name: "aes256", + methodName: shadowsocks.Method2022Blake3AES256GCM, + wantKind: shadowsocks.MethodKindAESGCM, + wantKeyLen: 32, + }, + { + name: "chacha20", + methodName: shadowsocks.Method2022Blake3ChaCha20Poly1305, + wantKind: shadowsocks.MethodKindChaCha20Poly1305, + wantKeyLen: 32, + }, + { + name: "invalid", + methodName: "invalid-method", + wantErr: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + m, err := shadowsocks.ParseMethod(tt.methodName) + if tt.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("ParseMethod() error = %v", err) + } + + if m.Kind != tt.wantKind { + t.Fatalf("Kind = %v, want %v", m.Kind, tt.wantKind) + } + if m.KeySize != tt.wantKeyLen { + t.Fatalf("KeySize = %d, want %d", m.KeySize, tt.wantKeyLen) + } + }) + } +} + +func TestIsSupportedMethod(t *testing.T) { + t.Parallel() + + if !shadowsocks.IsSupportedMethod(shadowsocks.Method2022Blake3AES128GCM) { + t.Fatal("expected aes128 method to be supported") + } + if shadowsocks.IsSupportedMethod("invalid-method") { + t.Fatal("expected invalid method to be unsupported") + } +} + +func TestMethodValidate(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + method shadowsocks.Method + wantErr bool + }{ + { + name: "valid", + method: shadowsocks.Method{ + Kind: shadowsocks.MethodKindAESGCM, + KeySize: 16, + SaltSize: 16, + NonceSize: 12, + TagSize: 16, + }, + }, + { + name: "invalid kind", + method: shadowsocks.Method{ + Kind: shadowsocks.MethodKindUnknown, + KeySize: 16, + SaltSize: 16, + NonceSize: 12, + TagSize: 16, + }, + wantErr: true, + }, + { + name: "invalid key size", + method: shadowsocks.Method{ + Kind: shadowsocks.MethodKindAESGCM, + KeySize: 0, + SaltSize: 16, + NonceSize: 12, + TagSize: 16, + }, + wantErr: true, + }, + { + name: "invalid salt size", + method: shadowsocks.Method{ + Kind: shadowsocks.MethodKindAESGCM, + KeySize: 16, + SaltSize: 0, + NonceSize: 12, + TagSize: 16, + }, + wantErr: true, + }, + { + name: "invalid nonce size", + method: shadowsocks.Method{ + Kind: shadowsocks.MethodKindAESGCM, + KeySize: 16, + SaltSize: 16, + NonceSize: 0, + TagSize: 16, + }, + wantErr: true, + }, + { + name: "invalid tag size", + method: shadowsocks.Method{ + Kind: shadowsocks.MethodKindAESGCM, + KeySize: 16, + SaltSize: 16, + NonceSize: 12, + TagSize: 0, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := tt.method.Validate() + if tt.wantErr && err == nil { + t.Fatal("expected error, got nil") + } + if !tt.wantErr && err != nil { + t.Fatalf("Validate() error = %v", err) + } + }) + } +} + +func TestMethodNewAEAD(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + methodName string + keyLen int + wantErr bool + }{ + { + name: "aes128", + methodName: shadowsocks.Method2022Blake3AES128GCM, + keyLen: 16, + }, + { + name: "aes256", + methodName: shadowsocks.Method2022Blake3AES256GCM, + keyLen: 32, + }, + { + name: "chacha20", + methodName: shadowsocks.Method2022Blake3ChaCha20Poly1305, + keyLen: 32, + }, + { + name: "invalid key length", + methodName: shadowsocks.Method2022Blake3AES128GCM, + keyLen: 15, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + m, err := shadowsocks.ParseMethod(tt.methodName) + if err != nil { + t.Fatalf("ParseMethod() error = %v", err) + } + + aead, err := m.NewAEAD(make([]byte, tt.keyLen)) + if tt.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("NewAEAD() error = %v", err) + } + + if aead.NonceSize() != m.NonceSize { + t.Fatalf("NonceSize = %d, want %d", aead.NonceSize(), m.NonceSize) + } + if aead.Overhead() != m.TagSize { + t.Fatalf("Overhead = %d, want %d", aead.Overhead(), m.TagSize) + } + }) + } +} From 0b4e54efe20871b1cee20a201067fec0428c0bc4 Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 15:59:31 +0300 Subject: [PATCH 05/25] shadowsocks FillSaltTo --- shadowsocks/key.go | 4 ++-- shadowsocks/key_test.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/shadowsocks/key.go b/shadowsocks/key.go index d1e95e8..ffbe956 100644 --- a/shadowsocks/key.go +++ b/shadowsocks/key.go @@ -32,8 +32,8 @@ func DecodePSKTo(dst []byte, method Method, s string) ([]byte, error) { return key, nil } -// FillSalt fills dst with a random salt for the given method. -func FillSalt(dst []byte, method Method) error { +// FillSaltTo fills dst with a random salt for the given method. +func FillSaltTo(dst []byte, method Method) error { if err := method.Validate(); err != nil { return err } diff --git a/shadowsocks/key_test.go b/shadowsocks/key_test.go index 1a7c2bc..36dd0d7 100644 --- a/shadowsocks/key_test.go +++ b/shadowsocks/key_test.go @@ -129,7 +129,7 @@ func TestFillSalt(t *testing.T) { t.Parallel() salt := make([]byte, method.SaltSize) - if err := shadowsocks.FillSalt(salt, method); err != nil { + if err := shadowsocks.FillSaltTo(salt, method); err != nil { t.Fatalf("FillSalt() error = %v", err) } @@ -146,7 +146,7 @@ func TestFillSalt(t *testing.T) { t.Parallel() salt := make([]byte, method.SaltSize-1) - err := shadowsocks.FillSalt(salt, method) + err := shadowsocks.FillSaltTo(salt, method) if err == nil { t.Fatal("expected error, got nil") } @@ -158,7 +158,7 @@ func TestFillSalt(t *testing.T) { t.Run("invalid method", func(t *testing.T) { t.Parallel() - err := shadowsocks.FillSalt(make([]byte, 16), shadowsocks.Method{}) + err := shadowsocks.FillSaltTo(make([]byte, 16), shadowsocks.Method{}) if err == nil { t.Fatal("expected error, got nil") } From 9d6242d6bf0a8a7682814eadb8d0d58c7321d4ec Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 16:32:38 +0300 Subject: [PATCH 06/25] shadowsocks tcp header --- shadowsocks.zip | Bin 0 -> 8782 bytes shadowsocks/addr.go | 195 ++++++++ shadowsocks/addr_test.go | 472 ++++++++++++++++++ shadowsocks/consts.go | 18 + shadowsocks/tcp_header_err.go | 14 + shadowsocks/tcp_request_fixed_header.go | 74 +++ shadowsocks/tcp_request_fixed_header_test.go | 163 ++++++ shadowsocks/tcp_request_variable_header.go | 120 +++++ .../tcp_request_variable_header_test.go | 331 ++++++++++++ shadowsocks/tcp_response_header.go | 96 ++++ shadowsocks/tcp_response_header_test.go | 241 +++++++++ 11 files changed, 1724 insertions(+) create mode 100644 shadowsocks.zip create mode 100644 shadowsocks/addr.go create mode 100644 shadowsocks/addr_test.go create mode 100644 shadowsocks/tcp_header_err.go create mode 100644 shadowsocks/tcp_request_fixed_header.go create mode 100644 shadowsocks/tcp_request_fixed_header_test.go create mode 100644 shadowsocks/tcp_request_variable_header.go create mode 100644 shadowsocks/tcp_request_variable_header_test.go create mode 100644 shadowsocks/tcp_response_header.go create mode 100644 shadowsocks/tcp_response_header_test.go diff --git a/shadowsocks.zip b/shadowsocks.zip new file mode 100644 index 0000000000000000000000000000000000000000..f0c605049167617c1b653be61eb819437c2cfe6b GIT binary patch literal 8782 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 T%+G;H&o9R3N*3YO&u{+^ghi#u literal 0 HcmV?d00001 diff --git a/shadowsocks/addr.go b/shadowsocks/addr.go new file mode 100644 index 0000000..7f44052 --- /dev/null +++ b/shadowsocks/addr.go @@ -0,0 +1,195 @@ +package shadowsocks + +import ( + "encoding/binary" + "errors" + "fmt" + "net" +) + +// Common validation and decode/encode errors for Shadowsocks addresses. +var ( + ErrInvalidAddrType = errors.New("invalid address type") + ErrInvalidAddr = errors.New("invalid address") + ErrInvalidDomain = errors.New("invalid domain") + ErrShortAddr = errors.New("short address") + ErrShortAddrBuffer = errors.New("short address buffer") +) + +// Addr represents a SOCKS5-style address field used by Shadowsocks. +type Addr struct { + AddrType byte + IP net.IP + Domain string + Port uint16 +} + +// Init initializes an Addr. +func (a *Addr) Init(addrType byte, ip net.IP, domain string, port uint16) { + a.AddrType = addrType + a.IP = ip + a.Domain = domain + a.Port = port +} + +// GetHost returns the address host as either the domain or IP string. +func (a *Addr) GetHost() string { + if a.AddrType == AddrTypeDomain { + return a.Domain + } + if a.IP == nil { + return "" + } + return a.IP.String() +} + +// Addr returns the address as a combined host:port string. +func (a *Addr) Addr() string { + return net.JoinHostPort(a.GetHost(), fmt.Sprint(a.Port)) +} + +// Validate checks the correctness of the address fields. +func (a *Addr) Validate() error { + switch a.AddrType { + case AddrTypeIPv4: + if a.IP == nil || a.IP.To4() == nil { + return ErrInvalidAddr + } + case AddrTypeIPv6: + if a.IP == nil || a.IP.To16() == nil || a.IP.To4() != nil { + return ErrInvalidAddr + } + case AddrTypeDomain: + if len(a.Domain) == 0 || len(a.Domain) > 255 { + return ErrInvalidDomain + } + default: + return ErrInvalidAddrType + } + + return nil +} + +// EncodedLen returns the number of bytes required to encode the address. +func (a *Addr) EncodedLen() int { + switch a.AddrType { + case AddrTypeIPv4: + return 1 + 4 + 2 + case AddrTypeIPv6: + return 1 + 16 + 2 + case AddrTypeDomain: + return 1 + 1 + len(a.Domain) + 2 + default: + return 0 + } +} + +// Decode decodes an address from src. +// It returns the number of bytes consumed. +func (a *Addr) Decode(src []byte) (int, error) { + if len(src) < 1 { + return 0, ErrShortAddr + } + + a.AddrType = src[0] + a.IP = nil + a.Domain = "" + + switch a.AddrType { + case AddrTypeIPv4: + if len(src) < 1+4+2 { + return 0, ErrShortAddr + } + a.IP = net.IP(src[1 : 1+4]).To4() + if a.IP == nil { + return 0, ErrInvalidAddr + } + a.Port = binary.BigEndian.Uint16(src[5:7]) + return 7, nil + + case AddrTypeIPv6: + if len(src) < 1+16+2 { + return 0, ErrShortAddr + } + a.IP = net.IP(src[1 : 1+16]).To16() + if a.IP == nil || a.IP.To4() != nil { + return 0, ErrInvalidAddr + } + a.Port = binary.BigEndian.Uint16(src[17:19]) + return 19, nil + + case AddrTypeDomain: + if len(src) < 2 { + return 0, ErrShortAddr + } + n := int(src[1]) + if len(src) < 1+1+n+2 { + return 0, ErrShortAddr + } + a.Domain = string(src[2 : 2+n]) + a.Port = binary.BigEndian.Uint16(src[2+n : 2+n+2]) + if err := a.Validate(); err != nil { + return 0, err + } + return 1 + 1 + n + 2, nil + + default: + return 0, ErrInvalidAddrType + } +} + +// EncodeTo encodes the address into dst. +// It returns the number of bytes written. +func (a *Addr) EncodeTo(dst []byte) (int, error) { + if err := a.Validate(); err != nil { + return 0, err + } + + n := a.EncodedLen() + if len(dst) < n { + return 0, ErrShortAddrBuffer + } + + dst[0] = a.AddrType + + switch a.AddrType { + case AddrTypeIPv4: + copy(dst[1:5], a.IP.To4()) + binary.BigEndian.PutUint16(dst[5:7], a.Port) + return 7, nil + + case AddrTypeIPv6: + copy(dst[1:17], a.IP.To16()) + binary.BigEndian.PutUint16(dst[17:19], a.Port) + return 19, nil + + case AddrTypeDomain: + dst[1] = byte(len(a.Domain)) + copy(dst[2:2+len(a.Domain)], a.Domain) + binary.BigEndian.PutUint16(dst[2+len(a.Domain):2+len(a.Domain)+2], a.Port) + return 1 + 1 + len(a.Domain) + 2, nil + + default: + return 0, ErrInvalidAddrType + } +} + +// String returns a human-readable representation of the address. +func (a *Addr) String() string { + var atype string + switch a.AddrType { + case AddrTypeIPv4: + atype = "IPv4" + case AddrTypeDomain: + atype = "DOMAIN" + case AddrTypeIPv6: + atype = "IPv6" + default: + atype = fmt.Sprintf("0x%02X", a.AddrType) + } + + return fmt.Sprintf( + "Addr{AddrType=%s, Host=%s, Port=%d}", + atype, a.GetHost(), a.Port, + ) +} diff --git a/shadowsocks/addr_test.go b/shadowsocks/addr_test.go new file mode 100644 index 0000000..0821e08 --- /dev/null +++ b/shadowsocks/addr_test.go @@ -0,0 +1,472 @@ +package shadowsocks_test + +import ( + "errors" + "net" + "testing" + + "github.com/33TU/socks/shadowsocks" +) + +func TestAddr_Init_Validate(t *testing.T) { + tests := []struct { + name string + addr shadowsocks.Addr + wantErr error + }{ + { + name: "valid ipv4", + addr: func() shadowsocks.Addr { + var a shadowsocks.Addr + a.Init(shadowsocks.AddrTypeIPv4, net.IPv4(127, 0, 0, 1), "", 1080) + return a + }(), + }, + { + name: "valid domain", + addr: func() shadowsocks.Addr { + var a shadowsocks.Addr + a.Init(shadowsocks.AddrTypeDomain, nil, "example.com", 443) + return a + }(), + }, + { + name: "valid ipv6", + addr: func() shadowsocks.Addr { + var a shadowsocks.Addr + a.Init(shadowsocks.AddrTypeIPv6, net.ParseIP("::1"), "", 5353) + return a + }(), + }, + { + name: "invalid addr type", + addr: func() shadowsocks.Addr { + var a shadowsocks.Addr + a.Init(0x99, net.IPv4(127, 0, 0, 1), "", 1080) + return a + }(), + wantErr: shadowsocks.ErrInvalidAddrType, + }, + { + name: "invalid ipv4 ip", + addr: func() shadowsocks.Addr { + var a shadowsocks.Addr + a.Init(shadowsocks.AddrTypeIPv4, nil, "", 1080) + return a + }(), + wantErr: shadowsocks.ErrInvalidAddr, + }, + { + name: "invalid ipv6 ip", + addr: func() shadowsocks.Addr { + var a shadowsocks.Addr + a.Init(shadowsocks.AddrTypeIPv6, net.IPv4(127, 0, 0, 1), "", 1080) + return a + }(), + wantErr: shadowsocks.ErrInvalidAddr, + }, + { + name: "missing domain", + addr: func() shadowsocks.Addr { + var a shadowsocks.Addr + a.Init(shadowsocks.AddrTypeDomain, nil, "", 1080) + return a + }(), + wantErr: shadowsocks.ErrInvalidDomain, + }, + { + name: "domain too long", + addr: func() shadowsocks.Addr { + var a shadowsocks.Addr + a.Init(shadowsocks.AddrTypeDomain, nil, string(make([]byte, 256)), 1080) + return a + }(), + wantErr: shadowsocks.ErrInvalidDomain, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.addr.Validate() + if !errors.Is(err, tt.wantErr) { + t.Errorf("Validate() error = %v, wantErr = %v", err, tt.wantErr) + } + }) + } +} + +func TestAddr_GetHost(t *testing.T) { + tests := []struct { + name string + addr shadowsocks.Addr + want string + }{ + { + name: "domain", + addr: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeDomain, + Domain: "example.com", + Port: 443, + }, + want: "example.com", + }, + { + name: "ipv4", + addr: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeIPv4, + IP: net.IPv4(127, 0, 0, 1), + Port: 1080, + }, + want: "127.0.0.1", + }, + { + name: "nil ip", + addr: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeIPv4, + IP: nil, + Port: 1080, + }, + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.addr.GetHost() + if got != tt.want { + t.Fatalf("GetHost() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestAddr_Addr(t *testing.T) { + tests := []struct { + name string + addr shadowsocks.Addr + want string + }{ + { + name: "domain", + addr: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeDomain, + Domain: "example.com", + Port: 443, + }, + want: "example.com:443", + }, + { + name: "ipv4", + addr: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeIPv4, + IP: net.IPv4(127, 0, 0, 1), + Port: 1080, + }, + want: "127.0.0.1:1080", + }, + { + name: "ipv6", + addr: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeIPv6, + IP: net.ParseIP("::1"), + Port: 53, + }, + want: "[::1]:53", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.addr.Addr() + if got != tt.want { + t.Fatalf("Addr() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestAddr_EncodedLen(t *testing.T) { + tests := []struct { + name string + addr shadowsocks.Addr + want int + }{ + { + name: "ipv4", + addr: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeIPv4, + IP: net.IPv4(127, 0, 0, 1), + Port: 1080, + }, + want: 7, + }, + { + name: "domain", + addr: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeDomain, + Domain: "example.com", + Port: 443, + }, + want: 1 + 1 + len("example.com") + 2, + }, + { + name: "ipv6", + addr: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeIPv6, + IP: net.ParseIP("::1"), + Port: 53, + }, + want: 19, + }, + { + name: "invalid", + addr: shadowsocks.Addr{ + AddrType: 0x99, + Port: 1, + }, + want: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.addr.EncodedLen() + if got != tt.want { + t.Errorf("EncodedLen() = %d, want %d", got, tt.want) + } + }) + } +} + +func TestAddr_EncodeTo_Decode_RoundTrip(t *testing.T) { + tests := []struct { + name string + addr shadowsocks.Addr + }{ + { + name: "ipv4", + addr: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeIPv4, + IP: net.IPv4(127, 0, 0, 1), + Port: 1080, + }, + }, + { + name: "domain", + addr: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeDomain, + Domain: "example.com", + Port: 443, + }, + }, + { + name: "ipv6", + addr: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeIPv6, + IP: net.ParseIP("2001:db8::1"), + Port: 5353, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := make([]byte, tt.addr.EncodedLen()) + + nw, err := tt.addr.EncodeTo(buf) + if err != nil { + t.Fatalf("EncodeTo() failed: %v", err) + } + if nw != len(buf) { + t.Fatalf("EncodeTo() wrote %d bytes, want %d", nw, len(buf)) + } + + var got shadowsocks.Addr + nr, err := got.Decode(buf) + if err != nil { + t.Fatalf("Decode() failed: %v", err) + } + if nr != len(buf) { + t.Fatalf("Decode() read %d bytes, want %d", nr, len(buf)) + } + + if got.AddrType != tt.addr.AddrType { + t.Fatalf("AddrType = %v, want %v", got.AddrType, tt.addr.AddrType) + } + if got.Port != tt.addr.Port { + t.Fatalf("Port = %d, want %d", got.Port, tt.addr.Port) + } + if got.Domain != tt.addr.Domain { + t.Fatalf("Domain = %q, want %q", got.Domain, tt.addr.Domain) + } + + switch tt.addr.AddrType { + case shadowsocks.AddrTypeIPv4: + if !got.IP.Equal(tt.addr.IP.To4()) { + t.Fatalf("IP = %v, want %v", got.IP, tt.addr.IP.To4()) + } + case shadowsocks.AddrTypeIPv6: + if !got.IP.Equal(tt.addr.IP.To16()) { + t.Fatalf("IP = %v, want %v", got.IP, tt.addr.IP.To16()) + } + } + }) + } +} + +func TestAddr_EncodeTo_Invalid(t *testing.T) { + tests := []struct { + name string + addr shadowsocks.Addr + bufLen int + wantErr error + }{ + { + name: "invalid addr type", + addr: shadowsocks.Addr{ + AddrType: 0x99, + IP: net.IPv4(127, 0, 0, 1), + Port: 1080, + }, + bufLen: 32, + wantErr: shadowsocks.ErrInvalidAddrType, + }, + { + name: "short buffer", + addr: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeIPv4, + IP: net.IPv4(127, 0, 0, 1), + Port: 1080, + }, + bufLen: 6, + wantErr: shadowsocks.ErrShortAddrBuffer, + }, + { + name: "invalid domain", + addr: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeDomain, + Domain: "", + Port: 80, + }, + bufLen: 32, + wantErr: shadowsocks.ErrInvalidDomain, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := make([]byte, tt.bufLen) + _, err := tt.addr.EncodeTo(buf) + if !errors.Is(err, tt.wantErr) { + t.Fatalf("EncodeTo() error = %v, wantErr = %v", err, tt.wantErr) + } + }) + } +} + +func TestAddr_Decode_Invalid(t *testing.T) { + tests := []struct { + name string + src []byte + wantErr error + }{ + { + name: "empty", + src: nil, + wantErr: shadowsocks.ErrShortAddr, + }, + { + name: "invalid addr type", + src: []byte{0x99}, + wantErr: shadowsocks.ErrInvalidAddrType, + }, + { + name: "short ipv4", + src: []byte{shadowsocks.AddrTypeIPv4, 127, 0, 0}, + wantErr: shadowsocks.ErrShortAddr, + }, + { + name: "short domain length byte missing payload", + src: []byte{shadowsocks.AddrTypeDomain, 5, 'a', 'b'}, + wantErr: shadowsocks.ErrShortAddr, + }, + { + name: "short ipv6", + src: []byte{shadowsocks.AddrTypeIPv6, 0, 1, 2}, + wantErr: shadowsocks.ErrShortAddr, + }, + { + name: "empty domain", + src: []byte{ + shadowsocks.AddrTypeDomain, + 0, + 0, 80, + }, + wantErr: shadowsocks.ErrInvalidDomain, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var a shadowsocks.Addr + _, err := a.Decode(tt.src) + if !errors.Is(err, tt.wantErr) { + t.Fatalf("Decode() error = %v, wantErr = %v", err, tt.wantErr) + } + }) + } +} + +func TestAddr_String(t *testing.T) { + tests := []struct { + name string + addr shadowsocks.Addr + want string + }{ + { + name: "ipv4", + addr: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeIPv4, + IP: net.IPv4(127, 0, 0, 1), + Port: 1080, + }, + want: "Addr{AddrType=IPv4, Host=127.0.0.1, Port=1080}", + }, + { + name: "domain", + addr: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeDomain, + Domain: "example.com", + Port: 443, + }, + want: "Addr{AddrType=DOMAIN, Host=example.com, Port=443}", + }, + { + name: "ipv6", + addr: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeIPv6, + IP: net.ParseIP("::1"), + Port: 53, + }, + want: "Addr{AddrType=IPv6, Host=::1, Port=53}", + }, + { + name: "unknown", + addr: shadowsocks.Addr{ + AddrType: 0x99, + Domain: "x", + Port: 1, + }, + want: "Addr{AddrType=0x99, Host=, Port=1}", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.addr.String() + if got != tt.want { + t.Fatalf("String() = %q, want %q", got, tt.want) + } + }) + } +} diff --git a/shadowsocks/consts.go b/shadowsocks/consts.go index 3c86b47..e2272df 100644 --- a/shadowsocks/consts.go +++ b/shadowsocks/consts.go @@ -6,3 +6,21 @@ const ( Method2022Blake3AES256GCM = "2022-blake3-aes-256-gcm" Method2022Blake3ChaCha20Poly1305 = "2022-blake3-chacha20-poly1305" ) + +// SOCKS5-style address types used inside Shadowsocks headers. +const ( + AddrTypeIPv4 = 0x01 + AddrTypeDomain = 0x03 + AddrTypeIPv6 = 0x04 +) + +// TCP header types for Shadowsocks 2022 stream protocol. +const ( + TCPHeaderTypeClientStream = 0x00 + TCPHeaderTypeServerStream = 0x01 +) + +const ( + TcpRequestFixedHeaderLen = 1 + 8 + 2 + TcpResponseFixedBaseLen = 1 + 8 + 2 +) diff --git a/shadowsocks/tcp_header_err.go b/shadowsocks/tcp_header_err.go new file mode 100644 index 0000000..b2135ba --- /dev/null +++ b/shadowsocks/tcp_header_err.go @@ -0,0 +1,14 @@ +package shadowsocks + +import "errors" + +// Common validation and decode/encode errors for Shadowsocks TCP headers. +var ( + ErrInvalidTCPHeaderType = errors.New("invalid TCP header type") + ErrInvalidTCPPaddingLength = errors.New("invalid TCP padding length") + ErrMissingTCPHeaderData = errors.New("missing TCP header data") + ErrShortTCPHeader = errors.New("short TCP header") + ErrShortTCPHeaderBuffer = errors.New("short TCP header buffer") + ErrMissingTCPResponseSalt = errors.New("missing TCP response salt") + ErrInvalidTCPResponseSaltLen = errors.New("invalid TCP response salt length") +) diff --git a/shadowsocks/tcp_request_fixed_header.go b/shadowsocks/tcp_request_fixed_header.go new file mode 100644 index 0000000..c272e66 --- /dev/null +++ b/shadowsocks/tcp_request_fixed_header.go @@ -0,0 +1,74 @@ +package shadowsocks + +import ( + "encoding/binary" + "fmt" +) + +// TCPRequestFixedHeader represents the fixed-length request header used by +// Shadowsocks 2022 TCP streams. +type TCPRequestFixedHeader struct { + Type byte + Timestamp uint64 + Length uint16 +} + +// Init initializes a TCPRequestFixedHeader. +func (h *TCPRequestFixedHeader) Init(typ byte, timestamp uint64, length uint16) { + h.Type = typ + h.Timestamp = timestamp + h.Length = length +} + +// Validate checks the correctness of the fixed request header fields. +func (h *TCPRequestFixedHeader) Validate() error { + if h.Type != TCPHeaderTypeClientStream { + return ErrInvalidTCPHeaderType + } + + return nil +} + +// EncodedLen returns the number of bytes required to encode the fixed request header. +func (h *TCPRequestFixedHeader) EncodedLen() int { + return TcpRequestFixedHeaderLen +} + +// Decode decodes a fixed request header from src. +// It returns the number of bytes consumed. +func (h *TCPRequestFixedHeader) Decode(src []byte) (int, error) { + if len(src) < TcpRequestFixedHeaderLen { + return 0, ErrShortTCPHeader + } + + h.Type = src[0] + h.Timestamp = binary.BigEndian.Uint64(src[1:9]) + h.Length = binary.BigEndian.Uint16(src[9:11]) + + return TcpRequestFixedHeaderLen, h.Validate() +} + +// EncodeTo encodes the fixed request header into dst. +// It returns the number of bytes written. +func (h *TCPRequestFixedHeader) EncodeTo(dst []byte) (int, error) { + if err := h.Validate(); err != nil { + return 0, err + } + if len(dst) < TcpRequestFixedHeaderLen { + return 0, ErrShortTCPHeaderBuffer + } + + dst[0] = h.Type + binary.BigEndian.PutUint64(dst[1:9], h.Timestamp) + binary.BigEndian.PutUint16(dst[9:11], h.Length) + + return TcpRequestFixedHeaderLen, nil +} + +// String returns a human-readable representation of the fixed request header. +func (h *TCPRequestFixedHeader) String() string { + return fmt.Sprintf( + "TCPRequestFixedHeader{Type:%d Timestamp:%d Length:%d}", + h.Type, h.Timestamp, h.Length, + ) +} diff --git a/shadowsocks/tcp_request_fixed_header_test.go b/shadowsocks/tcp_request_fixed_header_test.go new file mode 100644 index 0000000..a35e600 --- /dev/null +++ b/shadowsocks/tcp_request_fixed_header_test.go @@ -0,0 +1,163 @@ +package shadowsocks_test + +import ( + "errors" + "testing" + + "github.com/33TU/socks/shadowsocks" +) + +func TestTCPRequestFixedHeader_Init_Validate(t *testing.T) { + tests := []struct { + name string + hdr shadowsocks.TCPRequestFixedHeader + wantErr error + }{ + { + name: "valid", + hdr: func() shadowsocks.TCPRequestFixedHeader { + var h shadowsocks.TCPRequestFixedHeader + h.Init(shadowsocks.TCPHeaderTypeClientStream, 123456789, 42) + return h + }(), + }, + { + name: "invalid type", + hdr: func() shadowsocks.TCPRequestFixedHeader { + var h shadowsocks.TCPRequestFixedHeader + h.Init(0x99, 123456789, 42) + return h + }(), + wantErr: shadowsocks.ErrInvalidTCPHeaderType, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.hdr.Validate() + if !errors.Is(err, tt.wantErr) { + t.Fatalf("Validate() error = %v, wantErr = %v", err, tt.wantErr) + } + }) + } +} + +func TestTCPRequestFixedHeader_EncodedLen(t *testing.T) { + var h shadowsocks.TCPRequestFixedHeader + h.Init(shadowsocks.TCPHeaderTypeClientStream, 1, 2) + + if got := h.EncodedLen(); got != shadowsocks.TcpRequestFixedHeaderLen { + t.Fatalf("EncodedLen() = %d, want %d", got, shadowsocks.TcpRequestFixedHeaderLen) + } +} + +func TestTCPRequestFixedHeader_EncodeTo_Decode_RoundTrip(t *testing.T) { + var want shadowsocks.TCPRequestFixedHeader + want.Init(shadowsocks.TCPHeaderTypeClientStream, 123456789, 321) + + buf := make([]byte, want.EncodedLen()) + + nw, err := want.EncodeTo(buf) + if err != nil { + t.Fatalf("EncodeTo() failed: %v", err) + } + if nw != len(buf) { + t.Fatalf("EncodeTo() wrote %d bytes, want %d", nw, len(buf)) + } + + var got shadowsocks.TCPRequestFixedHeader + nr, err := got.Decode(buf) + if err != nil { + t.Fatalf("Decode() failed: %v", err) + } + if nr != len(buf) { + t.Fatalf("Decode() read %d bytes, want %d", nr, len(buf)) + } + + if got.Type != want.Type || got.Timestamp != want.Timestamp || got.Length != want.Length { + t.Fatalf("round-trip mismatch: got %+v, want %+v", got, want) + } +} + +func TestTCPRequestFixedHeader_EncodeTo_Invalid(t *testing.T) { + tests := []struct { + name string + hdr shadowsocks.TCPRequestFixedHeader + bufLen int + wantErr error + }{ + { + name: "invalid type", + hdr: shadowsocks.TCPRequestFixedHeader{ + Type: 0x99, + Timestamp: 1, + Length: 2, + }, + bufLen: 32, + wantErr: shadowsocks.ErrInvalidTCPHeaderType, + }, + { + name: "short buffer", + hdr: shadowsocks.TCPRequestFixedHeader{ + Type: shadowsocks.TCPHeaderTypeClientStream, + Timestamp: 1, + Length: 2, + }, + bufLen: shadowsocks.TcpRequestFixedHeaderLen - 1, + wantErr: shadowsocks.ErrShortTCPHeaderBuffer, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := make([]byte, tt.bufLen) + _, err := tt.hdr.EncodeTo(buf) + if !errors.Is(err, tt.wantErr) { + t.Fatalf("EncodeTo() error = %v, wantErr = %v", err, tt.wantErr) + } + }) + } +} + +func TestTCPRequestFixedHeader_Decode_Invalid(t *testing.T) { + tests := []struct { + name string + src []byte + wantErr error + }{ + { + name: "short header", + src: make([]byte, shadowsocks.TcpRequestFixedHeaderLen-1), + wantErr: shadowsocks.ErrShortTCPHeader, + }, + { + name: "invalid type", + src: []byte{ + 0x99, + 0, 0, 0, 0, 0, 0, 0, 1, + 0, 2, + }, + wantErr: shadowsocks.ErrInvalidTCPHeaderType, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var h shadowsocks.TCPRequestFixedHeader + _, err := h.Decode(tt.src) + if !errors.Is(err, tt.wantErr) { + t.Fatalf("Decode() error = %v, wantErr = %v", err, tt.wantErr) + } + }) + } +} + +func TestTCPRequestFixedHeader_String(t *testing.T) { + var h shadowsocks.TCPRequestFixedHeader + h.Init(shadowsocks.TCPHeaderTypeClientStream, 123, 45) + + want := "TCPRequestFixedHeader{Type:0 Timestamp:123 Length:45}" + if got := h.String(); got != want { + t.Fatalf("String() = %q, want %q", got, want) + } +} diff --git a/shadowsocks/tcp_request_variable_header.go b/shadowsocks/tcp_request_variable_header.go new file mode 100644 index 0000000..08f6f00 --- /dev/null +++ b/shadowsocks/tcp_request_variable_header.go @@ -0,0 +1,120 @@ +package shadowsocks + +import ( + "encoding/binary" + "fmt" +) + +// TCPRequestVariableHeader represents the variable-length request header used by +// Shadowsocks 2022 TCP streams. +type TCPRequestVariableHeader struct { + Target Addr + PaddingLen uint16 + Padding []byte + InitialData []byte +} + +// Init initializes a TCPRequestVariableHeader. +func (h *TCPRequestVariableHeader) Init(target Addr, padding, initialData []byte) { + h.Target = target + h.PaddingLen = uint16(len(padding)) + h.Padding = padding + h.InitialData = initialData +} + +// Validate checks the correctness of the variable request header fields. +func (h *TCPRequestVariableHeader) Validate() error { + if err := h.Target.Validate(); err != nil { + return err + } + if int(h.PaddingLen) != len(h.Padding) { + return ErrInvalidTCPPaddingLength + } + if len(h.Padding) == 0 && len(h.InitialData) == 0 { + return ErrMissingTCPHeaderData + } + + return nil +} + +// EncodedLen returns the number of bytes required to encode the variable request header. +func (h *TCPRequestVariableHeader) EncodedLen() int { + return h.Target.EncodedLen() + 2 + len(h.Padding) + len(h.InitialData) +} + +// Decode decodes a variable request header from src. +// It returns the number of bytes consumed. +func (h *TCPRequestVariableHeader) Decode(src []byte) (int, error) { + h.Target = Addr{} + h.PaddingLen = 0 + h.Padding = nil + h.InitialData = nil + + n, err := h.Target.Decode(src) + if err != nil { + return 0, err + } + if len(src[n:]) < 2 { + return 0, ErrShortTCPHeader + } + + h.PaddingLen = binary.BigEndian.Uint16(src[n : n+2]) + n += 2 + + if len(src[n:]) < int(h.PaddingLen) { + return 0, ErrShortTCPHeader + } + + if h.PaddingLen > 0 { + h.Padding = append(h.Padding[:0], src[n:n+int(h.PaddingLen)]...) + n += int(h.PaddingLen) + } + + if len(src[n:]) > 0 { + h.InitialData = append(h.InitialData[:0], src[n:]...) + n = len(src) + } + + if err := h.Validate(); err != nil { + return 0, err + } + + return n, nil +} + +// EncodeTo encodes the variable request header into dst. +// It returns the number of bytes written. +func (h *TCPRequestVariableHeader) EncodeTo(dst []byte) (int, error) { + if err := h.Validate(); err != nil { + return 0, err + } + + n := h.EncodedLen() + if len(dst) < n { + return 0, ErrShortTCPHeaderBuffer + } + + off, err := h.Target.EncodeTo(dst) + if err != nil { + return 0, err + } + + binary.BigEndian.PutUint16(dst[off:off+2], h.PaddingLen) + off += 2 + + copy(dst[off:off+len(h.Padding)], h.Padding) + off += len(h.Padding) + + copy(dst[off:off+len(h.InitialData)], h.InitialData) + off += len(h.InitialData) + + return off, nil +} + +// String returns a human-readable representation of the variable request header. +func (h *TCPRequestVariableHeader) String() string { + return fmt.Sprintf( + "TCPRequestVariableHeader{Target:%s PaddingLen:%d InitialDataLen:%d}", + h.Target.String(), h.PaddingLen, len(h.InitialData), + ) +} diff --git a/shadowsocks/tcp_request_variable_header_test.go b/shadowsocks/tcp_request_variable_header_test.go new file mode 100644 index 0000000..0de8941 --- /dev/null +++ b/shadowsocks/tcp_request_variable_header_test.go @@ -0,0 +1,331 @@ +package shadowsocks_test + +import ( + "bytes" + "errors" + "net" + "testing" + + "github.com/33TU/socks/shadowsocks" +) + +func TestTCPRequestVariableHeader_Init_Validate(t *testing.T) { + validTarget := shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeDomain, + Domain: "example.com", + Port: 443, + } + + tests := []struct { + name string + hdr shadowsocks.TCPRequestVariableHeader + wantErr error + }{ + { + name: "valid with padding", + hdr: func() shadowsocks.TCPRequestVariableHeader { + var h shadowsocks.TCPRequestVariableHeader + h.Init(validTarget, []byte{1, 2, 3}, nil) + return h + }(), + }, + { + name: "valid with initial data", + hdr: func() shadowsocks.TCPRequestVariableHeader { + var h shadowsocks.TCPRequestVariableHeader + h.Init(validTarget, nil, []byte("hello")) + return h + }(), + }, + { + name: "invalid target", + hdr: shadowsocks.TCPRequestVariableHeader{ + Target: shadowsocks.Addr{ + AddrType: 0x99, + Port: 80, + }, + PaddingLen: 1, + Padding: []byte{1}, + }, + wantErr: shadowsocks.ErrInvalidAddrType, + }, + { + name: "invalid padding length", + hdr: shadowsocks.TCPRequestVariableHeader{ + Target: validTarget, + PaddingLen: 5, + Padding: []byte{1, 2}, + }, + wantErr: shadowsocks.ErrInvalidTCPPaddingLength, + }, + { + name: "missing header data", + hdr: shadowsocks.TCPRequestVariableHeader{ + Target: validTarget, + PaddingLen: 0, + Padding: nil, + InitialData: nil, + }, + wantErr: shadowsocks.ErrMissingTCPHeaderData, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.hdr.Validate() + if !errors.Is(err, tt.wantErr) { + t.Fatalf("Validate() error = %v, wantErr = %v", err, tt.wantErr) + } + }) + } +} + +func TestTCPRequestVariableHeader_EncodedLen(t *testing.T) { + h := shadowsocks.TCPRequestVariableHeader{ + Target: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeDomain, + Domain: "example.com", + Port: 443, + }, + PaddingLen: 3, + Padding: []byte{1, 2, 3}, + InitialData: []byte("abc"), + } + + want := h.Target.EncodedLen() + 2 + 3 + 3 + if got := h.EncodedLen(); got != want { + t.Fatalf("EncodedLen() = %d, want %d", got, want) + } +} + +func TestTCPRequestVariableHeader_EncodeTo_Decode_RoundTrip(t *testing.T) { + tests := []struct { + name string + hdr shadowsocks.TCPRequestVariableHeader + }{ + { + name: "domain target", + hdr: shadowsocks.TCPRequestVariableHeader{ + Target: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeDomain, + Domain: "example.com", + Port: 443, + }, + PaddingLen: 2, + Padding: []byte{0xaa, 0xbb}, + InitialData: []byte("hello"), + }, + }, + { + name: "ipv4 target", + hdr: shadowsocks.TCPRequestVariableHeader{ + Target: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeIPv4, + IP: net.IPv4(127, 0, 0, 1), + Port: 1080, + }, + PaddingLen: 1, + Padding: []byte{0x01}, + InitialData: []byte("x"), + }, + }, + { + name: "ipv6 target", + hdr: shadowsocks.TCPRequestVariableHeader{ + Target: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeIPv6, + IP: net.ParseIP("2001:db8::1"), + Port: 53, + }, + PaddingLen: 3, + Padding: []byte{1, 2, 3}, + InitialData: []byte("payload"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := make([]byte, tt.hdr.EncodedLen()) + + nw, err := tt.hdr.EncodeTo(buf) + if err != nil { + t.Fatalf("EncodeTo() failed: %v", err) + } + if nw != len(buf) { + t.Fatalf("EncodeTo() wrote %d bytes, want %d", nw, len(buf)) + } + + var got shadowsocks.TCPRequestVariableHeader + nr, err := got.Decode(buf) + if err != nil { + t.Fatalf("Decode() failed: %v", err) + } + if nr != len(buf) { + t.Fatalf("Decode() read %d bytes, want %d", nr, len(buf)) + } + + if got.Target.AddrType != tt.hdr.Target.AddrType { + t.Fatalf("Target.AddrType = %v, want %v", got.Target.AddrType, tt.hdr.Target.AddrType) + } + if got.Target.Port != tt.hdr.Target.Port { + t.Fatalf("Target.Port = %d, want %d", got.Target.Port, tt.hdr.Target.Port) + } + if got.Target.Domain != tt.hdr.Target.Domain { + t.Fatalf("Target.Domain = %q, want %q", got.Target.Domain, tt.hdr.Target.Domain) + } + if tt.hdr.Target.IP != nil && !got.Target.IP.Equal(tt.hdr.Target.IP) { + t.Fatalf("Target.IP = %v, want %v", got.Target.IP, tt.hdr.Target.IP) + } + if got.PaddingLen != tt.hdr.PaddingLen { + t.Fatalf("PaddingLen = %d, want %d", got.PaddingLen, tt.hdr.PaddingLen) + } + if !bytes.Equal(got.Padding, tt.hdr.Padding) { + t.Fatalf("Padding = %v, want %v", got.Padding, tt.hdr.Padding) + } + if !bytes.Equal(got.InitialData, tt.hdr.InitialData) { + t.Fatalf("InitialData = %v, want %v", got.InitialData, tt.hdr.InitialData) + } + }) + } +} + +func TestTCPRequestVariableHeader_EncodeTo_Invalid(t *testing.T) { + validTarget := shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeDomain, + Domain: "example.com", + Port: 443, + } + + tests := []struct { + name string + hdr shadowsocks.TCPRequestVariableHeader + bufLen int + wantErr error + }{ + { + name: "invalid target", + hdr: shadowsocks.TCPRequestVariableHeader{ + Target: shadowsocks.Addr{ + AddrType: 0x99, + }, + PaddingLen: 1, + Padding: []byte{1}, + }, + bufLen: 64, + wantErr: shadowsocks.ErrInvalidAddrType, + }, + { + name: "invalid padding length", + hdr: shadowsocks.TCPRequestVariableHeader{ + Target: validTarget, + PaddingLen: 10, + Padding: []byte{1}, + }, + bufLen: 64, + wantErr: shadowsocks.ErrInvalidTCPPaddingLength, + }, + { + name: "missing header data", + hdr: shadowsocks.TCPRequestVariableHeader{ + Target: validTarget, + }, + bufLen: 64, + wantErr: shadowsocks.ErrMissingTCPHeaderData, + }, + { + name: "short buffer", + hdr: shadowsocks.TCPRequestVariableHeader{ + Target: validTarget, + PaddingLen: 1, + Padding: []byte{1}, + InitialData: []byte("a"), + }, + bufLen: 1, + wantErr: shadowsocks.ErrShortTCPHeaderBuffer, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := make([]byte, tt.bufLen) + _, err := tt.hdr.EncodeTo(buf) + if !errors.Is(err, tt.wantErr) { + t.Fatalf("EncodeTo() error = %v, wantErr = %v", err, tt.wantErr) + } + }) + } +} + +func TestTCPRequestVariableHeader_Decode_Invalid(t *testing.T) { + validTarget := shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeDomain, + Domain: "example.com", + Port: 443, + } + validTargetBuf := make([]byte, validTarget.EncodedLen()) + _, err := validTarget.EncodeTo(validTargetBuf) + if err != nil { + t.Fatalf("target EncodeTo() failed: %v", err) + } + + tests := []struct { + name string + src []byte + wantErr error + }{ + { + name: "invalid target", + src: []byte{0x99}, + wantErr: shadowsocks.ErrInvalidAddrType, + }, + { + name: "short after target before padding length", + src: validTargetBuf[:len(validTargetBuf)-1], + wantErr: shadowsocks.ErrShortAddr, + }, + { + name: "missing padding length bytes", + src: append(append([]byte{}, validTargetBuf...), 0x00), + wantErr: shadowsocks.ErrShortTCPHeader, + }, + { + name: "short padding bytes", + src: append(append(append([]byte{}, validTargetBuf...), 0x00, 0x02), 0x01), + wantErr: shadowsocks.ErrShortTCPHeader, + }, + { + name: "missing header data", + src: append(append([]byte{}, validTargetBuf...), 0x00, 0x00), + wantErr: shadowsocks.ErrMissingTCPHeaderData, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var h shadowsocks.TCPRequestVariableHeader + _, err := h.Decode(tt.src) + if !errors.Is(err, tt.wantErr) { + t.Fatalf("Decode() error = %v, wantErr = %v", err, tt.wantErr) + } + }) + } +} + +func TestTCPRequestVariableHeader_String(t *testing.T) { + h := shadowsocks.TCPRequestVariableHeader{ + Target: shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeDomain, + Domain: "example.com", + Port: 443, + }, + PaddingLen: 2, + Padding: []byte{1, 2}, + InitialData: []byte("abc"), + } + + want := `TCPRequestVariableHeader{Target:Addr{AddrType=DOMAIN, Host=example.com, Port=443} PaddingLen:2 InitialDataLen:3}` + if got := h.String(); got != want { + t.Fatalf("String() = %q, want %q", got, want) + } +} diff --git a/shadowsocks/tcp_response_header.go b/shadowsocks/tcp_response_header.go new file mode 100644 index 0000000..36d8b99 --- /dev/null +++ b/shadowsocks/tcp_response_header.go @@ -0,0 +1,96 @@ +package shadowsocks + +import ( + "encoding/binary" + "fmt" +) + +// TCPResponseHeader represents the fixed-length response header used by +// Shadowsocks 2022 TCP streams. +type TCPResponseHeader struct { + Type byte + Timestamp uint64 + RequestSalt []byte + Length uint16 +} + +// Init initializes a TCPResponseHeader. +func (h *TCPResponseHeader) Init(typ byte, timestamp uint64, requestSalt []byte, length uint16) { + h.Type = typ + h.Timestamp = timestamp + h.RequestSalt = requestSalt + h.Length = length +} + +// Validate checks the correctness of the response header fields. +func (h *TCPResponseHeader) Validate() error { + if h.Type != TCPHeaderTypeServerStream { + return ErrInvalidTCPHeaderType + } + if len(h.RequestSalt) == 0 { + return ErrMissingTCPResponseSalt + } + if int(h.Length) != len(h.RequestSalt) { + return ErrInvalidTCPResponseSaltLen + } + + return nil +} + +// EncodedLen returns the number of bytes required to encode the response header. +func (h *TCPResponseHeader) EncodedLen() int { + return TcpResponseFixedBaseLen + len(h.RequestSalt) +} + +// Decode decodes a response header from src. +// It returns the number of bytes consumed. +func (h *TCPResponseHeader) Decode(src []byte) (int, error) { + if len(src) < TcpResponseFixedBaseLen { + return 0, ErrShortTCPHeader + } + + h.Type = src[0] + h.Timestamp = binary.BigEndian.Uint64(src[1:9]) + h.Length = binary.BigEndian.Uint16(src[9:11]) + + need := TcpResponseFixedBaseLen + int(h.Length) + if len(src) < need { + return 0, ErrShortTCPHeader + } + + h.RequestSalt = append(h.RequestSalt[:0], src[11:need]...) + + if err := h.Validate(); err != nil { + return 0, err + } + + return need, nil +} + +// EncodeTo encodes the response header into dst. +// It returns the number of bytes written. +func (h *TCPResponseHeader) EncodeTo(dst []byte) (int, error) { + if err := h.Validate(); err != nil { + return 0, err + } + + n := h.EncodedLen() + if len(dst) < n { + return 0, ErrShortTCPHeaderBuffer + } + + dst[0] = h.Type + binary.BigEndian.PutUint64(dst[1:9], h.Timestamp) + binary.BigEndian.PutUint16(dst[9:11], h.Length) + copy(dst[11:11+len(h.RequestSalt)], h.RequestSalt) + + return n, nil +} + +// String returns a human-readable representation of the response header. +func (h *TCPResponseHeader) String() string { + return fmt.Sprintf( + "TCPResponseHeader{Type:%d Timestamp:%d RequestSaltLen:%d Length:%d}", + h.Type, h.Timestamp, len(h.RequestSalt), h.Length, + ) +} diff --git a/shadowsocks/tcp_response_header_test.go b/shadowsocks/tcp_response_header_test.go new file mode 100644 index 0000000..61e13d4 --- /dev/null +++ b/shadowsocks/tcp_response_header_test.go @@ -0,0 +1,241 @@ +package shadowsocks_test + +import ( + "bytes" + "errors" + "testing" + + "github.com/33TU/socks/shadowsocks" +) + +func TestTCPResponseHeader_Init_Validate(t *testing.T) { + tests := []struct { + name string + hdr shadowsocks.TCPResponseHeader + wantErr error + }{ + { + name: "valid", + hdr: func() shadowsocks.TCPResponseHeader { + var h shadowsocks.TCPResponseHeader + h.Init(shadowsocks.TCPHeaderTypeServerStream, 123456789, []byte{1, 2, 3, 4}, 4) + return h + }(), + }, + { + name: "invalid type", + hdr: func() shadowsocks.TCPResponseHeader { + var h shadowsocks.TCPResponseHeader + h.Init(0x99, 123456789, []byte{1, 2, 3, 4}, 4) + return h + }(), + wantErr: shadowsocks.ErrInvalidTCPHeaderType, + }, + { + name: "missing salt", + hdr: func() shadowsocks.TCPResponseHeader { + var h shadowsocks.TCPResponseHeader + h.Init(shadowsocks.TCPHeaderTypeServerStream, 123456789, nil, 0) + return h + }(), + wantErr: shadowsocks.ErrMissingTCPResponseSalt, + }, + { + name: "invalid salt length", + hdr: func() shadowsocks.TCPResponseHeader { + var h shadowsocks.TCPResponseHeader + h.Init(shadowsocks.TCPHeaderTypeServerStream, 123456789, []byte{1, 2, 3}, 4) + return h + }(), + wantErr: shadowsocks.ErrInvalidTCPResponseSaltLen, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.hdr.Validate() + if !errors.Is(err, tt.wantErr) { + t.Fatalf("Validate() error = %v, wantErr = %v", err, tt.wantErr) + } + }) + } +} + +func TestTCPResponseHeader_EncodedLen(t *testing.T) { + h := shadowsocks.TCPResponseHeader{ + Type: shadowsocks.TCPHeaderTypeServerStream, + Timestamp: 1, + RequestSalt: []byte{1, 2, 3, 4}, + Length: 4, + } + + want := shadowsocks.TcpResponseFixedBaseLen + 4 + if got := h.EncodedLen(); got != want { + t.Fatalf("EncodedLen() = %d, want %d", got, want) + } +} + +func TestTCPResponseHeader_EncodeTo_Decode_RoundTrip(t *testing.T) { + want := shadowsocks.TCPResponseHeader{ + Type: shadowsocks.TCPHeaderTypeServerStream, + Timestamp: 123456789, + RequestSalt: []byte{0xaa, 0xbb, 0xcc, 0xdd}, + Length: 4, + } + + buf := make([]byte, want.EncodedLen()) + + nw, err := want.EncodeTo(buf) + if err != nil { + t.Fatalf("EncodeTo() failed: %v", err) + } + if nw != len(buf) { + t.Fatalf("EncodeTo() wrote %d bytes, want %d", nw, len(buf)) + } + + var got shadowsocks.TCPResponseHeader + nr, err := got.Decode(buf) + if err != nil { + t.Fatalf("Decode() failed: %v", err) + } + if nr != len(buf) { + t.Fatalf("Decode() read %d bytes, want %d", nr, len(buf)) + } + + if got.Type != want.Type || got.Timestamp != want.Timestamp || got.Length != want.Length { + t.Fatalf("header mismatch: got %+v, want %+v", got, want) + } + if !bytes.Equal(got.RequestSalt, want.RequestSalt) { + t.Fatalf("RequestSalt = %v, want %v", got.RequestSalt, want.RequestSalt) + } +} + +func TestTCPResponseHeader_EncodeTo_Invalid(t *testing.T) { + tests := []struct { + name string + hdr shadowsocks.TCPResponseHeader + bufLen int + wantErr error + }{ + { + name: "invalid type", + hdr: shadowsocks.TCPResponseHeader{ + Type: 0x99, + Timestamp: 1, + RequestSalt: []byte{1, 2}, + Length: 2, + }, + bufLen: 32, + wantErr: shadowsocks.ErrInvalidTCPHeaderType, + }, + { + name: "missing salt", + hdr: shadowsocks.TCPResponseHeader{ + Type: shadowsocks.TCPHeaderTypeServerStream, + Timestamp: 1, + Length: 0, + }, + bufLen: 32, + wantErr: shadowsocks.ErrMissingTCPResponseSalt, + }, + { + name: "invalid salt length", + hdr: shadowsocks.TCPResponseHeader{ + Type: shadowsocks.TCPHeaderTypeServerStream, + Timestamp: 1, + RequestSalt: []byte{1, 2, 3}, + Length: 2, + }, + bufLen: 32, + wantErr: shadowsocks.ErrInvalidTCPResponseSaltLen, + }, + { + name: "short buffer", + hdr: shadowsocks.TCPResponseHeader{ + Type: shadowsocks.TCPHeaderTypeServerStream, + Timestamp: 1, + RequestSalt: []byte{1, 2, 3, 4}, + Length: 4, + }, + bufLen: shadowsocks.TcpResponseFixedBaseLen + 3, + wantErr: shadowsocks.ErrShortTCPHeaderBuffer, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := make([]byte, tt.bufLen) + _, err := tt.hdr.EncodeTo(buf) + if !errors.Is(err, tt.wantErr) { + t.Fatalf("EncodeTo() error = %v, wantErr = %v", err, tt.wantErr) + } + }) + } +} + +func TestTCPResponseHeader_Decode_Invalid(t *testing.T) { + tests := []struct { + name string + src []byte + wantErr error + }{ + { + name: "short fixed header", + src: make([]byte, shadowsocks.TcpResponseFixedBaseLen-1), + wantErr: shadowsocks.ErrShortTCPHeader, + }, + { + name: "invalid type", + src: []byte{ + 0x99, + 0, 0, 0, 0, 0, 0, 0, 1, + 0, 2, + 0xaa, 0xbb, + }, + wantErr: shadowsocks.ErrInvalidTCPHeaderType, + }, + { + name: "missing salt bytes", + src: []byte{ + shadowsocks.TCPHeaderTypeServerStream, + 0, 0, 0, 0, 0, 0, 0, 1, + 0, 2, + 0xaa, + }, + wantErr: shadowsocks.ErrShortTCPHeader, + }, + { + name: "empty salt", + src: []byte{ + shadowsocks.TCPHeaderTypeServerStream, + 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, + }, + wantErr: shadowsocks.ErrMissingTCPResponseSalt, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var h shadowsocks.TCPResponseHeader + _, err := h.Decode(tt.src) + if !errors.Is(err, tt.wantErr) { + t.Fatalf("Decode() error = %v, wantErr = %v", err, tt.wantErr) + } + }) + } +} + +func TestTCPResponseHeader_String(t *testing.T) { + h := shadowsocks.TCPResponseHeader{ + Type: shadowsocks.TCPHeaderTypeServerStream, + Timestamp: 123, + RequestSalt: []byte{1, 2, 3, 4}, + Length: 4, + } + + want := "TCPResponseHeader{Type:1 Timestamp:123 RequestSaltLen:4 Length:4}" + if got := h.String(); got != want { + t.Fatalf("String() = %q, want %q", got, want) + } +} From 45412087ebbb3281af6188b032dcbc92fe56867b Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 16:44:52 +0300 Subject: [PATCH 07/25] shadowsocks tcp stream cipher --- shadowsocks/consts.go | 7 + shadowsocks/method.go | 17 +- shadowsocks/server.go | 1 - shadowsocks/server_test.go | 1 - shadowsocks/tcp_stream_cipher.go | 258 ++++++++++++++ shadowsocks/tcp_stream_cipher_test.go | 489 ++++++++++++++++++++++++++ 6 files changed, 760 insertions(+), 13 deletions(-) delete mode 100644 shadowsocks/server.go delete mode 100644 shadowsocks/server_test.go create mode 100644 shadowsocks/tcp_stream_cipher.go create mode 100644 shadowsocks/tcp_stream_cipher_test.go diff --git a/shadowsocks/consts.go b/shadowsocks/consts.go index e2272df..2db6e94 100644 --- a/shadowsocks/consts.go +++ b/shadowsocks/consts.go @@ -24,3 +24,10 @@ const ( TcpRequestFixedHeaderLen = 1 + 8 + 2 TcpResponseFixedBaseLen = 1 + 8 + 2 ) + +const ( + AeadNonceSize = 12 + AeadTagSize = 16 +) + +const TcpChunkLengthLen = 2 diff --git a/shadowsocks/method.go b/shadowsocks/method.go index 4bf7338..6018d9f 100644 --- a/shadowsocks/method.go +++ b/shadowsocks/method.go @@ -8,11 +8,6 @@ import ( "golang.org/x/crypto/chacha20poly1305" ) -const ( - aeadNonceSize = 12 - aeadTagSize = 16 -) - // MethodKind identifies the AEAD cipher family used by a Shadowsocks method. type MethodKind uint8 @@ -36,24 +31,24 @@ var ( Kind: MethodKindAESGCM, KeySize: 16, SaltSize: 16, - NonceSize: aeadNonceSize, - TagSize: aeadTagSize, + NonceSize: AeadNonceSize, + TagSize: AeadTagSize, } method2022Blake3AES256GCM = Method{ Kind: MethodKindAESGCM, KeySize: 32, SaltSize: 32, - NonceSize: aeadNonceSize, - TagSize: aeadTagSize, + NonceSize: AeadNonceSize, + TagSize: AeadTagSize, } method2022Blake3ChaCha20Poly1305 = Method{ Kind: MethodKindChaCha20Poly1305, KeySize: 32, SaltSize: 32, - NonceSize: aeadNonceSize, - TagSize: aeadTagSize, + NonceSize: AeadNonceSize, + TagSize: AeadTagSize, } ) diff --git a/shadowsocks/server.go b/shadowsocks/server.go deleted file mode 100644 index 4b35346..0000000 --- a/shadowsocks/server.go +++ /dev/null @@ -1 +0,0 @@ -package shadowsocks diff --git a/shadowsocks/server_test.go b/shadowsocks/server_test.go deleted file mode 100644 index 1df463b..0000000 --- a/shadowsocks/server_test.go +++ /dev/null @@ -1 +0,0 @@ -package shadowsocks_test diff --git a/shadowsocks/tcp_stream_cipher.go b/shadowsocks/tcp_stream_cipher.go new file mode 100644 index 0000000..ede967b --- /dev/null +++ b/shadowsocks/tcp_stream_cipher.go @@ -0,0 +1,258 @@ +package shadowsocks + +import ( + "crypto/cipher" + "encoding/binary" + "fmt" +) + +// TCPStreamCipher manages AEAD state for a single Shadowsocks 2022 TCP stream. +type TCPStreamCipher struct { + Method Method + AEAD cipher.AEAD + Nonce [AeadNonceSize]byte +} + +// NewTCPStreamCipher creates a new TCP stream cipher from an already-derived subkey. +func NewTCPStreamCipher(method Method, subkey []byte) (*TCPStreamCipher, error) { + if err := method.Validate(); err != nil { + return nil, err + } + if len(subkey) != method.KeySize { + return nil, fmt.Errorf("invalid subkey length: got %d, want %d", len(subkey), method.KeySize) + } + + aead, err := method.NewAEAD(subkey) + if err != nil { + return nil, err + } + + return &TCPStreamCipher{ + Method: method, + AEAD: aead, + }, nil +} + +// NewTCPStreamCipherFromPSK creates a new TCP stream cipher from a PSK and salt. +func NewTCPStreamCipherFromPSK(method Method, psk, salt []byte) (*TCPStreamCipher, error) { + if err := method.Validate(); err != nil { + return nil, err + } + if len(psk) != method.KeySize { + return nil, fmt.Errorf("invalid PSK length: got %d, want %d", len(psk), method.KeySize) + } + if len(salt) != method.SaltSize { + return nil, fmt.Errorf("invalid salt length: got %d, want %d", len(salt), method.SaltSize) + } + + subkey := make([]byte, method.KeySize) + if err := DeriveSubkeyTo(subkey, method, psk, salt); err != nil { + return nil, err + } + + return NewTCPStreamCipher(method, subkey) +} + +// Reset resets the stream nonce to zero. +func (s *TCPStreamCipher) Reset() { + clear(s.Nonce[:]) +} + +// SealTo encrypts plaintext into dst using the current nonce and increments the nonce. +func (s *TCPStreamCipher) SealTo(dst, plaintext []byte) ([]byte, error) { + if s == nil { + return nil, fmt.Errorf("nil TCP stream cipher") + } + if err := s.Validate(); err != nil { + return nil, err + } + + out := s.AEAD.Seal(dst[:0], s.Nonce[:], plaintext, nil) + s.incNonce() + return out, nil +} + +// OpenTo decrypts ciphertext into dst using the current nonce and increments the nonce. +func (s *TCPStreamCipher) OpenTo(dst, ciphertext []byte) ([]byte, error) { + if s == nil { + return nil, fmt.Errorf("nil TCP stream cipher") + } + if err := s.Validate(); err != nil { + return nil, err + } + + out, err := s.AEAD.Open(dst[:0], s.Nonce[:], ciphertext, nil) + if err != nil { + return nil, err + } + + s.incNonce() + return out, nil +} + +// Validate checks whether the TCP stream cipher is internally valid. +func (s *TCPStreamCipher) Validate() error { + if s == nil { + return fmt.Errorf("nil TCP stream cipher") + } + if err := s.Method.Validate(); err != nil { + return err + } + if s.AEAD == nil { + return fmt.Errorf("missing AEAD") + } + if s.AEAD.NonceSize() != s.Method.NonceSize { + return fmt.Errorf("invalid AEAD nonce size: got %d, want %d", s.AEAD.NonceSize(), s.Method.NonceSize) + } + if s.AEAD.Overhead() != s.Method.TagSize { + return fmt.Errorf("invalid AEAD tag size: got %d, want %d", s.AEAD.Overhead(), s.Method.TagSize) + } + + return nil +} + +// EncryptedChunkLength returns the ciphertext size of a TCP chunk length field. +func (s *TCPStreamCipher) EncryptedChunkLength() int { + return TcpChunkLengthLen + s.Method.TagSize +} + +// EncryptedPayloadLength returns the ciphertext size of a TCP payload chunk. +func (s *TCPStreamCipher) EncryptedPayloadLength(payloadLen int) int { + return payloadLen + s.Method.TagSize +} + +// EncodeChunkLengthTo encrypts a 2-byte big-endian payload length into dst. +func (s *TCPStreamCipher) EncodeChunkLengthTo(dst []byte, payloadLen uint16) ([]byte, error) { + var buf [TcpChunkLengthLen]byte + binary.BigEndian.PutUint16(buf[:], payloadLen) + return s.SealTo(dst, buf[:]) +} + +// DecodeChunkLength decrypts and parses a 2-byte big-endian payload length. +func (s *TCPStreamCipher) DecodeChunkLength(src []byte) (uint16, error) { + var buf [TcpChunkLengthLen]byte + + plain, err := s.OpenTo(buf[:0], src) + if err != nil { + return 0, err + } + if len(plain) != TcpChunkLengthLen { + return 0, fmt.Errorf("invalid TCP chunk length size: got %d, want %d", len(plain), TcpChunkLengthLen) + } + + return binary.BigEndian.Uint16(plain), nil +} + +// EncodeChunkPayloadTo encrypts a TCP payload chunk into dst. +func (s *TCPStreamCipher) EncodeChunkPayloadTo(dst, payload []byte) ([]byte, error) { + if len(payload) > 0xFFFF { + return nil, fmt.Errorf("payload too large: got %d, max %d", len(payload), 0xFFFF) + } + return s.SealTo(dst, payload) +} + +// DecodeChunkPayloadTo decrypts a TCP payload chunk into dst. +func (s *TCPStreamCipher) DecodeChunkPayloadTo(dst, src []byte) ([]byte, error) { + return s.OpenTo(dst, src) +} + +// EncodeRequestFixedHeaderTo encodes and encrypts a TCP request fixed header into dst. +func (s *TCPStreamCipher) EncodeRequestFixedHeaderTo(dst []byte, h *TCPRequestFixedHeader) ([]byte, error) { + if h == nil { + return nil, fmt.Errorf("nil TCP request fixed header") + } + + buf := make([]byte, h.EncodedLen()) + n, err := h.EncodeTo(buf) + if err != nil { + return nil, err + } + + return s.SealTo(dst, buf[:n]) +} + +// DecodeRequestFixedHeader decrypts and decodes a TCP request fixed header from src. +func (s *TCPStreamCipher) DecodeRequestFixedHeader(src []byte) (TCPRequestFixedHeader, error) { + var h TCPRequestFixedHeader + buf := make([]byte, h.EncodedLen()) + + plain, err := s.OpenTo(buf[:0], src) + if err != nil { + return TCPRequestFixedHeader{}, err + } + if _, err := h.Decode(plain); err != nil { + return TCPRequestFixedHeader{}, err + } + + return h, nil +} + +// EncodeRequestVariableHeaderTo encodes and encrypts a TCP request variable header into dst. +func (s *TCPStreamCipher) EncodeRequestVariableHeaderTo(dst []byte, h *TCPRequestVariableHeader) ([]byte, error) { + if h == nil { + return nil, fmt.Errorf("nil TCP request variable header") + } + + buf := make([]byte, h.EncodedLen()) + n, err := h.EncodeTo(buf) + if err != nil { + return nil, err + } + + return s.SealTo(dst, buf[:n]) +} + +// DecodeRequestVariableHeader decrypts and decodes a TCP request variable header from src. +func (s *TCPStreamCipher) DecodeRequestVariableHeader(src []byte, scratch []byte) (TCPRequestVariableHeader, error) { + var h TCPRequestVariableHeader + + plain, err := s.OpenTo(scratch[:0], src) + if err != nil { + return TCPRequestVariableHeader{}, err + } + if _, err := h.Decode(plain); err != nil { + return TCPRequestVariableHeader{}, err + } + + return h, nil +} + +// EncodeResponseHeaderTo encodes and encrypts a TCP response header into dst. +func (s *TCPStreamCipher) EncodeResponseHeaderTo(dst []byte, h *TCPResponseHeader) ([]byte, error) { + if h == nil { + return nil, fmt.Errorf("nil TCP response header") + } + + buf := make([]byte, h.EncodedLen()) + n, err := h.EncodeTo(buf) + if err != nil { + return nil, err + } + + return s.SealTo(dst, buf[:n]) +} + +// DecodeResponseHeader decrypts and decodes a TCP response header from src. +func (s *TCPStreamCipher) DecodeResponseHeader(src []byte, scratch []byte) (TCPResponseHeader, error) { + var h TCPResponseHeader + + plain, err := s.OpenTo(scratch[:0], src) + if err != nil { + return TCPResponseHeader{}, err + } + if _, err := h.Decode(plain); err != nil { + return TCPResponseHeader{}, err + } + + return h, nil +} + +// incNonce increments the TCP stream nonce as a 12-byte little-endian integer. +func (s *TCPStreamCipher) incNonce() { + for i := range len(s.Nonce) { + s.Nonce[i]++ + if s.Nonce[i] != 0 { + return + } + } +} diff --git a/shadowsocks/tcp_stream_cipher_test.go b/shadowsocks/tcp_stream_cipher_test.go new file mode 100644 index 0000000..0e9b1d8 --- /dev/null +++ b/shadowsocks/tcp_stream_cipher_test.go @@ -0,0 +1,489 @@ +package shadowsocks_test + +import ( + "bytes" + "strings" + "testing" + + "github.com/33TU/socks/shadowsocks" +) + +func newTestMethod(t *testing.T) shadowsocks.Method { + t.Helper() + + method, err := shadowsocks.ParseMethod(shadowsocks.Method2022Blake3AES128GCM) + if err != nil { + t.Fatalf("ParseMethod() error = %v", err) + } + + return method +} + +func newTestPSKAndSalt(method shadowsocks.Method) ([]byte, []byte) { + psk := make([]byte, method.KeySize) + salt := make([]byte, method.SaltSize) + + for i := range psk { + psk[i] = byte(i + 1) + } + for i := range salt { + salt[i] = byte(i + 101) + } + + return psk, salt +} + +func newTestCipherPair(t *testing.T) (*shadowsocks.TCPStreamCipher, *shadowsocks.TCPStreamCipher, shadowsocks.Method) { + t.Helper() + + method := newTestMethod(t) + psk, salt := newTestPSKAndSalt(method) + + enc, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk, salt) + if err != nil { + t.Fatalf("NewTCPStreamCipherFromPSK(enc) error = %v", err) + } + + dec, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk, salt) + if err != nil { + t.Fatalf("NewTCPStreamCipherFromPSK(dec) error = %v", err) + } + + return enc, dec, method +} + +func TestNewTCPStreamCipher(t *testing.T) { + t.Parallel() + + method := newTestMethod(t) + + t.Run("valid", func(t *testing.T) { + t.Parallel() + + subkey := make([]byte, method.KeySize) + for i := range subkey { + subkey[i] = byte(i + 1) + } + + s, err := shadowsocks.NewTCPStreamCipher(method, subkey) + if err != nil { + t.Fatalf("NewTCPStreamCipher() error = %v", err) + } + if s == nil { + t.Fatal("NewTCPStreamCipher() returned nil cipher") + } + if err := s.Validate(); err != nil { + t.Fatalf("Validate() error = %v", err) + } + }) + + t.Run("invalid method", func(t *testing.T) { + t.Parallel() + + _, err := shadowsocks.NewTCPStreamCipher(shadowsocks.Method{}, make([]byte, 16)) + if err == nil { + t.Fatal("expected error, got nil") + } + }) + + t.Run("invalid subkey length", func(t *testing.T) { + t.Parallel() + + _, err := shadowsocks.NewTCPStreamCipher(method, make([]byte, method.KeySize-1)) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid subkey length") { + t.Fatalf("expected invalid subkey length error, got %q", err.Error()) + } + }) +} + +func TestNewTCPStreamCipherFromPSK(t *testing.T) { + t.Parallel() + + method := newTestMethod(t) + psk, salt := newTestPSKAndSalt(method) + + t.Run("valid", func(t *testing.T) { + t.Parallel() + + s, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk, salt) + if err != nil { + t.Fatalf("NewTCPStreamCipherFromPSK() error = %v", err) + } + if s == nil { + t.Fatal("NewTCPStreamCipherFromPSK() returned nil cipher") + } + if err := s.Validate(); err != nil { + t.Fatalf("Validate() error = %v", err) + } + }) + + t.Run("invalid psk length", func(t *testing.T) { + t.Parallel() + + _, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk[:len(psk)-1], salt) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid PSK length") { + t.Fatalf("expected invalid PSK length error, got %q", err.Error()) + } + }) + + t.Run("invalid salt length", func(t *testing.T) { + t.Parallel() + + _, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk, salt[:len(salt)-1]) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid salt length") { + t.Fatalf("expected invalid salt length error, got %q", err.Error()) + } + }) +} + +func TestTCPStreamCipher_Reset(t *testing.T) { + t.Parallel() + + s, _, _ := newTestCipherPair(t) + + for i := range s.Nonce { + s.Nonce[i] = 0xff + } + + s.Reset() + + var zero [shadowsocks.AeadNonceSize]byte + if s.Nonce != zero { + t.Fatalf("Nonce = %v, want zero", s.Nonce) + } +} + +func TestTCPStreamCipher_Validate(t *testing.T) { + t.Parallel() + + method := newTestMethod(t) + psk, salt := newTestPSKAndSalt(method) + + valid, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk, salt) + if err != nil { + t.Fatalf("NewTCPStreamCipherFromPSK() error = %v", err) + } + + t.Run("valid", func(t *testing.T) { + t.Parallel() + + if err := valid.Validate(); err != nil { + t.Fatalf("Validate() error = %v", err) + } + }) + + t.Run("nil cipher", func(t *testing.T) { + t.Parallel() + + var s *shadowsocks.TCPStreamCipher + err := s.Validate() + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "nil TCP stream cipher") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("missing aead", func(t *testing.T) { + t.Parallel() + + s := &shadowsocks.TCPStreamCipher{ + Method: method, + AEAD: nil, + } + + err := s.Validate() + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "missing AEAD") { + t.Fatalf("unexpected error: %v", err) + } + }) +} + +func TestTCPStreamCipher_SealTo_OpenTo(t *testing.T) { + t.Parallel() + + enc, dec, _ := newTestCipherPair(t) + plaintext := []byte("hello shadowsocks") + + ciphertext, err := enc.SealTo(nil, plaintext) + if err != nil { + t.Fatalf("SealTo() error = %v", err) + } + + got, err := dec.OpenTo(nil, ciphertext) + if err != nil { + t.Fatalf("OpenTo() error = %v", err) + } + + if !bytes.Equal(got, plaintext) { + t.Fatalf("OpenTo() = %q, want %q", got, plaintext) + } +} + +func TestTCPStreamCipher_SealTo_OpenTo_NilCipher(t *testing.T) { + t.Parallel() + + var s *shadowsocks.TCPStreamCipher + + if _, err := s.SealTo(nil, []byte("x")); err == nil { + t.Fatal("expected error from nil SealTo receiver") + } + if _, err := s.OpenTo(nil, []byte("x")); err == nil { + t.Fatal("expected error from nil OpenTo receiver") + } +} + +func TestTCPStreamCipher_OpenTo_WrongCipherFails(t *testing.T) { + t.Parallel() + + enc, _, method := newTestCipherPair(t) + + psk := make([]byte, method.KeySize) + salt := make([]byte, method.SaltSize) + for i := range psk { + psk[i] = byte(i + 9) + } + for i := range salt { + salt[i] = byte(i + 19) + } + + wrongDec, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk, salt) + if err != nil { + t.Fatalf("NewTCPStreamCipherFromPSK() error = %v", err) + } + + ciphertext, err := enc.SealTo(nil, []byte("hello")) + if err != nil { + t.Fatalf("SealTo() error = %v", err) + } + + if _, err := wrongDec.OpenTo(nil, ciphertext); err == nil { + t.Fatal("expected decrypt error, got nil") + } +} + +func TestTCPStreamCipher_EncryptedLengths(t *testing.T) { + t.Parallel() + + s, _, method := newTestCipherPair(t) + + if got, want := s.EncryptedChunkLength(), shadowsocks.TcpChunkLengthLen+method.TagSize; got != want { + t.Fatalf("EncryptedChunkLength() = %d, want %d", got, want) + } + + if got, want := s.EncryptedPayloadLength(123), 123+method.TagSize; got != want { + t.Fatalf("EncryptedPayloadLength() = %d, want %d", got, want) + } +} + +func TestTCPStreamCipher_ChunkLength_RoundTrip(t *testing.T) { + t.Parallel() + + enc, dec, method := newTestCipherPair(t) + + ciphertext, err := enc.EncodeChunkLengthTo(nil, 1234) + if err != nil { + t.Fatalf("EncodeChunkLengthTo() error = %v", err) + } + + if got, want := len(ciphertext), shadowsocks.TcpChunkLengthLen+method.TagSize; got != want { + t.Fatalf("len(ciphertext) = %d, want %d", got, want) + } + + n, err := dec.DecodeChunkLength(ciphertext) + if err != nil { + t.Fatalf("DecodeChunkLength() error = %v", err) + } + if n != 1234 { + t.Fatalf("DecodeChunkLength() = %d, want %d", n, 1234) + } +} + +func TestTCPStreamCipher_ChunkPayload_RoundTrip(t *testing.T) { + t.Parallel() + + enc, dec, method := newTestCipherPair(t) + payload := []byte("payload-data") + + ciphertext, err := enc.EncodeChunkPayloadTo(nil, payload) + if err != nil { + t.Fatalf("EncodeChunkPayloadTo() error = %v", err) + } + + if got, want := len(ciphertext), len(payload)+method.TagSize; got != want { + t.Fatalf("len(ciphertext) = %d, want %d", got, want) + } + + plain, err := dec.DecodeChunkPayloadTo(nil, ciphertext) + if err != nil { + t.Fatalf("DecodeChunkPayloadTo() error = %v", err) + } + if !bytes.Equal(plain, payload) { + t.Fatalf("DecodeChunkPayloadTo() = %q, want %q", plain, payload) + } +} + +func TestTCPStreamCipher_EncodeChunkPayloadTo_TooLarge(t *testing.T) { + t.Parallel() + + enc, _, _ := newTestCipherPair(t) + payload := make([]byte, 0x10000) + + _, err := enc.EncodeChunkPayloadTo(nil, payload) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "payload too large") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestTCPStreamCipher_RequestFixedHeader_RoundTrip(t *testing.T) { + t.Parallel() + + enc, dec, _ := newTestCipherPair(t) + + var want shadowsocks.TCPRequestFixedHeader + want.Init(shadowsocks.TCPHeaderTypeClientStream, 123456789, 321) + + ciphertext, err := enc.EncodeRequestFixedHeaderTo(nil, &want) + if err != nil { + t.Fatalf("EncodeRequestFixedHeaderTo() error = %v", err) + } + + got, err := dec.DecodeRequestFixedHeader(ciphertext) + if err != nil { + t.Fatalf("DecodeRequestFixedHeader() error = %v", err) + } + + if got.Type != want.Type || got.Timestamp != want.Timestamp || got.Length != want.Length { + t.Fatalf("got %+v, want %+v", got, want) + } +} + +func TestTCPStreamCipher_RequestFixedHeader_Nil(t *testing.T) { + t.Parallel() + + enc, _, _ := newTestCipherPair(t) + + _, err := enc.EncodeRequestFixedHeaderTo(nil, nil) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "nil TCP request fixed header") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestTCPStreamCipher_RequestVariableHeader_RoundTrip(t *testing.T) { + t.Parallel() + + enc, dec, _ := newTestCipherPair(t) + + var want shadowsocks.TCPRequestVariableHeader + want.Init( + shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeDomain, + Domain: "example.com", + Port: 443, + }, + []byte{1, 2, 3}, + []byte("hello"), + ) + + ciphertext, err := enc.EncodeRequestVariableHeaderTo(nil, &want) + if err != nil { + t.Fatalf("EncodeRequestVariableHeaderTo() error = %v", err) + } + + got, err := dec.DecodeRequestVariableHeader(ciphertext, nil) + if err != nil { + t.Fatalf("DecodeRequestVariableHeader() error = %v", err) + } + + if got.Target.AddrType != want.Target.AddrType { + t.Fatalf("Target.AddrType = %v, want %v", got.Target.AddrType, want.Target.AddrType) + } + if got.Target.Domain != want.Target.Domain { + t.Fatalf("Target.Domain = %q, want %q", got.Target.Domain, want.Target.Domain) + } + if got.Target.Port != want.Target.Port { + t.Fatalf("Target.Port = %d, want %d", got.Target.Port, want.Target.Port) + } + if got.PaddingLen != want.PaddingLen { + t.Fatalf("PaddingLen = %d, want %d", got.PaddingLen, want.PaddingLen) + } + if !bytes.Equal(got.Padding, want.Padding) { + t.Fatalf("Padding = %v, want %v", got.Padding, want.Padding) + } + if !bytes.Equal(got.InitialData, want.InitialData) { + t.Fatalf("InitialData = %v, want %v", got.InitialData, want.InitialData) + } +} + +func TestTCPStreamCipher_RequestVariableHeader_Nil(t *testing.T) { + t.Parallel() + + enc, _, _ := newTestCipherPair(t) + + _, err := enc.EncodeRequestVariableHeaderTo(nil, nil) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "nil TCP request variable header") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestTCPStreamCipher_ResponseHeader_RoundTrip(t *testing.T) { + t.Parallel() + + enc, dec, _ := newTestCipherPair(t) + + var want shadowsocks.TCPResponseHeader + want.Init(shadowsocks.TCPHeaderTypeServerStream, 123456789, []byte{1, 2, 3, 4}, 4) + + ciphertext, err := enc.EncodeResponseHeaderTo(nil, &want) + if err != nil { + t.Fatalf("EncodeResponseHeaderTo() error = %v", err) + } + + got, err := dec.DecodeResponseHeader(ciphertext, nil) + if err != nil { + t.Fatalf("DecodeResponseHeader() error = %v", err) + } + + if got.Type != want.Type || got.Timestamp != want.Timestamp || got.Length != want.Length { + t.Fatalf("got %+v, want %+v", got, want) + } + if !bytes.Equal(got.RequestSalt, want.RequestSalt) { + t.Fatalf("RequestSalt = %v, want %v", got.RequestSalt, want.RequestSalt) + } +} + +func TestTCPStreamCipher_ResponseHeader_Nil(t *testing.T) { + t.Parallel() + + enc, _, _ := newTestCipherPair(t) + + _, err := enc.EncodeResponseHeaderTo(nil, nil) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "nil TCP response header") { + t.Fatalf("unexpected error: %v", err) + } +} From 3f1375f45bf7f0ea68304bb2253ab39121b8f60a Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 17:50:21 +0300 Subject: [PATCH 08/25] shadow socks tcp streams --- shadowsocks/tcp_client_stream.go | 236 +++++++++++++ shadowsocks/tcp_client_stream_test.go | 453 ++++++++++++++++++++++++ shadowsocks/tcp_server_stream.go | 287 +++++++++++++++ shadowsocks/tcp_server_stream_test.go | 488 ++++++++++++++++++++++++++ shadowsocks/tcp_stream_cipher.go | 56 ++- shadowsocks/tcp_stream_cipher_test.go | 16 +- 6 files changed, 1509 insertions(+), 27 deletions(-) create mode 100644 shadowsocks/tcp_client_stream.go create mode 100644 shadowsocks/tcp_client_stream_test.go create mode 100644 shadowsocks/tcp_server_stream.go create mode 100644 shadowsocks/tcp_server_stream_test.go diff --git a/shadowsocks/tcp_client_stream.go b/shadowsocks/tcp_client_stream.go new file mode 100644 index 0000000..7a6d398 --- /dev/null +++ b/shadowsocks/tcp_client_stream.go @@ -0,0 +1,236 @@ +package shadowsocks + +import ( + "bytes" + "fmt" + "io" + "time" +) + +const tcpClientRequestStartStackBufSize = 1024 + +// TCPClientRequestStart represents the client-side Shadowsocks 2022 TCP startup state. +type TCPClientRequestStart struct { + Method Method + PSK []byte + RequestSalt []byte + RequestCipher *TCPStreamCipher +} + +// Init initializes the client request-start state from method, PSK, and request salt. +func (s *TCPClientRequestStart) Init(method Method, psk, requestSalt []byte) error { + if err := method.Validate(); err != nil { + return err + } + if len(psk) != method.KeySize { + return fmt.Errorf("invalid PSK length: got %d, want %d", len(psk), method.KeySize) + } + if len(requestSalt) != method.SaltSize { + return fmt.Errorf("invalid request salt length: got %d, want %d", len(requestSalt), method.SaltSize) + } + + requestCipher, err := NewTCPStreamCipherFromPSK(method, psk, requestSalt) + if err != nil { + return err + } + + s.Method = method + s.PSK = psk + s.RequestSalt = requestSalt + s.RequestCipher = requestCipher + + return nil +} + +// Validate checks whether the client request-start state is internally valid. +func (s *TCPClientRequestStart) Validate() error { + if s == nil { + return fmt.Errorf("nil TCP client request start") + } + if err := s.Method.Validate(); err != nil { + return err + } + if len(s.PSK) != s.Method.KeySize { + return fmt.Errorf("invalid PSK length: got %d, want %d", len(s.PSK), s.Method.KeySize) + } + if len(s.RequestSalt) != s.Method.SaltSize { + return fmt.Errorf("invalid request salt length: got %d, want %d", len(s.RequestSalt), s.Method.SaltSize) + } + if s.RequestCipher == nil { + return fmt.Errorf("missing request cipher") + } + if err := s.RequestCipher.Validate(); err != nil { + return err + } + + return nil +} + +// EncodedRequestStartLen returns the total encoded request-start length for the +// given plaintext variable header length. +func (s *TCPClientRequestStart) EncodedRequestStartLen(variableHeaderLen int) (int, error) { + if err := s.Validate(); err != nil { + return 0, err + } + if variableHeaderLen < 0 { + return 0, fmt.Errorf("invalid variable header length: %d", variableHeaderLen) + } + + return len(s.RequestSalt) + + (TcpRequestFixedHeaderLen + s.Method.TagSize) + + (variableHeaderLen + s.Method.TagSize), nil +} + +// WriteRequestStart writes a full client request start: +// +// request salt || encrypted request fixed header || encrypted request variable header +// +// The request fixed header Length field is set to the plaintext encoded variable +// header length. scratch may be nil. +func (s *TCPClientRequestStart) WriteRequestStart( + dst io.Writer, + timestamp time.Time, + target Addr, + padding []byte, + initialData []byte, + scratch []byte, +) (int64, error) { + if err := s.Validate(); err != nil { + return 0, err + } + + var variableHeader TCPRequestVariableHeader + variableHeader.Init(target, padding, initialData) + if err := variableHeader.Validate(); err != nil { + return 0, err + } + + var fixedHeader TCPRequestFixedHeader + fixedHeader.Init( + TCPHeaderTypeClientStream, + uint64(timestamp.Unix()), + uint16(variableHeader.EncodedLen()), + ) + + var stackBuf [tcpClientRequestStartStackBufSize]byte + out := stackBuf[:0] + + out = append(out, s.RequestSalt...) + + encFixed, err := s.RequestCipher.EncodeRequestFixedHeaderTo(scratch[:0], &fixedHeader, nil) + if err != nil { + return 0, err + } + out = append(out, encFixed...) + + encVariable, err := s.RequestCipher.EncodeRequestVariableHeaderTo(scratch[:0], &variableHeader, nil) + if err != nil { + return 0, err + } + out = append(out, encVariable...) + + n, err := dst.Write(out) + return int64(n), err +} + +// TCPClientResponseStart represents the parsed server response startup. +type TCPClientResponseStart struct { + ResponseSalt []byte + ResponseCipher *TCPStreamCipher + Header TCPResponseHeader +} + +// Validate checks whether the parsed client response-start state is internally valid. +func (s *TCPClientResponseStart) Validate(method Method, requestSalt []byte) error { + if s == nil { + return fmt.Errorf("nil TCP client response start") + } + if err := method.Validate(); err != nil { + return err + } + if len(s.ResponseSalt) != method.SaltSize { + return fmt.Errorf("invalid response salt length: got %d, want %d", len(s.ResponseSalt), method.SaltSize) + } + if s.ResponseCipher == nil { + return fmt.Errorf("missing response cipher") + } + if err := s.ResponseCipher.Validate(); err != nil { + return err + } + if err := s.Header.Validate(); err != nil { + return err + } + if len(s.Header.RequestSalt) != len(requestSalt) { + return fmt.Errorf("invalid echoed request salt length: got %d, want %d", len(s.Header.RequestSalt), len(requestSalt)) + } + if !bytes.Equal(s.Header.RequestSalt, requestSalt) { + return fmt.Errorf("response request salt mismatch") + } + + return nil +} + +// ReadResponseStart reads and decrypts the server response startup: +// +// response salt || encrypted response header +// +// scratch may be nil. +func (s *TCPClientRequestStart) ReadResponseStart( + src io.Reader, + scratch []byte, +) (*TCPClientResponseStart, int64, error) { + if err := s.Validate(); err != nil { + return nil, 0, err + } + + var total int64 + + responseSaltLen := s.Method.SaltSize + var responseSaltBuf []byte + if cap(scratch) >= responseSaltLen { + responseSaltBuf = scratch[:responseSaltLen] + } else { + responseSaltBuf = make([]byte, responseSaltLen) + } + + n, err := io.ReadFull(src, responseSaltBuf) + total += int64(n) + if err != nil { + return nil, total, err + } + + responseCipher, err := NewTCPStreamCipherFromPSK(s.Method, s.PSK, responseSaltBuf) + if err != nil { + return nil, total, err + } + + encHeaderLen := TcpResponseFixedBaseLen + s.Method.SaltSize + s.Method.TagSize + var encHeader []byte + if cap(encHeader) >= encHeaderLen { + encHeader = encHeader[:encHeaderLen] + } else { + encHeader = make([]byte, encHeaderLen) + } + + n, err = io.ReadFull(src, encHeader) + total += int64(n) + if err != nil { + return nil, total, err + } + + header, err := responseCipher.DecodeResponseHeader(encHeader, scratch[:0]) + if err != nil { + return nil, total, err + } + + resp := &TCPClientResponseStart{ + ResponseSalt: append([]byte(nil), responseSaltBuf...), + ResponseCipher: responseCipher, + Header: header, + } + if err := resp.Validate(s.Method, s.RequestSalt); err != nil { + return nil, total, err + } + + return resp, total, nil +} diff --git a/shadowsocks/tcp_client_stream_test.go b/shadowsocks/tcp_client_stream_test.go new file mode 100644 index 0000000..98c4e07 --- /dev/null +++ b/shadowsocks/tcp_client_stream_test.go @@ -0,0 +1,453 @@ +package shadowsocks_test + +import ( + "bytes" + "io" + "strings" + "testing" + "time" + + "github.com/33TU/socks/shadowsocks" +) + +func newTCPClientStartTestMethod(t *testing.T) shadowsocks.Method { + t.Helper() + + method, err := shadowsocks.ParseMethod(shadowsocks.Method2022Blake3AES128GCM) + if err != nil { + t.Fatalf("ParseMethod() error = %v", err) + } + + return method +} + +func newTCPClientStartTestPSKAndSalt(method shadowsocks.Method) ([]byte, []byte) { + psk := make([]byte, method.KeySize) + salt := make([]byte, method.SaltSize) + + for i := range psk { + psk[i] = byte(i + 1) + } + for i := range salt { + salt[i] = byte(i + 101) + } + + return psk, salt +} + +func TestTCPClientRequestStart_Init_Validate(t *testing.T) { + t.Parallel() + + method := newTCPClientStartTestMethod(t) + psk, requestSalt := newTCPClientStartTestPSKAndSalt(method) + + t.Run("valid", func(t *testing.T) { + t.Parallel() + + var s shadowsocks.TCPClientRequestStart + if err := s.Init(method, psk, requestSalt); err != nil { + t.Fatalf("Init() error = %v", err) + } + if err := s.Validate(); err != nil { + t.Fatalf("Validate() error = %v", err) + } + }) + + t.Run("invalid psk length", func(t *testing.T) { + t.Parallel() + + var s shadowsocks.TCPClientRequestStart + err := s.Init(method, psk[:len(psk)-1], requestSalt) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid PSK length") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("invalid request salt length", func(t *testing.T) { + t.Parallel() + + var s shadowsocks.TCPClientRequestStart + err := s.Init(method, psk, requestSalt[:len(requestSalt)-1]) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid request salt length") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("nil receiver validate", func(t *testing.T) { + t.Parallel() + + var s *shadowsocks.TCPClientRequestStart + err := s.Validate() + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "nil TCP client request start") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("missing request cipher", func(t *testing.T) { + t.Parallel() + + s := &shadowsocks.TCPClientRequestStart{ + Method: method, + PSK: psk, + RequestSalt: requestSalt, + } + + err := s.Validate() + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "missing request cipher") { + t.Fatalf("unexpected error: %v", err) + } + }) +} + +func TestTCPClientRequestStart_EncodedRequestStartLen(t *testing.T) { + t.Parallel() + + method := newTCPClientStartTestMethod(t) + psk, requestSalt := newTCPClientStartTestPSKAndSalt(method) + + var s shadowsocks.TCPClientRequestStart + if err := s.Init(method, psk, requestSalt); err != nil { + t.Fatalf("Init() error = %v", err) + } + + got, err := s.EncodedRequestStartLen(123) + if err != nil { + t.Fatalf("EncodedRequestStartLen() error = %v", err) + } + + want := len(requestSalt) + + (shadowsocks.TcpRequestFixedHeaderLen + method.TagSize) + + (123 + method.TagSize) + + if got != want { + t.Fatalf("EncodedRequestStartLen() = %d, want %d", got, want) + } + + _, err = s.EncodedRequestStartLen(-1) + if err == nil { + t.Fatal("expected error for negative variable header length") + } +} + +func TestTCPClientRequestStart_WriteRequestStart(t *testing.T) { + t.Parallel() + + method := newTCPClientStartTestMethod(t) + psk, requestSalt := newTCPClientStartTestPSKAndSalt(method) + + var s shadowsocks.TCPClientRequestStart + if err := s.Init(method, psk, requestSalt); err != nil { + t.Fatalf("Init() error = %v", err) + } + + ts := time.Unix(1700000000, 0) + target := shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeDomain, + Domain: "example.com", + Port: 443, + } + padding := []byte{1, 2, 3} + initialData := []byte("hello") + + var buf bytes.Buffer + n, err := s.WriteRequestStart(&buf, ts, target, padding, initialData, nil) + if err != nil { + t.Fatalf("WriteRequestStart() error = %v", err) + } + + var variableHeader shadowsocks.TCPRequestVariableHeader + variableHeader.Init(target, padding, initialData) + + wantLen, err := s.EncodedRequestStartLen(variableHeader.EncodedLen()) + if err != nil { + t.Fatalf("EncodedRequestStartLen() error = %v", err) + } + + if int(n) != wantLen { + t.Fatalf("WriteRequestStart() wrote %d bytes, want %d", n, wantLen) + } + if buf.Len() != wantLen { + t.Fatalf("buffer len = %d, want %d", buf.Len(), wantLen) + } + + out := buf.Bytes() + if !bytes.Equal(out[:len(requestSalt)], requestSalt) { + t.Fatalf("request salt mismatch: got %v, want %v", out[:len(requestSalt)], requestSalt) + } +} + +func TestTCPClientRequestStart_WriteRequestStart_InvalidTarget(t *testing.T) { + t.Parallel() + + method := newTCPClientStartTestMethod(t) + psk, requestSalt := newTCPClientStartTestPSKAndSalt(method) + + var s shadowsocks.TCPClientRequestStart + if err := s.Init(method, psk, requestSalt); err != nil { + t.Fatalf("Init() error = %v", err) + } + + var buf bytes.Buffer + _, err := s.WriteRequestStart( + &buf, + time.Unix(1700000000, 0), + shadowsocks.Addr{}, + nil, + []byte("x"), + nil, + ) + if err == nil { + t.Fatal("expected error, got nil") + } +} + +func TestTCPClientResponseStart_Validate(t *testing.T) { + t.Parallel() + + method := newTCPClientStartTestMethod(t) + psk, requestSalt := newTCPClientStartTestPSKAndSalt(method) + responseSalt := make([]byte, method.SaltSize) + for i := range responseSalt { + responseSalt[i] = byte(i + 51) + } + + responseCipher, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk, responseSalt) + if err != nil { + t.Fatalf("NewTCPStreamCipherFromPSK() error = %v", err) + } + + var hdr shadowsocks.TCPResponseHeader + hdr.Init(shadowsocks.TCPHeaderTypeServerStream, 1700000000, requestSalt, uint16(len(requestSalt))) + + t.Run("valid", func(t *testing.T) { + t.Parallel() + + s := &shadowsocks.TCPClientResponseStart{ + ResponseSalt: append([]byte(nil), responseSalt...), + ResponseCipher: responseCipher, + Header: hdr, + } + + if err := s.Validate(method, requestSalt); err != nil { + t.Fatalf("Validate() error = %v", err) + } + }) + + t.Run("nil receiver", func(t *testing.T) { + t.Parallel() + + var s *shadowsocks.TCPClientResponseStart + err := s.Validate(method, requestSalt) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "nil TCP client response start") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("bad response salt len", func(t *testing.T) { + t.Parallel() + + s := &shadowsocks.TCPClientResponseStart{ + ResponseSalt: responseSalt[:len(responseSalt)-1], + ResponseCipher: responseCipher, + Header: hdr, + } + + err := s.Validate(method, requestSalt) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid response salt length") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("missing response cipher", func(t *testing.T) { + t.Parallel() + + s := &shadowsocks.TCPClientResponseStart{ + ResponseSalt: responseSalt, + Header: hdr, + } + + err := s.Validate(method, requestSalt) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "missing response cipher") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("request salt mismatch", func(t *testing.T) { + t.Parallel() + + badRequestSalt := append([]byte(nil), requestSalt...) + badRequestSalt[0] ^= 0xff + + s := &shadowsocks.TCPClientResponseStart{ + ResponseSalt: responseSalt, + ResponseCipher: responseCipher, + Header: hdr, + } + + err := s.Validate(method, badRequestSalt) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "response request salt mismatch") { + t.Fatalf("unexpected error: %v", err) + } + }) +} + +func TestTCPClientRequestStart_ReadResponseStart(t *testing.T) { + t.Parallel() + + method := newTCPClientStartTestMethod(t) + psk, requestSalt := newTCPClientStartTestPSKAndSalt(method) + + var clientStart shadowsocks.TCPClientRequestStart + if err := clientStart.Init(method, psk, requestSalt); err != nil { + t.Fatalf("Init() error = %v", err) + } + + responseSalt := make([]byte, method.SaltSize) + for i := range responseSalt { + responseSalt[i] = byte(i + 201) + } + + responseCipher, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk, responseSalt) + if err != nil { + t.Fatalf("NewTCPStreamCipherFromPSK() error = %v", err) + } + + var hdr shadowsocks.TCPResponseHeader + hdr.Init( + shadowsocks.TCPHeaderTypeServerStream, + uint64(time.Unix(1700000100, 0).Unix()), + requestSalt, + uint16(len(requestSalt)), + ) + + encHeader, err := responseCipher.EncodeResponseHeaderTo(nil, &hdr, nil) + if err != nil { + t.Fatalf("EncodeResponseHeaderTo() error = %v", err) + } + + var wire bytes.Buffer + wire.Write(responseSalt) + wire.Write(encHeader) + + resp, n, err := clientStart.ReadResponseStart(&wire, nil) + if err != nil { + t.Fatalf("ReadResponseStart() error = %v", err) + } + + wantN := int64(len(responseSalt) + len(encHeader)) + if n != wantN { + t.Fatalf("ReadResponseStart() read %d bytes, want %d", n, wantN) + } + + if !bytes.Equal(resp.ResponseSalt, responseSalt) { + t.Fatalf("ResponseSalt = %v, want %v", resp.ResponseSalt, responseSalt) + } + if resp.ResponseCipher == nil { + t.Fatal("ResponseCipher is nil") + } + if resp.Header.Type != hdr.Type || resp.Header.Timestamp != hdr.Timestamp || resp.Header.Length != hdr.Length { + t.Fatalf("Header = %+v, want %+v", resp.Header, hdr) + } + if !bytes.Equal(resp.Header.RequestSalt, requestSalt) { + t.Fatalf("Header.RequestSalt = %v, want %v", resp.Header.RequestSalt, requestSalt) + } +} + +func TestTCPClientRequestStart_ReadResponseStart_ShortRead(t *testing.T) { + t.Parallel() + + method := newTCPClientStartTestMethod(t) + psk, requestSalt := newTCPClientStartTestPSKAndSalt(method) + + var clientStart shadowsocks.TCPClientRequestStart + if err := clientStart.Init(method, psk, requestSalt); err != nil { + t.Fatalf("Init() error = %v", err) + } + + short := bytes.NewReader(make([]byte, method.SaltSize-1)) + + _, n, err := clientStart.ReadResponseStart(short, nil) + if err == nil { + t.Fatal("expected error, got nil") + } + if n != int64(method.SaltSize-1) { + t.Fatalf("bytes read = %d, want %d", n, method.SaltSize-1) + } + if err != io.EOF && err != io.ErrUnexpectedEOF { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestTCPClientRequestStart_ReadResponseStart_RequestSaltMismatch(t *testing.T) { + t.Parallel() + + method := newTCPClientStartTestMethod(t) + psk, requestSalt := newTCPClientStartTestPSKAndSalt(method) + + var clientStart shadowsocks.TCPClientRequestStart + if err := clientStart.Init(method, psk, requestSalt); err != nil { + t.Fatalf("Init() error = %v", err) + } + + responseSalt := make([]byte, method.SaltSize) + for i := range responseSalt { + responseSalt[i] = byte(i + 201) + } + + responseCipher, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk, responseSalt) + if err != nil { + t.Fatalf("NewTCPStreamCipherFromPSK() error = %v", err) + } + + badRequestSalt := append([]byte(nil), requestSalt...) + badRequestSalt[0] ^= 0xff + + var hdr shadowsocks.TCPResponseHeader + hdr.Init( + shadowsocks.TCPHeaderTypeServerStream, + uint64(time.Unix(1700000100, 0).Unix()), + badRequestSalt, + uint16(len(badRequestSalt)), + ) + + encHeader, err := responseCipher.EncodeResponseHeaderTo(nil, &hdr, nil) + if err != nil { + t.Fatalf("EncodeResponseHeaderTo() error = %v", err) + } + + var wire bytes.Buffer + wire.Write(responseSalt) + wire.Write(encHeader) + + _, _, err = clientStart.ReadResponseStart(&wire, nil) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "response request salt mismatch") { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/shadowsocks/tcp_server_stream.go b/shadowsocks/tcp_server_stream.go new file mode 100644 index 0000000..906752e --- /dev/null +++ b/shadowsocks/tcp_server_stream.go @@ -0,0 +1,287 @@ +package shadowsocks + +import ( + "bytes" + "fmt" + "io" + "time" +) + +const tcpServerResponseStartStackBufSize = 256 + +// TCPServerRequestStart represents the parsed client request startup on the server side. +type TCPServerRequestStart struct { + Method Method + PSK []byte + RequestSalt []byte + RequestCipher *TCPStreamCipher + FixedHeader TCPRequestFixedHeader + Header TCPRequestVariableHeader +} + +// Init initializes the server request-start state from method, PSK, and request salt. +func (s *TCPServerRequestStart) Init(method Method, psk, requestSalt []byte) error { + if err := method.Validate(); err != nil { + return err + } + if len(psk) != method.KeySize { + return fmt.Errorf("invalid PSK length: got %d, want %d", len(psk), method.KeySize) + } + if len(requestSalt) != method.SaltSize { + return fmt.Errorf("invalid request salt length: got %d, want %d", len(requestSalt), method.SaltSize) + } + + requestCipher, err := NewTCPStreamCipherFromPSK(method, psk, requestSalt) + if err != nil { + return err + } + + s.Method = method + s.PSK = psk + s.RequestSalt = requestSalt + s.RequestCipher = requestCipher + + return nil +} + +// Validate checks whether the parsed server request-start state is internally valid. +func (s *TCPServerRequestStart) Validate() error { + if s == nil { + return fmt.Errorf("nil TCP server request start") + } + if err := s.Method.Validate(); err != nil { + return err + } + if len(s.PSK) != s.Method.KeySize { + return fmt.Errorf("invalid PSK length: got %d, want %d", len(s.PSK), s.Method.KeySize) + } + if len(s.RequestSalt) != s.Method.SaltSize { + return fmt.Errorf("invalid request salt length: got %d, want %d", len(s.RequestSalt), s.Method.SaltSize) + } + if s.RequestCipher == nil { + return fmt.Errorf("missing request cipher") + } + if err := s.RequestCipher.Validate(); err != nil { + return err + } + if err := s.FixedHeader.Validate(); err != nil { + return err + } + if err := s.Header.Validate(); err != nil { + return err + } + if int(s.FixedHeader.Length) != s.Header.EncodedLen() { + return fmt.Errorf("request variable header length mismatch: got %d, want %d", s.FixedHeader.Length, s.Header.EncodedLen()) + } + + return nil +} + +// ReadRequestStart reads and decrypts the full client request startup: +// +// request salt || encrypted request fixed header || encrypted request variable header +// +// scratch may be nil. +func (s *TCPServerRequestStart) ReadRequestStart( + src io.Reader, + method Method, + psk []byte, + scratch []byte, +) (int64, error) { + var total int64 + + if err := method.Validate(); err != nil { + return 0, err + } + if len(psk) != method.KeySize { + return 0, fmt.Errorf("invalid PSK length: got %d, want %d", len(psk), method.KeySize) + } + + requestSaltLen := method.SaltSize + var requestSaltBuf []byte + if cap(scratch) >= requestSaltLen { + requestSaltBuf = scratch[:requestSaltLen] + } else { + requestSaltBuf = make([]byte, requestSaltLen) + } + + n, err := io.ReadFull(src, requestSaltBuf) + total += int64(n) + if err != nil { + return total, err + } + + if err := s.Init(method, psk, requestSaltBuf); err != nil { + return total, err + } + + encFixedLen := TcpRequestFixedHeaderLen + method.TagSize + var encFixed []byte + if cap(scratch) >= encFixedLen { + encFixed = scratch[:encFixedLen] + } else { + encFixed = make([]byte, encFixedLen) + } + + n, err = io.ReadFull(src, encFixed) + total += int64(n) + if err != nil { + return total, err + } + + fixedHeader, err := s.RequestCipher.DecodeRequestFixedHeader(encFixed, scratch[:0]) + if err != nil { + return total, err + } + s.FixedHeader = fixedHeader + + encVariableLen := int(s.FixedHeader.Length) + method.TagSize + var encVariable []byte + if cap(scratch) >= encVariableLen { + encVariable = scratch[:encVariableLen] + } else { + encVariable = make([]byte, encVariableLen) + } + + n, err = io.ReadFull(src, encVariable) + total += int64(n) + if err != nil { + return total, err + } + + variableHeader, err := s.RequestCipher.DecodeRequestVariableHeader(encVariable, scratch[:0]) + if err != nil { + return total, err + } + s.Header = variableHeader + + if err := s.Validate(); err != nil { + return total, err + } + + // Keep our own copy of the request salt in case scratch was reused. + s.RequestSalt = append([]byte(nil), requestSaltBuf...) + + return total, nil +} + +// TCPServerResponseStart represents the server-side response startup state. +type TCPServerResponseStart struct { + Method Method + PSK []byte + ResponseSalt []byte + ResponseCipher *TCPStreamCipher + Header TCPResponseHeader +} + +// Init initializes the server response-start state from method, PSK, and response salt. +func (s *TCPServerResponseStart) Init(method Method, psk, responseSalt []byte) error { + if err := method.Validate(); err != nil { + return err + } + if len(psk) != method.KeySize { + return fmt.Errorf("invalid PSK length: got %d, want %d", len(psk), method.KeySize) + } + if len(responseSalt) != method.SaltSize { + return fmt.Errorf("invalid response salt length: got %d, want %d", len(responseSalt), method.SaltSize) + } + + responseCipher, err := NewTCPStreamCipherFromPSK(method, psk, responseSalt) + if err != nil { + return err + } + + s.Method = method + s.PSK = psk + s.ResponseSalt = responseSalt + s.ResponseCipher = responseCipher + + return nil +} + +// Validate checks whether the server response-start state is internally valid. +func (s *TCPServerResponseStart) Validate(requestSalt []byte) error { + if s == nil { + return fmt.Errorf("nil TCP server response start") + } + if err := s.Method.Validate(); err != nil { + return err + } + if len(s.PSK) != s.Method.KeySize { + return fmt.Errorf("invalid PSK length: got %d, want %d", len(s.PSK), s.Method.KeySize) + } + if len(s.ResponseSalt) != s.Method.SaltSize { + return fmt.Errorf("invalid response salt length: got %d, want %d", len(s.ResponseSalt), s.Method.SaltSize) + } + if s.ResponseCipher == nil { + return fmt.Errorf("missing response cipher") + } + if err := s.ResponseCipher.Validate(); err != nil { + return err + } + if err := s.Header.Validate(); err != nil { + return err + } + if !bytes.Equal(s.Header.RequestSalt, requestSalt) { + return fmt.Errorf("response request salt mismatch") + } + + return nil +} + +// WriteResponseStart writes the full server response startup: +// +// response salt || encrypted response header +// +// scratch may be nil. +func (s *TCPServerResponseStart) WriteResponseStart( + dst io.Writer, + timestamp time.Time, + requestSalt []byte, + scratch []byte, +) (int64, error) { + if s == nil { + return 0, fmt.Errorf("nil TCP server response start") + } + if err := s.Method.Validate(); err != nil { + return 0, err + } + if len(requestSalt) != s.Method.SaltSize { + return 0, fmt.Errorf("invalid request salt length: got %d, want %d", len(requestSalt), s.Method.SaltSize) + } + if len(s.ResponseSalt) != s.Method.SaltSize { + return 0, fmt.Errorf("invalid response salt length: got %d, want %d", len(s.ResponseSalt), s.Method.SaltSize) + } + if s.ResponseCipher == nil { + return 0, fmt.Errorf("missing response cipher") + } + if err := s.ResponseCipher.Validate(); err != nil { + return 0, err + } + + var header TCPResponseHeader + header.Init( + TCPHeaderTypeServerStream, + uint64(timestamp.Unix()), + requestSalt, + uint16(len(requestSalt)), + ) + if err := header.Validate(); err != nil { + return 0, err + } + s.Header = header + + var stackBuf [tcpServerResponseStartStackBufSize]byte + out := stackBuf[:0] + + out = append(out, s.ResponseSalt...) + + encHeader, err := s.ResponseCipher.EncodeResponseHeaderTo(scratch[:0], &s.Header, nil) + if err != nil { + return 0, err + } + out = append(out, encHeader...) + + n, err := dst.Write(out) + return int64(n), err +} diff --git a/shadowsocks/tcp_server_stream_test.go b/shadowsocks/tcp_server_stream_test.go new file mode 100644 index 0000000..66fe98f --- /dev/null +++ b/shadowsocks/tcp_server_stream_test.go @@ -0,0 +1,488 @@ +package shadowsocks_test + +import ( + "bytes" + "io" + "strings" + "testing" + "time" + + "github.com/33TU/socks/shadowsocks" +) + +func newTCPServerStartTestMethod(t *testing.T) shadowsocks.Method { + t.Helper() + + method, err := shadowsocks.ParseMethod(shadowsocks.Method2022Blake3AES128GCM) + if err != nil { + t.Fatalf("ParseMethod() error = %v", err) + } + + return method +} + +func newTCPServerStartTestPSKAndSalt(method shadowsocks.Method) ([]byte, []byte) { + psk := make([]byte, method.KeySize) + salt := make([]byte, method.SaltSize) + + for i := range psk { + psk[i] = byte(i + 1) + } + for i := range salt { + salt[i] = byte(i + 101) + } + + return psk, salt +} + +func TestTCPServerRequestStart_Init_Validate(t *testing.T) { + t.Parallel() + + method := newTCPServerStartTestMethod(t) + psk, requestSalt := newTCPServerStartTestPSKAndSalt(method) + + target := shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeDomain, + Domain: "example.com", + Port: 443, + } + + var fixed shadowsocks.TCPRequestFixedHeader + var variable shadowsocks.TCPRequestVariableHeader + variable.Init(target, []byte{1, 2, 3}, []byte("hello")) + fixed.Init( + shadowsocks.TCPHeaderTypeClientStream, + 1700000000, + uint16(variable.EncodedLen()), + ) + + t.Run("valid", func(t *testing.T) { + t.Parallel() + + var s shadowsocks.TCPServerRequestStart + if err := s.Init(method, psk, requestSalt); err != nil { + t.Fatalf("Init() error = %v", err) + } + + s.FixedHeader = fixed + s.Header = variable + + if err := s.Validate(); err != nil { + t.Fatalf("Validate() error = %v", err) + } + }) + + t.Run("invalid psk length", func(t *testing.T) { + t.Parallel() + + var s shadowsocks.TCPServerRequestStart + err := s.Init(method, psk[:len(psk)-1], requestSalt) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid PSK length") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("invalid request salt length", func(t *testing.T) { + t.Parallel() + + var s shadowsocks.TCPServerRequestStart + err := s.Init(method, psk, requestSalt[:len(requestSalt)-1]) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid request salt length") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("nil receiver validate", func(t *testing.T) { + t.Parallel() + + var s *shadowsocks.TCPServerRequestStart + err := s.Validate() + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "nil TCP server request start") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("missing request cipher", func(t *testing.T) { + t.Parallel() + + s := &shadowsocks.TCPServerRequestStart{ + Method: method, + PSK: psk, + RequestSalt: requestSalt, + FixedHeader: fixed, + Header: variable, + } + + err := s.Validate() + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "missing request cipher") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("variable header length mismatch", func(t *testing.T) { + t.Parallel() + + var s shadowsocks.TCPServerRequestStart + if err := s.Init(method, psk, requestSalt); err != nil { + t.Fatalf("Init() error = %v", err) + } + + s.FixedHeader.Init( + shadowsocks.TCPHeaderTypeClientStream, + 1700000000, + uint16(variable.EncodedLen()+1), + ) + s.Header = variable + + err := s.Validate() + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "request variable header length mismatch") { + t.Fatalf("unexpected error: %v", err) + } + }) +} + +func TestTCPServerRequestStart_ReadRequestStart(t *testing.T) { + t.Parallel() + + method := newTCPServerStartTestMethod(t) + psk, requestSalt := newTCPServerStartTestPSKAndSalt(method) + + clientRequestCipher, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk, requestSalt) + if err != nil { + t.Fatalf("NewTCPStreamCipherFromPSK() error = %v", err) + } + + target := shadowsocks.Addr{ + AddrType: shadowsocks.AddrTypeDomain, + Domain: "example.com", + Port: 443, + } + + var variableHeader shadowsocks.TCPRequestVariableHeader + variableHeader.Init(target, []byte{1, 2, 3}, []byte("hello")) + + var fixedHeader shadowsocks.TCPRequestFixedHeader + fixedHeader.Init( + shadowsocks.TCPHeaderTypeClientStream, + uint64(time.Unix(1700000000, 0).Unix()), + uint16(variableHeader.EncodedLen()), + ) + + encFixed, err := clientRequestCipher.EncodeRequestFixedHeaderTo(nil, &fixedHeader, nil) + if err != nil { + t.Fatalf("EncodeRequestFixedHeaderTo() error = %v", err) + } + + encVariable, err := clientRequestCipher.EncodeRequestVariableHeaderTo(nil, &variableHeader, nil) + if err != nil { + t.Fatalf("EncodeRequestVariableHeaderTo() error = %v", err) + } + + var wire bytes.Buffer + wire.Write(requestSalt) + wire.Write(encFixed) + wire.Write(encVariable) + + var s shadowsocks.TCPServerRequestStart + n, err := s.ReadRequestStart(&wire, method, psk, nil) + if err != nil { + t.Fatalf("ReadRequestStart() error = %v", err) + } + + wantN := int64(len(requestSalt) + len(encFixed) + len(encVariable)) + if n != wantN { + t.Fatalf("ReadRequestStart() read %d bytes, want %d", n, wantN) + } + + if !bytes.Equal(s.RequestSalt, requestSalt) { + t.Fatalf("RequestSalt = %v, want %v", s.RequestSalt, requestSalt) + } + if s.RequestCipher == nil { + t.Fatal("RequestCipher is nil") + } + + if s.FixedHeader.Type != fixedHeader.Type || + s.FixedHeader.Timestamp != fixedHeader.Timestamp || + s.FixedHeader.Length != fixedHeader.Length { + t.Fatalf("FixedHeader = %+v, want %+v", s.FixedHeader, fixedHeader) + } + + if s.Header.Target.AddrType != variableHeader.Target.AddrType { + t.Fatalf("Header.Target.AddrType = %v, want %v", s.Header.Target.AddrType, variableHeader.Target.AddrType) + } + if s.Header.Target.Domain != variableHeader.Target.Domain { + t.Fatalf("Header.Target.Domain = %q, want %q", s.Header.Target.Domain, variableHeader.Target.Domain) + } + if s.Header.Target.Port != variableHeader.Target.Port { + t.Fatalf("Header.Target.Port = %d, want %d", s.Header.Target.Port, variableHeader.Target.Port) + } + if s.Header.PaddingLen != variableHeader.PaddingLen { + t.Fatalf("Header.PaddingLen = %d, want %d", s.Header.PaddingLen, variableHeader.PaddingLen) + } + if !bytes.Equal(s.Header.Padding, variableHeader.Padding) { + t.Fatalf("Header.Padding = %v, want %v", s.Header.Padding, variableHeader.Padding) + } + if !bytes.Equal(s.Header.InitialData, variableHeader.InitialData) { + t.Fatalf("Header.InitialData = %v, want %v", s.Header.InitialData, variableHeader.InitialData) + } +} + +func TestTCPServerRequestStart_ReadRequestStart_ShortRead(t *testing.T) { + t.Parallel() + + method := newTCPServerStartTestMethod(t) + psk, _ := newTCPServerStartTestPSKAndSalt(method) + + short := bytes.NewReader(make([]byte, method.SaltSize-1)) + + var s shadowsocks.TCPServerRequestStart + n, err := s.ReadRequestStart(short, method, psk, nil) + if err == nil { + t.Fatal("expected error, got nil") + } + if n != int64(method.SaltSize-1) { + t.Fatalf("bytes read = %d, want %d", n, method.SaltSize-1) + } + if err != io.EOF && err != io.ErrUnexpectedEOF { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestTCPServerRequestStart_ReadRequestStart_InvalidPSK(t *testing.T) { + t.Parallel() + + method := newTCPServerStartTestMethod(t) + psk, requestSalt := newTCPServerStartTestPSKAndSalt(method) + + var wire bytes.Buffer + wire.Write(requestSalt) + + var s shadowsocks.TCPServerRequestStart + _, err := s.ReadRequestStart(&wire, method, psk[:len(psk)-1], nil) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid PSK length") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestTCPServerResponseStart_Init_Validate(t *testing.T) { + t.Parallel() + + method := newTCPServerStartTestMethod(t) + psk, responseSalt := newTCPServerStartTestPSKAndSalt(method) + requestSalt := make([]byte, method.SaltSize) + for i := range requestSalt { + requestSalt[i] = byte(i + 51) + } + + responseCipher, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk, responseSalt) + if err != nil { + t.Fatalf("NewTCPStreamCipherFromPSK() error = %v", err) + } + + var hdr shadowsocks.TCPResponseHeader + hdr.Init( + shadowsocks.TCPHeaderTypeServerStream, + 1700000000, + requestSalt, + uint16(len(requestSalt)), + ) + + t.Run("valid", func(t *testing.T) { + t.Parallel() + + var s shadowsocks.TCPServerResponseStart + if err := s.Init(method, psk, responseSalt); err != nil { + t.Fatalf("Init() error = %v", err) + } + s.Header = hdr + + if err := s.Validate(requestSalt); err != nil { + t.Fatalf("Validate() error = %v", err) + } + }) + + t.Run("invalid psk length", func(t *testing.T) { + t.Parallel() + + var s shadowsocks.TCPServerResponseStart + err := s.Init(method, psk[:len(psk)-1], responseSalt) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid PSK length") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("invalid response salt length", func(t *testing.T) { + t.Parallel() + + var s shadowsocks.TCPServerResponseStart + err := s.Init(method, psk, responseSalt[:len(responseSalt)-1]) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid response salt length") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("nil receiver validate", func(t *testing.T) { + t.Parallel() + + var s *shadowsocks.TCPServerResponseStart + err := s.Validate(requestSalt) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "nil TCP server response start") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("missing response cipher", func(t *testing.T) { + t.Parallel() + + s := &shadowsocks.TCPServerResponseStart{ + Method: method, + PSK: psk, + ResponseSalt: responseSalt, + Header: hdr, + } + + err := s.Validate(requestSalt) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "missing response cipher") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("request salt mismatch", func(t *testing.T) { + t.Parallel() + + s := &shadowsocks.TCPServerResponseStart{ + Method: method, + PSK: psk, + ResponseSalt: responseSalt, + ResponseCipher: responseCipher, + Header: hdr, + } + + badRequestSalt := append([]byte(nil), requestSalt...) + badRequestSalt[0] ^= 0xff + + err := s.Validate(badRequestSalt) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "response request salt mismatch") { + t.Fatalf("unexpected error: %v", err) + } + }) +} + +func TestTCPServerResponseStart_WriteResponseStart(t *testing.T) { + t.Parallel() + + method := newTCPServerStartTestMethod(t) + psk, responseSalt := newTCPServerStartTestPSKAndSalt(method) + requestSalt := make([]byte, method.SaltSize) + for i := range requestSalt { + requestSalt[i] = byte(i + 51) + } + + var s shadowsocks.TCPServerResponseStart + if err := s.Init(method, psk, responseSalt); err != nil { + t.Fatalf("Init() error = %v", err) + } + + ts := time.Unix(1700000100, 0) + + var buf bytes.Buffer + n, err := s.WriteResponseStart(&buf, ts, requestSalt, nil) + if err != nil { + t.Fatalf("WriteResponseStart() error = %v", err) + } + + wantLen := len(responseSalt) + (shadowsocks.TcpResponseFixedBaseLen + method.SaltSize + method.TagSize) + if int(n) != wantLen { + t.Fatalf("WriteResponseStart() wrote %d bytes, want %d", n, wantLen) + } + if buf.Len() != wantLen { + t.Fatalf("buffer len = %d, want %d", buf.Len(), wantLen) + } + + out := buf.Bytes() + if !bytes.Equal(out[:len(responseSalt)], responseSalt) { + t.Fatalf("response salt mismatch: got %v, want %v", out[:len(responseSalt)], responseSalt) + } + + if s.Header.Type != shadowsocks.TCPHeaderTypeServerStream { + t.Fatalf("Header.Type = %v, want %v", s.Header.Type, shadowsocks.TCPHeaderTypeServerStream) + } + if s.Header.Timestamp != uint64(ts.Unix()) { + t.Fatalf("Header.Timestamp = %d, want %d", s.Header.Timestamp, uint64(ts.Unix())) + } + if !bytes.Equal(s.Header.RequestSalt, requestSalt) { + t.Fatalf("Header.RequestSalt = %v, want %v", s.Header.RequestSalt, requestSalt) + } +} + +func TestTCPServerResponseStart_WriteResponseStart_InvalidRequestSalt(t *testing.T) { + t.Parallel() + + method := newTCPServerStartTestMethod(t) + psk, responseSalt := newTCPServerStartTestPSKAndSalt(method) + + var s shadowsocks.TCPServerResponseStart + if err := s.Init(method, psk, responseSalt); err != nil { + t.Fatalf("Init() error = %v", err) + } + + var buf bytes.Buffer + _, err := s.WriteResponseStart(&buf, time.Unix(1700000100, 0), responseSalt[:len(responseSalt)-1], nil) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid request salt length") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestTCPServerResponseStart_WriteResponseStart_NilReceiver(t *testing.T) { + t.Parallel() + + method := newTCPServerStartTestMethod(t) + _, requestSalt := newTCPServerStartTestPSKAndSalt(method) + + var s *shadowsocks.TCPServerResponseStart + var buf bytes.Buffer + + _, err := s.WriteResponseStart(&buf, time.Unix(1700000100, 0), requestSalt, nil) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "nil TCP server response start") { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/shadowsocks/tcp_stream_cipher.go b/shadowsocks/tcp_stream_cipher.go index ede967b..7f49d04 100644 --- a/shadowsocks/tcp_stream_cipher.go +++ b/shadowsocks/tcp_stream_cipher.go @@ -129,10 +129,8 @@ func (s *TCPStreamCipher) EncodeChunkLengthTo(dst []byte, payloadLen uint16) ([] } // DecodeChunkLength decrypts and parses a 2-byte big-endian payload length. -func (s *TCPStreamCipher) DecodeChunkLength(src []byte) (uint16, error) { - var buf [TcpChunkLengthLen]byte - - plain, err := s.OpenTo(buf[:0], src) +func (s *TCPStreamCipher) DecodeChunkLength(src []byte, scratch []byte) (uint16, error) { + plain, err := s.OpenTo(scratch[:0], src) if err != nil { return 0, err } @@ -157,26 +155,32 @@ func (s *TCPStreamCipher) DecodeChunkPayloadTo(dst, src []byte) ([]byte, error) } // EncodeRequestFixedHeaderTo encodes and encrypts a TCP request fixed header into dst. -func (s *TCPStreamCipher) EncodeRequestFixedHeaderTo(dst []byte, h *TCPRequestFixedHeader) ([]byte, error) { +// scratch is used as the plaintext scratch buffer and may be nil. +func (s *TCPStreamCipher) EncodeRequestFixedHeaderTo(dst []byte, h *TCPRequestFixedHeader, scratch []byte) ([]byte, error) { if h == nil { return nil, fmt.Errorf("nil TCP request fixed header") } - buf := make([]byte, h.EncodedLen()) - n, err := h.EncodeTo(buf) + plain := scratch[:0] + if cap(plain) >= h.EncodedLen() { + plain = plain[:h.EncodedLen()] + } else { + plain = make([]byte, h.EncodedLen()) + } + + n, err := h.EncodeTo(plain) if err != nil { return nil, err } - return s.SealTo(dst, buf[:n]) + return s.SealTo(dst, plain[:n]) } // DecodeRequestFixedHeader decrypts and decodes a TCP request fixed header from src. -func (s *TCPStreamCipher) DecodeRequestFixedHeader(src []byte) (TCPRequestFixedHeader, error) { +func (s *TCPStreamCipher) DecodeRequestFixedHeader(src []byte, scratch []byte) (TCPRequestFixedHeader, error) { var h TCPRequestFixedHeader - buf := make([]byte, h.EncodedLen()) - plain, err := s.OpenTo(buf[:0], src) + plain, err := s.OpenTo(scratch[:0], src) if err != nil { return TCPRequestFixedHeader{}, err } @@ -188,18 +192,25 @@ func (s *TCPStreamCipher) DecodeRequestFixedHeader(src []byte) (TCPRequestFixedH } // EncodeRequestVariableHeaderTo encodes and encrypts a TCP request variable header into dst. -func (s *TCPStreamCipher) EncodeRequestVariableHeaderTo(dst []byte, h *TCPRequestVariableHeader) ([]byte, error) { +// scratch is used as the plaintext scratch buffer and may be nil. +func (s *TCPStreamCipher) EncodeRequestVariableHeaderTo(dst []byte, h *TCPRequestVariableHeader, scratch []byte) ([]byte, error) { if h == nil { return nil, fmt.Errorf("nil TCP request variable header") } - buf := make([]byte, h.EncodedLen()) - n, err := h.EncodeTo(buf) + plain := scratch[:0] + if cap(plain) >= h.EncodedLen() { + plain = plain[:h.EncodedLen()] + } else { + plain = make([]byte, h.EncodedLen()) + } + + n, err := h.EncodeTo(plain) if err != nil { return nil, err } - return s.SealTo(dst, buf[:n]) + return s.SealTo(dst, plain[:n]) } // DecodeRequestVariableHeader decrypts and decodes a TCP request variable header from src. @@ -218,18 +229,25 @@ func (s *TCPStreamCipher) DecodeRequestVariableHeader(src []byte, scratch []byte } // EncodeResponseHeaderTo encodes and encrypts a TCP response header into dst. -func (s *TCPStreamCipher) EncodeResponseHeaderTo(dst []byte, h *TCPResponseHeader) ([]byte, error) { +// scratch is used as the plaintext scratch buffer and may be nil. +func (s *TCPStreamCipher) EncodeResponseHeaderTo(dst []byte, h *TCPResponseHeader, scratch []byte) ([]byte, error) { if h == nil { return nil, fmt.Errorf("nil TCP response header") } - buf := make([]byte, h.EncodedLen()) - n, err := h.EncodeTo(buf) + plain := scratch[:0] + if cap(plain) >= h.EncodedLen() { + plain = plain[:h.EncodedLen()] + } else { + plain = make([]byte, h.EncodedLen()) + } + + n, err := h.EncodeTo(plain) if err != nil { return nil, err } - return s.SealTo(dst, buf[:n]) + return s.SealTo(dst, plain[:n]) } // DecodeResponseHeader decrypts and decodes a TCP response header from src. diff --git a/shadowsocks/tcp_stream_cipher_test.go b/shadowsocks/tcp_stream_cipher_test.go index 0e9b1d8..9ccc557 100644 --- a/shadowsocks/tcp_stream_cipher_test.go +++ b/shadowsocks/tcp_stream_cipher_test.go @@ -303,7 +303,7 @@ func TestTCPStreamCipher_ChunkLength_RoundTrip(t *testing.T) { t.Fatalf("len(ciphertext) = %d, want %d", got, want) } - n, err := dec.DecodeChunkLength(ciphertext) + n, err := dec.DecodeChunkLength(ciphertext, nil) if err != nil { t.Fatalf("DecodeChunkLength() error = %v", err) } @@ -359,12 +359,12 @@ func TestTCPStreamCipher_RequestFixedHeader_RoundTrip(t *testing.T) { var want shadowsocks.TCPRequestFixedHeader want.Init(shadowsocks.TCPHeaderTypeClientStream, 123456789, 321) - ciphertext, err := enc.EncodeRequestFixedHeaderTo(nil, &want) + ciphertext, err := enc.EncodeRequestFixedHeaderTo(nil, &want, nil) if err != nil { t.Fatalf("EncodeRequestFixedHeaderTo() error = %v", err) } - got, err := dec.DecodeRequestFixedHeader(ciphertext) + got, err := dec.DecodeRequestFixedHeader(ciphertext, nil) if err != nil { t.Fatalf("DecodeRequestFixedHeader() error = %v", err) } @@ -379,7 +379,7 @@ func TestTCPStreamCipher_RequestFixedHeader_Nil(t *testing.T) { enc, _, _ := newTestCipherPair(t) - _, err := enc.EncodeRequestFixedHeaderTo(nil, nil) + _, err := enc.EncodeRequestFixedHeaderTo(nil, nil, nil) if err == nil { t.Fatal("expected error, got nil") } @@ -404,7 +404,7 @@ func TestTCPStreamCipher_RequestVariableHeader_RoundTrip(t *testing.T) { []byte("hello"), ) - ciphertext, err := enc.EncodeRequestVariableHeaderTo(nil, &want) + ciphertext, err := enc.EncodeRequestVariableHeaderTo(nil, &want, nil) if err != nil { t.Fatalf("EncodeRequestVariableHeaderTo() error = %v", err) } @@ -439,7 +439,7 @@ func TestTCPStreamCipher_RequestVariableHeader_Nil(t *testing.T) { enc, _, _ := newTestCipherPair(t) - _, err := enc.EncodeRequestVariableHeaderTo(nil, nil) + _, err := enc.EncodeRequestVariableHeaderTo(nil, nil, nil) if err == nil { t.Fatal("expected error, got nil") } @@ -456,7 +456,7 @@ func TestTCPStreamCipher_ResponseHeader_RoundTrip(t *testing.T) { var want shadowsocks.TCPResponseHeader want.Init(shadowsocks.TCPHeaderTypeServerStream, 123456789, []byte{1, 2, 3, 4}, 4) - ciphertext, err := enc.EncodeResponseHeaderTo(nil, &want) + ciphertext, err := enc.EncodeResponseHeaderTo(nil, &want, nil) if err != nil { t.Fatalf("EncodeResponseHeaderTo() error = %v", err) } @@ -479,7 +479,7 @@ func TestTCPStreamCipher_ResponseHeader_Nil(t *testing.T) { enc, _, _ := newTestCipherPair(t) - _, err := enc.EncodeResponseHeaderTo(nil, nil) + _, err := enc.EncodeResponseHeaderTo(nil, nil, nil) if err == nil { t.Fatal("expected error, got nil") } From 4259f22bb6b20cbddea188b5443f2c53c76c12c5 Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 17:53:30 +0300 Subject: [PATCH 09/25] shadowsocks replay cache --- shadowsocks/replay_cache.go | 122 +++++++++++++++++++ shadowsocks/replay_cache_test.go | 198 +++++++++++++++++++++++++++++++ 2 files changed, 320 insertions(+) create mode 100644 shadowsocks/replay_cache.go create mode 100644 shadowsocks/replay_cache_test.go diff --git a/shadowsocks/replay_cache.go b/shadowsocks/replay_cache.go new file mode 100644 index 0000000..3acc666 --- /dev/null +++ b/shadowsocks/replay_cache.go @@ -0,0 +1,122 @@ +package shadowsocks + +import ( + "sync" + "time" +) + +// ReplayCache stores recently seen TCP salts for replay protection. +type ReplayCache struct { + mu sync.Mutex + items map[string]time.Time +} + +// NewReplayCache creates a new replay cache. +func NewReplayCache() *ReplayCache { + return &ReplayCache{ + items: make(map[string]time.Time), + } +} + +// Seen reports whether salt is already present and not expired. +func (c *ReplayCache) Seen(salt []byte, now time.Time) bool { + if c == nil { + return false + } + + c.mu.Lock() + defer c.mu.Unlock() + + c.purgeExpiredLocked(now) + + expiry, ok := c.items[string(salt)] + if !ok { + return false + } + if !expiry.After(now) { + delete(c.items, string(salt)) + return false + } + + return true +} + +// Add stores salt until now+ttl. +func (c *ReplayCache) Add(salt []byte, now time.Time, ttl time.Duration) { + if c == nil || len(salt) == 0 || ttl <= 0 { + return + } + + c.mu.Lock() + defer c.mu.Unlock() + + c.purgeExpiredLocked(now) + c.items[string(salt)] = now.Add(ttl) +} + +// SeenOrAdd reports whether salt is already present and not expired. +// If not present, it stores the salt until now+ttl and returns false. +func (c *ReplayCache) SeenOrAdd(salt []byte, now time.Time, ttl time.Duration) bool { + if c == nil || len(salt) == 0 || ttl <= 0 { + return false + } + + c.mu.Lock() + defer c.mu.Unlock() + + c.purgeExpiredLocked(now) + + key := string(salt) + expiry, ok := c.items[key] + if ok && expiry.After(now) { + return true + } + + c.items[key] = now.Add(ttl) + return false +} + +// Remove deletes a salt from the cache. +func (c *ReplayCache) Remove(salt []byte) { + if c == nil || len(salt) == 0 { + return + } + + c.mu.Lock() + defer c.mu.Unlock() + + delete(c.items, string(salt)) +} + +// PurgeExpired removes expired entries. +func (c *ReplayCache) PurgeExpired(now time.Time) { + if c == nil { + return + } + + c.mu.Lock() + defer c.mu.Unlock() + + c.purgeExpiredLocked(now) +} + +// Len returns the number of currently stored entries after purging expired ones. +func (c *ReplayCache) Len(now time.Time) int { + if c == nil { + return 0 + } + + c.mu.Lock() + defer c.mu.Unlock() + + c.purgeExpiredLocked(now) + return len(c.items) +} + +func (c *ReplayCache) purgeExpiredLocked(now time.Time) { + for k, expiry := range c.items { + if !expiry.After(now) { + delete(c.items, k) + } + } +} diff --git a/shadowsocks/replay_cache_test.go b/shadowsocks/replay_cache_test.go new file mode 100644 index 0000000..39ed751 --- /dev/null +++ b/shadowsocks/replay_cache_test.go @@ -0,0 +1,198 @@ +package shadowsocks_test + +import ( + "testing" + "time" + + "github.com/33TU/socks/shadowsocks" +) + +func TestNewReplayCache(t *testing.T) { + t.Parallel() + + c := shadowsocks.NewReplayCache() + if c == nil { + t.Fatal("NewReplayCache() returned nil") + } + if got := c.Len(time.Now()); got != 0 { + t.Fatalf("Len() = %d, want 0", got) + } +} + +func TestReplayCache_SeenAndAdd(t *testing.T) { + t.Parallel() + + c := shadowsocks.NewReplayCache() + now := time.Unix(1700000000, 0) + salt := []byte("salt1") + + if c.Seen(salt, now) { + t.Fatal("Seen() = true before Add, want false") + } + + c.Add(salt, now, time.Minute) + + if !c.Seen(salt, now) { + t.Fatal("Seen() = false after Add, want true") + } + + if got := c.Len(now); got != 1 { + t.Fatalf("Len() = %d, want 1", got) + } +} + +func TestReplayCache_SeenOrAdd(t *testing.T) { + t.Parallel() + + c := shadowsocks.NewReplayCache() + now := time.Unix(1700000000, 0) + salt := []byte("salt1") + + if got := c.SeenOrAdd(salt, now, time.Minute); got { + t.Fatal("SeenOrAdd() first call = true, want false") + } + + if got := c.SeenOrAdd(salt, now, time.Minute); !got { + t.Fatal("SeenOrAdd() second call = false, want true") + } + + if got := c.Len(now); got != 1 { + t.Fatalf("Len() = %d, want 1", got) + } +} + +func TestReplayCache_Expiry(t *testing.T) { + t.Parallel() + + c := shadowsocks.NewReplayCache() + now := time.Unix(1700000000, 0) + salt := []byte("salt1") + + c.Add(salt, now, time.Minute) + + if !c.Seen(salt, now.Add(30*time.Second)) { + t.Fatal("Seen() before expiry = false, want true") + } + + if c.Seen(salt, now.Add(61*time.Second)) { + t.Fatal("Seen() after expiry = true, want false") + } + + if got := c.Len(now.Add(61 * time.Second)); got != 0 { + t.Fatalf("Len() after expiry = %d, want 0", got) + } +} + +func TestReplayCache_Remove(t *testing.T) { + t.Parallel() + + c := shadowsocks.NewReplayCache() + now := time.Unix(1700000000, 0) + salt := []byte("salt1") + + c.Add(salt, now, time.Minute) + c.Remove(salt) + + if c.Seen(salt, now) { + t.Fatal("Seen() after Remove = true, want false") + } + + if got := c.Len(now); got != 0 { + t.Fatalf("Len() = %d, want 0", got) + } +} + +func TestReplayCache_PurgeExpired(t *testing.T) { + t.Parallel() + + c := shadowsocks.NewReplayCache() + now := time.Unix(1700000000, 0) + + c.Add([]byte("salt1"), now, time.Minute) + c.Add([]byte("salt2"), now, 2*time.Minute) + + c.PurgeExpired(now.Add(90 * time.Second)) + + if got := c.Len(now.Add(90 * time.Second)); got != 1 { + t.Fatalf("Len() = %d, want 1", got) + } + + if c.Seen([]byte("salt1"), now.Add(90*time.Second)) { + t.Fatal("salt1 still present after expiry") + } + if !c.Seen([]byte("salt2"), now.Add(90*time.Second)) { + t.Fatal("salt2 missing before expiry") + } +} + +func TestReplayCache_NilCache(t *testing.T) { + t.Parallel() + + var c *shadowsocks.ReplayCache + now := time.Unix(1700000000, 0) + salt := []byte("salt1") + + if c.Seen(salt, now) { + t.Fatal("Seen() on nil cache = true, want false") + } + if got := c.SeenOrAdd(salt, now, time.Minute); got { + t.Fatal("SeenOrAdd() on nil cache = true, want false") + } + + c.Add(salt, now, time.Minute) + c.Remove(salt) + c.PurgeExpired(now) + + if got := c.Len(now); got != 0 { + t.Fatalf("Len() on nil cache = %d, want 0", got) + } +} + +func TestReplayCache_EmptySaltOrNonPositiveTTL(t *testing.T) { + t.Parallel() + + c := shadowsocks.NewReplayCache() + now := time.Unix(1700000000, 0) + + c.Add(nil, now, time.Minute) + c.Add([]byte{}, now, time.Minute) + c.Add([]byte("salt1"), now, 0) + c.Add([]byte("salt2"), now, -time.Second) + + if got := c.Len(now); got != 0 { + t.Fatalf("Len() = %d, want 0", got) + } + + if got := c.SeenOrAdd(nil, now, time.Minute); got { + t.Fatal("SeenOrAdd(nil) = true, want false") + } + if got := c.SeenOrAdd([]byte{}, now, time.Minute); got { + t.Fatal("SeenOrAdd(empty) = true, want false") + } + if got := c.SeenOrAdd([]byte("salt1"), now, 0); got { + t.Fatal("SeenOrAdd(ttl=0) = true, want false") + } + if got := c.SeenOrAdd([]byte("salt2"), now, -time.Second); got { + t.Fatal("SeenOrAdd(ttl<0) = true, want false") + } +} + +func TestReplayCache_SeenOrAdd_ReaddsExpired(t *testing.T) { + t.Parallel() + + c := shadowsocks.NewReplayCache() + now := time.Unix(1700000000, 0) + salt := []byte("salt1") + + if got := c.SeenOrAdd(salt, now, time.Minute); got { + t.Fatal("SeenOrAdd() first call = true, want false") + } + + if got := c.SeenOrAdd(salt, now.Add(61*time.Second), time.Minute); got { + t.Fatal("SeenOrAdd() after expiry = true, want false") + } + + if got := c.Len(now.Add(61 * time.Second)); got != 1 { + t.Fatalf("Len() = %d, want 1", got) + } +} From 73f630b6f26364b62e6ac861ad93e6f1483a1d80 Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 18:45:13 +0300 Subject: [PATCH 10/25] shadowsocks encode improvements --- shadowsocks (1).zip | Bin 0 -> 32375 bytes shadowsocks/addr.go | 36 ++++++++---------- shadowsocks/addr_test.go | 18 ++------- shadowsocks/tcp_header_err.go | 3 +- shadowsocks/tcp_request_fixed_header.go | 16 +++----- shadowsocks/tcp_request_fixed_header_test.go | 16 ++------ shadowsocks/tcp_request_variable_header.go | 28 +++++--------- .../tcp_request_variable_header_test.go | 20 ++-------- shadowsocks/tcp_response_header.go | 22 ++++------- shadowsocks/tcp_response_header_test.go | 19 ++------- shadowsocks/tcp_stream_cipher.go | 34 +++-------------- 11 files changed, 60 insertions(+), 152 deletions(-) create mode 100644 shadowsocks (1).zip diff --git a/shadowsocks (1).zip b/shadowsocks (1).zip new file mode 100644 index 0000000000000000000000000000000000000000..ccad9ef37a86b1d89360609dbe8ab3493d70ab9b GIT binary patch literal 32375 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

66RL3yW! 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 D;_0}{ literal 0 HcmV?d00001 diff --git a/shadowsocks/addr.go b/shadowsocks/addr.go index 7f44052..7e5ae37 100644 --- a/shadowsocks/addr.go +++ b/shadowsocks/addr.go @@ -138,39 +138,33 @@ func (a *Addr) Decode(src []byte) (int, error) { } } -// EncodeTo encodes the address into dst. -// It returns the number of bytes written. -func (a *Addr) EncodeTo(dst []byte) (int, error) { +// EncodeTo encodes the address into dst and returns the extended slice. +func (a *Addr) EncodeTo(dst []byte) ([]byte, error) { if err := a.Validate(); err != nil { - return 0, err + return nil, err } - n := a.EncodedLen() - if len(dst) < n { - return 0, ErrShortAddrBuffer - } - - dst[0] = a.AddrType + dst = append(dst, a.AddrType) switch a.AddrType { case AddrTypeIPv4: - copy(dst[1:5], a.IP.To4()) - binary.BigEndian.PutUint16(dst[5:7], a.Port) - return 7, nil + dst = append(dst, a.IP.To4()...) + dst = binary.BigEndian.AppendUint16(dst, a.Port) + return dst, nil case AddrTypeIPv6: - copy(dst[1:17], a.IP.To16()) - binary.BigEndian.PutUint16(dst[17:19], a.Port) - return 19, nil + dst = append(dst, a.IP.To16()...) + dst = binary.BigEndian.AppendUint16(dst, a.Port) + return dst, nil case AddrTypeDomain: - dst[1] = byte(len(a.Domain)) - copy(dst[2:2+len(a.Domain)], a.Domain) - binary.BigEndian.PutUint16(dst[2+len(a.Domain):2+len(a.Domain)+2], a.Port) - return 1 + 1 + len(a.Domain) + 2, nil + dst = append(dst, byte(len(a.Domain))) + dst = append(dst, a.Domain...) + dst = binary.BigEndian.AppendUint16(dst, a.Port) + return dst, nil default: - return 0, ErrInvalidAddrType + return nil, ErrInvalidAddrType } } diff --git a/shadowsocks/addr_test.go b/shadowsocks/addr_test.go index 0821e08..5764979 100644 --- a/shadowsocks/addr_test.go +++ b/shadowsocks/addr_test.go @@ -273,12 +273,12 @@ func TestAddr_EncodeTo_Decode_RoundTrip(t *testing.T) { t.Run(tt.name, func(t *testing.T) { buf := make([]byte, tt.addr.EncodedLen()) - nw, err := tt.addr.EncodeTo(buf) + bw, err := tt.addr.EncodeTo(buf[:0]) if err != nil { t.Fatalf("EncodeTo() failed: %v", err) } - if nw != len(buf) { - t.Fatalf("EncodeTo() wrote %d bytes, want %d", nw, len(buf)) + if len(bw) != len(buf) { + t.Fatalf("EncodeTo() wrote %d bytes, want %d", len(bw), len(buf)) } var got shadowsocks.Addr @@ -331,16 +331,6 @@ func TestAddr_EncodeTo_Invalid(t *testing.T) { bufLen: 32, wantErr: shadowsocks.ErrInvalidAddrType, }, - { - name: "short buffer", - addr: shadowsocks.Addr{ - AddrType: shadowsocks.AddrTypeIPv4, - IP: net.IPv4(127, 0, 0, 1), - Port: 1080, - }, - bufLen: 6, - wantErr: shadowsocks.ErrShortAddrBuffer, - }, { name: "invalid domain", addr: shadowsocks.Addr{ @@ -356,7 +346,7 @@ func TestAddr_EncodeTo_Invalid(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { buf := make([]byte, tt.bufLen) - _, err := tt.addr.EncodeTo(buf) + _, err := tt.addr.EncodeTo(buf[:0]) if !errors.Is(err, tt.wantErr) { t.Fatalf("EncodeTo() error = %v, wantErr = %v", err, tt.wantErr) } diff --git a/shadowsocks/tcp_header_err.go b/shadowsocks/tcp_header_err.go index b2135ba..9cd05c8 100644 --- a/shadowsocks/tcp_header_err.go +++ b/shadowsocks/tcp_header_err.go @@ -2,13 +2,12 @@ package shadowsocks import "errors" -// Common validation and decode/encode errors for Shadowsocks TCP headers. +// Common validation and decode errors for Shadowsocks TCP headers. var ( ErrInvalidTCPHeaderType = errors.New("invalid TCP header type") ErrInvalidTCPPaddingLength = errors.New("invalid TCP padding length") ErrMissingTCPHeaderData = errors.New("missing TCP header data") ErrShortTCPHeader = errors.New("short TCP header") - ErrShortTCPHeaderBuffer = errors.New("short TCP header buffer") ErrMissingTCPResponseSalt = errors.New("missing TCP response salt") ErrInvalidTCPResponseSaltLen = errors.New("invalid TCP response salt length") ) diff --git a/shadowsocks/tcp_request_fixed_header.go b/shadowsocks/tcp_request_fixed_header.go index c272e66..0ba74b1 100644 --- a/shadowsocks/tcp_request_fixed_header.go +++ b/shadowsocks/tcp_request_fixed_header.go @@ -49,20 +49,16 @@ func (h *TCPRequestFixedHeader) Decode(src []byte) (int, error) { } // EncodeTo encodes the fixed request header into dst. -// It returns the number of bytes written. -func (h *TCPRequestFixedHeader) EncodeTo(dst []byte) (int, error) { +func (h *TCPRequestFixedHeader) EncodeTo(dst []byte) ([]byte, error) { if err := h.Validate(); err != nil { - return 0, err - } - if len(dst) < TcpRequestFixedHeaderLen { - return 0, ErrShortTCPHeaderBuffer + return nil, err } - dst[0] = h.Type - binary.BigEndian.PutUint64(dst[1:9], h.Timestamp) - binary.BigEndian.PutUint16(dst[9:11], h.Length) + dst = append(dst, h.Type) + dst = binary.BigEndian.AppendUint64(dst, h.Timestamp) + dst = binary.BigEndian.AppendUint16(dst, h.Length) - return TcpRequestFixedHeaderLen, nil + return dst, nil } // String returns a human-readable representation of the fixed request header. diff --git a/shadowsocks/tcp_request_fixed_header_test.go b/shadowsocks/tcp_request_fixed_header_test.go index a35e600..b741123 100644 --- a/shadowsocks/tcp_request_fixed_header_test.go +++ b/shadowsocks/tcp_request_fixed_header_test.go @@ -57,12 +57,12 @@ func TestTCPRequestFixedHeader_EncodeTo_Decode_RoundTrip(t *testing.T) { buf := make([]byte, want.EncodedLen()) - nw, err := want.EncodeTo(buf) + bw, err := want.EncodeTo(buf[:0]) if err != nil { t.Fatalf("EncodeTo() failed: %v", err) } - if nw != len(buf) { - t.Fatalf("EncodeTo() wrote %d bytes, want %d", nw, len(buf)) + if len(bw) != len(buf) { + t.Fatalf("EncodeTo() wrote %d bytes, want %d", len(bw), len(buf)) } var got shadowsocks.TCPRequestFixedHeader @@ -96,16 +96,6 @@ func TestTCPRequestFixedHeader_EncodeTo_Invalid(t *testing.T) { bufLen: 32, wantErr: shadowsocks.ErrInvalidTCPHeaderType, }, - { - name: "short buffer", - hdr: shadowsocks.TCPRequestFixedHeader{ - Type: shadowsocks.TCPHeaderTypeClientStream, - Timestamp: 1, - Length: 2, - }, - bufLen: shadowsocks.TcpRequestFixedHeaderLen - 1, - wantErr: shadowsocks.ErrShortTCPHeaderBuffer, - }, } for _, tt := range tests { diff --git a/shadowsocks/tcp_request_variable_header.go b/shadowsocks/tcp_request_variable_header.go index 08f6f00..1143920 100644 --- a/shadowsocks/tcp_request_variable_header.go +++ b/shadowsocks/tcp_request_variable_header.go @@ -83,32 +83,22 @@ func (h *TCPRequestVariableHeader) Decode(src []byte) (int, error) { } // EncodeTo encodes the variable request header into dst. -// It returns the number of bytes written. -func (h *TCPRequestVariableHeader) EncodeTo(dst []byte) (int, error) { +func (h *TCPRequestVariableHeader) EncodeTo(dst []byte) ([]byte, error) { if err := h.Validate(); err != nil { - return 0, err - } - - n := h.EncodedLen() - if len(dst) < n { - return 0, ErrShortTCPHeaderBuffer + return nil, err } - off, err := h.Target.EncodeTo(dst) + var err error + dst, err = h.Target.EncodeTo(dst) if err != nil { - return 0, err + return nil, err } - binary.BigEndian.PutUint16(dst[off:off+2], h.PaddingLen) - off += 2 - - copy(dst[off:off+len(h.Padding)], h.Padding) - off += len(h.Padding) - - copy(dst[off:off+len(h.InitialData)], h.InitialData) - off += len(h.InitialData) + dst = binary.BigEndian.AppendUint16(dst, h.PaddingLen) + dst = append(dst, h.Padding...) + dst = append(dst, h.InitialData...) - return off, nil + return dst, nil } // String returns a human-readable representation of the variable request header. diff --git a/shadowsocks/tcp_request_variable_header_test.go b/shadowsocks/tcp_request_variable_header_test.go index 0de8941..295ac9b 100644 --- a/shadowsocks/tcp_request_variable_header_test.go +++ b/shadowsocks/tcp_request_variable_header_test.go @@ -148,12 +148,12 @@ func TestTCPRequestVariableHeader_EncodeTo_Decode_RoundTrip(t *testing.T) { t.Run(tt.name, func(t *testing.T) { buf := make([]byte, tt.hdr.EncodedLen()) - nw, err := tt.hdr.EncodeTo(buf) + bw, err := tt.hdr.EncodeTo(buf[:0]) if err != nil { t.Fatalf("EncodeTo() failed: %v", err) } - if nw != len(buf) { - t.Fatalf("EncodeTo() wrote %d bytes, want %d", nw, len(buf)) + if len(bw) != len(buf) { + t.Fatalf("EncodeTo() wrote %d bytes, want %d", len(bw), len(buf)) } var got shadowsocks.TCPRequestVariableHeader @@ -233,17 +233,6 @@ func TestTCPRequestVariableHeader_EncodeTo_Invalid(t *testing.T) { bufLen: 64, wantErr: shadowsocks.ErrMissingTCPHeaderData, }, - { - name: "short buffer", - hdr: shadowsocks.TCPRequestVariableHeader{ - Target: validTarget, - PaddingLen: 1, - Padding: []byte{1}, - InitialData: []byte("a"), - }, - bufLen: 1, - wantErr: shadowsocks.ErrShortTCPHeaderBuffer, - }, } for _, tt := range tests { @@ -263,8 +252,7 @@ func TestTCPRequestVariableHeader_Decode_Invalid(t *testing.T) { Domain: "example.com", Port: 443, } - validTargetBuf := make([]byte, validTarget.EncodedLen()) - _, err := validTarget.EncodeTo(validTargetBuf) + validTargetBuf, err := validTarget.EncodeTo(nil) if err != nil { t.Fatalf("target EncodeTo() failed: %v", err) } diff --git a/shadowsocks/tcp_response_header.go b/shadowsocks/tcp_response_header.go index 36d8b99..e10a577 100644 --- a/shadowsocks/tcp_response_header.go +++ b/shadowsocks/tcp_response_header.go @@ -67,24 +67,18 @@ func (h *TCPResponseHeader) Decode(src []byte) (int, error) { return need, nil } -// EncodeTo encodes the response header into dst. -// It returns the number of bytes written. -func (h *TCPResponseHeader) EncodeTo(dst []byte) (int, error) { +// EncodeTo encodes the response header into dst and returns the extended slice. +func (h *TCPResponseHeader) EncodeTo(dst []byte) ([]byte, error) { if err := h.Validate(); err != nil { - return 0, err - } - - n := h.EncodedLen() - if len(dst) < n { - return 0, ErrShortTCPHeaderBuffer + return nil, err } - dst[0] = h.Type - binary.BigEndian.PutUint64(dst[1:9], h.Timestamp) - binary.BigEndian.PutUint16(dst[9:11], h.Length) - copy(dst[11:11+len(h.RequestSalt)], h.RequestSalt) + dst = append(dst, h.Type) + dst = binary.BigEndian.AppendUint64(dst, h.Timestamp) + dst = binary.BigEndian.AppendUint16(dst, h.Length) + dst = append(dst, h.RequestSalt...) - return n, nil + return dst, nil } // String returns a human-readable representation of the response header. diff --git a/shadowsocks/tcp_response_header_test.go b/shadowsocks/tcp_response_header_test.go index 61e13d4..9ac1aec 100644 --- a/shadowsocks/tcp_response_header_test.go +++ b/shadowsocks/tcp_response_header_test.go @@ -85,12 +85,12 @@ func TestTCPResponseHeader_EncodeTo_Decode_RoundTrip(t *testing.T) { buf := make([]byte, want.EncodedLen()) - nw, err := want.EncodeTo(buf) + bw, err := want.EncodeTo(buf[:0]) if err != nil { t.Fatalf("EncodeTo() failed: %v", err) } - if nw != len(buf) { - t.Fatalf("EncodeTo() wrote %d bytes, want %d", nw, len(buf)) + if len(bw) != len(buf) { + t.Fatalf("EncodeTo() wrote %d bytes, want %d", len(bw), len(buf)) } var got shadowsocks.TCPResponseHeader @@ -149,23 +149,12 @@ func TestTCPResponseHeader_EncodeTo_Invalid(t *testing.T) { bufLen: 32, wantErr: shadowsocks.ErrInvalidTCPResponseSaltLen, }, - { - name: "short buffer", - hdr: shadowsocks.TCPResponseHeader{ - Type: shadowsocks.TCPHeaderTypeServerStream, - Timestamp: 1, - RequestSalt: []byte{1, 2, 3, 4}, - Length: 4, - }, - bufLen: shadowsocks.TcpResponseFixedBaseLen + 3, - wantErr: shadowsocks.ErrShortTCPHeaderBuffer, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { buf := make([]byte, tt.bufLen) - _, err := tt.hdr.EncodeTo(buf) + _, err := tt.hdr.EncodeTo(buf[:0]) if !errors.Is(err, tt.wantErr) { t.Fatalf("EncodeTo() error = %v, wantErr = %v", err, tt.wantErr) } diff --git a/shadowsocks/tcp_stream_cipher.go b/shadowsocks/tcp_stream_cipher.go index 7f49d04..cb49bf4 100644 --- a/shadowsocks/tcp_stream_cipher.go +++ b/shadowsocks/tcp_stream_cipher.go @@ -155,25 +155,17 @@ func (s *TCPStreamCipher) DecodeChunkPayloadTo(dst, src []byte) ([]byte, error) } // EncodeRequestFixedHeaderTo encodes and encrypts a TCP request fixed header into dst. -// scratch is used as the plaintext scratch buffer and may be nil. func (s *TCPStreamCipher) EncodeRequestFixedHeaderTo(dst []byte, h *TCPRequestFixedHeader, scratch []byte) ([]byte, error) { if h == nil { return nil, fmt.Errorf("nil TCP request fixed header") } - plain := scratch[:0] - if cap(plain) >= h.EncodedLen() { - plain = plain[:h.EncodedLen()] - } else { - plain = make([]byte, h.EncodedLen()) - } - - n, err := h.EncodeTo(plain) + plain, err := h.EncodeTo(scratch[:0]) if err != nil { return nil, err } - return s.SealTo(dst, plain[:n]) + return s.SealTo(dst, plain) } // DecodeRequestFixedHeader decrypts and decodes a TCP request fixed header from src. @@ -198,19 +190,12 @@ func (s *TCPStreamCipher) EncodeRequestVariableHeaderTo(dst []byte, h *TCPReques return nil, fmt.Errorf("nil TCP request variable header") } - plain := scratch[:0] - if cap(plain) >= h.EncodedLen() { - plain = plain[:h.EncodedLen()] - } else { - plain = make([]byte, h.EncodedLen()) - } - - n, err := h.EncodeTo(plain) + plain, err := h.EncodeTo(scratch[:0]) if err != nil { return nil, err } - return s.SealTo(dst, plain[:n]) + return s.SealTo(dst, plain) } // DecodeRequestVariableHeader decrypts and decodes a TCP request variable header from src. @@ -235,19 +220,12 @@ func (s *TCPStreamCipher) EncodeResponseHeaderTo(dst []byte, h *TCPResponseHeade return nil, fmt.Errorf("nil TCP response header") } - plain := scratch[:0] - if cap(plain) >= h.EncodedLen() { - plain = plain[:h.EncodedLen()] - } else { - plain = make([]byte, h.EncodedLen()) - } - - n, err := h.EncodeTo(plain) + plain, err := h.EncodeTo(scratch[:0]) if err != nil { return nil, err } - return s.SealTo(dst, plain[:n]) + return s.SealTo(dst, plain) } // DecodeResponseHeader decrypts and decodes a TCP response header from src. From eb7872d3df8d703c19f218519e98db394e768063 Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 18:54:08 +0300 Subject: [PATCH 11/25] shadowsocks improve tcp server/client streams --- shadowsocks/tcp_client_stream.go | 44 +++++++++++------------ shadowsocks/tcp_client_stream_test.go | 9 +++-- shadowsocks/tcp_server_stream.go | 50 +++++++++++---------------- shadowsocks/tcp_server_stream_test.go | 12 +++---- shadowsocks/tcp_stream_cipher.go | 4 +-- 5 files changed, 55 insertions(+), 64 deletions(-) diff --git a/shadowsocks/tcp_client_stream.go b/shadowsocks/tcp_client_stream.go index 7a6d398..e16f6fb 100644 --- a/shadowsocks/tcp_client_stream.go +++ b/shadowsocks/tcp_client_stream.go @@ -5,6 +5,8 @@ import ( "fmt" "io" "time" + + ibuf "github.com/33TU/socks/internal" ) const tcpClientRequestStartStackBufSize = 1024 @@ -86,14 +88,13 @@ func (s *TCPClientRequestStart) EncodedRequestStartLen(variableHeaderLen int) (i // request salt || encrypted request fixed header || encrypted request variable header // // The request fixed header Length field is set to the plaintext encoded variable -// header length. scratch may be nil. +// header length. func (s *TCPClientRequestStart) WriteRequestStart( dst io.Writer, timestamp time.Time, target Addr, padding []byte, initialData []byte, - scratch []byte, ) (int64, error) { if err := s.Validate(); err != nil { return 0, err @@ -112,22 +113,29 @@ func (s *TCPClientRequestStart) WriteRequestStart( uint16(variableHeader.EncodedLen()), ) + scratchLen := TcpRequestFixedHeaderLen + if variableHeader.EncodedLen() > scratchLen { + scratchLen = variableHeader.EncodedLen() + } + + plainScratch := ibuf.GetBytes(scratchLen) + defer ibuf.PutBytes(plainScratch) + var stackBuf [tcpClientRequestStartStackBufSize]byte out := stackBuf[:0] out = append(out, s.RequestSalt...) - encFixed, err := s.RequestCipher.EncodeRequestFixedHeaderTo(scratch[:0], &fixedHeader, nil) + var err error + out, err = s.RequestCipher.EncodeRequestFixedHeaderTo(out, &fixedHeader, plainScratch[:0]) if err != nil { return 0, err } - out = append(out, encFixed...) - encVariable, err := s.RequestCipher.EncodeRequestVariableHeaderTo(scratch[:0], &variableHeader, nil) + out, err = s.RequestCipher.EncodeRequestVariableHeaderTo(out, &variableHeader, plainScratch[:0]) if err != nil { return 0, err } - out = append(out, encVariable...) n, err := dst.Write(out) return int64(n), err @@ -173,11 +181,8 @@ func (s *TCPClientResponseStart) Validate(method Method, requestSalt []byte) err // ReadResponseStart reads and decrypts the server response startup: // // response salt || encrypted response header -// -// scratch may be nil. func (s *TCPClientRequestStart) ReadResponseStart( src io.Reader, - scratch []byte, ) (*TCPClientResponseStart, int64, error) { if err := s.Validate(); err != nil { return nil, 0, err @@ -186,12 +191,8 @@ func (s *TCPClientRequestStart) ReadResponseStart( var total int64 responseSaltLen := s.Method.SaltSize - var responseSaltBuf []byte - if cap(scratch) >= responseSaltLen { - responseSaltBuf = scratch[:responseSaltLen] - } else { - responseSaltBuf = make([]byte, responseSaltLen) - } + responseSaltBuf := ibuf.GetBytes(responseSaltLen) + defer ibuf.PutBytes(responseSaltBuf) n, err := io.ReadFull(src, responseSaltBuf) total += int64(n) @@ -205,12 +206,8 @@ func (s *TCPClientRequestStart) ReadResponseStart( } encHeaderLen := TcpResponseFixedBaseLen + s.Method.SaltSize + s.Method.TagSize - var encHeader []byte - if cap(encHeader) >= encHeaderLen { - encHeader = encHeader[:encHeaderLen] - } else { - encHeader = make([]byte, encHeaderLen) - } + encHeader := ibuf.GetBytes(encHeaderLen) + defer ibuf.PutBytes(encHeader) n, err = io.ReadFull(src, encHeader) total += int64(n) @@ -218,7 +215,10 @@ func (s *TCPClientRequestStart) ReadResponseStart( return nil, total, err } - header, err := responseCipher.DecodeResponseHeader(encHeader, scratch[:0]) + plainScratch := ibuf.GetBytes(TcpResponseFixedBaseLen + s.Method.SaltSize) + defer ibuf.PutBytes(plainScratch) + + header, err := responseCipher.DecodeResponseHeader(encHeader, plainScratch[:0]) if err != nil { return nil, total, err } diff --git a/shadowsocks/tcp_client_stream_test.go b/shadowsocks/tcp_client_stream_test.go index 98c4e07..42d97e0 100644 --- a/shadowsocks/tcp_client_stream_test.go +++ b/shadowsocks/tcp_client_stream_test.go @@ -162,7 +162,7 @@ func TestTCPClientRequestStart_WriteRequestStart(t *testing.T) { initialData := []byte("hello") var buf bytes.Buffer - n, err := s.WriteRequestStart(&buf, ts, target, padding, initialData, nil) + n, err := s.WriteRequestStart(&buf, ts, target, padding, initialData) if err != nil { t.Fatalf("WriteRequestStart() error = %v", err) } @@ -206,7 +206,6 @@ func TestTCPClientRequestStart_WriteRequestStart_InvalidTarget(t *testing.T) { shadowsocks.Addr{}, nil, []byte("x"), - nil, ) if err == nil { t.Fatal("expected error, got nil") @@ -353,7 +352,7 @@ func TestTCPClientRequestStart_ReadResponseStart(t *testing.T) { wire.Write(responseSalt) wire.Write(encHeader) - resp, n, err := clientStart.ReadResponseStart(&wire, nil) + resp, n, err := clientStart.ReadResponseStart(&wire) if err != nil { t.Fatalf("ReadResponseStart() error = %v", err) } @@ -390,7 +389,7 @@ func TestTCPClientRequestStart_ReadResponseStart_ShortRead(t *testing.T) { short := bytes.NewReader(make([]byte, method.SaltSize-1)) - _, n, err := clientStart.ReadResponseStart(short, nil) + _, n, err := clientStart.ReadResponseStart(short) if err == nil { t.Fatal("expected error, got nil") } @@ -443,7 +442,7 @@ func TestTCPClientRequestStart_ReadResponseStart_RequestSaltMismatch(t *testing. wire.Write(responseSalt) wire.Write(encHeader) - _, _, err = clientStart.ReadResponseStart(&wire, nil) + _, _, err = clientStart.ReadResponseStart(&wire) if err == nil { t.Fatal("expected error, got nil") } diff --git a/shadowsocks/tcp_server_stream.go b/shadowsocks/tcp_server_stream.go index 906752e..b65e350 100644 --- a/shadowsocks/tcp_server_stream.go +++ b/shadowsocks/tcp_server_stream.go @@ -5,6 +5,8 @@ import ( "fmt" "io" "time" + + ibuf "github.com/33TU/socks/internal" ) const tcpServerResponseStartStackBufSize = 256 @@ -80,13 +82,10 @@ func (s *TCPServerRequestStart) Validate() error { // ReadRequestStart reads and decrypts the full client request startup: // // request salt || encrypted request fixed header || encrypted request variable header -// -// scratch may be nil. func (s *TCPServerRequestStart) ReadRequestStart( src io.Reader, method Method, psk []byte, - scratch []byte, ) (int64, error) { var total int64 @@ -98,12 +97,8 @@ func (s *TCPServerRequestStart) ReadRequestStart( } requestSaltLen := method.SaltSize - var requestSaltBuf []byte - if cap(scratch) >= requestSaltLen { - requestSaltBuf = scratch[:requestSaltLen] - } else { - requestSaltBuf = make([]byte, requestSaltLen) - } + requestSaltBuf := ibuf.GetBytes(requestSaltLen) + defer ibuf.PutBytes(requestSaltBuf) n, err := io.ReadFull(src, requestSaltBuf) total += int64(n) @@ -116,12 +111,8 @@ func (s *TCPServerRequestStart) ReadRequestStart( } encFixedLen := TcpRequestFixedHeaderLen + method.TagSize - var encFixed []byte - if cap(scratch) >= encFixedLen { - encFixed = scratch[:encFixedLen] - } else { - encFixed = make([]byte, encFixedLen) - } + encFixed := ibuf.GetBytes(encFixedLen) + defer ibuf.PutBytes(encFixed) n, err = io.ReadFull(src, encFixed) total += int64(n) @@ -129,19 +120,18 @@ func (s *TCPServerRequestStart) ReadRequestStart( return total, err } - fixedHeader, err := s.RequestCipher.DecodeRequestFixedHeader(encFixed, scratch[:0]) + fixedPlainScratch := ibuf.GetBytes(TcpRequestFixedHeaderLen) + defer ibuf.PutBytes(fixedPlainScratch) + + fixedHeader, err := s.RequestCipher.DecodeRequestFixedHeader(encFixed, fixedPlainScratch[:0]) if err != nil { return total, err } s.FixedHeader = fixedHeader encVariableLen := int(s.FixedHeader.Length) + method.TagSize - var encVariable []byte - if cap(scratch) >= encVariableLen { - encVariable = scratch[:encVariableLen] - } else { - encVariable = make([]byte, encVariableLen) - } + encVariable := ibuf.GetBytes(encVariableLen) + defer ibuf.PutBytes(encVariable) n, err = io.ReadFull(src, encVariable) total += int64(n) @@ -149,7 +139,10 @@ func (s *TCPServerRequestStart) ReadRequestStart( return total, err } - variableHeader, err := s.RequestCipher.DecodeRequestVariableHeader(encVariable, scratch[:0]) + variablePlainScratch := ibuf.GetBytes(int(s.FixedHeader.Length)) + defer ibuf.PutBytes(variablePlainScratch) + + variableHeader, err := s.RequestCipher.DecodeRequestVariableHeader(encVariable, variablePlainScratch[:0]) if err != nil { return total, err } @@ -159,7 +152,6 @@ func (s *TCPServerRequestStart) ReadRequestStart( return total, err } - // Keep our own copy of the request salt in case scratch was reused. s.RequestSalt = append([]byte(nil), requestSaltBuf...) return total, nil @@ -232,13 +224,10 @@ func (s *TCPServerResponseStart) Validate(requestSalt []byte) error { // WriteResponseStart writes the full server response startup: // // response salt || encrypted response header -// -// scratch may be nil. func (s *TCPServerResponseStart) WriteResponseStart( dst io.Writer, timestamp time.Time, requestSalt []byte, - scratch []byte, ) (int64, error) { if s == nil { return 0, fmt.Errorf("nil TCP server response start") @@ -271,16 +260,19 @@ func (s *TCPServerResponseStart) WriteResponseStart( } s.Header = header + plainScratch := ibuf.GetBytes(s.Header.EncodedLen()) + defer ibuf.PutBytes(plainScratch) + var stackBuf [tcpServerResponseStartStackBufSize]byte out := stackBuf[:0] out = append(out, s.ResponseSalt...) - encHeader, err := s.ResponseCipher.EncodeResponseHeaderTo(scratch[:0], &s.Header, nil) + var err error + out, err = s.ResponseCipher.EncodeResponseHeaderTo(out, &s.Header, plainScratch[:0]) if err != nil { return 0, err } - out = append(out, encHeader...) n, err := dst.Write(out) return int64(n), err diff --git a/shadowsocks/tcp_server_stream_test.go b/shadowsocks/tcp_server_stream_test.go index 66fe98f..6178d5c 100644 --- a/shadowsocks/tcp_server_stream_test.go +++ b/shadowsocks/tcp_server_stream_test.go @@ -199,7 +199,7 @@ func TestTCPServerRequestStart_ReadRequestStart(t *testing.T) { wire.Write(encVariable) var s shadowsocks.TCPServerRequestStart - n, err := s.ReadRequestStart(&wire, method, psk, nil) + n, err := s.ReadRequestStart(&wire, method, psk) if err != nil { t.Fatalf("ReadRequestStart() error = %v", err) } @@ -251,7 +251,7 @@ func TestTCPServerRequestStart_ReadRequestStart_ShortRead(t *testing.T) { short := bytes.NewReader(make([]byte, method.SaltSize-1)) var s shadowsocks.TCPServerRequestStart - n, err := s.ReadRequestStart(short, method, psk, nil) + n, err := s.ReadRequestStart(short, method, psk) if err == nil { t.Fatal("expected error, got nil") } @@ -273,7 +273,7 @@ func TestTCPServerRequestStart_ReadRequestStart_InvalidPSK(t *testing.T) { wire.Write(requestSalt) var s shadowsocks.TCPServerRequestStart - _, err := s.ReadRequestStart(&wire, method, psk[:len(psk)-1], nil) + _, err := s.ReadRequestStart(&wire, method, psk[:len(psk)-1]) if err == nil { t.Fatal("expected error, got nil") } @@ -419,7 +419,7 @@ func TestTCPServerResponseStart_WriteResponseStart(t *testing.T) { ts := time.Unix(1700000100, 0) var buf bytes.Buffer - n, err := s.WriteResponseStart(&buf, ts, requestSalt, nil) + n, err := s.WriteResponseStart(&buf, ts, requestSalt) if err != nil { t.Fatalf("WriteResponseStart() error = %v", err) } @@ -460,7 +460,7 @@ func TestTCPServerResponseStart_WriteResponseStart_InvalidRequestSalt(t *testing } var buf bytes.Buffer - _, err := s.WriteResponseStart(&buf, time.Unix(1700000100, 0), responseSalt[:len(responseSalt)-1], nil) + _, err := s.WriteResponseStart(&buf, time.Unix(1700000100, 0), responseSalt[:len(responseSalt)-1]) if err == nil { t.Fatal("expected error, got nil") } @@ -478,7 +478,7 @@ func TestTCPServerResponseStart_WriteResponseStart_NilReceiver(t *testing.T) { var s *shadowsocks.TCPServerResponseStart var buf bytes.Buffer - _, err := s.WriteResponseStart(&buf, time.Unix(1700000100, 0), requestSalt, nil) + _, err := s.WriteResponseStart(&buf, time.Unix(1700000100, 0), requestSalt) if err == nil { t.Fatal("expected error, got nil") } diff --git a/shadowsocks/tcp_stream_cipher.go b/shadowsocks/tcp_stream_cipher.go index cb49bf4..6bf98de 100644 --- a/shadowsocks/tcp_stream_cipher.go +++ b/shadowsocks/tcp_stream_cipher.go @@ -67,7 +67,7 @@ func (s *TCPStreamCipher) SealTo(dst, plaintext []byte) ([]byte, error) { return nil, err } - out := s.AEAD.Seal(dst[:0], s.Nonce[:], plaintext, nil) + out := s.AEAD.Seal(dst, s.Nonce[:], plaintext, nil) s.incNonce() return out, nil } @@ -81,7 +81,7 @@ func (s *TCPStreamCipher) OpenTo(dst, ciphertext []byte) ([]byte, error) { return nil, err } - out, err := s.AEAD.Open(dst[:0], s.Nonce[:], ciphertext, nil) + out, err := s.AEAD.Open(dst, s.Nonce[:], ciphertext, nil) if err != nil { return nil, err } From e96caedab8112904dac57df8990540901b5c510f Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 19:10:29 +0300 Subject: [PATCH 12/25] tcp chunk --- shadowsocks/tcp_chunk_io.go | 143 ++++++++++++ shadowsocks/tcp_chunk_io_test.go | 362 +++++++++++++++++++++++++++++++ 2 files changed, 505 insertions(+) create mode 100644 shadowsocks/tcp_chunk_io.go create mode 100644 shadowsocks/tcp_chunk_io_test.go diff --git a/shadowsocks/tcp_chunk_io.go b/shadowsocks/tcp_chunk_io.go new file mode 100644 index 0000000..fede3b6 --- /dev/null +++ b/shadowsocks/tcp_chunk_io.go @@ -0,0 +1,143 @@ +package shadowsocks + +import ( + "fmt" + "io" + + ibuf "github.com/33TU/socks/internal" +) + +// TCPChunkReader reads encrypted Shadowsocks 2022 TCP chunks. +type TCPChunkReader struct { + Cipher *TCPStreamCipher +} + +// Init initializes the chunk reader for a TCP stream cipher. +func (r *TCPChunkReader) Init(c *TCPStreamCipher) error { + if c == nil { + return fmt.Errorf("nil TCP stream cipher") + } + if err := c.Validate(); err != nil { + return err + } + + r.Cipher = c + return nil +} + +// Validate checks whether the chunk reader is internally valid. +func (r *TCPChunkReader) Validate() error { + if r == nil { + return fmt.Errorf("nil TCP chunk reader") + } + if r.Cipher == nil { + return fmt.Errorf("missing TCP stream cipher") + } + return r.Cipher.Validate() +} + +// ReadChunkTo reads a full encrypted TCP chunk from src, decrypts it, and +// appends the plaintext payload into dst. It returns the resulting slice and +// total bytes read. +func (r *TCPChunkReader) ReadChunkTo(dst []byte, src io.Reader) ([]byte, int64, error) { + if err := r.Validate(); err != nil { + return nil, 0, err + } + + var total int64 + + encLenBuf := ibuf.GetBytes(r.Cipher.EncryptedChunkLength()) + defer ibuf.PutBytes(encLenBuf) + + n, err := io.ReadFull(src, encLenBuf) + total += int64(n) + if err != nil { + return nil, total, err + } + + lenScratch := ibuf.GetBytes(TcpChunkLengthLen) + defer ibuf.PutBytes(lenScratch) + + payloadLen, err := r.Cipher.DecodeChunkLength(encLenBuf, lenScratch[:0]) + if err != nil { + return nil, total, err + } + + encPayloadBuf := ibuf.GetBytes(r.Cipher.EncryptedPayloadLength(int(payloadLen))) + defer ibuf.PutBytes(encPayloadBuf) + + n, err = io.ReadFull(src, encPayloadBuf) + total += int64(n) + if err != nil { + return nil, total, err + } + + dst, err = r.Cipher.DecodeChunkPayloadTo(dst, encPayloadBuf) + if err != nil { + return nil, total, err + } + + return dst, total, nil +} + +// TCPChunkWriter writes encrypted Shadowsocks 2022 TCP chunks. +type TCPChunkWriter struct { + Cipher *TCPStreamCipher +} + +// Init initializes the chunk writer for a TCP stream cipher. +func (w *TCPChunkWriter) Init(c *TCPStreamCipher) error { + if c == nil { + return fmt.Errorf("nil TCP stream cipher") + } + if err := c.Validate(); err != nil { + return err + } + + w.Cipher = c + return nil +} + +// Validate checks whether the chunk writer is internally valid. +func (w *TCPChunkWriter) Validate() error { + if w == nil { + return fmt.Errorf("nil TCP chunk writer") + } + if w.Cipher == nil { + return fmt.Errorf("missing TCP stream cipher") + } + return w.Cipher.Validate() +} + +// WriteChunk writes a full encrypted TCP chunk to dst and returns the total +// bytes written. +func (w *TCPChunkWriter) WriteChunk(dst io.Writer, payload []byte) (int64, error) { + if err := w.Validate(); err != nil { + return 0, err + } + if len(payload) > 0xFFFF { + return 0, fmt.Errorf("payload too large: got %d, max %d", len(payload), 0xFFFF) + } + + out := ibuf.GetBytes( + w.Cipher.EncryptedChunkLength() + + w.Cipher.EncryptedPayloadLength(len(payload)), + ) + defer ibuf.PutBytes(out) + + out = out[:0] + + var err error + out, err = w.Cipher.EncodeChunkLengthTo(out, uint16(len(payload))) + if err != nil { + return 0, err + } + + out, err = w.Cipher.EncodeChunkPayloadTo(out, payload) + if err != nil { + return 0, err + } + + n, err := dst.Write(out) + return int64(n), err +} diff --git a/shadowsocks/tcp_chunk_io_test.go b/shadowsocks/tcp_chunk_io_test.go new file mode 100644 index 0000000..c887714 --- /dev/null +++ b/shadowsocks/tcp_chunk_io_test.go @@ -0,0 +1,362 @@ +package shadowsocks_test + +import ( + "bytes" + "io" + "strings" + "testing" + + "github.com/33TU/socks/shadowsocks" +) + +func newTCPChunkTestMethod(t *testing.T) shadowsocks.Method { + t.Helper() + + method, err := shadowsocks.ParseMethod(shadowsocks.Method2022Blake3AES128GCM) + if err != nil { + t.Fatalf("ParseMethod() error = %v", err) + } + + return method +} + +func newTCPChunkTestPSKAndSalt(method shadowsocks.Method) ([]byte, []byte) { + psk := make([]byte, method.KeySize) + salt := make([]byte, method.SaltSize) + + for i := range psk { + psk[i] = byte(i + 1) + } + for i := range salt { + salt[i] = byte(i + 101) + } + + return psk, salt +} + +func newTCPChunkCipherPair(t *testing.T) (*shadowsocks.TCPStreamCipher, *shadowsocks.TCPStreamCipher) { + t.Helper() + + method := newTCPChunkTestMethod(t) + psk, salt := newTCPChunkTestPSKAndSalt(method) + + enc, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk, salt) + if err != nil { + t.Fatalf("NewTCPStreamCipherFromPSK(enc) error = %v", err) + } + + dec, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk, salt) + if err != nil { + t.Fatalf("NewTCPStreamCipherFromPSK(dec) error = %v", err) + } + + return enc, dec +} + +func TestTCPChunkReader_Init_Validate(t *testing.T) { + t.Parallel() + + _, dec := newTCPChunkCipherPair(t) + + t.Run("valid", func(t *testing.T) { + t.Parallel() + + var r shadowsocks.TCPChunkReader + if err := r.Init(dec); err != nil { + t.Fatalf("Init() error = %v", err) + } + if err := r.Validate(); err != nil { + t.Fatalf("Validate() error = %v", err) + } + }) + + t.Run("nil cipher", func(t *testing.T) { + t.Parallel() + + var r shadowsocks.TCPChunkReader + err := r.Init(nil) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "nil TCP stream cipher") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("nil reader validate", func(t *testing.T) { + t.Parallel() + + var r *shadowsocks.TCPChunkReader + err := r.Validate() + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "nil TCP chunk reader") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("missing cipher", func(t *testing.T) { + t.Parallel() + + r := &shadowsocks.TCPChunkReader{} + err := r.Validate() + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "missing TCP stream cipher") { + t.Fatalf("unexpected error: %v", err) + } + }) +} + +func TestTCPChunkWriter_Init_Validate(t *testing.T) { + t.Parallel() + + enc, _ := newTCPChunkCipherPair(t) + + t.Run("valid", func(t *testing.T) { + t.Parallel() + + var w shadowsocks.TCPChunkWriter + if err := w.Init(enc); err != nil { + t.Fatalf("Init() error = %v", err) + } + if err := w.Validate(); err != nil { + t.Fatalf("Validate() error = %v", err) + } + }) + + t.Run("nil cipher", func(t *testing.T) { + t.Parallel() + + var w shadowsocks.TCPChunkWriter + err := w.Init(nil) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "nil TCP stream cipher") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("nil writer validate", func(t *testing.T) { + t.Parallel() + + var w *shadowsocks.TCPChunkWriter + err := w.Validate() + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "nil TCP chunk writer") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("missing cipher", func(t *testing.T) { + t.Parallel() + + w := &shadowsocks.TCPChunkWriter{} + err := w.Validate() + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "missing TCP stream cipher") { + t.Fatalf("unexpected error: %v", err) + } + }) +} + +func TestTCPChunkWriterReader_RoundTrip(t *testing.T) { + t.Parallel() + + enc, dec := newTCPChunkCipherPair(t) + + var w shadowsocks.TCPChunkWriter + if err := w.Init(enc); err != nil { + t.Fatalf("writer Init() error = %v", err) + } + + var r shadowsocks.TCPChunkReader + if err := r.Init(dec); err != nil { + t.Fatalf("reader Init() error = %v", err) + } + + payload := []byte("hello chunk") + + var buf bytes.Buffer + nw, err := w.WriteChunk(&buf, payload) + if err != nil { + t.Fatalf("WriteChunk() error = %v", err) + } + if nw != int64(buf.Len()) { + t.Fatalf("WriteChunk() wrote %d bytes, buffer has %d", nw, buf.Len()) + } + + got, nr, err := r.ReadChunkTo(nil, &buf) + if err != nil { + t.Fatalf("ReadChunk() error = %v", err) + } + if nr != nw { + t.Fatalf("ReadChunk() read %d bytes, want %d", nr, nw) + } + if !bytes.Equal(got, payload) { + t.Fatalf("ReadChunk() = %q, want %q", got, payload) + } +} + +func TestTCPChunkReader_ReadChunkTo_AppendsToDst(t *testing.T) { + t.Parallel() + + enc, dec := newTCPChunkCipherPair(t) + + var w shadowsocks.TCPChunkWriter + if err := w.Init(enc); err != nil { + t.Fatalf("writer Init() error = %v", err) + } + + var r shadowsocks.TCPChunkReader + if err := r.Init(dec); err != nil { + t.Fatalf("reader Init() error = %v", err) + } + + payload := []byte("payload") + prefix := []byte("prefix:") + + var buf bytes.Buffer + if _, err := w.WriteChunk(&buf, payload); err != nil { + t.Fatalf("WriteChunk() error = %v", err) + } + + got, _, err := r.ReadChunkTo(append([]byte(nil), prefix...), &buf) + if err != nil { + t.Fatalf("ReadChunkTo() error = %v", err) + } + + want := append(append([]byte(nil), prefix...), payload...) + if !bytes.Equal(got, want) { + t.Fatalf("ReadChunkTo() = %q, want %q", got, want) + } +} + +func TestTCPChunkWriter_WriteChunk_TooLarge(t *testing.T) { + t.Parallel() + + enc, _ := newTCPChunkCipherPair(t) + + var w shadowsocks.TCPChunkWriter + if err := w.Init(enc); err != nil { + t.Fatalf("Init() error = %v", err) + } + + payload := make([]byte, 0x10000) + _, err := w.WriteChunk(io.Discard, payload) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "payload too large") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestTCPChunkReader_ReadChunk_ShortLengthRead(t *testing.T) { + t.Parallel() + + _, dec := newTCPChunkCipherPair(t) + + var r shadowsocks.TCPChunkReader + if err := r.Init(dec); err != nil { + t.Fatalf("Init() error = %v", err) + } + + short := bytes.NewReader([]byte{1, 2, 3}) + + _, n, err := r.ReadChunkTo(nil, short) + if err == nil { + t.Fatal("expected error, got nil") + } + if n != 3 { + t.Fatalf("ReadChunk() read %d bytes, want 3", n) + } + if err != io.EOF && err != io.ErrUnexpectedEOF { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestTCPChunkReader_ReadChunk_ShortPayloadRead(t *testing.T) { + t.Parallel() + + enc, dec := newTCPChunkCipherPair(t) + + var w shadowsocks.TCPChunkWriter + if err := w.Init(enc); err != nil { + t.Fatalf("writer Init() error = %v", err) + } + + var r shadowsocks.TCPChunkReader + if err := r.Init(dec); err != nil { + t.Fatalf("reader Init() error = %v", err) + } + + payload := []byte("hello payload") + var buf bytes.Buffer + nw, err := w.WriteChunk(&buf, payload) + if err != nil { + t.Fatalf("WriteChunk() error = %v", err) + } + + wire := buf.Bytes() + shortWire := wire[:len(wire)-2] + + _, nr, err := r.ReadChunkTo(nil, bytes.NewReader(shortWire)) + if err == nil { + t.Fatal("expected error, got nil") + } + if nr != nw-2 { + t.Fatalf("ReadChunk() read %d bytes, want %d", nr, nw-2) + } + if err != io.EOF && err != io.ErrUnexpectedEOF { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestTCPChunkReader_ReadChunk_WrongCipherFails(t *testing.T) { + t.Parallel() + + enc, _ := newTCPChunkCipherPair(t) + + method := newTCPChunkTestMethod(t) + psk := make([]byte, method.KeySize) + salt := make([]byte, method.SaltSize) + for i := range psk { + psk[i] = byte(i + 9) + } + for i := range salt { + salt[i] = byte(i + 19) + } + + wrongDec, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk, salt) + if err != nil { + t.Fatalf("NewTCPStreamCipherFromPSK() error = %v", err) + } + + var w shadowsocks.TCPChunkWriter + if err := w.Init(enc); err != nil { + t.Fatalf("writer Init() error = %v", err) + } + + var r shadowsocks.TCPChunkReader + if err := r.Init(wrongDec); err != nil { + t.Fatalf("reader Init() error = %v", err) + } + + var buf bytes.Buffer + if _, err := w.WriteChunk(&buf, []byte("hello")); err != nil { + t.Fatalf("WriteChunk() error = %v", err) + } + + _, _, err = r.ReadChunkTo(nil, &buf) + if err == nil { + t.Fatal("expected decrypt error, got nil") + } +} From a9e525a0dc5ffe370b14c30291e022ac59405ebe Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 19:27:43 +0300 Subject: [PATCH 13/25] shadowsocks tcp_conn and dialer --- shadowsocks/dialer.go | 173 ++++++++++ shadowsocks/dialer_test.go | 309 +++++++++++++++++ shadowsocks/tcp_conn.go | 51 +++ shadowsocks/tcp_conn_test.go | 317 ++++++++++++++++++ .../{tcp_header_err.go => tcp_errors.go} | 0 5 files changed, 850 insertions(+) create mode 100644 shadowsocks/tcp_conn.go create mode 100644 shadowsocks/tcp_conn_test.go rename shadowsocks/{tcp_header_err.go => tcp_errors.go} (100%) diff --git a/shadowsocks/dialer.go b/shadowsocks/dialer.go index 086c889..47c16e8 100644 --- a/shadowsocks/dialer.go +++ b/shadowsocks/dialer.go @@ -1,9 +1,11 @@ package shadowsocks import ( + "context" "fmt" "net" "net/url" + "time" socksnet "github.com/33TU/socks/net" ) @@ -93,3 +95,174 @@ func NewDialerFromURLString(rawURL string, dialer socksnet.Dialer) (*Dialer, err } return NewDialerFromURL(u, dialer) } + +// ProxyAddress returns the configured Shadowsocks proxy address. +func (d *Dialer) ProxyAddress() string { + return d.ProxyAddr +} + +// DialContext establishes a TCP connection via the Shadowsocks proxy. +func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + conn, err := d.dialProxy(ctx, network) + if err != nil { + return nil, err + } + + return d.DialConnContext(ctx, conn, network, address) +} + +// Dial establishes a TCP connection via the Shadowsocks proxy using background context. +func (d *Dialer) Dial(network, address string) (net.Conn, error) { + return d.DialContext(context.Background(), network, address) +} + +// DialConnContext upgrades an existing connection into a Shadowsocks TCP stream. +func (d *Dialer) DialConnContext(ctx context.Context, conn net.Conn, network, address string) (net.Conn, error) { + if d == nil { + conn.Close() + return nil, fmt.Errorf("nil shadowsocks dialer") + } + if d.Config == nil { + conn.Close() + return nil, fmt.Errorf("missing shadowsocks config") + } + if err := d.Config.Validate(); err != nil { + conn.Close() + return nil, err + } + + method, err := ParseMethod(d.Config.Method) + if err != nil { + conn.Close() + return nil, err + } + + psk, err := DecodePSKTo(nil, method, d.Config.PSK) + if err != nil { + conn.Close() + return nil, err + } + + target, err := parseTargetAddr(address) + if err != nil { + conn.Close() + return nil, err + } + + requestSalt := make([]byte, method.SaltSize) + if err := FillSaltTo(requestSalt, method); err != nil { + conn.Close() + return nil, err + } + + // cancellation and deadline handling + cleanup := bindConnToContext(ctx, conn) + defer cleanup() + + var reqStart TCPClientRequestStart + if err := reqStart.Init(method, psk, requestSalt); err != nil { + conn.Close() + return nil, err + } + + if _, err := reqStart.WriteRequestStart(conn, time.Now(), target, []byte{0}, nil); err != nil { + conn.Close() + return nil, err + } + + respStart, _, err := reqStart.ReadResponseStart(conn) + if err != nil { + conn.Close() + return nil, err + } + + var reader TCPChunkReader + if err := reader.Init(respStart.ResponseCipher); err != nil { + conn.Close() + return nil, err + } + + var writer TCPChunkWriter + if err := writer.Init(reqStart.RequestCipher); err != nil { + conn.Close() + return nil, err + } + + return &TcpConn{ + Conn: conn, + Reader: reader, + Writer: writer, + }, nil +} + +// DialConn upgrades an existing connection using background context. +func (d *Dialer) DialConn(conn net.Conn, network, address string) (net.Conn, error) { + return d.DialConnContext(context.Background(), conn, network, address) +} + +// dialProxy connects to the Shadowsocks proxy server. +func (d *Dialer) dialProxy(ctx context.Context, network string) (net.Conn, error) { + dialer := d.Dialer + if dialer == nil { + dialer = socksnet.DefaultDialer + } + return dialer.DialContext(ctx, network, d.ProxyAddr) +} + +func parseTargetAddr(address string) (Addr, error) { + host, portStr, err := net.SplitHostPort(address) + if err != nil { + return Addr{}, err + } + + port, err := net.DefaultResolver.LookupPort(context.Background(), "tcp", portStr) + if err != nil { + return Addr{}, err + } + + ip := net.ParseIP(host) + switch { + case ip != nil && ip.To4() != nil: + return Addr{ + AddrType: AddrTypeIPv4, + IP: ip.To4(), + Port: uint16(port), + }, nil + + case ip != nil && ip.To16() != nil: + return Addr{ + AddrType: AddrTypeIPv6, + IP: ip.To16(), + Port: uint16(port), + }, nil + + default: + return Addr{ + AddrType: AddrTypeDomain, + Domain: host, + Port: uint16(port), + }, nil + } +} + +// bindConnToContext sets connection deadlines based on context and ensures cleanup on cancellation. +func bindConnToContext(ctx context.Context, conn net.Conn) (cleanup func()) { + if deadline, ok := ctx.Deadline(); ok { + _ = conn.SetDeadline(deadline) + } + + exitCh := make(chan struct{}) + + go func() { + select { + case <-ctx.Done(): + _ = conn.Close() + case <-exitCh: + } + }() + + return func() { + close(exitCh) + _ = conn.SetDeadline(time.Time{}) + } +} diff --git a/shadowsocks/dialer_test.go b/shadowsocks/dialer_test.go index f4ee080..75b766b 100644 --- a/shadowsocks/dialer_test.go +++ b/shadowsocks/dialer_test.go @@ -2,10 +2,14 @@ package shadowsocks_test import ( "bytes" + "context" "encoding/base64" + "io" + "net" "net/url" "strings" "testing" + "time" socksnet "github.com/33TU/socks/net" "github.com/33TU/socks/shadowsocks" @@ -15,6 +19,69 @@ func mustBase64Key(n int) string { return base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{0x42}, n)) } +func startMockShadowsocksServer(t *testing.T, handle func(net.Conn)) (string, func()) { + t.Helper() + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + + go func() { + for { + conn, err := ln.Accept() + if err != nil { + return + } + go handle(conn) + } + }() + + return ln.Addr().String(), func() { _ = ln.Close() } +} + +func decodePSKForMethod(t *testing.T, method shadowsocks.Method, psk string) []byte { + t.Helper() + + raw, err := shadowsocks.DecodePSKTo(nil, method, psk) + if err != nil { + t.Fatalf("DecodePSKTo() error = %v", err) + } + return raw +} + +func TestNewDialer(t *testing.T) { + t.Parallel() + + cfg := &shadowsocks.Config{ + Method: shadowsocks.Method2022Blake3AES256GCM, + PSK: mustBase64Key(32), + } + + d := shadowsocks.NewDialer("127.0.0.1:8388", cfg, nil) + if d == nil { + t.Fatal("NewDialer() returned nil") + } + if d.ProxyAddr != "127.0.0.1:8388" { + t.Fatalf("ProxyAddr = %q, want %q", d.ProxyAddr, "127.0.0.1:8388") + } + if d.Config != cfg { + t.Fatal("Config pointer mismatch") + } + if d.Dialer == nil { + t.Fatal("Dialer is nil, want default dialer") + } +} + +func TestDialer_ProxyAddress(t *testing.T) { + t.Parallel() + + d := &shadowsocks.Dialer{ProxyAddr: "127.0.0.1:8388"} + if got := d.ProxyAddress(); got != "127.0.0.1:8388" { + t.Fatalf("ProxyAddress() = %q, want %q", got, "127.0.0.1:8388") + } +} + func TestNewDialerFromURL(t *testing.T) { t.Parallel() @@ -200,3 +267,245 @@ func TestNewDialerFromURLString(t *testing.T) { t.Fatalf("Config.Tag = %q, want %q", d.Config.Tag, "local") } } + +func TestNewDialerFromURLString_Invalid(t *testing.T) { + t.Parallel() + + _, err := shadowsocks.NewDialerFromURLString("://bad url", nil) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid proxy URL") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestDialer_DialConnContext_Errors(t *testing.T) { + t.Parallel() + + t.Run("nil dialer", func(t *testing.T) { + t.Parallel() + + c1, c2 := net.Pipe() + defer c2.Close() + + var d *shadowsocks.Dialer + _, err := d.DialConnContext(context.Background(), c1, "tcp", "example.com:443") + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "nil shadowsocks dialer") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("missing config", func(t *testing.T) { + t.Parallel() + + c1, c2 := net.Pipe() + defer c2.Close() + + d := &shadowsocks.Dialer{} + _, err := d.DialConnContext(context.Background(), c1, "tcp", "example.com:443") + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "missing shadowsocks config") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("invalid config", func(t *testing.T) { + t.Parallel() + + c1, c2 := net.Pipe() + defer c2.Close() + + d := &shadowsocks.Dialer{ + Config: &shadowsocks.Config{ + Method: shadowsocks.Method2022Blake3AES128GCM, + PSK: mustBase64Key(32), + }, + } + _, err := d.DialConnContext(context.Background(), c1, "tcp", "example.com:443") + if err == nil { + t.Fatal("expected error, got nil") + } + }) + + t.Run("invalid target address", func(t *testing.T) { + t.Parallel() + + c1, c2 := net.Pipe() + defer c2.Close() + + d := &shadowsocks.Dialer{ + Config: &shadowsocks.Config{ + Method: shadowsocks.Method2022Blake3AES128GCM, + PSK: mustBase64Key(16), + }, + } + _, err := d.DialConnContext(context.Background(), c1, "tcp", "not-a-hostport") + if err == nil { + t.Fatal("expected error, got nil") + } + }) +} + +func TestDialer_DialContext_Success(t *testing.T) { + t.Parallel() + + methodName := shadowsocks.Method2022Blake3AES128GCM + pskB64 := mustBase64Key(16) + + method, err := shadowsocks.ParseMethod(methodName) + if err != nil { + t.Fatalf("ParseMethod() error = %v", err) + } + psk := decodePSKForMethod(t, method, pskB64) + + proxyAddr, stop := startMockShadowsocksServer(t, func(c net.Conn) { + defer c.Close() + + var reqStart shadowsocks.TCPServerRequestStart + _, err := reqStart.ReadRequestStart(c, method, psk) + if err != nil { + t.Errorf("server: ReadRequestStart() error = %v", err) + return + } + + if reqStart.Header.Target.AddrType != shadowsocks.AddrTypeDomain { + t.Errorf("server: target type = %v, want domain", reqStart.Header.Target.AddrType) + return + } + if reqStart.Header.Target.Domain != "example.com" { + t.Errorf("server: target domain = %q, want %q", reqStart.Header.Target.Domain, "example.com") + return + } + if reqStart.Header.Target.Port != 443 { + t.Errorf("server: target port = %d, want %d", reqStart.Header.Target.Port, 443) + return + } + + responseSalt := bytes.Repeat([]byte{0x33}, method.SaltSize) + + var respStart shadowsocks.TCPServerResponseStart + if err := respStart.Init(method, psk, responseSalt); err != nil { + t.Errorf("server: response Init() error = %v", err) + return + } + + if _, err := respStart.WriteResponseStart(c, time.Now(), reqStart.RequestSalt); err != nil { + t.Errorf("server: WriteResponseStart() error = %v", err) + return + } + + var reader shadowsocks.TCPChunkReader + if err := reader.Init(reqStart.RequestCipher); err != nil { + t.Errorf("server: reader Init() error = %v", err) + return + } + + var writer shadowsocks.TCPChunkWriter + if err := writer.Init(respStart.ResponseCipher); err != nil { + t.Errorf("server: writer Init() error = %v", err) + return + } + + payload, _, err := reader.ReadChunkTo(nil, c) + if err != nil { + t.Errorf("server: ReadChunk() error = %v", err) + return + } + if string(payload) != "ping" { + t.Errorf("server: payload = %q, want %q", payload, "ping") + return + } + + if _, err := writer.WriteChunk(c, []byte("pong")); err != nil { + t.Errorf("server: WriteChunk() error = %v", err) + return + } + }) + defer stop() + + d := shadowsocks.NewDialer(proxyAddr, &shadowsocks.Config{ + Method: methodName, + PSK: pskB64, + }, nil) + + conn, err := d.DialContext(context.Background(), "tcp", "example.com:443") + if err != nil { + t.Fatalf("DialContext() error = %v", err) + } + defer conn.Close() + + if _, err := conn.Write([]byte("ping")); err != nil { + t.Fatalf("conn.Write() error = %v", err) + } + + buf := make([]byte, 4) + if _, err := io.ReadFull(conn, buf); err != nil { + t.Fatalf("conn.Read() error = %v", err) + } + if string(buf) != "pong" { + t.Fatalf("response = %q, want %q", buf, "pong") + } +} + +func TestDialer_DialContext_Deadline(t *testing.T) { + t.Parallel() + + methodName := shadowsocks.Method2022Blake3AES128GCM + pskB64 := mustBase64Key(16) + + method, err := shadowsocks.ParseMethod(methodName) + if err != nil { + t.Fatalf("ParseMethod() error = %v", err) + } + psk := decodePSKForMethod(t, method, pskB64) + + proxyAddr, stop := startMockShadowsocksServer(t, func(c net.Conn) { + defer c.Close() + + var reqStart shadowsocks.TCPServerRequestStart + if _, err := reqStart.ReadRequestStart(c, method, psk); err != nil { + return + } + + responseSalt := bytes.Repeat([]byte{0x33}, method.SaltSize) + + var respStart shadowsocks.TCPServerResponseStart + if err := respStart.Init(method, psk, responseSalt); err != nil { + return + } + if _, err := respStart.WriteResponseStart(c, time.Now(), reqStart.RequestSalt); err != nil { + return + } + + time.Sleep(200 * time.Millisecond) + }) + defer stop() + + d := shadowsocks.NewDialer(proxyAddr, &shadowsocks.Config{ + Method: methodName, + PSK: pskB64, + }, nil) + + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + conn, err := d.DialContext(ctx, "tcp", "example.com:443") + if err != nil { + t.Fatalf("DialContext() error = %v", err) + } + defer conn.Close() + + time.Sleep(150 * time.Millisecond) + + buf := make([]byte, 1) + _, err = conn.Read(buf) + if err == nil { + t.Fatal("expected read error after deadline") + } +} diff --git a/shadowsocks/tcp_conn.go b/shadowsocks/tcp_conn.go new file mode 100644 index 0000000..8992823 --- /dev/null +++ b/shadowsocks/tcp_conn.go @@ -0,0 +1,51 @@ +package shadowsocks + +import "net" + +// TcpConn wraps a proxy TCP connection and exposes plain application reads/writes +// while handling Shadowsocks 2022 TCP chunk framing internally. +type TcpConn struct { + net.Conn + Reader TCPChunkReader + Writer TCPChunkWriter + readBuf []byte +} + +func (c *TcpConn) Read(p []byte) (int, error) { + if len(c.readBuf) == 0 { + buf, _, err := c.Reader.ReadChunkTo(nil, c.Conn) + if err != nil { + return 0, err + } + c.readBuf = buf + } + + n := copy(p, c.readBuf) + c.readBuf = c.readBuf[n:] + return n, nil +} + +func (c *TcpConn) Write(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + + written := 0 + for len(p) > 0 { + nn := len(p) + if nn > 0xFFFF { + nn = 0xFFFF + } + + if _, err := c.Writer.WriteChunk(c.Conn, p[:nn]); err != nil { + return written, err + } + + written += nn + p = p[nn:] + } + + return written, nil +} + +var _ net.Conn = (*TcpConn)(nil) diff --git a/shadowsocks/tcp_conn_test.go b/shadowsocks/tcp_conn_test.go new file mode 100644 index 0000000..5037c48 --- /dev/null +++ b/shadowsocks/tcp_conn_test.go @@ -0,0 +1,317 @@ +package shadowsocks_test + +import ( + "bytes" + "errors" + "net" + "testing" + + "github.com/33TU/socks/shadowsocks" +) + +func newTCPConnTestMethod(t *testing.T) shadowsocks.Method { + t.Helper() + + method, err := shadowsocks.ParseMethod(shadowsocks.Method2022Blake3AES128GCM) + if err != nil { + t.Fatalf("ParseMethod() error = %v", err) + } + + return method +} + +func newTCPConnTestPSKAndSalt(method shadowsocks.Method) ([]byte, []byte) { + psk := make([]byte, method.KeySize) + salt := make([]byte, method.SaltSize) + + for i := range psk { + psk[i] = byte(i + 1) + } + for i := range salt { + salt[i] = byte(i + 101) + } + + return psk, salt +} + +func newTCPConnCipherPair(t *testing.T) (*shadowsocks.TCPStreamCipher, *shadowsocks.TCPStreamCipher) { + t.Helper() + + method := newTCPConnTestMethod(t) + psk, salt := newTCPConnTestPSKAndSalt(method) + + enc, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk, salt) + if err != nil { + t.Fatalf("NewTCPStreamCipherFromPSK(enc) error = %v", err) + } + + dec, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk, salt) + if err != nil { + t.Fatalf("NewTCPStreamCipherFromPSK(dec) error = %v", err) + } + + return enc, dec +} + +func TestTCPConn_Read(t *testing.T) { + t.Parallel() + + serverConn, clientConn := net.Pipe() + defer serverConn.Close() + defer clientConn.Close() + + enc, dec := newTCPConnCipherPair(t) + + var writer shadowsocks.TCPChunkWriter + if err := writer.Init(enc); err != nil { + t.Fatalf("writer.Init() error = %v", err) + } + + var reader shadowsocks.TCPChunkReader + if err := reader.Init(dec); err != nil { + t.Fatalf("reader.Init() error = %v", err) + } + + c := &shadowsocks.TcpConn{ + Conn: clientConn, + Reader: reader, + Writer: writer, + } + + want := []byte("hello world") + + go func() { + defer serverConn.Close() + if _, err := writer.WriteChunk(serverConn, want); err != nil { + t.Errorf("WriteChunk() error = %v", err) + } + }() + + buf := make([]byte, len(want)) + n, err := c.Read(buf) + if err != nil { + t.Fatalf("Read() error = %v", err) + } + if n != len(want) { + t.Fatalf("Read() n = %d, want %d", n, len(want)) + } + if !bytes.Equal(buf[:n], want) { + t.Fatalf("Read() = %q, want %q", buf[:n], want) + } +} + +func TestTCPConn_Read_PartialBuffered(t *testing.T) { + t.Parallel() + + serverConn, clientConn := net.Pipe() + defer serverConn.Close() + defer clientConn.Close() + + enc, dec := newTCPConnCipherPair(t) + + var writer shadowsocks.TCPChunkWriter + if err := writer.Init(enc); err != nil { + t.Fatalf("writer.Init() error = %v", err) + } + + var reader shadowsocks.TCPChunkReader + if err := reader.Init(dec); err != nil { + t.Fatalf("reader.Init() error = %v", err) + } + + c := &shadowsocks.TcpConn{ + Conn: clientConn, + Reader: reader, + Writer: writer, + } + + want := []byte("hello world") + + go func() { + defer serverConn.Close() + if _, err := writer.WriteChunk(serverConn, want); err != nil { + t.Errorf("WriteChunk() error = %v", err) + } + }() + + buf1 := make([]byte, 5) + n1, err := c.Read(buf1) + if err != nil { + t.Fatalf("first Read() error = %v", err) + } + if n1 != 5 { + t.Fatalf("first Read() n = %d, want 5", n1) + } + if !bytes.Equal(buf1[:n1], []byte("hello")) { + t.Fatalf("first Read() = %q, want %q", buf1[:n1], "hello") + } + + buf2 := make([]byte, 6) + n2, err := c.Read(buf2) + if err != nil { + t.Fatalf("second Read() error = %v", err) + } + if n2 != 6 { + t.Fatalf("second Read() n = %d, want 6", n2) + } + if !bytes.Equal(buf2[:n2], []byte(" world")) { + t.Fatalf("second Read() = %q, want %q", buf2[:n2], " world") + } +} + +func TestTCPConn_Write(t *testing.T) { + t.Parallel() + + serverConn, clientConn := net.Pipe() + defer serverConn.Close() + defer clientConn.Close() + + enc, dec := newTCPConnCipherPair(t) + + var writer shadowsocks.TCPChunkWriter + if err := writer.Init(enc); err != nil { + t.Fatalf("writer.Init() error = %v", err) + } + + var reader shadowsocks.TCPChunkReader + if err := reader.Init(dec); err != nil { + t.Fatalf("reader.Init() error = %v", err) + } + + c := &shadowsocks.TcpConn{ + Conn: clientConn, + Reader: reader, + Writer: writer, + } + + want := []byte("hello world") + errCh := make(chan error, 1) + + go func() { + defer serverConn.Close() + + got, _, err := reader.ReadChunkTo(nil, serverConn) + if err != nil { + errCh <- err + return + } + if !bytes.Equal(got, want) { + errCh <- errors.New("payload mismatch") + return + } + errCh <- nil + }() + + n, err := c.Write(want) + if err != nil { + t.Fatalf("Write() error = %v", err) + } + if n != len(want) { + t.Fatalf("Write() n = %d, want %d", n, len(want)) + } + + if err := <-errCh; err != nil { + t.Fatalf("server read error = %v", err) + } +} + +func TestTCPConn_Write_Empty(t *testing.T) { + t.Parallel() + + serverConn, clientConn := net.Pipe() + defer serverConn.Close() + defer clientConn.Close() + + enc, dec := newTCPConnCipherPair(t) + + var writer shadowsocks.TCPChunkWriter + if err := writer.Init(enc); err != nil { + t.Fatalf("writer.Init() error = %v", err) + } + + var reader shadowsocks.TCPChunkReader + if err := reader.Init(dec); err != nil { + t.Fatalf("reader.Init() error = %v", err) + } + + c := &shadowsocks.TcpConn{ + Conn: clientConn, + Reader: reader, + Writer: writer, + } + + n, err := c.Write(nil) + if err != nil { + t.Fatalf("Write() error = %v", err) + } + if n != 0 { + t.Fatalf("Write() n = %d, want 0", n) + } +} + +func TestTCPConn_Write_SplitsLargePayload(t *testing.T) { + t.Parallel() + + serverConn, clientConn := net.Pipe() + defer serverConn.Close() + defer clientConn.Close() + + enc, dec := newTCPConnCipherPair(t) + + var writer shadowsocks.TCPChunkWriter + if err := writer.Init(enc); err != nil { + t.Fatalf("writer.Init() error = %v", err) + } + + var reader shadowsocks.TCPChunkReader + if err := reader.Init(dec); err != nil { + t.Fatalf("reader.Init() error = %v", err) + } + + c := &shadowsocks.TcpConn{ + Conn: clientConn, + Reader: reader, + Writer: writer, + } + + want := bytes.Repeat([]byte("a"), 0x10000+123) + done := make(chan error, 1) + + go func() { + defer serverConn.Close() + + var got []byte + + part1, _, err := reader.ReadChunkTo(nil, serverConn) + if err != nil { + done <- err + return + } + got = append(got, part1...) + + part2, _, err := reader.ReadChunkTo(nil, serverConn) + if err != nil { + done <- err + return + } + got = append(got, part2...) + + if !bytes.Equal(got, want) { + done <- errors.New("payload mismatch") + return + } + done <- nil + }() + + n, err := c.Write(want) + if err != nil { + t.Fatalf("Write() error = %v", err) + } + if n != len(want) { + t.Fatalf("Write() n = %d, want %d", n, len(want)) + } + + if err := <-done; err != nil { + t.Fatalf("server read error = %v", err) + } +} diff --git a/shadowsocks/tcp_header_err.go b/shadowsocks/tcp_errors.go similarity index 100% rename from shadowsocks/tcp_header_err.go rename to shadowsocks/tcp_errors.go From 964cff89fcecebd7ba3065f9c83318ee17486f04 Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 19:29:19 +0300 Subject: [PATCH 14/25] remove backups --- shadowsocks (1).zip | Bin 32375 -> 0 bytes shadowsocks.zip | Bin 8782 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 shadowsocks (1).zip delete mode 100644 shadowsocks.zip diff --git a/shadowsocks (1).zip b/shadowsocks (1).zip deleted file mode 100644 index ccad9ef37a86b1d89360609dbe8ab3493d70ab9b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32375 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

66RL3yW! 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 D;_0}{ diff --git a/shadowsocks.zip b/shadowsocks.zip deleted file mode 100644 index f0c605049167617c1b653be61eb819437c2cfe6b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8782 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 T%+G;H&o9R3N*3YO&u{+^ghi#u From 398d7365db536e0cbc515561ef6709a599ef55dd Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 20:21:32 +0300 Subject: [PATCH 15/25] shadowsocks example+fixes --- examples/shadowsocks-dial/main.go | 57 ++++++++++++++++++ shadowsocks/dialer.go | 20 ++----- shadowsocks/tcp_client_stream.go | 34 +++++++++-- shadowsocks/tcp_conn.go | 80 +++++++++++++++++++++++-- shadowsocks/tcp_response_header.go | 27 ++++----- shadowsocks/tcp_response_header_test.go | 72 ++++++++++------------ shadowsocks/tcp_stream_cipher.go | 2 +- shadowsocks/tcp_stream_cipher_test.go | 6 +- 8 files changed, 213 insertions(+), 85 deletions(-) create mode 100644 examples/shadowsocks-dial/main.go diff --git a/examples/shadowsocks-dial/main.go b/examples/shadowsocks-dial/main.go new file mode 100644 index 0000000..cae3030 --- /dev/null +++ b/examples/shadowsocks-dial/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "context" + "flag" + "fmt" + "io" + "log" + "net" + "net/http" + "os" + + "github.com/33TU/socks/shadowsocks" +) + +func main() { + proxyURL := flag.String( + "proxy", + os.Getenv("SS_PROXY_URL"), + `Shadowsocks proxy URL, e.g. ss://2022-blake3-aes-128-gcm:iDN+jVYAcTkUxwNICMTQRA==@127.0.0.1:8388`, + ) + + flag.Parse() + + if *proxyURL == "" { + log.Fatal("missing proxy URL; set -proxy or SS_PROXY_URL") + } + + dialer, err := shadowsocks.NewDialerFromURLString(*proxyURL, nil) + if err != nil { + log.Fatalf("failed to create dialer: %v", err) + } + + transport := &http.Transport{ + DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return dialer.DialContext(ctx, network, address) + }, + } + + client := &http.Client{ + Transport: transport, + } + + resp, err := client.Get("https://httpbin.org/ip") + if err != nil { + log.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalf("failed to read response body: %v", err) + } + + fmt.Printf("status: %s\n", resp.Status) + fmt.Printf("body:\n%s\n", body) +} diff --git a/shadowsocks/dialer.go b/shadowsocks/dialer.go index 47c16e8..ac29493 100644 --- a/shadowsocks/dialer.go +++ b/shadowsocks/dialer.go @@ -3,6 +3,7 @@ package shadowsocks import ( "context" "fmt" + "log" "net" "net/url" "time" @@ -165,23 +166,12 @@ func (d *Dialer) DialConnContext(ctx context.Context, conn net.Conn, network, ad return nil, err } + log.Println("writing request start") if _, err := reqStart.WriteRequestStart(conn, time.Now(), target, []byte{0}, nil); err != nil { conn.Close() return nil, err } - respStart, _, err := reqStart.ReadResponseStart(conn) - if err != nil { - conn.Close() - return nil, err - } - - var reader TCPChunkReader - if err := reader.Init(respStart.ResponseCipher); err != nil { - conn.Close() - return nil, err - } - var writer TCPChunkWriter if err := writer.Init(reqStart.RequestCipher); err != nil { conn.Close() @@ -189,9 +179,9 @@ func (d *Dialer) DialConnContext(ctx context.Context, conn net.Conn, network, ad } return &TcpConn{ - Conn: conn, - Reader: reader, - Writer: writer, + Conn: conn, + Writer: writer, + reqStart: &reqStart, }, nil } diff --git a/shadowsocks/tcp_client_stream.go b/shadowsocks/tcp_client_stream.go index e16f6fb..94aab68 100644 --- a/shadowsocks/tcp_client_stream.go +++ b/shadowsocks/tcp_client_stream.go @@ -146,6 +146,7 @@ type TCPClientResponseStart struct { ResponseSalt []byte ResponseCipher *TCPStreamCipher Header TCPResponseHeader + InitialPayload []byte } // Validate checks whether the parsed client response-start state is internally valid. @@ -174,13 +175,16 @@ func (s *TCPClientResponseStart) Validate(method Method, requestSalt []byte) err if !bytes.Equal(s.Header.RequestSalt, requestSalt) { return fmt.Errorf("response request salt mismatch") } + if int(s.Header.Length) != len(s.InitialPayload) { + return fmt.Errorf("invalid initial payload length: got %d, want %d", len(s.InitialPayload), s.Header.Length) + } return nil } // ReadResponseStart reads and decrypts the server response startup: // -// response salt || encrypted response header +// response salt || encrypted response header || encrypted first response payload func (s *TCPClientRequestStart) ReadResponseStart( src io.Reader, ) (*TCPClientResponseStart, int64, error) { @@ -205,7 +209,7 @@ func (s *TCPClientRequestStart) ReadResponseStart( return nil, total, err } - encHeaderLen := TcpResponseFixedBaseLen + s.Method.SaltSize + s.Method.TagSize + encHeaderLen := 1 + 8 + s.Method.SaltSize + 2 + s.Method.TagSize encHeader := ibuf.GetBytes(encHeaderLen) defer ibuf.PutBytes(encHeader) @@ -215,10 +219,29 @@ func (s *TCPClientRequestStart) ReadResponseStart( return nil, total, err } - plainScratch := ibuf.GetBytes(TcpResponseFixedBaseLen + s.Method.SaltSize) - defer ibuf.PutBytes(plainScratch) + plainHeaderScratch := ibuf.GetBytes(1 + 8 + s.Method.SaltSize + 2) + defer ibuf.PutBytes(plainHeaderScratch) + + header, err := responseCipher.DecodeResponseHeader(encHeader, plainHeaderScratch[:0]) + if err != nil { + return nil, total, err + } + + firstPayloadLen := int(header.Length) + encPayloadLen := responseCipher.EncryptedPayloadLength(firstPayloadLen) + encPayloadBuf := ibuf.GetBytes(encPayloadLen) + defer ibuf.PutBytes(encPayloadBuf) + + n, err = io.ReadFull(src, encPayloadBuf) + total += int64(n) + if err != nil { + return nil, total, err + } + + payloadScratch := ibuf.GetBytes(firstPayloadLen) + defer ibuf.PutBytes(payloadScratch) - header, err := responseCipher.DecodeResponseHeader(encHeader, plainScratch[:0]) + initialPayload, err := responseCipher.DecodeChunkPayloadTo(payloadScratch[:0], encPayloadBuf) if err != nil { return nil, total, err } @@ -227,6 +250,7 @@ func (s *TCPClientRequestStart) ReadResponseStart( ResponseSalt: append([]byte(nil), responseSaltBuf...), ResponseCipher: responseCipher, Header: header, + InitialPayload: append([]byte(nil), initialPayload...), } if err := resp.Validate(s.Method, s.RequestSalt); err != nil { return nil, total, err diff --git a/shadowsocks/tcp_conn.go b/shadowsocks/tcp_conn.go index 8992823..eb8c613 100644 --- a/shadowsocks/tcp_conn.go +++ b/shadowsocks/tcp_conn.go @@ -1,17 +1,83 @@ package shadowsocks -import "net" +import ( + "fmt" + "net" + "sync" +) -// TcpConn wraps a proxy TCP connection and exposes plain application reads/writes -// while handling Shadowsocks 2022 TCP chunk framing internally. type TcpConn struct { net.Conn - Reader TCPChunkReader - Writer TCPChunkWriter + + Reader TCPChunkReader + Writer TCPChunkWriter + readBuf []byte + + reqStart *TCPClientRequestStart + respStart *TCPClientResponseStart + + readInitMu sync.Mutex + writeInitMu sync.Mutex +} + +func (c *TcpConn) ensureReadReady() error { + c.readInitMu.Lock() + defer c.readInitMu.Unlock() + + if c.Reader.Cipher != nil { + return nil + } + + if c.respStart != nil { + if c.respStart.ResponseCipher == nil { + return fmt.Errorf("missing TCP client response cipher") + } + if err := c.Reader.Init(c.respStart.ResponseCipher); err != nil { + return err + } + c.readBuf = append(c.readBuf[:0], c.respStart.InitialPayload...) + return nil + } + + if c.reqStart == nil { + return fmt.Errorf("missing TCP client request start") + } + + respStart, _, err := c.reqStart.ReadResponseStart(c.Conn) + if err != nil { + return err + } + + if err := c.Reader.Init(respStart.ResponseCipher); err != nil { + return err + } + + c.respStart = respStart + c.readBuf = append(c.readBuf[:0], respStart.InitialPayload...) + return nil +} + +func (c *TcpConn) ensureWriteReady() error { + c.writeInitMu.Lock() + defer c.writeInitMu.Unlock() + + if c.Writer.Cipher != nil { + return nil + } + + if c.reqStart == nil { + return fmt.Errorf("missing TCP client request start") + } + + return c.Writer.Init(c.reqStart.RequestCipher) } func (c *TcpConn) Read(p []byte) (int, error) { + if err := c.ensureReadReady(); err != nil { + return 0, err + } + if len(c.readBuf) == 0 { buf, _, err := c.Reader.ReadChunkTo(nil, c.Conn) if err != nil { @@ -30,6 +96,10 @@ func (c *TcpConn) Write(p []byte) (int, error) { return 0, nil } + if err := c.ensureWriteReady(); err != nil { + return 0, err + } + written := 0 for len(p) > 0 { nn := len(p) diff --git a/shadowsocks/tcp_response_header.go b/shadowsocks/tcp_response_header.go index e10a577..408d752 100644 --- a/shadowsocks/tcp_response_header.go +++ b/shadowsocks/tcp_response_header.go @@ -30,35 +30,30 @@ func (h *TCPResponseHeader) Validate() error { if len(h.RequestSalt) == 0 { return ErrMissingTCPResponseSalt } - if int(h.Length) != len(h.RequestSalt) { - return ErrInvalidTCPResponseSaltLen - } - return nil } // EncodedLen returns the number of bytes required to encode the response header. func (h *TCPResponseHeader) EncodedLen() int { - return TcpResponseFixedBaseLen + len(h.RequestSalt) + return 1 + 8 + len(h.RequestSalt) + 2 } -// Decode decodes a response header from src. +// Decode decodes a response header from src using the expected request salt length. // It returns the number of bytes consumed. -func (h *TCPResponseHeader) Decode(src []byte) (int, error) { - if len(src) < TcpResponseFixedBaseLen { - return 0, ErrShortTCPHeader +func (h *TCPResponseHeader) Decode(src []byte, requestSaltLen int) (int, error) { + if requestSaltLen <= 0 { + return 0, ErrInvalidTCPResponseSaltLen } - h.Type = src[0] - h.Timestamp = binary.BigEndian.Uint64(src[1:9]) - h.Length = binary.BigEndian.Uint16(src[9:11]) - - need := TcpResponseFixedBaseLen + int(h.Length) + need := 1 + 8 + requestSaltLen + 2 if len(src) < need { return 0, ErrShortTCPHeader } - h.RequestSalt = append(h.RequestSalt[:0], src[11:need]...) + h.Type = src[0] + h.Timestamp = binary.BigEndian.Uint64(src[1:9]) + h.RequestSalt = append(h.RequestSalt[:0], src[9:9+requestSaltLen]...) + h.Length = binary.BigEndian.Uint16(src[9+requestSaltLen : 9+requestSaltLen+2]) if err := h.Validate(); err != nil { return 0, err @@ -75,8 +70,8 @@ func (h *TCPResponseHeader) EncodeTo(dst []byte) ([]byte, error) { dst = append(dst, h.Type) dst = binary.BigEndian.AppendUint64(dst, h.Timestamp) - dst = binary.BigEndian.AppendUint16(dst, h.Length) dst = append(dst, h.RequestSalt...) + dst = binary.BigEndian.AppendUint16(dst, h.Length) return dst, nil } diff --git a/shadowsocks/tcp_response_header_test.go b/shadowsocks/tcp_response_header_test.go index 9ac1aec..4809e08 100644 --- a/shadowsocks/tcp_response_header_test.go +++ b/shadowsocks/tcp_response_header_test.go @@ -40,15 +40,6 @@ func TestTCPResponseHeader_Init_Validate(t *testing.T) { }(), wantErr: shadowsocks.ErrMissingTCPResponseSalt, }, - { - name: "invalid salt length", - hdr: func() shadowsocks.TCPResponseHeader { - var h shadowsocks.TCPResponseHeader - h.Init(shadowsocks.TCPHeaderTypeServerStream, 123456789, []byte{1, 2, 3}, 4) - return h - }(), - wantErr: shadowsocks.ErrInvalidTCPResponseSaltLen, - }, } for _, tt := range tests { @@ -80,21 +71,19 @@ func TestTCPResponseHeader_EncodeTo_Decode_RoundTrip(t *testing.T) { Type: shadowsocks.TCPHeaderTypeServerStream, Timestamp: 123456789, RequestSalt: []byte{0xaa, 0xbb, 0xcc, 0xdd}, - Length: 4, + Length: 4, // first response payload length } - buf := make([]byte, want.EncodedLen()) - - bw, err := want.EncodeTo(buf[:0]) + buf, err := want.EncodeTo(nil) if err != nil { t.Fatalf("EncodeTo() failed: %v", err) } - if len(bw) != len(buf) { - t.Fatalf("EncodeTo() wrote %d bytes, want %d", len(bw), len(buf)) + if len(buf) != want.EncodedLen() { + t.Fatalf("EncodeTo() wrote %d bytes, want %d", len(buf), want.EncodedLen()) } var got shadowsocks.TCPResponseHeader - nr, err := got.Decode(buf) + nr, err := got.Decode(buf, len(want.RequestSalt)) if err != nil { t.Fatalf("Decode() failed: %v", err) } @@ -138,17 +127,6 @@ func TestTCPResponseHeader_EncodeTo_Invalid(t *testing.T) { bufLen: 32, wantErr: shadowsocks.ErrMissingTCPResponseSalt, }, - { - name: "invalid salt length", - hdr: shadowsocks.TCPResponseHeader{ - Type: shadowsocks.TCPHeaderTypeServerStream, - Timestamp: 1, - RequestSalt: []byte{1, 2, 3}, - Length: 2, - }, - bufLen: 32, - wantErr: shadowsocks.ErrInvalidTCPResponseSaltLen, - }, } for _, tt := range tests { @@ -164,50 +142,62 @@ func TestTCPResponseHeader_EncodeTo_Invalid(t *testing.T) { func TestTCPResponseHeader_Decode_Invalid(t *testing.T) { tests := []struct { - name string - src []byte - wantErr error + name string + src []byte + requestSaltLen int + wantErr error }{ { - name: "short fixed header", - src: make([]byte, shadowsocks.TcpResponseFixedBaseLen-1), - wantErr: shadowsocks.ErrShortTCPHeader, + name: "invalid request salt length", + src: []byte{}, + requestSaltLen: 0, + wantErr: shadowsocks.ErrInvalidTCPResponseSaltLen, + }, + { + name: "short fixed header", + src: make([]byte, 1+8+2+2-1), // type + timestamp + 2-byte salt + length - 1 + requestSaltLen: 2, + wantErr: shadowsocks.ErrShortTCPHeader, }, { name: "invalid type", src: []byte{ 0x99, 0, 0, 0, 0, 0, 0, 0, 1, - 0, 2, 0xaa, 0xbb, + 0, 2, }, - wantErr: shadowsocks.ErrInvalidTCPHeaderType, + requestSaltLen: 2, + wantErr: shadowsocks.ErrInvalidTCPHeaderType, }, { name: "missing salt bytes", src: []byte{ shadowsocks.TCPHeaderTypeServerStream, 0, 0, 0, 0, 0, 0, 0, 1, - 0, 2, 0xaa, + 0, 2, }, - wantErr: shadowsocks.ErrShortTCPHeader, + requestSaltLen: 2, + wantErr: shadowsocks.ErrShortTCPHeader, }, { - name: "empty salt", + name: "missing length bytes after salt", src: []byte{ shadowsocks.TCPHeaderTypeServerStream, 0, 0, 0, 0, 0, 0, 0, 1, - 0, 0, + 0xaa, 0xbb, + 0, }, - wantErr: shadowsocks.ErrMissingTCPResponseSalt, + requestSaltLen: 2, + wantErr: shadowsocks.ErrShortTCPHeader, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var h shadowsocks.TCPResponseHeader - _, err := h.Decode(tt.src) + _, err := h.Decode(tt.src, tt.requestSaltLen) if !errors.Is(err, tt.wantErr) { t.Fatalf("Decode() error = %v, wantErr = %v", err, tt.wantErr) } diff --git a/shadowsocks/tcp_stream_cipher.go b/shadowsocks/tcp_stream_cipher.go index 6bf98de..daf6b71 100644 --- a/shadowsocks/tcp_stream_cipher.go +++ b/shadowsocks/tcp_stream_cipher.go @@ -236,7 +236,7 @@ func (s *TCPStreamCipher) DecodeResponseHeader(src []byte, scratch []byte) (TCPR if err != nil { return TCPResponseHeader{}, err } - if _, err := h.Decode(plain); err != nil { + if _, err := h.Decode(plain, s.Method.SaltSize); err != nil { return TCPResponseHeader{}, err } diff --git a/shadowsocks/tcp_stream_cipher_test.go b/shadowsocks/tcp_stream_cipher_test.go index 9ccc557..8e629cf 100644 --- a/shadowsocks/tcp_stream_cipher_test.go +++ b/shadowsocks/tcp_stream_cipher_test.go @@ -451,10 +451,12 @@ func TestTCPStreamCipher_RequestVariableHeader_Nil(t *testing.T) { func TestTCPStreamCipher_ResponseHeader_RoundTrip(t *testing.T) { t.Parallel() - enc, dec, _ := newTestCipherPair(t) + enc, dec, method := newTestCipherPair(t) + + requestSalt := bytes.Repeat([]byte{0xaa}, method.SaltSize) var want shadowsocks.TCPResponseHeader - want.Init(shadowsocks.TCPHeaderTypeServerStream, 123456789, []byte{1, 2, 3, 4}, 4) + want.Init(shadowsocks.TCPHeaderTypeServerStream, 123456789, requestSalt, 4) ciphertext, err := enc.EncodeResponseHeaderTo(nil, &want, nil) if err != nil { From b9684efac91d6050bb98602c6974f1e3bfc9d937 Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 20:35:37 +0300 Subject: [PATCH 16/25] tcp and dialer fixes --- shadowsocks/dialer.go | 2 - shadowsocks/dialer_test.go | 44 +++++++++++++------- shadowsocks/tcp_client_stream_test.go | 60 ++++++++++++++++++++++++--- shadowsocks/tcp_server_stream.go | 21 +++++++--- shadowsocks/tcp_server_stream_test.go | 55 ++++++++++++++++++++---- 5 files changed, 146 insertions(+), 36 deletions(-) diff --git a/shadowsocks/dialer.go b/shadowsocks/dialer.go index ac29493..e7ef669 100644 --- a/shadowsocks/dialer.go +++ b/shadowsocks/dialer.go @@ -3,7 +3,6 @@ package shadowsocks import ( "context" "fmt" - "log" "net" "net/url" "time" @@ -166,7 +165,6 @@ func (d *Dialer) DialConnContext(ctx context.Context, conn net.Conn, network, ad return nil, err } - log.Println("writing request start") if _, err := reqStart.WriteRequestStart(conn, time.Now(), target, []byte{0}, nil); err != nil { conn.Close() return nil, err diff --git a/shadowsocks/dialer_test.go b/shadowsocks/dialer_test.go index 75b766b..f7a8f56 100644 --- a/shadowsocks/dialer_test.go +++ b/shadowsocks/dialer_test.go @@ -395,23 +395,12 @@ func TestDialer_DialContext_Success(t *testing.T) { return } - if _, err := respStart.WriteResponseStart(c, time.Now(), reqStart.RequestSalt); err != nil { - t.Errorf("server: WriteResponseStart() error = %v", err) - return - } - var reader shadowsocks.TCPChunkReader if err := reader.Init(reqStart.RequestCipher); err != nil { t.Errorf("server: reader Init() error = %v", err) return } - var writer shadowsocks.TCPChunkWriter - if err := writer.Init(respStart.ResponseCipher); err != nil { - t.Errorf("server: writer Init() error = %v", err) - return - } - payload, _, err := reader.ReadChunkTo(nil, c) if err != nil { t.Errorf("server: ReadChunk() error = %v", err) @@ -422,8 +411,35 @@ func TestDialer_DialContext_Success(t *testing.T) { return } - if _, err := writer.WriteChunk(c, []byte("pong")); err != nil { - t.Errorf("server: WriteChunk() error = %v", err) + initialPayload := []byte("pong") + + var hdr shadowsocks.TCPResponseHeader + hdr.Init( + shadowsocks.TCPHeaderTypeServerStream, + uint64(time.Now().Unix()), + reqStart.RequestSalt, + uint16(len(initialPayload)), + ) + + encHeader, err := respStart.ResponseCipher.EncodeResponseHeaderTo(nil, &hdr, nil) + if err != nil { + t.Errorf("server: EncodeResponseHeaderTo() error = %v", err) + return + } + + encPayload, err := respStart.ResponseCipher.EncodeChunkPayloadTo(nil, initialPayload) + if err != nil { + t.Errorf("server: EncodeChunkPayloadTo() error = %v", err) + return + } + + var wire bytes.Buffer + wire.Write(respStart.ResponseSalt) + wire.Write(encHeader) + wire.Write(encPayload) + + if _, err := c.Write(wire.Bytes()); err != nil { + t.Errorf("server: response start write error = %v", err) return } }) @@ -479,7 +495,7 @@ func TestDialer_DialContext_Deadline(t *testing.T) { if err := respStart.Init(method, psk, responseSalt); err != nil { return } - if _, err := respStart.WriteResponseStart(c, time.Now(), reqStart.RequestSalt); err != nil { + if _, err := respStart.WriteResponseStart(c, time.Now(), reqStart.RequestSalt, nil); err != nil { return } diff --git a/shadowsocks/tcp_client_stream_test.go b/shadowsocks/tcp_client_stream_test.go index 42d97e0..1eb4510 100644 --- a/shadowsocks/tcp_client_stream_test.go +++ b/shadowsocks/tcp_client_stream_test.go @@ -227,8 +227,14 @@ func TestTCPClientResponseStart_Validate(t *testing.T) { t.Fatalf("NewTCPStreamCipherFromPSK() error = %v", err) } + initialPayload := []byte("hello") var hdr shadowsocks.TCPResponseHeader - hdr.Init(shadowsocks.TCPHeaderTypeServerStream, 1700000000, requestSalt, uint16(len(requestSalt))) + hdr.Init( + shadowsocks.TCPHeaderTypeServerStream, + 1700000000, + requestSalt, + uint16(len(initialPayload)), + ) t.Run("valid", func(t *testing.T) { t.Parallel() @@ -237,6 +243,7 @@ func TestTCPClientResponseStart_Validate(t *testing.T) { ResponseSalt: append([]byte(nil), responseSalt...), ResponseCipher: responseCipher, Header: hdr, + InitialPayload: append([]byte(nil), initialPayload...), } if err := s.Validate(method, requestSalt); err != nil { @@ -264,6 +271,7 @@ func TestTCPClientResponseStart_Validate(t *testing.T) { ResponseSalt: responseSalt[:len(responseSalt)-1], ResponseCipher: responseCipher, Header: hdr, + InitialPayload: append([]byte(nil), initialPayload...), } err := s.Validate(method, requestSalt) @@ -279,8 +287,9 @@ func TestTCPClientResponseStart_Validate(t *testing.T) { t.Parallel() s := &shadowsocks.TCPClientResponseStart{ - ResponseSalt: responseSalt, - Header: hdr, + ResponseSalt: responseSalt, + Header: hdr, + InitialPayload: append([]byte(nil), initialPayload...), } err := s.Validate(method, requestSalt) @@ -302,6 +311,7 @@ func TestTCPClientResponseStart_Validate(t *testing.T) { ResponseSalt: responseSalt, ResponseCipher: responseCipher, Header: hdr, + InitialPayload: append([]byte(nil), initialPayload...), } err := s.Validate(method, badRequestSalt) @@ -312,6 +322,25 @@ func TestTCPClientResponseStart_Validate(t *testing.T) { t.Fatalf("unexpected error: %v", err) } }) + + t.Run("initial payload length mismatch", func(t *testing.T) { + t.Parallel() + + s := &shadowsocks.TCPClientResponseStart{ + ResponseSalt: responseSalt, + ResponseCipher: responseCipher, + Header: hdr, + InitialPayload: nil, + } + + err := s.Validate(method, requestSalt) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid initial payload length") { + t.Fatalf("unexpected error: %v", err) + } + }) } func TestTCPClientRequestStart_ReadResponseStart(t *testing.T) { @@ -335,12 +364,14 @@ func TestTCPClientRequestStart_ReadResponseStart(t *testing.T) { t.Fatalf("NewTCPStreamCipherFromPSK() error = %v", err) } + initialPayload := []byte("hello") + var hdr shadowsocks.TCPResponseHeader hdr.Init( shadowsocks.TCPHeaderTypeServerStream, uint64(time.Unix(1700000100, 0).Unix()), requestSalt, - uint16(len(requestSalt)), + uint16(len(initialPayload)), ) encHeader, err := responseCipher.EncodeResponseHeaderTo(nil, &hdr, nil) @@ -348,16 +379,22 @@ func TestTCPClientRequestStart_ReadResponseStart(t *testing.T) { t.Fatalf("EncodeResponseHeaderTo() error = %v", err) } + encPayload, err := responseCipher.EncodeChunkPayloadTo(nil, initialPayload) + if err != nil { + t.Fatalf("EncodeChunkPayloadTo() error = %v", err) + } + var wire bytes.Buffer wire.Write(responseSalt) wire.Write(encHeader) + wire.Write(encPayload) resp, n, err := clientStart.ReadResponseStart(&wire) if err != nil { t.Fatalf("ReadResponseStart() error = %v", err) } - wantN := int64(len(responseSalt) + len(encHeader)) + wantN := int64(len(responseSalt) + len(encHeader) + len(encPayload)) if n != wantN { t.Fatalf("ReadResponseStart() read %d bytes, want %d", n, wantN) } @@ -374,6 +411,9 @@ func TestTCPClientRequestStart_ReadResponseStart(t *testing.T) { if !bytes.Equal(resp.Header.RequestSalt, requestSalt) { t.Fatalf("Header.RequestSalt = %v, want %v", resp.Header.RequestSalt, requestSalt) } + if !bytes.Equal(resp.InitialPayload, initialPayload) { + t.Fatalf("InitialPayload = %q, want %q", resp.InitialPayload, initialPayload) + } } func TestTCPClientRequestStart_ReadResponseStart_ShortRead(t *testing.T) { @@ -425,12 +465,14 @@ func TestTCPClientRequestStart_ReadResponseStart_RequestSaltMismatch(t *testing. badRequestSalt := append([]byte(nil), requestSalt...) badRequestSalt[0] ^= 0xff + initialPayload := []byte("hello") + var hdr shadowsocks.TCPResponseHeader hdr.Init( shadowsocks.TCPHeaderTypeServerStream, uint64(time.Unix(1700000100, 0).Unix()), badRequestSalt, - uint16(len(badRequestSalt)), + uint16(len(initialPayload)), ) encHeader, err := responseCipher.EncodeResponseHeaderTo(nil, &hdr, nil) @@ -438,9 +480,15 @@ func TestTCPClientRequestStart_ReadResponseStart_RequestSaltMismatch(t *testing. t.Fatalf("EncodeResponseHeaderTo() error = %v", err) } + encPayload, err := responseCipher.EncodeChunkPayloadTo(nil, initialPayload) + if err != nil { + t.Fatalf("EncodeChunkPayloadTo() error = %v", err) + } + var wire bytes.Buffer wire.Write(responseSalt) wire.Write(encHeader) + wire.Write(encPayload) _, _, err = clientStart.ReadResponseStart(&wire) if err == nil { diff --git a/shadowsocks/tcp_server_stream.go b/shadowsocks/tcp_server_stream.go index b65e350..aa2583e 100644 --- a/shadowsocks/tcp_server_stream.go +++ b/shadowsocks/tcp_server_stream.go @@ -164,6 +164,7 @@ type TCPServerResponseStart struct { ResponseSalt []byte ResponseCipher *TCPStreamCipher Header TCPResponseHeader + InitialPayload []byte } // Init initializes the server response-start state from method, PSK, and response salt. @@ -217,17 +218,21 @@ func (s *TCPServerResponseStart) Validate(requestSalt []byte) error { if !bytes.Equal(s.Header.RequestSalt, requestSalt) { return fmt.Errorf("response request salt mismatch") } + if int(s.Header.Length) != len(s.InitialPayload) { + return fmt.Errorf("invalid initial payload length: got %d, want %d", len(s.InitialPayload), s.Header.Length) + } return nil } // WriteResponseStart writes the full server response startup: // -// response salt || encrypted response header +// response salt || encrypted response header || encrypted first response payload func (s *TCPServerResponseStart) WriteResponseStart( dst io.Writer, timestamp time.Time, requestSalt []byte, + initialPayload []byte, ) (int64, error) { if s == nil { return 0, fmt.Errorf("nil TCP server response start") @@ -253,15 +258,16 @@ func (s *TCPServerResponseStart) WriteResponseStart( TCPHeaderTypeServerStream, uint64(timestamp.Unix()), requestSalt, - uint16(len(requestSalt)), + uint16(len(initialPayload)), ) if err := header.Validate(); err != nil { return 0, err } s.Header = header + s.InitialPayload = append(s.InitialPayload[:0], initialPayload...) - plainScratch := ibuf.GetBytes(s.Header.EncodedLen()) - defer ibuf.PutBytes(plainScratch) + headerPlainScratch := ibuf.GetBytes(s.Header.EncodedLen()) + defer ibuf.PutBytes(headerPlainScratch) var stackBuf [tcpServerResponseStartStackBufSize]byte out := stackBuf[:0] @@ -269,7 +275,12 @@ func (s *TCPServerResponseStart) WriteResponseStart( out = append(out, s.ResponseSalt...) var err error - out, err = s.ResponseCipher.EncodeResponseHeaderTo(out, &s.Header, plainScratch[:0]) + out, err = s.ResponseCipher.EncodeResponseHeaderTo(out, &s.Header, headerPlainScratch[:0]) + if err != nil { + return 0, err + } + + out, err = s.ResponseCipher.EncodeChunkPayloadTo(out, s.InitialPayload) if err != nil { return 0, err } diff --git a/shadowsocks/tcp_server_stream_test.go b/shadowsocks/tcp_server_stream_test.go index 6178d5c..a2cf9e6 100644 --- a/shadowsocks/tcp_server_stream_test.go +++ b/shadowsocks/tcp_server_stream_test.go @@ -297,12 +297,14 @@ func TestTCPServerResponseStart_Init_Validate(t *testing.T) { t.Fatalf("NewTCPStreamCipherFromPSK() error = %v", err) } + initialPayload := []byte("pong") + var hdr shadowsocks.TCPResponseHeader hdr.Init( shadowsocks.TCPHeaderTypeServerStream, 1700000000, requestSalt, - uint16(len(requestSalt)), + uint16(len(initialPayload)), ) t.Run("valid", func(t *testing.T) { @@ -313,6 +315,7 @@ func TestTCPServerResponseStart_Init_Validate(t *testing.T) { t.Fatalf("Init() error = %v", err) } s.Header = hdr + s.InitialPayload = append([]byte(nil), initialPayload...) if err := s.Validate(requestSalt); err != nil { t.Fatalf("Validate() error = %v", err) @@ -362,10 +365,11 @@ func TestTCPServerResponseStart_Init_Validate(t *testing.T) { t.Parallel() s := &shadowsocks.TCPServerResponseStart{ - Method: method, - PSK: psk, - ResponseSalt: responseSalt, - Header: hdr, + Method: method, + PSK: psk, + ResponseSalt: responseSalt, + Header: hdr, + InitialPayload: append([]byte(nil), initialPayload...), } err := s.Validate(requestSalt) @@ -386,6 +390,7 @@ func TestTCPServerResponseStart_Init_Validate(t *testing.T) { ResponseSalt: responseSalt, ResponseCipher: responseCipher, Header: hdr, + InitialPayload: append([]byte(nil), initialPayload...), } badRequestSalt := append([]byte(nil), requestSalt...) @@ -399,6 +404,27 @@ func TestTCPServerResponseStart_Init_Validate(t *testing.T) { t.Fatalf("unexpected error: %v", err) } }) + + t.Run("initial payload length mismatch", func(t *testing.T) { + t.Parallel() + + s := &shadowsocks.TCPServerResponseStart{ + Method: method, + PSK: psk, + ResponseSalt: responseSalt, + ResponseCipher: responseCipher, + Header: hdr, + InitialPayload: nil, + } + + err := s.Validate(requestSalt) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid initial payload length") { + t.Fatalf("unexpected error: %v", err) + } + }) } func TestTCPServerResponseStart_WriteResponseStart(t *testing.T) { @@ -417,14 +443,19 @@ func TestTCPServerResponseStart_WriteResponseStart(t *testing.T) { } ts := time.Unix(1700000100, 0) + initialPayload := []byte("pong") var buf bytes.Buffer - n, err := s.WriteResponseStart(&buf, ts, requestSalt) + n, err := s.WriteResponseStart(&buf, ts, requestSalt, initialPayload) if err != nil { t.Fatalf("WriteResponseStart() error = %v", err) } - wantLen := len(responseSalt) + (shadowsocks.TcpResponseFixedBaseLen + method.SaltSize + method.TagSize) + headerPlainLen := 1 + 8 + len(requestSalt) + 2 + wantLen := len(responseSalt) + + (headerPlainLen + method.TagSize) + + (len(initialPayload) + method.TagSize) + if int(n) != wantLen { t.Fatalf("WriteResponseStart() wrote %d bytes, want %d", n, wantLen) } @@ -446,6 +477,12 @@ func TestTCPServerResponseStart_WriteResponseStart(t *testing.T) { if !bytes.Equal(s.Header.RequestSalt, requestSalt) { t.Fatalf("Header.RequestSalt = %v, want %v", s.Header.RequestSalt, requestSalt) } + if s.Header.Length != uint16(len(initialPayload)) { + t.Fatalf("Header.Length = %d, want %d", s.Header.Length, len(initialPayload)) + } + if !bytes.Equal(s.InitialPayload, initialPayload) { + t.Fatalf("InitialPayload = %q, want %q", s.InitialPayload, initialPayload) + } } func TestTCPServerResponseStart_WriteResponseStart_InvalidRequestSalt(t *testing.T) { @@ -460,7 +497,7 @@ func TestTCPServerResponseStart_WriteResponseStart_InvalidRequestSalt(t *testing } var buf bytes.Buffer - _, err := s.WriteResponseStart(&buf, time.Unix(1700000100, 0), responseSalt[:len(responseSalt)-1]) + _, err := s.WriteResponseStart(&buf, time.Unix(1700000100, 0), responseSalt[:len(responseSalt)-1], nil) if err == nil { t.Fatal("expected error, got nil") } @@ -478,7 +515,7 @@ func TestTCPServerResponseStart_WriteResponseStart_NilReceiver(t *testing.T) { var s *shadowsocks.TCPServerResponseStart var buf bytes.Buffer - _, err := s.WriteResponseStart(&buf, time.Unix(1700000100, 0), requestSalt) + _, err := s.WriteResponseStart(&buf, time.Unix(1700000100, 0), requestSalt, nil) if err == nil { t.Fatal("expected error, got nil") } From 1a5ce0b42d39791372ad3f9b054e13ab7e076e24 Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 21:41:53 +0300 Subject: [PATCH 17/25] simplify shadowsocks --- shadowsocks/dialer.go | 20 +- shadowsocks/tcp_client_stream.go | 206 +++----------------- shadowsocks/tcp_conn.go | 48 ++--- shadowsocks/tcp_server_stream.go | 238 +++++------------------ shadowsocks/tcp_stream_start.go | 318 +++++++++++++++++++++++++++++++ 5 files changed, 424 insertions(+), 406 deletions(-) create mode 100644 shadowsocks/tcp_stream_start.go diff --git a/shadowsocks/dialer.go b/shadowsocks/dialer.go index e7ef669..3bd6583 100644 --- a/shadowsocks/dialer.go +++ b/shadowsocks/dialer.go @@ -159,27 +159,25 @@ func (d *Dialer) DialConnContext(ctx context.Context, conn net.Conn, network, ad cleanup := bindConnToContext(ctx, conn) defer cleanup() - var reqStart TCPClientRequestStart - if err := reqStart.Init(method, psk, requestSalt); err != nil { - conn.Close() - return nil, err - } - - if _, err := reqStart.WriteRequestStart(conn, time.Now(), target, []byte{0}, nil); err != nil { + requestCipher, _, err := WriteTCPRequestStart(conn, method, psk, requestSalt, time.Now(), target, []byte{0}, nil) + if err != nil { conn.Close() return nil, err } var writer TCPChunkWriter - if err := writer.Init(reqStart.RequestCipher); err != nil { + if err := writer.Init(requestCipher); err != nil { conn.Close() return nil, err } return &TcpConn{ - Conn: conn, - Writer: writer, - reqStart: &reqStart, + Conn: conn, + Writer: writer, + requestMethod: method, + requestPSK: append([]byte(nil), psk...), + requestSalt: append([]byte(nil), requestSalt...), + requestCipher: requestCipher, }, nil } diff --git a/shadowsocks/tcp_client_stream.go b/shadowsocks/tcp_client_stream.go index 94aab68..b57cfe9 100644 --- a/shadowsocks/tcp_client_stream.go +++ b/shadowsocks/tcp_client_stream.go @@ -1,17 +1,13 @@ package shadowsocks import ( - "bytes" "fmt" "io" "time" - - ibuf "github.com/33TU/socks/internal" ) -const tcpClientRequestStartStackBufSize = 1024 - // TCPClientRequestStart represents the client-side Shadowsocks 2022 TCP startup state. +// Deprecated: prefer WriteTCPRequestStart and ReadTCPResponseStart. type TCPClientRequestStart struct { Method Method PSK []byte @@ -19,7 +15,6 @@ type TCPClientRequestStart struct { RequestCipher *TCPStreamCipher } -// Init initializes the client request-start state from method, PSK, and request salt. func (s *TCPClientRequestStart) Init(method Method, psk, requestSalt []byte) error { if err := method.Validate(); err != nil { return err @@ -30,21 +25,17 @@ func (s *TCPClientRequestStart) Init(method Method, psk, requestSalt []byte) err if len(requestSalt) != method.SaltSize { return fmt.Errorf("invalid request salt length: got %d, want %d", len(requestSalt), method.SaltSize) } - - requestCipher, err := NewTCPStreamCipherFromPSK(method, psk, requestSalt) + cipher, err := NewTCPStreamCipherFromPSK(method, psk, requestSalt) if err != nil { return err } - s.Method = method - s.PSK = psk - s.RequestSalt = requestSalt - s.RequestCipher = requestCipher - + s.PSK = append(s.PSK[:0], psk...) + s.RequestSalt = append(s.RequestSalt[:0], requestSalt...) + s.RequestCipher = cipher return nil } -// Validate checks whether the client request-start state is internally valid. func (s *TCPClientRequestStart) Validate() error { if s == nil { return fmt.Errorf("nil TCP client request start") @@ -61,87 +52,29 @@ func (s *TCPClientRequestStart) Validate() error { if s.RequestCipher == nil { return fmt.Errorf("missing request cipher") } - if err := s.RequestCipher.Validate(); err != nil { - return err - } - - return nil + return s.RequestCipher.Validate() } -// EncodedRequestStartLen returns the total encoded request-start length for the -// given plaintext variable header length. func (s *TCPClientRequestStart) EncodedRequestStartLen(variableHeaderLen int) (int, error) { if err := s.Validate(); err != nil { return 0, err } - if variableHeaderLen < 0 { - return 0, fmt.Errorf("invalid variable header length: %d", variableHeaderLen) - } - - return len(s.RequestSalt) + - (TcpRequestFixedHeaderLen + s.Method.TagSize) + - (variableHeaderLen + s.Method.TagSize), nil + return EncodedTCPRequestStartLen(s.Method, s.RequestSalt, variableHeaderLen) } -// WriteRequestStart writes a full client request start: -// -// request salt || encrypted request fixed header || encrypted request variable header -// -// The request fixed header Length field is set to the plaintext encoded variable -// header length. -func (s *TCPClientRequestStart) WriteRequestStart( - dst io.Writer, - timestamp time.Time, - target Addr, - padding []byte, - initialData []byte, -) (int64, error) { +func (s *TCPClientRequestStart) WriteRequestStart(dst io.Writer, timestamp time.Time, target Addr, padding []byte, initialData []byte) (int64, error) { if err := s.Validate(); err != nil { return 0, err } - - var variableHeader TCPRequestVariableHeader - variableHeader.Init(target, padding, initialData) - if err := variableHeader.Validate(); err != nil { - return 0, err - } - - var fixedHeader TCPRequestFixedHeader - fixedHeader.Init( - TCPHeaderTypeClientStream, - uint64(timestamp.Unix()), - uint16(variableHeader.EncodedLen()), - ) - - scratchLen := TcpRequestFixedHeaderLen - if variableHeader.EncodedLen() > scratchLen { - scratchLen = variableHeader.EncodedLen() - } - - plainScratch := ibuf.GetBytes(scratchLen) - defer ibuf.PutBytes(plainScratch) - - var stackBuf [tcpClientRequestStartStackBufSize]byte - out := stackBuf[:0] - - out = append(out, s.RequestSalt...) - - var err error - out, err = s.RequestCipher.EncodeRequestFixedHeaderTo(out, &fixedHeader, plainScratch[:0]) - if err != nil { - return 0, err - } - - out, err = s.RequestCipher.EncodeRequestVariableHeaderTo(out, &variableHeader, plainScratch[:0]) - if err != nil { - return 0, err + cipher, n, err := WriteTCPRequestStart(dst, s.Method, s.PSK, s.RequestSalt, timestamp, target, padding, initialData) + if err == nil { + s.RequestCipher = cipher } - - n, err := dst.Write(out) - return int64(n), err + return n, err } // TCPClientResponseStart represents the parsed server response startup. +// Deprecated: prefer ParsedTCPResponseStart. type TCPClientResponseStart struct { ResponseSalt []byte ResponseCipher *TCPStreamCipher @@ -149,112 +82,31 @@ type TCPClientResponseStart struct { InitialPayload []byte } -// Validate checks whether the parsed client response-start state is internally valid. func (s *TCPClientResponseStart) Validate(method Method, requestSalt []byte) error { if s == nil { return fmt.Errorf("nil TCP client response start") } - if err := method.Validate(); err != nil { - return err - } - if len(s.ResponseSalt) != method.SaltSize { - return fmt.Errorf("invalid response salt length: got %d, want %d", len(s.ResponseSalt), method.SaltSize) - } - if s.ResponseCipher == nil { - return fmt.Errorf("missing response cipher") - } - if err := s.ResponseCipher.Validate(); err != nil { - return err - } - if err := s.Header.Validate(); err != nil { - return err - } - if len(s.Header.RequestSalt) != len(requestSalt) { - return fmt.Errorf("invalid echoed request salt length: got %d, want %d", len(s.Header.RequestSalt), len(requestSalt)) - } - if !bytes.Equal(s.Header.RequestSalt, requestSalt) { - return fmt.Errorf("response request salt mismatch") - } - if int(s.Header.Length) != len(s.InitialPayload) { - return fmt.Errorf("invalid initial payload length: got %d, want %d", len(s.InitialPayload), s.Header.Length) + parsed := &ParsedTCPResponseStart{ + Salt: s.ResponseSalt, + Cipher: s.ResponseCipher, + Header: s.Header, + InitialPayload: s.InitialPayload, } - - return nil + return parsed.Validate(method, requestSalt) } -// ReadResponseStart reads and decrypts the server response startup: -// -// response salt || encrypted response header || encrypted first response payload -func (s *TCPClientRequestStart) ReadResponseStart( - src io.Reader, -) (*TCPClientResponseStart, int64, error) { +func (s *TCPClientRequestStart) ReadResponseStart(src io.Reader) (*TCPClientResponseStart, int64, error) { if err := s.Validate(); err != nil { return nil, 0, err } - - var total int64 - - responseSaltLen := s.Method.SaltSize - responseSaltBuf := ibuf.GetBytes(responseSaltLen) - defer ibuf.PutBytes(responseSaltBuf) - - n, err := io.ReadFull(src, responseSaltBuf) - total += int64(n) - if err != nil { - return nil, total, err - } - - responseCipher, err := NewTCPStreamCipherFromPSK(s.Method, s.PSK, responseSaltBuf) - if err != nil { - return nil, total, err - } - - encHeaderLen := 1 + 8 + s.Method.SaltSize + 2 + s.Method.TagSize - encHeader := ibuf.GetBytes(encHeaderLen) - defer ibuf.PutBytes(encHeader) - - n, err = io.ReadFull(src, encHeader) - total += int64(n) - if err != nil { - return nil, total, err - } - - plainHeaderScratch := ibuf.GetBytes(1 + 8 + s.Method.SaltSize + 2) - defer ibuf.PutBytes(plainHeaderScratch) - - header, err := responseCipher.DecodeResponseHeader(encHeader, plainHeaderScratch[:0]) + parsed, n, err := ReadTCPResponseStart(src, s.Method, s.PSK, s.RequestSalt) if err != nil { - return nil, total, err - } - - firstPayloadLen := int(header.Length) - encPayloadLen := responseCipher.EncryptedPayloadLength(firstPayloadLen) - encPayloadBuf := ibuf.GetBytes(encPayloadLen) - defer ibuf.PutBytes(encPayloadBuf) - - n, err = io.ReadFull(src, encPayloadBuf) - total += int64(n) - if err != nil { - return nil, total, err - } - - payloadScratch := ibuf.GetBytes(firstPayloadLen) - defer ibuf.PutBytes(payloadScratch) - - initialPayload, err := responseCipher.DecodeChunkPayloadTo(payloadScratch[:0], encPayloadBuf) - if err != nil { - return nil, total, err - } - - resp := &TCPClientResponseStart{ - ResponseSalt: append([]byte(nil), responseSaltBuf...), - ResponseCipher: responseCipher, - Header: header, - InitialPayload: append([]byte(nil), initialPayload...), - } - if err := resp.Validate(s.Method, s.RequestSalt); err != nil { - return nil, total, err - } - - return resp, total, nil + return nil, n, err + } + return &TCPClientResponseStart{ + ResponseSalt: parsed.Salt, + ResponseCipher: parsed.Cipher, + Header: parsed.Header, + InitialPayload: parsed.InitialPayload, + }, n, nil } diff --git a/shadowsocks/tcp_conn.go b/shadowsocks/tcp_conn.go index eb8c613..b813c08 100644 --- a/shadowsocks/tcp_conn.go +++ b/shadowsocks/tcp_conn.go @@ -14,8 +14,12 @@ type TcpConn struct { readBuf []byte - reqStart *TCPClientRequestStart - respStart *TCPClientResponseStart + requestMethod Method + requestPSK []byte + requestSalt []byte + requestCipher *TCPStreamCipher + + responseStart *ParsedTCPResponseStart readInitMu sync.Mutex writeInitMu sync.Mutex @@ -29,31 +33,35 @@ func (c *TcpConn) ensureReadReady() error { return nil } - if c.respStart != nil { - if c.respStart.ResponseCipher == nil { - return fmt.Errorf("missing TCP client response cipher") + if c.responseStart != nil { + if c.responseStart.Cipher == nil { + return fmt.Errorf("missing TCP response cipher") } - if err := c.Reader.Init(c.respStart.ResponseCipher); err != nil { + if err := c.Reader.Init(c.responseStart.Cipher); err != nil { return err } - c.readBuf = append(c.readBuf[:0], c.respStart.InitialPayload...) + c.readBuf = append(c.readBuf[:0], c.responseStart.InitialPayload...) return nil } - if c.reqStart == nil { - return fmt.Errorf("missing TCP client request start") + if err := c.requestMethod.Validate(); err != nil { + return err + } + if len(c.requestPSK) != c.requestMethod.KeySize { + return fmt.Errorf("invalid request PSK length: got %d, want %d", len(c.requestPSK), c.requestMethod.KeySize) + } + if len(c.requestSalt) != c.requestMethod.SaltSize { + return fmt.Errorf("invalid request salt length: got %d, want %d", len(c.requestSalt), c.requestMethod.SaltSize) } - respStart, _, err := c.reqStart.ReadResponseStart(c.Conn) + respStart, _, err := ReadTCPResponseStart(c.Conn, c.requestMethod, c.requestPSK, c.requestSalt) if err != nil { return err } - - if err := c.Reader.Init(respStart.ResponseCipher); err != nil { + if err := c.Reader.Init(respStart.Cipher); err != nil { return err } - - c.respStart = respStart + c.responseStart = respStart c.readBuf = append(c.readBuf[:0], respStart.InitialPayload...) return nil } @@ -65,12 +73,10 @@ func (c *TcpConn) ensureWriteReady() error { if c.Writer.Cipher != nil { return nil } - - if c.reqStart == nil { - return fmt.Errorf("missing TCP client request start") + if c.requestCipher == nil { + return fmt.Errorf("missing TCP request cipher") } - - return c.Writer.Init(c.reqStart.RequestCipher) + return c.Writer.Init(c.requestCipher) } func (c *TcpConn) Read(p []byte) (int, error) { @@ -95,7 +101,6 @@ func (c *TcpConn) Write(p []byte) (int, error) { if len(p) == 0 { return 0, nil } - if err := c.ensureWriteReady(); err != nil { return 0, err } @@ -106,15 +111,12 @@ func (c *TcpConn) Write(p []byte) (int, error) { if nn > 0xFFFF { nn = 0xFFFF } - if _, err := c.Writer.WriteChunk(c.Conn, p[:nn]); err != nil { return written, err } - written += nn p = p[nn:] } - return written, nil } diff --git a/shadowsocks/tcp_server_stream.go b/shadowsocks/tcp_server_stream.go index aa2583e..cbe929f 100644 --- a/shadowsocks/tcp_server_stream.go +++ b/shadowsocks/tcp_server_stream.go @@ -1,17 +1,13 @@ package shadowsocks import ( - "bytes" "fmt" "io" "time" - - ibuf "github.com/33TU/socks/internal" ) -const tcpServerResponseStartStackBufSize = 256 - // TCPServerRequestStart represents the parsed client request startup on the server side. +// Deprecated: prefer ParsedTCPRequestStart. type TCPServerRequestStart struct { Method Method PSK []byte @@ -21,7 +17,6 @@ type TCPServerRequestStart struct { Header TCPRequestVariableHeader } -// Init initializes the server request-start state from method, PSK, and request salt. func (s *TCPServerRequestStart) Init(method Method, psk, requestSalt []byte) error { if err := method.Validate(); err != nil { return err @@ -32,132 +27,46 @@ func (s *TCPServerRequestStart) Init(method Method, psk, requestSalt []byte) err if len(requestSalt) != method.SaltSize { return fmt.Errorf("invalid request salt length: got %d, want %d", len(requestSalt), method.SaltSize) } - - requestCipher, err := NewTCPStreamCipherFromPSK(method, psk, requestSalt) + cipher, err := NewTCPStreamCipherFromPSK(method, psk, requestSalt) if err != nil { return err } - s.Method = method - s.PSK = psk - s.RequestSalt = requestSalt - s.RequestCipher = requestCipher - + s.PSK = append(s.PSK[:0], psk...) + s.RequestSalt = append(s.RequestSalt[:0], requestSalt...) + s.RequestCipher = cipher return nil } -// Validate checks whether the parsed server request-start state is internally valid. func (s *TCPServerRequestStart) Validate() error { if s == nil { return fmt.Errorf("nil TCP server request start") } - if err := s.Method.Validate(); err != nil { - return err - } - if len(s.PSK) != s.Method.KeySize { - return fmt.Errorf("invalid PSK length: got %d, want %d", len(s.PSK), s.Method.KeySize) - } - if len(s.RequestSalt) != s.Method.SaltSize { - return fmt.Errorf("invalid request salt length: got %d, want %d", len(s.RequestSalt), s.Method.SaltSize) - } - if s.RequestCipher == nil { - return fmt.Errorf("missing request cipher") + parsed := &ParsedTCPRequestStart{ + Salt: s.RequestSalt, + Cipher: s.RequestCipher, + Fixed: s.FixedHeader, + Header: s.Header, } - if err := s.RequestCipher.Validate(); err != nil { - return err - } - if err := s.FixedHeader.Validate(); err != nil { - return err - } - if err := s.Header.Validate(); err != nil { - return err - } - if int(s.FixedHeader.Length) != s.Header.EncodedLen() { - return fmt.Errorf("request variable header length mismatch: got %d, want %d", s.FixedHeader.Length, s.Header.EncodedLen()) - } - - return nil + return parsed.Validate(s.Method) } -// ReadRequestStart reads and decrypts the full client request startup: -// -// request salt || encrypted request fixed header || encrypted request variable header -func (s *TCPServerRequestStart) ReadRequestStart( - src io.Reader, - method Method, - psk []byte, -) (int64, error) { - var total int64 - - if err := method.Validate(); err != nil { - return 0, err - } - if len(psk) != method.KeySize { - return 0, fmt.Errorf("invalid PSK length: got %d, want %d", len(psk), method.KeySize) - } - - requestSaltLen := method.SaltSize - requestSaltBuf := ibuf.GetBytes(requestSaltLen) - defer ibuf.PutBytes(requestSaltBuf) - - n, err := io.ReadFull(src, requestSaltBuf) - total += int64(n) - if err != nil { - return total, err - } - - if err := s.Init(method, psk, requestSaltBuf); err != nil { - return total, err - } - - encFixedLen := TcpRequestFixedHeaderLen + method.TagSize - encFixed := ibuf.GetBytes(encFixedLen) - defer ibuf.PutBytes(encFixed) - - n, err = io.ReadFull(src, encFixed) - total += int64(n) - if err != nil { - return total, err - } - - fixedPlainScratch := ibuf.GetBytes(TcpRequestFixedHeaderLen) - defer ibuf.PutBytes(fixedPlainScratch) - - fixedHeader, err := s.RequestCipher.DecodeRequestFixedHeader(encFixed, fixedPlainScratch[:0]) +func (s *TCPServerRequestStart) ReadRequestStart(src io.Reader, method Method, psk []byte) (int64, error) { + parsed, n, err := ReadTCPRequestStart(src, method, psk) if err != nil { - return total, err + return n, err } - s.FixedHeader = fixedHeader - - encVariableLen := int(s.FixedHeader.Length) + method.TagSize - encVariable := ibuf.GetBytes(encVariableLen) - defer ibuf.PutBytes(encVariable) - - n, err = io.ReadFull(src, encVariable) - total += int64(n) - if err != nil { - return total, err - } - - variablePlainScratch := ibuf.GetBytes(int(s.FixedHeader.Length)) - defer ibuf.PutBytes(variablePlainScratch) - - variableHeader, err := s.RequestCipher.DecodeRequestVariableHeader(encVariable, variablePlainScratch[:0]) - if err != nil { - return total, err - } - s.Header = variableHeader - - if err := s.Validate(); err != nil { - return total, err - } - - s.RequestSalt = append([]byte(nil), requestSaltBuf...) - - return total, nil + s.Method = method + s.PSK = append(s.PSK[:0], psk...) + s.RequestSalt = parsed.Salt + s.RequestCipher = parsed.Cipher + s.FixedHeader = parsed.Fixed + s.Header = parsed.Header + return n, nil } // TCPServerResponseStart represents the server-side response startup state. +// Deprecated: prefer WriteTCPResponseStart. type TCPServerResponseStart struct { Method Method PSK []byte @@ -167,7 +76,6 @@ type TCPServerResponseStart struct { InitialPayload []byte } -// Init initializes the server response-start state from method, PSK, and response salt. func (s *TCPServerResponseStart) Init(method Method, psk, responseSalt []byte) error { if err := method.Validate(); err != nil { return err @@ -178,70 +86,39 @@ func (s *TCPServerResponseStart) Init(method Method, psk, responseSalt []byte) e if len(responseSalt) != method.SaltSize { return fmt.Errorf("invalid response salt length: got %d, want %d", len(responseSalt), method.SaltSize) } - - responseCipher, err := NewTCPStreamCipherFromPSK(method, psk, responseSalt) + cipher, err := NewTCPStreamCipherFromPSK(method, psk, responseSalt) if err != nil { return err } - s.Method = method - s.PSK = psk - s.ResponseSalt = responseSalt - s.ResponseCipher = responseCipher - + s.PSK = append(s.PSK[:0], psk...) + s.ResponseSalt = append(s.ResponseSalt[:0], responseSalt...) + s.ResponseCipher = cipher return nil } -// Validate checks whether the server response-start state is internally valid. func (s *TCPServerResponseStart) Validate(requestSalt []byte) error { if s == nil { return fmt.Errorf("nil TCP server response start") } - if err := s.Method.Validate(); err != nil { - return err - } - if len(s.PSK) != s.Method.KeySize { - return fmt.Errorf("invalid PSK length: got %d, want %d", len(s.PSK), s.Method.KeySize) - } - if len(s.ResponseSalt) != s.Method.SaltSize { - return fmt.Errorf("invalid response salt length: got %d, want %d", len(s.ResponseSalt), s.Method.SaltSize) - } - if s.ResponseCipher == nil { - return fmt.Errorf("missing response cipher") - } - if err := s.ResponseCipher.Validate(); err != nil { - return err - } - if err := s.Header.Validate(); err != nil { - return err + parsed := &ParsedTCPResponseStart{ + Salt: s.ResponseSalt, + Cipher: s.ResponseCipher, + Header: s.Header, + InitialPayload: s.InitialPayload, } - if !bytes.Equal(s.Header.RequestSalt, requestSalt) { - return fmt.Errorf("response request salt mismatch") - } - if int(s.Header.Length) != len(s.InitialPayload) { - return fmt.Errorf("invalid initial payload length: got %d, want %d", len(s.InitialPayload), s.Header.Length) - } - - return nil + return parsed.Validate(s.Method, requestSalt) } -// WriteResponseStart writes the full server response startup: -// -// response salt || encrypted response header || encrypted first response payload -func (s *TCPServerResponseStart) WriteResponseStart( - dst io.Writer, - timestamp time.Time, - requestSalt []byte, - initialPayload []byte, -) (int64, error) { +func (s *TCPServerResponseStart) WriteResponseStart(dst io.Writer, timestamp time.Time, requestSalt []byte, initialPayload []byte) (int64, error) { if s == nil { return 0, fmt.Errorf("nil TCP server response start") } if err := s.Method.Validate(); err != nil { return 0, err } - if len(requestSalt) != s.Method.SaltSize { - return 0, fmt.Errorf("invalid request salt length: got %d, want %d", len(requestSalt), s.Method.SaltSize) + if len(s.PSK) != s.Method.KeySize { + return 0, fmt.Errorf("invalid PSK length: got %d, want %d", len(s.PSK), s.Method.KeySize) } if len(s.ResponseSalt) != s.Method.SaltSize { return 0, fmt.Errorf("invalid response salt length: got %d, want %d", len(s.ResponseSalt), s.Method.SaltSize) @@ -249,42 +126,13 @@ func (s *TCPServerResponseStart) WriteResponseStart( if s.ResponseCipher == nil { return 0, fmt.Errorf("missing response cipher") } - if err := s.ResponseCipher.Validate(); err != nil { - return 0, err + cipher, n, err := WriteTCPResponseStart(dst, s.Method, s.PSK, s.ResponseSalt, timestamp, requestSalt, initialPayload) + if err == nil { + s.ResponseCipher = cipher + var header TCPResponseHeader + header.Init(TCPHeaderTypeServerStream, uint64(timestamp.Unix()), requestSalt, uint16(len(initialPayload))) + s.Header = header + s.InitialPayload = append(s.InitialPayload[:0], initialPayload...) } - - var header TCPResponseHeader - header.Init( - TCPHeaderTypeServerStream, - uint64(timestamp.Unix()), - requestSalt, - uint16(len(initialPayload)), - ) - if err := header.Validate(); err != nil { - return 0, err - } - s.Header = header - s.InitialPayload = append(s.InitialPayload[:0], initialPayload...) - - headerPlainScratch := ibuf.GetBytes(s.Header.EncodedLen()) - defer ibuf.PutBytes(headerPlainScratch) - - var stackBuf [tcpServerResponseStartStackBufSize]byte - out := stackBuf[:0] - - out = append(out, s.ResponseSalt...) - - var err error - out, err = s.ResponseCipher.EncodeResponseHeaderTo(out, &s.Header, headerPlainScratch[:0]) - if err != nil { - return 0, err - } - - out, err = s.ResponseCipher.EncodeChunkPayloadTo(out, s.InitialPayload) - if err != nil { - return 0, err - } - - n, err := dst.Write(out) - return int64(n), err + return n, err } diff --git a/shadowsocks/tcp_stream_start.go b/shadowsocks/tcp_stream_start.go new file mode 100644 index 0000000..2c9b333 --- /dev/null +++ b/shadowsocks/tcp_stream_start.go @@ -0,0 +1,318 @@ +package shadowsocks + +import ( + "bytes" + "fmt" + "io" + "time" + + ibuf "github.com/33TU/socks/internal" +) + +const ( + tcpRequestStartStackBufSize = 1024 + tcpResponseStartStackBufSize = 256 +) + +// ParsedTCPRequestStart is the parsed startup state for a TCP request stream. +type ParsedTCPRequestStart struct { + Salt []byte + Cipher *TCPStreamCipher + Fixed TCPRequestFixedHeader + Header TCPRequestVariableHeader +} + +func (s *ParsedTCPRequestStart) Validate(method Method) error { + if s == nil { + return fmt.Errorf("nil parsed TCP request start") + } + if err := method.Validate(); err != nil { + return err + } + if len(s.Salt) != method.SaltSize { + return fmt.Errorf("invalid request salt length: got %d, want %d", len(s.Salt), method.SaltSize) + } + if s.Cipher == nil { + return fmt.Errorf("missing request cipher") + } + if err := s.Cipher.Validate(); err != nil { + return err + } + if err := s.Fixed.Validate(); err != nil { + return err + } + if err := s.Header.Validate(); err != nil { + return err + } + if int(s.Fixed.Length) != s.Header.EncodedLen() { + return fmt.Errorf("request variable header length mismatch: got %d, want %d", s.Fixed.Length, s.Header.EncodedLen()) + } + return nil +} + +// ParsedTCPResponseStart is the parsed startup state for a TCP response stream. +type ParsedTCPResponseStart struct { + Salt []byte + Cipher *TCPStreamCipher + Header TCPResponseHeader + InitialPayload []byte +} + +func (s *ParsedTCPResponseStart) Validate(method Method, expectedRequestSalt []byte) error { + if s == nil { + return fmt.Errorf("nil parsed TCP response start") + } + if err := method.Validate(); err != nil { + return err + } + if len(s.Salt) != method.SaltSize { + return fmt.Errorf("invalid response salt length: got %d, want %d", len(s.Salt), method.SaltSize) + } + if s.Cipher == nil { + return fmt.Errorf("missing response cipher") + } + if err := s.Cipher.Validate(); err != nil { + return err + } + if err := s.Header.Validate(); err != nil { + return err + } + if len(s.Header.RequestSalt) != len(expectedRequestSalt) { + return fmt.Errorf("invalid echoed request salt length: got %d, want %d", len(s.Header.RequestSalt), len(expectedRequestSalt)) + } + if !bytes.Equal(s.Header.RequestSalt, expectedRequestSalt) { + return fmt.Errorf("response request salt mismatch") + } + if int(s.Header.Length) != len(s.InitialPayload) { + return fmt.Errorf("invalid initial payload length: got %d, want %d", len(s.InitialPayload), s.Header.Length) + } + return nil +} + +func EncodedTCPRequestStartLen(method Method, requestSalt []byte, variableHeaderLen int) (int, error) { + if err := method.Validate(); err != nil { + return 0, err + } + if len(requestSalt) != method.SaltSize { + return 0, fmt.Errorf("invalid request salt length: got %d, want %d", len(requestSalt), method.SaltSize) + } + if variableHeaderLen < 0 { + return 0, fmt.Errorf("invalid variable header length: %d", variableHeaderLen) + } + return len(requestSalt) + (TcpRequestFixedHeaderLen + method.TagSize) + (variableHeaderLen + method.TagSize), nil +} + +// WriteTCPRequestStart writes request salt, encrypted fixed header, and encrypted variable header. +func WriteTCPRequestStart(dst io.Writer, method Method, psk, requestSalt []byte, timestamp time.Time, target Addr, padding, initialData []byte) (*TCPStreamCipher, int64, error) { + if err := method.Validate(); err != nil { + return nil, 0, err + } + requestCipher, err := NewTCPStreamCipherFromPSK(method, psk, requestSalt) + if err != nil { + return nil, 0, err + } + + var variableHeader TCPRequestVariableHeader + variableHeader.Init(target, padding, initialData) + if err := variableHeader.Validate(); err != nil { + return nil, 0, err + } + + var fixedHeader TCPRequestFixedHeader + fixedHeader.Init(TCPHeaderTypeClientStream, uint64(timestamp.Unix()), uint16(variableHeader.EncodedLen())) + + scratchLen := TcpRequestFixedHeaderLen + if variableHeader.EncodedLen() > scratchLen { + scratchLen = variableHeader.EncodedLen() + } + plainScratch := ibuf.GetBytes(scratchLen) + defer ibuf.PutBytes(plainScratch) + + var stackBuf [tcpRequestStartStackBufSize]byte + out := stackBuf[:0] + out = append(out, requestSalt...) + + out, err = requestCipher.EncodeRequestFixedHeaderTo(out, &fixedHeader, plainScratch[:0]) + if err != nil { + return nil, 0, err + } + out, err = requestCipher.EncodeRequestVariableHeaderTo(out, &variableHeader, plainScratch[:0]) + if err != nil { + return nil, 0, err + } + + n, err := dst.Write(out) + return requestCipher, int64(n), err +} + +// ReadTCPRequestStart reads and decrypts a full client request startup. +func ReadTCPRequestStart(src io.Reader, method Method, psk []byte) (*ParsedTCPRequestStart, int64, error) { + var total int64 + if err := method.Validate(); err != nil { + return nil, 0, err + } + if len(psk) != method.KeySize { + return nil, 0, fmt.Errorf("invalid PSK length: got %d, want %d", len(psk), method.KeySize) + } + + requestSaltBuf := ibuf.GetBytes(method.SaltSize) + defer ibuf.PutBytes(requestSaltBuf) + n, err := io.ReadFull(src, requestSaltBuf) + total += int64(n) + if err != nil { + return nil, total, err + } + + requestCipher, err := NewTCPStreamCipherFromPSK(method, psk, requestSaltBuf) + if err != nil { + return nil, total, err + } + + encFixed := ibuf.GetBytes(TcpRequestFixedHeaderLen + method.TagSize) + defer ibuf.PutBytes(encFixed) + n, err = io.ReadFull(src, encFixed) + total += int64(n) + if err != nil { + return nil, total, err + } + + fixedPlainScratch := ibuf.GetBytes(TcpRequestFixedHeaderLen) + defer ibuf.PutBytes(fixedPlainScratch) + fixedHeader, err := requestCipher.DecodeRequestFixedHeader(encFixed, fixedPlainScratch[:0]) + if err != nil { + return nil, total, err + } + + encVariable := ibuf.GetBytes(int(fixedHeader.Length) + method.TagSize) + defer ibuf.PutBytes(encVariable) + n, err = io.ReadFull(src, encVariable) + total += int64(n) + if err != nil { + return nil, total, err + } + + variablePlainScratch := ibuf.GetBytes(int(fixedHeader.Length)) + defer ibuf.PutBytes(variablePlainScratch) + variableHeader, err := requestCipher.DecodeRequestVariableHeader(encVariable, variablePlainScratch[:0]) + if err != nil { + return nil, total, err + } + + parsed := &ParsedTCPRequestStart{ + Salt: append([]byte(nil), requestSaltBuf...), + Cipher: requestCipher, + Fixed: fixedHeader, + Header: variableHeader, + } + if err := parsed.Validate(method); err != nil { + return nil, total, err + } + return parsed, total, nil +} + +// WriteTCPResponseStart writes response salt, encrypted response header, and first encrypted payload. +func WriteTCPResponseStart(dst io.Writer, method Method, psk, responseSalt []byte, timestamp time.Time, requestSalt, initialPayload []byte) (*TCPStreamCipher, int64, error) { + if err := method.Validate(); err != nil { + return nil, 0, err + } + if len(requestSalt) != method.SaltSize { + return nil, 0, fmt.Errorf("invalid request salt length: got %d, want %d", len(requestSalt), method.SaltSize) + } + responseCipher, err := NewTCPStreamCipherFromPSK(method, psk, responseSalt) + if err != nil { + return nil, 0, err + } + + var header TCPResponseHeader + header.Init(TCPHeaderTypeServerStream, uint64(timestamp.Unix()), requestSalt, uint16(len(initialPayload))) + if err := header.Validate(); err != nil { + return nil, 0, err + } + + headerPlainScratch := ibuf.GetBytes(header.EncodedLen()) + defer ibuf.PutBytes(headerPlainScratch) + + var stackBuf [tcpResponseStartStackBufSize]byte + out := stackBuf[:0] + out = append(out, responseSalt...) + out, err = responseCipher.EncodeResponseHeaderTo(out, &header, headerPlainScratch[:0]) + if err != nil { + return nil, 0, err + } + out, err = responseCipher.EncodeChunkPayloadTo(out, initialPayload) + if err != nil { + return nil, 0, err + } + + n, err := dst.Write(out) + return responseCipher, int64(n), err +} + +// ReadTCPResponseStart reads and decrypts a full server response startup. +func ReadTCPResponseStart(src io.Reader, method Method, psk, expectedRequestSalt []byte) (*ParsedTCPResponseStart, int64, error) { + var total int64 + if err := method.Validate(); err != nil { + return nil, 0, err + } + if len(psk) != method.KeySize { + return nil, 0, fmt.Errorf("invalid PSK length: got %d, want %d", len(psk), method.KeySize) + } + if len(expectedRequestSalt) != method.SaltSize { + return nil, 0, fmt.Errorf("invalid request salt length: got %d, want %d", len(expectedRequestSalt), method.SaltSize) + } + + responseSaltBuf := ibuf.GetBytes(method.SaltSize) + defer ibuf.PutBytes(responseSaltBuf) + n, err := io.ReadFull(src, responseSaltBuf) + total += int64(n) + if err != nil { + return nil, total, err + } + + responseCipher, err := NewTCPStreamCipherFromPSK(method, psk, responseSaltBuf) + if err != nil { + return nil, total, err + } + + encHeaderLen := 1 + 8 + method.SaltSize + 2 + method.TagSize + encHeader := ibuf.GetBytes(encHeaderLen) + defer ibuf.PutBytes(encHeader) + n, err = io.ReadFull(src, encHeader) + total += int64(n) + if err != nil { + return nil, total, err + } + + plainHeaderScratch := ibuf.GetBytes(1 + 8 + method.SaltSize + 2) + defer ibuf.PutBytes(plainHeaderScratch) + header, err := responseCipher.DecodeResponseHeader(encHeader, plainHeaderScratch[:0]) + if err != nil { + return nil, total, err + } + + encPayloadBuf := ibuf.GetBytes(responseCipher.EncryptedPayloadLength(int(header.Length))) + defer ibuf.PutBytes(encPayloadBuf) + n, err = io.ReadFull(src, encPayloadBuf) + total += int64(n) + if err != nil { + return nil, total, err + } + + payloadScratch := ibuf.GetBytes(int(header.Length)) + defer ibuf.PutBytes(payloadScratch) + initialPayload, err := responseCipher.DecodeChunkPayloadTo(payloadScratch[:0], encPayloadBuf) + if err != nil { + return nil, total, err + } + + parsed := &ParsedTCPResponseStart{ + Salt: append([]byte(nil), responseSaltBuf...), + Cipher: responseCipher, + Header: header, + InitialPayload: append([]byte(nil), initialPayload...), + } + if err := parsed.Validate(method, expectedRequestSalt); err != nil { + return nil, total, err + } + return parsed, total, nil +} From e3e285c5894a1c274e08008711fe685a86cffcfa Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 22:44:45 +0300 Subject: [PATCH 18/25] simplidy dialer and tcp conn --- shadowsocks/dialer.go | 24 +---- shadowsocks/tcp_conn.go | 210 +++++++++++++++++++++++++++++++--------- 2 files changed, 164 insertions(+), 70 deletions(-) diff --git a/shadowsocks/dialer.go b/shadowsocks/dialer.go index 3bd6583..b80ff56 100644 --- a/shadowsocks/dialer.go +++ b/shadowsocks/dialer.go @@ -149,36 +149,16 @@ func (d *Dialer) DialConnContext(ctx context.Context, conn net.Conn, network, ad return nil, err } - requestSalt := make([]byte, method.SaltSize) - if err := FillSaltTo(requestSalt, method); err != nil { - conn.Close() - return nil, err - } - // cancellation and deadline handling cleanup := bindConnToContext(ctx, conn) defer cleanup() - requestCipher, _, err := WriteTCPRequestStart(conn, method, psk, requestSalt, time.Now(), target, []byte{0}, nil) + ssConn, err := NewClientTCPConn(conn, method, psk, target, []byte{0}, nil) if err != nil { conn.Close() return nil, err } - - var writer TCPChunkWriter - if err := writer.Init(requestCipher); err != nil { - conn.Close() - return nil, err - } - - return &TcpConn{ - Conn: conn, - Writer: writer, - requestMethod: method, - requestPSK: append([]byte(nil), psk...), - requestSalt: append([]byte(nil), requestSalt...), - requestCipher: requestCipher, - }, nil + return ssConn, nil } // DialConn upgrades an existing connection using background context. diff --git a/shadowsocks/tcp_conn.go b/shadowsocks/tcp_conn.go index b813c08..b763a17 100644 --- a/shadowsocks/tcp_conn.go +++ b/shadowsocks/tcp_conn.go @@ -3,85 +3,196 @@ package shadowsocks import ( "fmt" "net" - "sync" + "time" ) type TcpConn struct { net.Conn - Reader TCPChunkReader - Writer TCPChunkWriter - + Reader TCPChunkReader + Writer TCPChunkWriter readBuf []byte - requestMethod Method - requestPSK []byte - requestSalt []byte - requestCipher *TCPStreamCipher + responseMethod Method + responsePSK []byte + requestSalt []byte +} + +func NewClientTCPConn( + conn net.Conn, + method Method, + psk []byte, + target Addr, + padding []byte, + initialPayload []byte, +) (*TcpConn, error) { + if conn == nil { + return nil, fmt.Errorf("nil net.Conn") + } + if err := method.Validate(); err != nil { + return nil, err + } + if len(psk) != method.KeySize { + return nil, fmt.Errorf("invalid PSK length: got %d, want %d", len(psk), method.KeySize) + } + if err := target.Validate(); err != nil { + return nil, err + } + + requestSalt := make([]byte, method.SaltSize) + if err := FillSaltTo(requestSalt, method); err != nil { + return nil, err + } + + requestCipher, _, err := WriteTCPRequestStart( + conn, + method, + psk, + requestSalt, + time.Now(), + target, + padding, + initialPayload, + ) + if err != nil { + return nil, err + } - responseStart *ParsedTCPResponseStart + var writer TCPChunkWriter + if err := writer.Init(requestCipher); err != nil { + return nil, err + } - readInitMu sync.Mutex - writeInitMu sync.Mutex + return &TcpConn{ + Conn: conn, + Writer: writer, + responseMethod: method, + responsePSK: append([]byte(nil), psk...), + requestSalt: append([]byte(nil), requestSalt...), + }, nil } -func (c *TcpConn) ensureReadReady() error { - c.readInitMu.Lock() - defer c.readInitMu.Unlock() +func NewServerTCPConn(conn net.Conn, method Method, psk []byte) (*TcpConn, *ParsedTCPRequestStart, error) { + if conn == nil { + return nil, nil, fmt.Errorf("nil net.Conn") + } + if err := method.Validate(); err != nil { + return nil, nil, err + } + if len(psk) != method.KeySize { + return nil, nil, fmt.Errorf("invalid PSK length: got %d, want %d", len(psk), method.KeySize) + } + + reqStart, _, err := ReadTCPRequestStart(conn, method, psk) + if err != nil { + return nil, nil, err + } - if c.Reader.Cipher != nil { - return nil + var reader TCPChunkReader + if err := reader.Init(reqStart.Cipher); err != nil { + return nil, nil, err } - if c.responseStart != nil { - if c.responseStart.Cipher == nil { - return fmt.Errorf("missing TCP response cipher") - } - if err := c.Reader.Init(c.responseStart.Cipher); err != nil { - return err - } - c.readBuf = append(c.readBuf[:0], c.responseStart.InitialPayload...) - return nil + c := &TcpConn{ + Conn: conn, + Reader: reader, } - if err := c.requestMethod.Validate(); err != nil { + if len(reqStart.Header.InitialData) > 0 { + c.readBuf = append([]byte(nil), reqStart.Header.InitialData...) + } + + return c, reqStart, nil +} + +func (c *TcpConn) InitResponse(method Method, psk []byte, requestSalt []byte, initialPayload []byte) error { + if c == nil { + return fmt.Errorf("nil TcpConn") + } + if c.Conn == nil { + return fmt.Errorf("nil net.Conn") + } + if err := method.Validate(); err != nil { return err } - if len(c.requestPSK) != c.requestMethod.KeySize { - return fmt.Errorf("invalid request PSK length: got %d, want %d", len(c.requestPSK), c.requestMethod.KeySize) + if len(psk) != method.KeySize { + return fmt.Errorf("invalid PSK length: got %d, want %d", len(psk), method.KeySize) } - if len(c.requestSalt) != c.requestMethod.SaltSize { - return fmt.Errorf("invalid request salt length: got %d, want %d", len(c.requestSalt), c.requestMethod.SaltSize) + if len(requestSalt) != method.SaltSize { + return fmt.Errorf("invalid request salt length: got %d, want %d", len(requestSalt), method.SaltSize) } - respStart, _, err := ReadTCPResponseStart(c.Conn, c.requestMethod, c.requestPSK, c.requestSalt) - if err != nil { + responseSalt := make([]byte, method.SaltSize) + if err := FillSaltTo(responseSalt, method); err != nil { return err } - if err := c.Reader.Init(respStart.Cipher); err != nil { + + responseCipher, _, err := WriteTCPResponseStart( + c.Conn, + method, + psk, + responseSalt, + time.Now(), + requestSalt, + initialPayload, + ) + if err != nil { return err } - c.responseStart = respStart - c.readBuf = append(c.readBuf[:0], respStart.InitialPayload...) - return nil -} -func (c *TcpConn) ensureWriteReady() error { - c.writeInitMu.Lock() - defer c.writeInitMu.Unlock() + return c.Writer.Init(responseCipher) +} - if c.Writer.Cipher != nil { +func (c *TcpConn) ensureClientResponseReady() error { + if c == nil { + return fmt.Errorf("nil TcpConn") + } + if c.Conn == nil { + return fmt.Errorf("nil net.Conn") + } + if c.Reader.Cipher != nil { return nil } - if c.requestCipher == nil { - return fmt.Errorf("missing TCP request cipher") + if err := c.responseMethod.Validate(); err != nil { + return err + } + if len(c.responsePSK) != c.responseMethod.KeySize { + return fmt.Errorf("invalid response PSK length: got %d, want %d", len(c.responsePSK), c.responseMethod.KeySize) + } + if len(c.requestSalt) != c.responseMethod.SaltSize { + return fmt.Errorf("invalid request salt length: got %d, want %d", len(c.requestSalt), c.responseMethod.SaltSize) + } + + respStart, _, err := ReadTCPResponseStart(c.Conn, c.responseMethod, c.responsePSK, c.requestSalt) + if err != nil { + return err } - return c.Writer.Init(c.requestCipher) + + if err := c.Reader.Init(respStart.Cipher); err != nil { + return err + } + + if len(respStart.InitialPayload) > 0 { + c.readBuf = append(c.readBuf[:0], respStart.InitialPayload...) + } + + return nil } func (c *TcpConn) Read(p []byte) (int, error) { - if err := c.ensureReadReady(); err != nil { - return 0, err + if len(p) == 0 { + return 0, nil + } + if c == nil { + return 0, fmt.Errorf("nil TcpConn") + } + if c.Reader.Cipher == nil { + if err := c.ensureClientResponseReady(); err != nil { + return 0, err + } + } + if c.Reader.Cipher == nil { + return 0, fmt.Errorf("TCP reader not initialized") } if len(c.readBuf) == 0 { @@ -101,8 +212,11 @@ func (c *TcpConn) Write(p []byte) (int, error) { if len(p) == 0 { return 0, nil } - if err := c.ensureWriteReady(); err != nil { - return 0, err + if c == nil { + return 0, fmt.Errorf("nil TcpConn") + } + if c.Writer.Cipher == nil { + return 0, fmt.Errorf("TCP writer not initialized") } written := 0 From 1f171ac791c7260248a466cb940c692f0fcd94bc Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 22:47:23 +0300 Subject: [PATCH 19/25] tidy tcp conn --- shadowsocks/doc.txt | 307 ++++++++++++++++++++++++++++++++++++++++ shadowsocks/tcp_conn.go | 5 +- 2 files changed, 308 insertions(+), 4 deletions(-) create mode 100644 shadowsocks/doc.txt diff --git a/shadowsocks/doc.txt b/shadowsocks/doc.txt new file mode 100644 index 0000000..08e31a6 --- /dev/null +++ b/shadowsocks/doc.txt @@ -0,0 +1,307 @@ +# Shadowsocks 2022 Edition: Secure L4 Tunnel with Symmetric Encryption + +## Abstract + +This document defines the 2022 Edition of the Shadowsocks protocol. Improving upon Shadowsocks AEAD (2017), Shadowsocks 2022 addresses well-known issues of the previous editions, drops usage of obsolete cryptography, optimizes for security and performance, and leaves room for future extensions. + +## 1. Overview + +Shadowsocks 2022 is a secure proxy protocol for TCP and UDP traffic. The protocol uses [AEAD](https://en.wikipedia.org/wiki/Authenticated_encryption) with a pre-shared symmetric key to protect payload integrity and confidentiality. The proxy traffic is indistinguishable from a random byte stream, and therefore can circumvent firewalls and Internet censors that rely on [DPI (Deep Packet Inspection)](https://en.wikipedia.org/wiki/Deep_packet_inspection). + +Compared to [previous editions](https://github.com/shadowsocks/shadowsocks-org/blob/master/whitepaper/whitepaper.md) of the protocol family, Shadowsocks 2022 allows and mandates full replay protection. Each message has its unique type and cannot be used for unintended purposes. The session-based UDP proxying significantly reduces protocol overhead and improves reliability and efficiency. Obsolete cryptographic functions have been replaced by their modern counterparts. + +As with previous editions, Shadowsocks 2022 does not provide forward secrecy. It is believed that using a pre-shared key without performing handshakes is best for its use cases. + +A Shadowsocks 2022 implementation consists of a server, a client, and optionally a relay. This document specifies requirements that implementations must follow. + +### 1.1. Document Structure + +This document describes the Shadowsocks 2022 Edition and is structured as follows: + +- Section 2 describes requirements on the encryption key and how to derive session subkeys. +- Section 3 defines the encoding details of the required AES-GCM methods and the process for handling requests and responses. +- Section 4 defines the encoding details of the optional ChaCha-Poly1305 methods. + +### 1.2. Terms and Definitions + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119](https://www.rfc-editor.org/info/rfc2119) [RFC8174](https://www.rfc-editor.org/info/rfc8174) when, and only when, they appear in all capitals, as shown here. + +Commonly used terms in this document are described below. + +- Shadowsocks AEAD: The original AEAD construction of Shadowsocks, standardized in 2017. + +## 2. Encryption/Decryption Keys + +A pre-shared key is used to derive session subkeys, which are subsequently used to encrypt/decrypt traffic for the session. The pre-shared key is also used directly in some places. + +### 2.1. PSK + +Unlike previous editions, Shadowsocks 2022 requires that a cryptographically-secure fixed-length PSK to be directly provided by the user. Implementations MUST NOT use the old `EVP_BytesToKey` function or any other method to generate keys from passwords. + +The PSK is encoded in base64 for convenience. Practically, it can be generated with `openssl rand -base64 `. The key size depends on the chosen method. This change was inspired by WireGuard. + +| Method | Key Bytes | Salt Bytes | +| ----------------------- | --------: | ---------: | +| 2022-blake3-aes-128-gcm | 16 | 16 | +| 2022-blake3-aes-256-gcm | 32 | 32 | + +### 2.2. Subkey Derivation + +Shadowsocks 2022's subkey derivation uses [BLAKE3](https://raw.githubusercontent.com/BLAKE3-team/BLAKE3-specs/master/blake3.pdf)'s key derivation mode, which replaces the obsolete HKDF_SHA1 function in previous editions. A randomly generated salt is appended to the PSK to be used as key material. The salt has the same length as the pre-shared key. + +``` +session_subkey := blake3::derive_key(context: "shadowsocks 2022 session subkey", key_material: key + salt) +``` + +## 3. Required Methods + +Method `2022-blake3-aes-128-gcm` and `2022-blake3-aes-256-gcm` MUST be implemented by all implementations. `2022` reflects the fast-changing and flexible nature of the protocol. + +### 3.1. TCP + +TCP connections over a Shadowsocks 2022 tunnel maps 1:1 to proxy connections. Each proxy connection carries 2 proxy streams: request stream and response stream. A client initiates a proxy connection by starting a request stream, and the server sends back response over the response stream. These streams carry chunks of data encrypted by the session subkey. + +For payload transfer, Shadowsocks 2022 inherits the length-chunk-payload-chunk model from Shadowsocks AEAD, with some minor tweaks to improve performance. Standalone header chunks are added to both request and response streams to improve security and protect against replay attacks. + +#### 3.1.1. Encryption and Decryption + +Each proxy stream derives its own session subkey with a random salt for encryption and decryption. A 12-byte little-endian integer is used as nonce, and is incremented after each encryption or decryption operation. + +``` +u96le counter +aead := aead_new(key: session_subkey) +ciphertext := aead.seal(nonce: counter, plaintext) +plaintext := aead.open(nonce: counter, ciphertext) +``` + +#### 3.1.2. Format + +A request stream starts with one random salt and two standalone header chunks, followed repeatedly by one length chunk and one payload chunk. Each chunk is independently encrypted/decrypted using the AEAD cipher. + +A response stream also starts with a random salt, but only has one fixed-length header chunk, which also acts as the first length chunk. + +A length chunk is a 16-bit big-endian unsigned integer that describes the payload length in the next payload chunk. Servers and clients rely on length chunks to know how many bytes to read for the next payload chunk. + +A payload chunk can have up to 0xFFFF (65535) bytes of unencrypted payload. The 0x3FFF (16383) length cap in Shadowsocks AEAD does not apply to this edition. + +``` ++----------------+ +| length chunk | ++----------------+ +| u16 big-endian | ++----------------+ + ++---------------+ +| payload chunk | ++---------------+ +| variable | ++---------------+ + +Request stream: ++--------+------------------------+---------------------------+------------------------+---------------------------+---+ +| salt | encrypted header chunk | encrypted header chunk | encrypted length chunk | encrypted payload chunk |...| ++--------+------------------------+---------------------------+------------------------+---------------------------+---+ +| 16/32B | 11B + 16B tag | variable length + 16B tag | 2B length + 16B tag | variable length + 16B tag |...| ++--------+------------------------+---------------------------+------------------------+---------------------------+---+ + +Response stream: ++--------+------------------------+---------------------------+------------------------+---------------------------+---+ +| salt | encrypted header chunk | encrypted payload chunk | encrypted length chunk | encrypted payload chunk |...| ++--------+------------------------+---------------------------+------------------------+---------------------------+---+ +| 16/32B | 27/43B + 16B tag | variable length + 16B tag | 2B length + 16B tag | variable length + 16B tag |...| ++--------+------------------------+---------------------------+------------------------+---------------------------+---+ +``` + +#### 3.1.3. Header + +``` +Request fixed-length header: ++------+------------------+--------+ +| type | timestamp | length | ++------+------------------+--------+ +| 1B | u64be unix epoch | u16be | ++------+------------------+--------+ + +Request variable-length header: ++------+----------+-------+----------------+----------+-----------------+ +| ATYP | address | port | padding length | padding | initial payload | ++------+----------+-------+----------------+----------+-----------------+ +| 1B | variable | u16be | u16be | variable | variable | ++------+----------+-------+----------------+----------+-----------------+ + +Response fixed-length header: ++------+------------------+----------------+--------+ +| type | timestamp | request salt | length | ++------+------------------+----------------+--------+ +| 1B | u64be unix epoch | 16/32B | u16be | ++------+------------------+----------------+--------+ + +HeaderTypeClientStream = 0 +HeaderTypeServerStream = 1 +MinPaddingLength = 0 +MaxPaddingLength = 900 +``` + +- 1-byte type: Differentiates between client and server messages. A request stream has type `0`. A response stream has type `1`. +- 8-byte Unix epoch timestamp: Messages with over 30 seconds of time difference MUST be treated as replay. +- Length: Indicates the next chunk's plaintext length (not including the authentication tag). +- ATYP + address + port: Target address in [SOCKS5 address format](https://datatracker.ietf.org/doc/html/rfc1928#section-5). +- Request salt in response header: This maps a response stream to a request stream. The client MUST check this field in response header against the request salt. + +#### 3.1.3. Detection Prevention + +The random salt and header chunks MUST be buffered and sent in one write call to the underlying socket. Separate writes can result in predictable packet sizes, which could reveal the protocol in use. + +To process the salt and the fixed-length header, servers and clients MUST make exactly one read call. If the amount of data received is not enough for decryption, or decryption fails, or header validation fails, the server MUST act in a way that does not exhibit the amount of bytes consumed by the server. This defends against probes that send one byte at a time to detect how many bytes the server consumes before closing the connection. + +In such circumstances, do not immediately close the socket. Closing the socket with unread data causes RST to be sent. This reveals the exact number of bytes consumed by the server. Implementations MAY choose to employ one of the following strategies: + +1. To consistently send RST even when the receive buffer is empty, set `SO_LINGER` to true with a zero timeout, then close the socket. +2. To consistently send FIN even when the receive buffer has unread data, shut down the write half of the connection by calling `shutdown(SHUT_WR)`, then drain the connection for any further received data. +3. To consistently send FIN even when the receive buffer has unread data, but disallow unlimited writes, shut down the write half of the connection by calling `shutdown(SHUT_WR)`, then `epoll` for `EPOLLRDHUP`. Read until EOF then close the connection. This limits the amount of data the other party can send to the size of your socket receive buffer. + +In a request header, either initial payload or padding MUST be present. When making a request header, if payload is not available, add non-zero random length padding. + +For client implementations, a simple approach is to always send random length padding. To accommodate TCP Fast Open (TFO), clients MAY wait a short amount of time (typically less than one second) for client-first protocols to write the first payload, before carrying on to establish a proxy connection and write the header. + +Servers MUST reject the request if the variable-length header chunk does not contain payload and the padding length is 0. +Servers MUST enforce that the request header (including padding) does not extend beyond the header chunks. + +For response streams, the header is always sent along with payload. No padding is needed. + +#### 3.1.4. Replay Protection + +Servers MUST store all incoming salts for 60 seconds. When a new TCP session is established, the first received message is decrypted and its timestamp MUST be checked against system time. If the time difference is within 30 seconds, then the salt is checked against all stored salts. If no repeated salt is discovered, then the salt is added to the pool and the session is successfully established. + +Some techniques in implementations of previous editions are no longer necessary and SHOULD NOT be implemented for Shadowsocks 2022: + +- Clients do not need to check the salt in response streams, because the response header includes an associated request salt. +- Outgoing salts do not need to be added to the salt pool, because the header has a type field that indicates the direction of the stream. +- For salt storage, implementations MUST NOT use Bloom filters or anything that could return a false positive result, because salts only have to be stored for 60 seconds. + +### 3.2. UDP + +Shadowsocks 2022 completely overhauled UDP relay. Each UDP relay session has a unique session ID, which is also used as salt to derive the session subkey. A packet ID acts as packet counter for the session. The session ID and packet ID are combined and encrypted in a separate header. + +Clients create UDP relay sessions based on source address and port. When a client receives a packet from a new source address and port, it opens a new relay session, and subsequent packets from that source are sent over the same session. + +Servers manage UDP relay sessions by session ID. Each client session corresponds to one outgoing UDP socket on the server. + +#### 3.2.1. Encryption and Decryption + +The separate header is encrypted/decrypted with the pre-shared key using an AES block cipher. The body is encrypted/decrypted with the session subkey using an AEAD cipher specific to the session. + +``` +block_cipher := aes_new(psk) +encrypted_separate_header := block_cipher.encrypt(separate_header) +decrypted_separate_header := block_cipher.decrypt(encrypted_separate_header) + +session_subkey := blake3::derive_key(context: "shadowsocks 2022 session subkey", key_material: key + separate_header[0..8]) +session_aead_cipher := aes_gcm_new(session_subkey) +encrypted_body := session_aead_cipher.seal(nonce: separate_header[4..16], body) +decrypted_body := session_aead_cipher.open(nonce: separate_header[4..16], body) +``` + +#### 3.2.2. Format and Separate Header + +A UDP packet consists of a separate header and an AEAD-encrypted body. The separate header consists of an 8-byte session ID and an 8-byte big-endian unsigned integer as packet ID. The body is made up of the main header and payload. + +``` +Packet: ++---------------------------+---------------------------+ +| encrypted separate header | encrypted body | ++---------------------------+---------------------------+ +| 16B | variable length + 16B tag | ++---------------------------+---------------------------+ + +Separate header: ++------------+-----------+ +| session ID | packet ID | ++------------+-----------+ +| 8B | u64be | ++------------+-----------+ +``` + +UDP sessions are initiated by clients. To start a UDP session, the client generates a new random session ID and maintains a counter starting at zero as packet ID. These are used in client-to-server messages and are usually referred to as client session ID and client packet ID. + +Servers use client session IDs to identify UDP sessions. For server-to-client messages, a different set of session ID and packet ID is used, and may be referred to as server session ID and server packet ID. Like the client session ID, the server session ID MUST be randomly generated. + +#### 3.2.3. Main Header + +The main header, or message header, is the header at the start of the body. The client-to-server message header consists of type, timestamp, padding and SOCKS address. The server-to-client message header has an additional client session ID field, which maps the server session to a client session. + +``` +Client-to-server message header: ++------+------------------+----------------+----------+------+----------+-------+ +| type | timestamp | padding length | padding | ATYP | address | port | ++------+------------------+----------------+----------+------+----------+-------+ +| 1B | u64be unix epoch | u16be | variable | 1B | variable | u16be | ++------+------------------+----------------+----------+------+----------+-------+ + +Server-to-client message header: ++------+------------------+-------------------+----------------+----------+------+----------+-------+ +| type | timestamp | client session ID | padding length | padding | ATYP | address | port | ++------+------------------+-------------------+----------------+----------+------+----------+-------+ +| 1B | u64be unix epoch | 8B | u16be | variable | 1B | variable | u16be | ++------+------------------+-------------------+----------------+----------+------+----------+-------+ + +HeaderTypeClientPacket = 0 +HeaderTypeServerPacket = 1 +``` + +- 1-byte type: Differentiates between client and server messages. A client message has type `0`. A server message has type `1`. +- 8-byte Unix epoch timestamp: Messages with over 30 seconds of time difference MUST be treated as replay. +- Padding length: Specifies the length of the optional padding. Implementations MAY allow users to select from a list of predefined padding policies. Care SHOULD be taken to not exceed the network path's MTU when padding packets. + +#### 3.2.4. Session ID based Routing and Sliding Window Replay Protection + +Servers MUST route packets based on client session ID, not packet source address. When a server receives a packet with a new client session ID, a new relay session is created, and subsequent packets from that client session are sent over this relay session. + +A relay session MUST keep track of the last seen client address. When a packet is received from the client and is successfully validated, the last seen client address MUST be updated. Return packets MUST be sent to this address. This allows UDP sessions to survive client network changes. + +Each relay session MUST be remembered for at least 60 seconds. A shorter NAT timeout may allow attackers to successfully replay packets from an already forgotten client session. + +To handle server restarts, clients MUST allow each client session to be associated with more than one server session. Each association MUST be remembered for no less than the NAT timeout, which is at least 60 seconds. Alternatively, clients MAY choose to keep track of one old server session and one current server session, and reject newer server sessions when the last packet received from the old session is less than 1 minute old. + +Clients and servers MUST employ a sliding window filter for each relay session to check incoming packets for duplicate or out-of-window packet IDs. Existing implementations from WireGuard MAY be used. The packet ID MAY be checked as soon as the separate header is decrypted, but the sliding window state MUST NOT be updated before successful header validation, which filters out packets that are semantically invalid or have a bad timestamp. + +## 4. Optional Methods + +Implementations MAY choose to implement `2022-blake3-chacha20-poly1305`, `2022-blake3-chacha12-poly1305` and `2022-blake3-chacha8-poly1305` when support for CPUs without AES instructions is a priority. The use of reduced-round ChaCha20 variants is justified by [this paper](https://eprint.iacr.org/2019/1492.pdf). + +For TCP, AES-GCM is simply replaced by ChaCha-Poly1305. For UDP, a slightly different construction is used. + +### 4.1. UDP Construction + +`2022-blake3-chacha20-poly1305` uses XChaCha20-Poly1305 with the pre-shared key directly and a random nonce for each message. + +A UDP packet starts with the random nonce, followed by an encrypted body. The session ID and packet ID are merged into the main header. + +The same sliding window filter is used for replay protection. It is not necessary to check for repeated nonce. + +``` +Packet: ++-------+---------------------------+ +| nonce | encrypted body | ++-------+---------------------------+ +| 24B | variable length + 16B tag | ++-------+---------------------------+ + +Client-to-server message header: ++-------------------+------------------+------+------------------+----------------+----------+------+----------+-------+ +| client session ID | client packet ID | type | timestamp | padding length | padding | ATYP | address | port | ++-------------------+------------------+------+------------------+----------------+----------+------+----------+-------+ +| 8B | u64be | 1B | u64be unix epoch | u16be | variable | 1B | variable | u16be | ++-------------------+------------------+------+------------------+----------------+----------+------+----------+-------+ + +Server-to-client message header: ++-------------------+------------------+------+------------------+-------------------+----------------+----------+------+----------+-------+ +| server session ID | server packet ID | type | timestamp | client session ID | padding length | padding | ATYP | address | port | ++-------------------+------------------+------+------------------+-------------------+----------------+----------+------+----------+-------+ +| 8B | u64be | 1B | u64be unix epoch | 8B | u16be | variable | 1B | variable | u16be | ++-------------------+------------------+------+------------------+-------------------+----------------+----------+------+----------+-------+ +``` + +## Acknowledgement + +I would like to thank @zonyitoo, @xiaokangwang, and @nekohasekai for their input on the design of the protocol. \ No newline at end of file diff --git a/shadowsocks/tcp_conn.go b/shadowsocks/tcp_conn.go index b763a17..490bdfe 100644 --- a/shadowsocks/tcp_conn.go +++ b/shadowsocks/tcp_conn.go @@ -221,10 +221,7 @@ func (c *TcpConn) Write(p []byte) (int, error) { written := 0 for len(p) > 0 { - nn := len(p) - if nn > 0xFFFF { - nn = 0xFFFF - } + nn := min(len(p), 0xFFFF) if _, err := c.Writer.WriteChunk(c.Conn, p[:nn]); err != nil { return written, err } From 80433a870e31da4d9909f5441e9843b389ed053d Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 23:08:37 +0300 Subject: [PATCH 20/25] refactor and combine files --- shadowsocks/consts.go | 12 + shadowsocks/dialer_test.go | 71 +-- shadowsocks/tcp_client_stream.go | 112 ---- shadowsocks/tcp_client_stream_test.go | 500 ----------------- shadowsocks/tcp_errors.go | 13 - shadowsocks/tcp_request_fixed_header.go | 70 --- shadowsocks/tcp_request_fixed_header_test.go | 153 ----- ...riable_header.go => tcp_request_header.go} | 66 +++ ...der_test.go => tcp_request_header_test.go} | 147 +++++ shadowsocks/tcp_server_stream.go | 138 ----- shadowsocks/tcp_server_stream_test.go | 525 ------------------ .../{tcp_stream_start.go => tcp_stream.go} | 50 +- 12 files changed, 276 insertions(+), 1581 deletions(-) delete mode 100644 shadowsocks/tcp_client_stream.go delete mode 100644 shadowsocks/tcp_client_stream_test.go delete mode 100644 shadowsocks/tcp_errors.go delete mode 100644 shadowsocks/tcp_request_fixed_header.go delete mode 100644 shadowsocks/tcp_request_fixed_header_test.go rename shadowsocks/{tcp_request_variable_header.go => tcp_request_header.go} (59%) rename shadowsocks/{tcp_request_variable_header_test.go => tcp_request_header_test.go} (68%) delete mode 100644 shadowsocks/tcp_server_stream.go delete mode 100644 shadowsocks/tcp_server_stream_test.go rename shadowsocks/{tcp_stream_start.go => tcp_stream.go} (87%) diff --git a/shadowsocks/consts.go b/shadowsocks/consts.go index 2db6e94..3f7dead 100644 --- a/shadowsocks/consts.go +++ b/shadowsocks/consts.go @@ -1,5 +1,7 @@ package shadowsocks +import "errors" + // Encryption method constants for Shadowsocks AEAD-2022. const ( Method2022Blake3AES128GCM = "2022-blake3-aes-128-gcm" @@ -31,3 +33,13 @@ const ( ) const TcpChunkLengthLen = 2 + +// Common validation and decode errors for Shadowsocks TCP headers. +var ( + ErrInvalidTCPHeaderType = errors.New("invalid TCP header type") // res and req + ErrInvalidTCPPaddingLength = errors.New("invalid TCP padding length") // req + ErrMissingTCPHeaderData = errors.New("missing TCP header data") // req + ErrShortTCPHeader = errors.New("short TCP header") // res and req + ErrMissingTCPResponseSalt = errors.New("missing TCP response salt") // res + ErrInvalidTCPResponseSaltLen = errors.New("invalid TCP response salt length") // res +) diff --git a/shadowsocks/dialer_test.go b/shadowsocks/dialer_test.go index f7a8f56..5edadc5 100644 --- a/shadowsocks/dialer_test.go +++ b/shadowsocks/dialer_test.go @@ -367,10 +367,9 @@ func TestDialer_DialContext_Success(t *testing.T) { proxyAddr, stop := startMockShadowsocksServer(t, func(c net.Conn) { defer c.Close() - var reqStart shadowsocks.TCPServerRequestStart - _, err := reqStart.ReadRequestStart(c, method, psk) + reqStart, _, err := shadowsocks.ReadTCPRequestStart(c, method, psk) if err != nil { - t.Errorf("server: ReadRequestStart() error = %v", err) + t.Errorf("server: ReadTCPRequestStart() error = %v", err) return } @@ -387,23 +386,15 @@ func TestDialer_DialContext_Success(t *testing.T) { return } - responseSalt := bytes.Repeat([]byte{0x33}, method.SaltSize) - - var respStart shadowsocks.TCPServerResponseStart - if err := respStart.Init(method, psk, responseSalt); err != nil { - t.Errorf("server: response Init() error = %v", err) - return - } - var reader shadowsocks.TCPChunkReader - if err := reader.Init(reqStart.RequestCipher); err != nil { + if err := reader.Init(reqStart.Cipher); err != nil { t.Errorf("server: reader Init() error = %v", err) return } payload, _, err := reader.ReadChunkTo(nil, c) if err != nil { - t.Errorf("server: ReadChunk() error = %v", err) + t.Errorf("server: ReadChunkTo() error = %v", err) return } if string(payload) != "ping" { @@ -411,35 +402,20 @@ func TestDialer_DialContext_Success(t *testing.T) { return } + responseSalt := bytes.Repeat([]byte{0x33}, method.SaltSize) initialPayload := []byte("pong") - var hdr shadowsocks.TCPResponseHeader - hdr.Init( - shadowsocks.TCPHeaderTypeServerStream, - uint64(time.Now().Unix()), - reqStart.RequestSalt, - uint16(len(initialPayload)), + _, _, err = shadowsocks.WriteTCPResponseStart( + c, + method, + psk, + responseSalt, + time.Now(), + reqStart.Salt, + initialPayload, ) - - encHeader, err := respStart.ResponseCipher.EncodeResponseHeaderTo(nil, &hdr, nil) - if err != nil { - t.Errorf("server: EncodeResponseHeaderTo() error = %v", err) - return - } - - encPayload, err := respStart.ResponseCipher.EncodeChunkPayloadTo(nil, initialPayload) if err != nil { - t.Errorf("server: EncodeChunkPayloadTo() error = %v", err) - return - } - - var wire bytes.Buffer - wire.Write(respStart.ResponseSalt) - wire.Write(encHeader) - wire.Write(encPayload) - - if _, err := c.Write(wire.Bytes()); err != nil { - t.Errorf("server: response start write error = %v", err) + t.Errorf("server: WriteTCPResponseStart() error = %v", err) return } }) @@ -484,18 +460,23 @@ func TestDialer_DialContext_Deadline(t *testing.T) { proxyAddr, stop := startMockShadowsocksServer(t, func(c net.Conn) { defer c.Close() - var reqStart shadowsocks.TCPServerRequestStart - if _, err := reqStart.ReadRequestStart(c, method, psk); err != nil { + reqStart, _, err := shadowsocks.ReadTCPRequestStart(c, method, psk) + if err != nil { return } responseSalt := bytes.Repeat([]byte{0x33}, method.SaltSize) - var respStart shadowsocks.TCPServerResponseStart - if err := respStart.Init(method, psk, responseSalt); err != nil { - return - } - if _, err := respStart.WriteResponseStart(c, time.Now(), reqStart.RequestSalt, nil); err != nil { + _, _, err = shadowsocks.WriteTCPResponseStart( + c, + method, + psk, + responseSalt, + time.Now(), + reqStart.Salt, + nil, + ) + if err != nil { return } diff --git a/shadowsocks/tcp_client_stream.go b/shadowsocks/tcp_client_stream.go deleted file mode 100644 index b57cfe9..0000000 --- a/shadowsocks/tcp_client_stream.go +++ /dev/null @@ -1,112 +0,0 @@ -package shadowsocks - -import ( - "fmt" - "io" - "time" -) - -// TCPClientRequestStart represents the client-side Shadowsocks 2022 TCP startup state. -// Deprecated: prefer WriteTCPRequestStart and ReadTCPResponseStart. -type TCPClientRequestStart struct { - Method Method - PSK []byte - RequestSalt []byte - RequestCipher *TCPStreamCipher -} - -func (s *TCPClientRequestStart) Init(method Method, psk, requestSalt []byte) error { - if err := method.Validate(); err != nil { - return err - } - if len(psk) != method.KeySize { - return fmt.Errorf("invalid PSK length: got %d, want %d", len(psk), method.KeySize) - } - if len(requestSalt) != method.SaltSize { - return fmt.Errorf("invalid request salt length: got %d, want %d", len(requestSalt), method.SaltSize) - } - cipher, err := NewTCPStreamCipherFromPSK(method, psk, requestSalt) - if err != nil { - return err - } - s.Method = method - s.PSK = append(s.PSK[:0], psk...) - s.RequestSalt = append(s.RequestSalt[:0], requestSalt...) - s.RequestCipher = cipher - return nil -} - -func (s *TCPClientRequestStart) Validate() error { - if s == nil { - return fmt.Errorf("nil TCP client request start") - } - if err := s.Method.Validate(); err != nil { - return err - } - if len(s.PSK) != s.Method.KeySize { - return fmt.Errorf("invalid PSK length: got %d, want %d", len(s.PSK), s.Method.KeySize) - } - if len(s.RequestSalt) != s.Method.SaltSize { - return fmt.Errorf("invalid request salt length: got %d, want %d", len(s.RequestSalt), s.Method.SaltSize) - } - if s.RequestCipher == nil { - return fmt.Errorf("missing request cipher") - } - return s.RequestCipher.Validate() -} - -func (s *TCPClientRequestStart) EncodedRequestStartLen(variableHeaderLen int) (int, error) { - if err := s.Validate(); err != nil { - return 0, err - } - return EncodedTCPRequestStartLen(s.Method, s.RequestSalt, variableHeaderLen) -} - -func (s *TCPClientRequestStart) WriteRequestStart(dst io.Writer, timestamp time.Time, target Addr, padding []byte, initialData []byte) (int64, error) { - if err := s.Validate(); err != nil { - return 0, err - } - cipher, n, err := WriteTCPRequestStart(dst, s.Method, s.PSK, s.RequestSalt, timestamp, target, padding, initialData) - if err == nil { - s.RequestCipher = cipher - } - return n, err -} - -// TCPClientResponseStart represents the parsed server response startup. -// Deprecated: prefer ParsedTCPResponseStart. -type TCPClientResponseStart struct { - ResponseSalt []byte - ResponseCipher *TCPStreamCipher - Header TCPResponseHeader - InitialPayload []byte -} - -func (s *TCPClientResponseStart) Validate(method Method, requestSalt []byte) error { - if s == nil { - return fmt.Errorf("nil TCP client response start") - } - parsed := &ParsedTCPResponseStart{ - Salt: s.ResponseSalt, - Cipher: s.ResponseCipher, - Header: s.Header, - InitialPayload: s.InitialPayload, - } - return parsed.Validate(method, requestSalt) -} - -func (s *TCPClientRequestStart) ReadResponseStart(src io.Reader) (*TCPClientResponseStart, int64, error) { - if err := s.Validate(); err != nil { - return nil, 0, err - } - parsed, n, err := ReadTCPResponseStart(src, s.Method, s.PSK, s.RequestSalt) - if err != nil { - return nil, n, err - } - return &TCPClientResponseStart{ - ResponseSalt: parsed.Salt, - ResponseCipher: parsed.Cipher, - Header: parsed.Header, - InitialPayload: parsed.InitialPayload, - }, n, nil -} diff --git a/shadowsocks/tcp_client_stream_test.go b/shadowsocks/tcp_client_stream_test.go deleted file mode 100644 index 1eb4510..0000000 --- a/shadowsocks/tcp_client_stream_test.go +++ /dev/null @@ -1,500 +0,0 @@ -package shadowsocks_test - -import ( - "bytes" - "io" - "strings" - "testing" - "time" - - "github.com/33TU/socks/shadowsocks" -) - -func newTCPClientStartTestMethod(t *testing.T) shadowsocks.Method { - t.Helper() - - method, err := shadowsocks.ParseMethod(shadowsocks.Method2022Blake3AES128GCM) - if err != nil { - t.Fatalf("ParseMethod() error = %v", err) - } - - return method -} - -func newTCPClientStartTestPSKAndSalt(method shadowsocks.Method) ([]byte, []byte) { - psk := make([]byte, method.KeySize) - salt := make([]byte, method.SaltSize) - - for i := range psk { - psk[i] = byte(i + 1) - } - for i := range salt { - salt[i] = byte(i + 101) - } - - return psk, salt -} - -func TestTCPClientRequestStart_Init_Validate(t *testing.T) { - t.Parallel() - - method := newTCPClientStartTestMethod(t) - psk, requestSalt := newTCPClientStartTestPSKAndSalt(method) - - t.Run("valid", func(t *testing.T) { - t.Parallel() - - var s shadowsocks.TCPClientRequestStart - if err := s.Init(method, psk, requestSalt); err != nil { - t.Fatalf("Init() error = %v", err) - } - if err := s.Validate(); err != nil { - t.Fatalf("Validate() error = %v", err) - } - }) - - t.Run("invalid psk length", func(t *testing.T) { - t.Parallel() - - var s shadowsocks.TCPClientRequestStart - err := s.Init(method, psk[:len(psk)-1], requestSalt) - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "invalid PSK length") { - t.Fatalf("unexpected error: %v", err) - } - }) - - t.Run("invalid request salt length", func(t *testing.T) { - t.Parallel() - - var s shadowsocks.TCPClientRequestStart - err := s.Init(method, psk, requestSalt[:len(requestSalt)-1]) - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "invalid request salt length") { - t.Fatalf("unexpected error: %v", err) - } - }) - - t.Run("nil receiver validate", func(t *testing.T) { - t.Parallel() - - var s *shadowsocks.TCPClientRequestStart - err := s.Validate() - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "nil TCP client request start") { - t.Fatalf("unexpected error: %v", err) - } - }) - - t.Run("missing request cipher", func(t *testing.T) { - t.Parallel() - - s := &shadowsocks.TCPClientRequestStart{ - Method: method, - PSK: psk, - RequestSalt: requestSalt, - } - - err := s.Validate() - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "missing request cipher") { - t.Fatalf("unexpected error: %v", err) - } - }) -} - -func TestTCPClientRequestStart_EncodedRequestStartLen(t *testing.T) { - t.Parallel() - - method := newTCPClientStartTestMethod(t) - psk, requestSalt := newTCPClientStartTestPSKAndSalt(method) - - var s shadowsocks.TCPClientRequestStart - if err := s.Init(method, psk, requestSalt); err != nil { - t.Fatalf("Init() error = %v", err) - } - - got, err := s.EncodedRequestStartLen(123) - if err != nil { - t.Fatalf("EncodedRequestStartLen() error = %v", err) - } - - want := len(requestSalt) + - (shadowsocks.TcpRequestFixedHeaderLen + method.TagSize) + - (123 + method.TagSize) - - if got != want { - t.Fatalf("EncodedRequestStartLen() = %d, want %d", got, want) - } - - _, err = s.EncodedRequestStartLen(-1) - if err == nil { - t.Fatal("expected error for negative variable header length") - } -} - -func TestTCPClientRequestStart_WriteRequestStart(t *testing.T) { - t.Parallel() - - method := newTCPClientStartTestMethod(t) - psk, requestSalt := newTCPClientStartTestPSKAndSalt(method) - - var s shadowsocks.TCPClientRequestStart - if err := s.Init(method, psk, requestSalt); err != nil { - t.Fatalf("Init() error = %v", err) - } - - ts := time.Unix(1700000000, 0) - target := shadowsocks.Addr{ - AddrType: shadowsocks.AddrTypeDomain, - Domain: "example.com", - Port: 443, - } - padding := []byte{1, 2, 3} - initialData := []byte("hello") - - var buf bytes.Buffer - n, err := s.WriteRequestStart(&buf, ts, target, padding, initialData) - if err != nil { - t.Fatalf("WriteRequestStart() error = %v", err) - } - - var variableHeader shadowsocks.TCPRequestVariableHeader - variableHeader.Init(target, padding, initialData) - - wantLen, err := s.EncodedRequestStartLen(variableHeader.EncodedLen()) - if err != nil { - t.Fatalf("EncodedRequestStartLen() error = %v", err) - } - - if int(n) != wantLen { - t.Fatalf("WriteRequestStart() wrote %d bytes, want %d", n, wantLen) - } - if buf.Len() != wantLen { - t.Fatalf("buffer len = %d, want %d", buf.Len(), wantLen) - } - - out := buf.Bytes() - if !bytes.Equal(out[:len(requestSalt)], requestSalt) { - t.Fatalf("request salt mismatch: got %v, want %v", out[:len(requestSalt)], requestSalt) - } -} - -func TestTCPClientRequestStart_WriteRequestStart_InvalidTarget(t *testing.T) { - t.Parallel() - - method := newTCPClientStartTestMethod(t) - psk, requestSalt := newTCPClientStartTestPSKAndSalt(method) - - var s shadowsocks.TCPClientRequestStart - if err := s.Init(method, psk, requestSalt); err != nil { - t.Fatalf("Init() error = %v", err) - } - - var buf bytes.Buffer - _, err := s.WriteRequestStart( - &buf, - time.Unix(1700000000, 0), - shadowsocks.Addr{}, - nil, - []byte("x"), - ) - if err == nil { - t.Fatal("expected error, got nil") - } -} - -func TestTCPClientResponseStart_Validate(t *testing.T) { - t.Parallel() - - method := newTCPClientStartTestMethod(t) - psk, requestSalt := newTCPClientStartTestPSKAndSalt(method) - responseSalt := make([]byte, method.SaltSize) - for i := range responseSalt { - responseSalt[i] = byte(i + 51) - } - - responseCipher, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk, responseSalt) - if err != nil { - t.Fatalf("NewTCPStreamCipherFromPSK() error = %v", err) - } - - initialPayload := []byte("hello") - var hdr shadowsocks.TCPResponseHeader - hdr.Init( - shadowsocks.TCPHeaderTypeServerStream, - 1700000000, - requestSalt, - uint16(len(initialPayload)), - ) - - t.Run("valid", func(t *testing.T) { - t.Parallel() - - s := &shadowsocks.TCPClientResponseStart{ - ResponseSalt: append([]byte(nil), responseSalt...), - ResponseCipher: responseCipher, - Header: hdr, - InitialPayload: append([]byte(nil), initialPayload...), - } - - if err := s.Validate(method, requestSalt); err != nil { - t.Fatalf("Validate() error = %v", err) - } - }) - - t.Run("nil receiver", func(t *testing.T) { - t.Parallel() - - var s *shadowsocks.TCPClientResponseStart - err := s.Validate(method, requestSalt) - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "nil TCP client response start") { - t.Fatalf("unexpected error: %v", err) - } - }) - - t.Run("bad response salt len", func(t *testing.T) { - t.Parallel() - - s := &shadowsocks.TCPClientResponseStart{ - ResponseSalt: responseSalt[:len(responseSalt)-1], - ResponseCipher: responseCipher, - Header: hdr, - InitialPayload: append([]byte(nil), initialPayload...), - } - - err := s.Validate(method, requestSalt) - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "invalid response salt length") { - t.Fatalf("unexpected error: %v", err) - } - }) - - t.Run("missing response cipher", func(t *testing.T) { - t.Parallel() - - s := &shadowsocks.TCPClientResponseStart{ - ResponseSalt: responseSalt, - Header: hdr, - InitialPayload: append([]byte(nil), initialPayload...), - } - - err := s.Validate(method, requestSalt) - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "missing response cipher") { - t.Fatalf("unexpected error: %v", err) - } - }) - - t.Run("request salt mismatch", func(t *testing.T) { - t.Parallel() - - badRequestSalt := append([]byte(nil), requestSalt...) - badRequestSalt[0] ^= 0xff - - s := &shadowsocks.TCPClientResponseStart{ - ResponseSalt: responseSalt, - ResponseCipher: responseCipher, - Header: hdr, - InitialPayload: append([]byte(nil), initialPayload...), - } - - err := s.Validate(method, badRequestSalt) - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "response request salt mismatch") { - t.Fatalf("unexpected error: %v", err) - } - }) - - t.Run("initial payload length mismatch", func(t *testing.T) { - t.Parallel() - - s := &shadowsocks.TCPClientResponseStart{ - ResponseSalt: responseSalt, - ResponseCipher: responseCipher, - Header: hdr, - InitialPayload: nil, - } - - err := s.Validate(method, requestSalt) - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "invalid initial payload length") { - t.Fatalf("unexpected error: %v", err) - } - }) -} - -func TestTCPClientRequestStart_ReadResponseStart(t *testing.T) { - t.Parallel() - - method := newTCPClientStartTestMethod(t) - psk, requestSalt := newTCPClientStartTestPSKAndSalt(method) - - var clientStart shadowsocks.TCPClientRequestStart - if err := clientStart.Init(method, psk, requestSalt); err != nil { - t.Fatalf("Init() error = %v", err) - } - - responseSalt := make([]byte, method.SaltSize) - for i := range responseSalt { - responseSalt[i] = byte(i + 201) - } - - responseCipher, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk, responseSalt) - if err != nil { - t.Fatalf("NewTCPStreamCipherFromPSK() error = %v", err) - } - - initialPayload := []byte("hello") - - var hdr shadowsocks.TCPResponseHeader - hdr.Init( - shadowsocks.TCPHeaderTypeServerStream, - uint64(time.Unix(1700000100, 0).Unix()), - requestSalt, - uint16(len(initialPayload)), - ) - - encHeader, err := responseCipher.EncodeResponseHeaderTo(nil, &hdr, nil) - if err != nil { - t.Fatalf("EncodeResponseHeaderTo() error = %v", err) - } - - encPayload, err := responseCipher.EncodeChunkPayloadTo(nil, initialPayload) - if err != nil { - t.Fatalf("EncodeChunkPayloadTo() error = %v", err) - } - - var wire bytes.Buffer - wire.Write(responseSalt) - wire.Write(encHeader) - wire.Write(encPayload) - - resp, n, err := clientStart.ReadResponseStart(&wire) - if err != nil { - t.Fatalf("ReadResponseStart() error = %v", err) - } - - wantN := int64(len(responseSalt) + len(encHeader) + len(encPayload)) - if n != wantN { - t.Fatalf("ReadResponseStart() read %d bytes, want %d", n, wantN) - } - - if !bytes.Equal(resp.ResponseSalt, responseSalt) { - t.Fatalf("ResponseSalt = %v, want %v", resp.ResponseSalt, responseSalt) - } - if resp.ResponseCipher == nil { - t.Fatal("ResponseCipher is nil") - } - if resp.Header.Type != hdr.Type || resp.Header.Timestamp != hdr.Timestamp || resp.Header.Length != hdr.Length { - t.Fatalf("Header = %+v, want %+v", resp.Header, hdr) - } - if !bytes.Equal(resp.Header.RequestSalt, requestSalt) { - t.Fatalf("Header.RequestSalt = %v, want %v", resp.Header.RequestSalt, requestSalt) - } - if !bytes.Equal(resp.InitialPayload, initialPayload) { - t.Fatalf("InitialPayload = %q, want %q", resp.InitialPayload, initialPayload) - } -} - -func TestTCPClientRequestStart_ReadResponseStart_ShortRead(t *testing.T) { - t.Parallel() - - method := newTCPClientStartTestMethod(t) - psk, requestSalt := newTCPClientStartTestPSKAndSalt(method) - - var clientStart shadowsocks.TCPClientRequestStart - if err := clientStart.Init(method, psk, requestSalt); err != nil { - t.Fatalf("Init() error = %v", err) - } - - short := bytes.NewReader(make([]byte, method.SaltSize-1)) - - _, n, err := clientStart.ReadResponseStart(short) - if err == nil { - t.Fatal("expected error, got nil") - } - if n != int64(method.SaltSize-1) { - t.Fatalf("bytes read = %d, want %d", n, method.SaltSize-1) - } - if err != io.EOF && err != io.ErrUnexpectedEOF { - t.Fatalf("unexpected error: %v", err) - } -} - -func TestTCPClientRequestStart_ReadResponseStart_RequestSaltMismatch(t *testing.T) { - t.Parallel() - - method := newTCPClientStartTestMethod(t) - psk, requestSalt := newTCPClientStartTestPSKAndSalt(method) - - var clientStart shadowsocks.TCPClientRequestStart - if err := clientStart.Init(method, psk, requestSalt); err != nil { - t.Fatalf("Init() error = %v", err) - } - - responseSalt := make([]byte, method.SaltSize) - for i := range responseSalt { - responseSalt[i] = byte(i + 201) - } - - responseCipher, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk, responseSalt) - if err != nil { - t.Fatalf("NewTCPStreamCipherFromPSK() error = %v", err) - } - - badRequestSalt := append([]byte(nil), requestSalt...) - badRequestSalt[0] ^= 0xff - - initialPayload := []byte("hello") - - var hdr shadowsocks.TCPResponseHeader - hdr.Init( - shadowsocks.TCPHeaderTypeServerStream, - uint64(time.Unix(1700000100, 0).Unix()), - badRequestSalt, - uint16(len(initialPayload)), - ) - - encHeader, err := responseCipher.EncodeResponseHeaderTo(nil, &hdr, nil) - if err != nil { - t.Fatalf("EncodeResponseHeaderTo() error = %v", err) - } - - encPayload, err := responseCipher.EncodeChunkPayloadTo(nil, initialPayload) - if err != nil { - t.Fatalf("EncodeChunkPayloadTo() error = %v", err) - } - - var wire bytes.Buffer - wire.Write(responseSalt) - wire.Write(encHeader) - wire.Write(encPayload) - - _, _, err = clientStart.ReadResponseStart(&wire) - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "response request salt mismatch") { - t.Fatalf("unexpected error: %v", err) - } -} diff --git a/shadowsocks/tcp_errors.go b/shadowsocks/tcp_errors.go deleted file mode 100644 index 9cd05c8..0000000 --- a/shadowsocks/tcp_errors.go +++ /dev/null @@ -1,13 +0,0 @@ -package shadowsocks - -import "errors" - -// Common validation and decode errors for Shadowsocks TCP headers. -var ( - ErrInvalidTCPHeaderType = errors.New("invalid TCP header type") - ErrInvalidTCPPaddingLength = errors.New("invalid TCP padding length") - ErrMissingTCPHeaderData = errors.New("missing TCP header data") - ErrShortTCPHeader = errors.New("short TCP header") - ErrMissingTCPResponseSalt = errors.New("missing TCP response salt") - ErrInvalidTCPResponseSaltLen = errors.New("invalid TCP response salt length") -) diff --git a/shadowsocks/tcp_request_fixed_header.go b/shadowsocks/tcp_request_fixed_header.go deleted file mode 100644 index 0ba74b1..0000000 --- a/shadowsocks/tcp_request_fixed_header.go +++ /dev/null @@ -1,70 +0,0 @@ -package shadowsocks - -import ( - "encoding/binary" - "fmt" -) - -// TCPRequestFixedHeader represents the fixed-length request header used by -// Shadowsocks 2022 TCP streams. -type TCPRequestFixedHeader struct { - Type byte - Timestamp uint64 - Length uint16 -} - -// Init initializes a TCPRequestFixedHeader. -func (h *TCPRequestFixedHeader) Init(typ byte, timestamp uint64, length uint16) { - h.Type = typ - h.Timestamp = timestamp - h.Length = length -} - -// Validate checks the correctness of the fixed request header fields. -func (h *TCPRequestFixedHeader) Validate() error { - if h.Type != TCPHeaderTypeClientStream { - return ErrInvalidTCPHeaderType - } - - return nil -} - -// EncodedLen returns the number of bytes required to encode the fixed request header. -func (h *TCPRequestFixedHeader) EncodedLen() int { - return TcpRequestFixedHeaderLen -} - -// Decode decodes a fixed request header from src. -// It returns the number of bytes consumed. -func (h *TCPRequestFixedHeader) Decode(src []byte) (int, error) { - if len(src) < TcpRequestFixedHeaderLen { - return 0, ErrShortTCPHeader - } - - h.Type = src[0] - h.Timestamp = binary.BigEndian.Uint64(src[1:9]) - h.Length = binary.BigEndian.Uint16(src[9:11]) - - return TcpRequestFixedHeaderLen, h.Validate() -} - -// EncodeTo encodes the fixed request header into dst. -func (h *TCPRequestFixedHeader) EncodeTo(dst []byte) ([]byte, error) { - if err := h.Validate(); err != nil { - return nil, err - } - - dst = append(dst, h.Type) - dst = binary.BigEndian.AppendUint64(dst, h.Timestamp) - dst = binary.BigEndian.AppendUint16(dst, h.Length) - - return dst, nil -} - -// String returns a human-readable representation of the fixed request header. -func (h *TCPRequestFixedHeader) String() string { - return fmt.Sprintf( - "TCPRequestFixedHeader{Type:%d Timestamp:%d Length:%d}", - h.Type, h.Timestamp, h.Length, - ) -} diff --git a/shadowsocks/tcp_request_fixed_header_test.go b/shadowsocks/tcp_request_fixed_header_test.go deleted file mode 100644 index b741123..0000000 --- a/shadowsocks/tcp_request_fixed_header_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package shadowsocks_test - -import ( - "errors" - "testing" - - "github.com/33TU/socks/shadowsocks" -) - -func TestTCPRequestFixedHeader_Init_Validate(t *testing.T) { - tests := []struct { - name string - hdr shadowsocks.TCPRequestFixedHeader - wantErr error - }{ - { - name: "valid", - hdr: func() shadowsocks.TCPRequestFixedHeader { - var h shadowsocks.TCPRequestFixedHeader - h.Init(shadowsocks.TCPHeaderTypeClientStream, 123456789, 42) - return h - }(), - }, - { - name: "invalid type", - hdr: func() shadowsocks.TCPRequestFixedHeader { - var h shadowsocks.TCPRequestFixedHeader - h.Init(0x99, 123456789, 42) - return h - }(), - wantErr: shadowsocks.ErrInvalidTCPHeaderType, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.hdr.Validate() - if !errors.Is(err, tt.wantErr) { - t.Fatalf("Validate() error = %v, wantErr = %v", err, tt.wantErr) - } - }) - } -} - -func TestTCPRequestFixedHeader_EncodedLen(t *testing.T) { - var h shadowsocks.TCPRequestFixedHeader - h.Init(shadowsocks.TCPHeaderTypeClientStream, 1, 2) - - if got := h.EncodedLen(); got != shadowsocks.TcpRequestFixedHeaderLen { - t.Fatalf("EncodedLen() = %d, want %d", got, shadowsocks.TcpRequestFixedHeaderLen) - } -} - -func TestTCPRequestFixedHeader_EncodeTo_Decode_RoundTrip(t *testing.T) { - var want shadowsocks.TCPRequestFixedHeader - want.Init(shadowsocks.TCPHeaderTypeClientStream, 123456789, 321) - - buf := make([]byte, want.EncodedLen()) - - bw, err := want.EncodeTo(buf[:0]) - if err != nil { - t.Fatalf("EncodeTo() failed: %v", err) - } - if len(bw) != len(buf) { - t.Fatalf("EncodeTo() wrote %d bytes, want %d", len(bw), len(buf)) - } - - var got shadowsocks.TCPRequestFixedHeader - nr, err := got.Decode(buf) - if err != nil { - t.Fatalf("Decode() failed: %v", err) - } - if nr != len(buf) { - t.Fatalf("Decode() read %d bytes, want %d", nr, len(buf)) - } - - if got.Type != want.Type || got.Timestamp != want.Timestamp || got.Length != want.Length { - t.Fatalf("round-trip mismatch: got %+v, want %+v", got, want) - } -} - -func TestTCPRequestFixedHeader_EncodeTo_Invalid(t *testing.T) { - tests := []struct { - name string - hdr shadowsocks.TCPRequestFixedHeader - bufLen int - wantErr error - }{ - { - name: "invalid type", - hdr: shadowsocks.TCPRequestFixedHeader{ - Type: 0x99, - Timestamp: 1, - Length: 2, - }, - bufLen: 32, - wantErr: shadowsocks.ErrInvalidTCPHeaderType, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - buf := make([]byte, tt.bufLen) - _, err := tt.hdr.EncodeTo(buf) - if !errors.Is(err, tt.wantErr) { - t.Fatalf("EncodeTo() error = %v, wantErr = %v", err, tt.wantErr) - } - }) - } -} - -func TestTCPRequestFixedHeader_Decode_Invalid(t *testing.T) { - tests := []struct { - name string - src []byte - wantErr error - }{ - { - name: "short header", - src: make([]byte, shadowsocks.TcpRequestFixedHeaderLen-1), - wantErr: shadowsocks.ErrShortTCPHeader, - }, - { - name: "invalid type", - src: []byte{ - 0x99, - 0, 0, 0, 0, 0, 0, 0, 1, - 0, 2, - }, - wantErr: shadowsocks.ErrInvalidTCPHeaderType, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var h shadowsocks.TCPRequestFixedHeader - _, err := h.Decode(tt.src) - if !errors.Is(err, tt.wantErr) { - t.Fatalf("Decode() error = %v, wantErr = %v", err, tt.wantErr) - } - }) - } -} - -func TestTCPRequestFixedHeader_String(t *testing.T) { - var h shadowsocks.TCPRequestFixedHeader - h.Init(shadowsocks.TCPHeaderTypeClientStream, 123, 45) - - want := "TCPRequestFixedHeader{Type:0 Timestamp:123 Length:45}" - if got := h.String(); got != want { - t.Fatalf("String() = %q, want %q", got, want) - } -} diff --git a/shadowsocks/tcp_request_variable_header.go b/shadowsocks/tcp_request_header.go similarity index 59% rename from shadowsocks/tcp_request_variable_header.go rename to shadowsocks/tcp_request_header.go index 1143920..4d5a87e 100644 --- a/shadowsocks/tcp_request_variable_header.go +++ b/shadowsocks/tcp_request_header.go @@ -5,6 +5,72 @@ import ( "fmt" ) +// TCPRequestFixedHeader represents the fixed-length request header used by +// Shadowsocks 2022 TCP streams. +type TCPRequestFixedHeader struct { + Type byte + Timestamp uint64 + Length uint16 +} + +// Init initializes a TCPRequestFixedHeader. +func (h *TCPRequestFixedHeader) Init(typ byte, timestamp uint64, length uint16) { + h.Type = typ + h.Timestamp = timestamp + h.Length = length +} + +// Validate checks the correctness of the fixed request header fields. +func (h *TCPRequestFixedHeader) Validate() error { + if h.Type != TCPHeaderTypeClientStream { + return ErrInvalidTCPHeaderType + } + + return nil +} + +// EncodedLen returns the number of bytes required to encode the fixed request header. +func (h *TCPRequestFixedHeader) EncodedLen() int { + return TcpRequestFixedHeaderLen +} + +// Decode decodes a fixed request header from src. +// It returns the number of bytes consumed. +func (h *TCPRequestFixedHeader) Decode(src []byte) (int, error) { + if len(src) < TcpRequestFixedHeaderLen { + return 0, ErrShortTCPHeader + } + + h.Type = src[0] + h.Timestamp = binary.BigEndian.Uint64(src[1:9]) + h.Length = binary.BigEndian.Uint16(src[9:11]) + + return TcpRequestFixedHeaderLen, h.Validate() +} + +// EncodeTo encodes the fixed request header into dst. +func (h *TCPRequestFixedHeader) EncodeTo(dst []byte) ([]byte, error) { + if err := h.Validate(); err != nil { + return nil, err + } + + dst = append(dst, h.Type) + dst = binary.BigEndian.AppendUint64(dst, h.Timestamp) + dst = binary.BigEndian.AppendUint16(dst, h.Length) + + return dst, nil +} + +// String returns a human-readable representation of the fixed request header. +func (h *TCPRequestFixedHeader) String() string { + return fmt.Sprintf( + "TCPRequestFixedHeader{Type:%d Timestamp:%d Length:%d}", + h.Type, h.Timestamp, h.Length, + ) +} + +//////// + // TCPRequestVariableHeader represents the variable-length request header used by // Shadowsocks 2022 TCP streams. type TCPRequestVariableHeader struct { diff --git a/shadowsocks/tcp_request_variable_header_test.go b/shadowsocks/tcp_request_header_test.go similarity index 68% rename from shadowsocks/tcp_request_variable_header_test.go rename to shadowsocks/tcp_request_header_test.go index 295ac9b..1120d17 100644 --- a/shadowsocks/tcp_request_variable_header_test.go +++ b/shadowsocks/tcp_request_header_test.go @@ -9,6 +9,153 @@ import ( "github.com/33TU/socks/shadowsocks" ) +func TestTCPRequestFixedHeader_Init_Validate(t *testing.T) { + tests := []struct { + name string + hdr shadowsocks.TCPRequestFixedHeader + wantErr error + }{ + { + name: "valid", + hdr: func() shadowsocks.TCPRequestFixedHeader { + var h shadowsocks.TCPRequestFixedHeader + h.Init(shadowsocks.TCPHeaderTypeClientStream, 123456789, 42) + return h + }(), + }, + { + name: "invalid type", + hdr: func() shadowsocks.TCPRequestFixedHeader { + var h shadowsocks.TCPRequestFixedHeader + h.Init(0x99, 123456789, 42) + return h + }(), + wantErr: shadowsocks.ErrInvalidTCPHeaderType, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.hdr.Validate() + if !errors.Is(err, tt.wantErr) { + t.Fatalf("Validate() error = %v, wantErr = %v", err, tt.wantErr) + } + }) + } +} + +func TestTCPRequestFixedHeader_EncodedLen(t *testing.T) { + var h shadowsocks.TCPRequestFixedHeader + h.Init(shadowsocks.TCPHeaderTypeClientStream, 1, 2) + + if got := h.EncodedLen(); got != shadowsocks.TcpRequestFixedHeaderLen { + t.Fatalf("EncodedLen() = %d, want %d", got, shadowsocks.TcpRequestFixedHeaderLen) + } +} + +func TestTCPRequestFixedHeader_EncodeTo_Decode_RoundTrip(t *testing.T) { + var want shadowsocks.TCPRequestFixedHeader + want.Init(shadowsocks.TCPHeaderTypeClientStream, 123456789, 321) + + buf := make([]byte, want.EncodedLen()) + + bw, err := want.EncodeTo(buf[:0]) + if err != nil { + t.Fatalf("EncodeTo() failed: %v", err) + } + if len(bw) != len(buf) { + t.Fatalf("EncodeTo() wrote %d bytes, want %d", len(bw), len(buf)) + } + + var got shadowsocks.TCPRequestFixedHeader + nr, err := got.Decode(buf) + if err != nil { + t.Fatalf("Decode() failed: %v", err) + } + if nr != len(buf) { + t.Fatalf("Decode() read %d bytes, want %d", nr, len(buf)) + } + + if got.Type != want.Type || got.Timestamp != want.Timestamp || got.Length != want.Length { + t.Fatalf("round-trip mismatch: got %+v, want %+v", got, want) + } +} + +func TestTCPRequestFixedHeader_EncodeTo_Invalid(t *testing.T) { + tests := []struct { + name string + hdr shadowsocks.TCPRequestFixedHeader + bufLen int + wantErr error + }{ + { + name: "invalid type", + hdr: shadowsocks.TCPRequestFixedHeader{ + Type: 0x99, + Timestamp: 1, + Length: 2, + }, + bufLen: 32, + wantErr: shadowsocks.ErrInvalidTCPHeaderType, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := make([]byte, tt.bufLen) + _, err := tt.hdr.EncodeTo(buf) + if !errors.Is(err, tt.wantErr) { + t.Fatalf("EncodeTo() error = %v, wantErr = %v", err, tt.wantErr) + } + }) + } +} + +func TestTCPRequestFixedHeader_Decode_Invalid(t *testing.T) { + tests := []struct { + name string + src []byte + wantErr error + }{ + { + name: "short header", + src: make([]byte, shadowsocks.TcpRequestFixedHeaderLen-1), + wantErr: shadowsocks.ErrShortTCPHeader, + }, + { + name: "invalid type", + src: []byte{ + 0x99, + 0, 0, 0, 0, 0, 0, 0, 1, + 0, 2, + }, + wantErr: shadowsocks.ErrInvalidTCPHeaderType, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var h shadowsocks.TCPRequestFixedHeader + _, err := h.Decode(tt.src) + if !errors.Is(err, tt.wantErr) { + t.Fatalf("Decode() error = %v, wantErr = %v", err, tt.wantErr) + } + }) + } +} + +func TestTCPRequestFixedHeader_String(t *testing.T) { + var h shadowsocks.TCPRequestFixedHeader + h.Init(shadowsocks.TCPHeaderTypeClientStream, 123, 45) + + want := "TCPRequestFixedHeader{Type:0 Timestamp:123 Length:45}" + if got := h.String(); got != want { + t.Fatalf("String() = %q, want %q", got, want) + } +} + +/////// + func TestTCPRequestVariableHeader_Init_Validate(t *testing.T) { validTarget := shadowsocks.Addr{ AddrType: shadowsocks.AddrTypeDomain, diff --git a/shadowsocks/tcp_server_stream.go b/shadowsocks/tcp_server_stream.go deleted file mode 100644 index cbe929f..0000000 --- a/shadowsocks/tcp_server_stream.go +++ /dev/null @@ -1,138 +0,0 @@ -package shadowsocks - -import ( - "fmt" - "io" - "time" -) - -// TCPServerRequestStart represents the parsed client request startup on the server side. -// Deprecated: prefer ParsedTCPRequestStart. -type TCPServerRequestStart struct { - Method Method - PSK []byte - RequestSalt []byte - RequestCipher *TCPStreamCipher - FixedHeader TCPRequestFixedHeader - Header TCPRequestVariableHeader -} - -func (s *TCPServerRequestStart) Init(method Method, psk, requestSalt []byte) error { - if err := method.Validate(); err != nil { - return err - } - if len(psk) != method.KeySize { - return fmt.Errorf("invalid PSK length: got %d, want %d", len(psk), method.KeySize) - } - if len(requestSalt) != method.SaltSize { - return fmt.Errorf("invalid request salt length: got %d, want %d", len(requestSalt), method.SaltSize) - } - cipher, err := NewTCPStreamCipherFromPSK(method, psk, requestSalt) - if err != nil { - return err - } - s.Method = method - s.PSK = append(s.PSK[:0], psk...) - s.RequestSalt = append(s.RequestSalt[:0], requestSalt...) - s.RequestCipher = cipher - return nil -} - -func (s *TCPServerRequestStart) Validate() error { - if s == nil { - return fmt.Errorf("nil TCP server request start") - } - parsed := &ParsedTCPRequestStart{ - Salt: s.RequestSalt, - Cipher: s.RequestCipher, - Fixed: s.FixedHeader, - Header: s.Header, - } - return parsed.Validate(s.Method) -} - -func (s *TCPServerRequestStart) ReadRequestStart(src io.Reader, method Method, psk []byte) (int64, error) { - parsed, n, err := ReadTCPRequestStart(src, method, psk) - if err != nil { - return n, err - } - s.Method = method - s.PSK = append(s.PSK[:0], psk...) - s.RequestSalt = parsed.Salt - s.RequestCipher = parsed.Cipher - s.FixedHeader = parsed.Fixed - s.Header = parsed.Header - return n, nil -} - -// TCPServerResponseStart represents the server-side response startup state. -// Deprecated: prefer WriteTCPResponseStart. -type TCPServerResponseStart struct { - Method Method - PSK []byte - ResponseSalt []byte - ResponseCipher *TCPStreamCipher - Header TCPResponseHeader - InitialPayload []byte -} - -func (s *TCPServerResponseStart) Init(method Method, psk, responseSalt []byte) error { - if err := method.Validate(); err != nil { - return err - } - if len(psk) != method.KeySize { - return fmt.Errorf("invalid PSK length: got %d, want %d", len(psk), method.KeySize) - } - if len(responseSalt) != method.SaltSize { - return fmt.Errorf("invalid response salt length: got %d, want %d", len(responseSalt), method.SaltSize) - } - cipher, err := NewTCPStreamCipherFromPSK(method, psk, responseSalt) - if err != nil { - return err - } - s.Method = method - s.PSK = append(s.PSK[:0], psk...) - s.ResponseSalt = append(s.ResponseSalt[:0], responseSalt...) - s.ResponseCipher = cipher - return nil -} - -func (s *TCPServerResponseStart) Validate(requestSalt []byte) error { - if s == nil { - return fmt.Errorf("nil TCP server response start") - } - parsed := &ParsedTCPResponseStart{ - Salt: s.ResponseSalt, - Cipher: s.ResponseCipher, - Header: s.Header, - InitialPayload: s.InitialPayload, - } - return parsed.Validate(s.Method, requestSalt) -} - -func (s *TCPServerResponseStart) WriteResponseStart(dst io.Writer, timestamp time.Time, requestSalt []byte, initialPayload []byte) (int64, error) { - if s == nil { - return 0, fmt.Errorf("nil TCP server response start") - } - if err := s.Method.Validate(); err != nil { - return 0, err - } - if len(s.PSK) != s.Method.KeySize { - return 0, fmt.Errorf("invalid PSK length: got %d, want %d", len(s.PSK), s.Method.KeySize) - } - if len(s.ResponseSalt) != s.Method.SaltSize { - return 0, fmt.Errorf("invalid response salt length: got %d, want %d", len(s.ResponseSalt), s.Method.SaltSize) - } - if s.ResponseCipher == nil { - return 0, fmt.Errorf("missing response cipher") - } - cipher, n, err := WriteTCPResponseStart(dst, s.Method, s.PSK, s.ResponseSalt, timestamp, requestSalt, initialPayload) - if err == nil { - s.ResponseCipher = cipher - var header TCPResponseHeader - header.Init(TCPHeaderTypeServerStream, uint64(timestamp.Unix()), requestSalt, uint16(len(initialPayload))) - s.Header = header - s.InitialPayload = append(s.InitialPayload[:0], initialPayload...) - } - return n, err -} diff --git a/shadowsocks/tcp_server_stream_test.go b/shadowsocks/tcp_server_stream_test.go deleted file mode 100644 index a2cf9e6..0000000 --- a/shadowsocks/tcp_server_stream_test.go +++ /dev/null @@ -1,525 +0,0 @@ -package shadowsocks_test - -import ( - "bytes" - "io" - "strings" - "testing" - "time" - - "github.com/33TU/socks/shadowsocks" -) - -func newTCPServerStartTestMethod(t *testing.T) shadowsocks.Method { - t.Helper() - - method, err := shadowsocks.ParseMethod(shadowsocks.Method2022Blake3AES128GCM) - if err != nil { - t.Fatalf("ParseMethod() error = %v", err) - } - - return method -} - -func newTCPServerStartTestPSKAndSalt(method shadowsocks.Method) ([]byte, []byte) { - psk := make([]byte, method.KeySize) - salt := make([]byte, method.SaltSize) - - for i := range psk { - psk[i] = byte(i + 1) - } - for i := range salt { - salt[i] = byte(i + 101) - } - - return psk, salt -} - -func TestTCPServerRequestStart_Init_Validate(t *testing.T) { - t.Parallel() - - method := newTCPServerStartTestMethod(t) - psk, requestSalt := newTCPServerStartTestPSKAndSalt(method) - - target := shadowsocks.Addr{ - AddrType: shadowsocks.AddrTypeDomain, - Domain: "example.com", - Port: 443, - } - - var fixed shadowsocks.TCPRequestFixedHeader - var variable shadowsocks.TCPRequestVariableHeader - variable.Init(target, []byte{1, 2, 3}, []byte("hello")) - fixed.Init( - shadowsocks.TCPHeaderTypeClientStream, - 1700000000, - uint16(variable.EncodedLen()), - ) - - t.Run("valid", func(t *testing.T) { - t.Parallel() - - var s shadowsocks.TCPServerRequestStart - if err := s.Init(method, psk, requestSalt); err != nil { - t.Fatalf("Init() error = %v", err) - } - - s.FixedHeader = fixed - s.Header = variable - - if err := s.Validate(); err != nil { - t.Fatalf("Validate() error = %v", err) - } - }) - - t.Run("invalid psk length", func(t *testing.T) { - t.Parallel() - - var s shadowsocks.TCPServerRequestStart - err := s.Init(method, psk[:len(psk)-1], requestSalt) - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "invalid PSK length") { - t.Fatalf("unexpected error: %v", err) - } - }) - - t.Run("invalid request salt length", func(t *testing.T) { - t.Parallel() - - var s shadowsocks.TCPServerRequestStart - err := s.Init(method, psk, requestSalt[:len(requestSalt)-1]) - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "invalid request salt length") { - t.Fatalf("unexpected error: %v", err) - } - }) - - t.Run("nil receiver validate", func(t *testing.T) { - t.Parallel() - - var s *shadowsocks.TCPServerRequestStart - err := s.Validate() - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "nil TCP server request start") { - t.Fatalf("unexpected error: %v", err) - } - }) - - t.Run("missing request cipher", func(t *testing.T) { - t.Parallel() - - s := &shadowsocks.TCPServerRequestStart{ - Method: method, - PSK: psk, - RequestSalt: requestSalt, - FixedHeader: fixed, - Header: variable, - } - - err := s.Validate() - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "missing request cipher") { - t.Fatalf("unexpected error: %v", err) - } - }) - - t.Run("variable header length mismatch", func(t *testing.T) { - t.Parallel() - - var s shadowsocks.TCPServerRequestStart - if err := s.Init(method, psk, requestSalt); err != nil { - t.Fatalf("Init() error = %v", err) - } - - s.FixedHeader.Init( - shadowsocks.TCPHeaderTypeClientStream, - 1700000000, - uint16(variable.EncodedLen()+1), - ) - s.Header = variable - - err := s.Validate() - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "request variable header length mismatch") { - t.Fatalf("unexpected error: %v", err) - } - }) -} - -func TestTCPServerRequestStart_ReadRequestStart(t *testing.T) { - t.Parallel() - - method := newTCPServerStartTestMethod(t) - psk, requestSalt := newTCPServerStartTestPSKAndSalt(method) - - clientRequestCipher, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk, requestSalt) - if err != nil { - t.Fatalf("NewTCPStreamCipherFromPSK() error = %v", err) - } - - target := shadowsocks.Addr{ - AddrType: shadowsocks.AddrTypeDomain, - Domain: "example.com", - Port: 443, - } - - var variableHeader shadowsocks.TCPRequestVariableHeader - variableHeader.Init(target, []byte{1, 2, 3}, []byte("hello")) - - var fixedHeader shadowsocks.TCPRequestFixedHeader - fixedHeader.Init( - shadowsocks.TCPHeaderTypeClientStream, - uint64(time.Unix(1700000000, 0).Unix()), - uint16(variableHeader.EncodedLen()), - ) - - encFixed, err := clientRequestCipher.EncodeRequestFixedHeaderTo(nil, &fixedHeader, nil) - if err != nil { - t.Fatalf("EncodeRequestFixedHeaderTo() error = %v", err) - } - - encVariable, err := clientRequestCipher.EncodeRequestVariableHeaderTo(nil, &variableHeader, nil) - if err != nil { - t.Fatalf("EncodeRequestVariableHeaderTo() error = %v", err) - } - - var wire bytes.Buffer - wire.Write(requestSalt) - wire.Write(encFixed) - wire.Write(encVariable) - - var s shadowsocks.TCPServerRequestStart - n, err := s.ReadRequestStart(&wire, method, psk) - if err != nil { - t.Fatalf("ReadRequestStart() error = %v", err) - } - - wantN := int64(len(requestSalt) + len(encFixed) + len(encVariable)) - if n != wantN { - t.Fatalf("ReadRequestStart() read %d bytes, want %d", n, wantN) - } - - if !bytes.Equal(s.RequestSalt, requestSalt) { - t.Fatalf("RequestSalt = %v, want %v", s.RequestSalt, requestSalt) - } - if s.RequestCipher == nil { - t.Fatal("RequestCipher is nil") - } - - if s.FixedHeader.Type != fixedHeader.Type || - s.FixedHeader.Timestamp != fixedHeader.Timestamp || - s.FixedHeader.Length != fixedHeader.Length { - t.Fatalf("FixedHeader = %+v, want %+v", s.FixedHeader, fixedHeader) - } - - if s.Header.Target.AddrType != variableHeader.Target.AddrType { - t.Fatalf("Header.Target.AddrType = %v, want %v", s.Header.Target.AddrType, variableHeader.Target.AddrType) - } - if s.Header.Target.Domain != variableHeader.Target.Domain { - t.Fatalf("Header.Target.Domain = %q, want %q", s.Header.Target.Domain, variableHeader.Target.Domain) - } - if s.Header.Target.Port != variableHeader.Target.Port { - t.Fatalf("Header.Target.Port = %d, want %d", s.Header.Target.Port, variableHeader.Target.Port) - } - if s.Header.PaddingLen != variableHeader.PaddingLen { - t.Fatalf("Header.PaddingLen = %d, want %d", s.Header.PaddingLen, variableHeader.PaddingLen) - } - if !bytes.Equal(s.Header.Padding, variableHeader.Padding) { - t.Fatalf("Header.Padding = %v, want %v", s.Header.Padding, variableHeader.Padding) - } - if !bytes.Equal(s.Header.InitialData, variableHeader.InitialData) { - t.Fatalf("Header.InitialData = %v, want %v", s.Header.InitialData, variableHeader.InitialData) - } -} - -func TestTCPServerRequestStart_ReadRequestStart_ShortRead(t *testing.T) { - t.Parallel() - - method := newTCPServerStartTestMethod(t) - psk, _ := newTCPServerStartTestPSKAndSalt(method) - - short := bytes.NewReader(make([]byte, method.SaltSize-1)) - - var s shadowsocks.TCPServerRequestStart - n, err := s.ReadRequestStart(short, method, psk) - if err == nil { - t.Fatal("expected error, got nil") - } - if n != int64(method.SaltSize-1) { - t.Fatalf("bytes read = %d, want %d", n, method.SaltSize-1) - } - if err != io.EOF && err != io.ErrUnexpectedEOF { - t.Fatalf("unexpected error: %v", err) - } -} - -func TestTCPServerRequestStart_ReadRequestStart_InvalidPSK(t *testing.T) { - t.Parallel() - - method := newTCPServerStartTestMethod(t) - psk, requestSalt := newTCPServerStartTestPSKAndSalt(method) - - var wire bytes.Buffer - wire.Write(requestSalt) - - var s shadowsocks.TCPServerRequestStart - _, err := s.ReadRequestStart(&wire, method, psk[:len(psk)-1]) - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "invalid PSK length") { - t.Fatalf("unexpected error: %v", err) - } -} - -func TestTCPServerResponseStart_Init_Validate(t *testing.T) { - t.Parallel() - - method := newTCPServerStartTestMethod(t) - psk, responseSalt := newTCPServerStartTestPSKAndSalt(method) - requestSalt := make([]byte, method.SaltSize) - for i := range requestSalt { - requestSalt[i] = byte(i + 51) - } - - responseCipher, err := shadowsocks.NewTCPStreamCipherFromPSK(method, psk, responseSalt) - if err != nil { - t.Fatalf("NewTCPStreamCipherFromPSK() error = %v", err) - } - - initialPayload := []byte("pong") - - var hdr shadowsocks.TCPResponseHeader - hdr.Init( - shadowsocks.TCPHeaderTypeServerStream, - 1700000000, - requestSalt, - uint16(len(initialPayload)), - ) - - t.Run("valid", func(t *testing.T) { - t.Parallel() - - var s shadowsocks.TCPServerResponseStart - if err := s.Init(method, psk, responseSalt); err != nil { - t.Fatalf("Init() error = %v", err) - } - s.Header = hdr - s.InitialPayload = append([]byte(nil), initialPayload...) - - if err := s.Validate(requestSalt); err != nil { - t.Fatalf("Validate() error = %v", err) - } - }) - - t.Run("invalid psk length", func(t *testing.T) { - t.Parallel() - - var s shadowsocks.TCPServerResponseStart - err := s.Init(method, psk[:len(psk)-1], responseSalt) - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "invalid PSK length") { - t.Fatalf("unexpected error: %v", err) - } - }) - - t.Run("invalid response salt length", func(t *testing.T) { - t.Parallel() - - var s shadowsocks.TCPServerResponseStart - err := s.Init(method, psk, responseSalt[:len(responseSalt)-1]) - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "invalid response salt length") { - t.Fatalf("unexpected error: %v", err) - } - }) - - t.Run("nil receiver validate", func(t *testing.T) { - t.Parallel() - - var s *shadowsocks.TCPServerResponseStart - err := s.Validate(requestSalt) - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "nil TCP server response start") { - t.Fatalf("unexpected error: %v", err) - } - }) - - t.Run("missing response cipher", func(t *testing.T) { - t.Parallel() - - s := &shadowsocks.TCPServerResponseStart{ - Method: method, - PSK: psk, - ResponseSalt: responseSalt, - Header: hdr, - InitialPayload: append([]byte(nil), initialPayload...), - } - - err := s.Validate(requestSalt) - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "missing response cipher") { - t.Fatalf("unexpected error: %v", err) - } - }) - - t.Run("request salt mismatch", func(t *testing.T) { - t.Parallel() - - s := &shadowsocks.TCPServerResponseStart{ - Method: method, - PSK: psk, - ResponseSalt: responseSalt, - ResponseCipher: responseCipher, - Header: hdr, - InitialPayload: append([]byte(nil), initialPayload...), - } - - badRequestSalt := append([]byte(nil), requestSalt...) - badRequestSalt[0] ^= 0xff - - err := s.Validate(badRequestSalt) - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "response request salt mismatch") { - t.Fatalf("unexpected error: %v", err) - } - }) - - t.Run("initial payload length mismatch", func(t *testing.T) { - t.Parallel() - - s := &shadowsocks.TCPServerResponseStart{ - Method: method, - PSK: psk, - ResponseSalt: responseSalt, - ResponseCipher: responseCipher, - Header: hdr, - InitialPayload: nil, - } - - err := s.Validate(requestSalt) - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "invalid initial payload length") { - t.Fatalf("unexpected error: %v", err) - } - }) -} - -func TestTCPServerResponseStart_WriteResponseStart(t *testing.T) { - t.Parallel() - - method := newTCPServerStartTestMethod(t) - psk, responseSalt := newTCPServerStartTestPSKAndSalt(method) - requestSalt := make([]byte, method.SaltSize) - for i := range requestSalt { - requestSalt[i] = byte(i + 51) - } - - var s shadowsocks.TCPServerResponseStart - if err := s.Init(method, psk, responseSalt); err != nil { - t.Fatalf("Init() error = %v", err) - } - - ts := time.Unix(1700000100, 0) - initialPayload := []byte("pong") - - var buf bytes.Buffer - n, err := s.WriteResponseStart(&buf, ts, requestSalt, initialPayload) - if err != nil { - t.Fatalf("WriteResponseStart() error = %v", err) - } - - headerPlainLen := 1 + 8 + len(requestSalt) + 2 - wantLen := len(responseSalt) + - (headerPlainLen + method.TagSize) + - (len(initialPayload) + method.TagSize) - - if int(n) != wantLen { - t.Fatalf("WriteResponseStart() wrote %d bytes, want %d", n, wantLen) - } - if buf.Len() != wantLen { - t.Fatalf("buffer len = %d, want %d", buf.Len(), wantLen) - } - - out := buf.Bytes() - if !bytes.Equal(out[:len(responseSalt)], responseSalt) { - t.Fatalf("response salt mismatch: got %v, want %v", out[:len(responseSalt)], responseSalt) - } - - if s.Header.Type != shadowsocks.TCPHeaderTypeServerStream { - t.Fatalf("Header.Type = %v, want %v", s.Header.Type, shadowsocks.TCPHeaderTypeServerStream) - } - if s.Header.Timestamp != uint64(ts.Unix()) { - t.Fatalf("Header.Timestamp = %d, want %d", s.Header.Timestamp, uint64(ts.Unix())) - } - if !bytes.Equal(s.Header.RequestSalt, requestSalt) { - t.Fatalf("Header.RequestSalt = %v, want %v", s.Header.RequestSalt, requestSalt) - } - if s.Header.Length != uint16(len(initialPayload)) { - t.Fatalf("Header.Length = %d, want %d", s.Header.Length, len(initialPayload)) - } - if !bytes.Equal(s.InitialPayload, initialPayload) { - t.Fatalf("InitialPayload = %q, want %q", s.InitialPayload, initialPayload) - } -} - -func TestTCPServerResponseStart_WriteResponseStart_InvalidRequestSalt(t *testing.T) { - t.Parallel() - - method := newTCPServerStartTestMethod(t) - psk, responseSalt := newTCPServerStartTestPSKAndSalt(method) - - var s shadowsocks.TCPServerResponseStart - if err := s.Init(method, psk, responseSalt); err != nil { - t.Fatalf("Init() error = %v", err) - } - - var buf bytes.Buffer - _, err := s.WriteResponseStart(&buf, time.Unix(1700000100, 0), responseSalt[:len(responseSalt)-1], nil) - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "invalid request salt length") { - t.Fatalf("unexpected error: %v", err) - } -} - -func TestTCPServerResponseStart_WriteResponseStart_NilReceiver(t *testing.T) { - t.Parallel() - - method := newTCPServerStartTestMethod(t) - _, requestSalt := newTCPServerStartTestPSKAndSalt(method) - - var s *shadowsocks.TCPServerResponseStart - var buf bytes.Buffer - - _, err := s.WriteResponseStart(&buf, time.Unix(1700000100, 0), requestSalt, nil) - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "nil TCP server response start") { - t.Fatalf("unexpected error: %v", err) - } -} diff --git a/shadowsocks/tcp_stream_start.go b/shadowsocks/tcp_stream.go similarity index 87% rename from shadowsocks/tcp_stream_start.go rename to shadowsocks/tcp_stream.go index 2c9b333..5c77682 100644 --- a/shadowsocks/tcp_stream_start.go +++ b/shadowsocks/tcp_stream.go @@ -6,7 +6,7 @@ import ( "io" "time" - ibuf "github.com/33TU/socks/internal" + "github.com/33TU/socks/internal" ) const ( @@ -125,8 +125,8 @@ func WriteTCPRequestStart(dst io.Writer, method Method, psk, requestSalt []byte, if variableHeader.EncodedLen() > scratchLen { scratchLen = variableHeader.EncodedLen() } - plainScratch := ibuf.GetBytes(scratchLen) - defer ibuf.PutBytes(plainScratch) + plainScratch := internal.GetBytes(scratchLen) + defer internal.PutBytes(plainScratch) var stackBuf [tcpRequestStartStackBufSize]byte out := stackBuf[:0] @@ -155,8 +155,8 @@ func ReadTCPRequestStart(src io.Reader, method Method, psk []byte) (*ParsedTCPRe return nil, 0, fmt.Errorf("invalid PSK length: got %d, want %d", len(psk), method.KeySize) } - requestSaltBuf := ibuf.GetBytes(method.SaltSize) - defer ibuf.PutBytes(requestSaltBuf) + requestSaltBuf := internal.GetBytes(method.SaltSize) + defer internal.PutBytes(requestSaltBuf) n, err := io.ReadFull(src, requestSaltBuf) total += int64(n) if err != nil { @@ -168,31 +168,31 @@ func ReadTCPRequestStart(src io.Reader, method Method, psk []byte) (*ParsedTCPRe return nil, total, err } - encFixed := ibuf.GetBytes(TcpRequestFixedHeaderLen + method.TagSize) - defer ibuf.PutBytes(encFixed) + encFixed := internal.GetBytes(TcpRequestFixedHeaderLen + method.TagSize) + defer internal.PutBytes(encFixed) n, err = io.ReadFull(src, encFixed) total += int64(n) if err != nil { return nil, total, err } - fixedPlainScratch := ibuf.GetBytes(TcpRequestFixedHeaderLen) - defer ibuf.PutBytes(fixedPlainScratch) + fixedPlainScratch := internal.GetBytes(TcpRequestFixedHeaderLen) + defer internal.PutBytes(fixedPlainScratch) fixedHeader, err := requestCipher.DecodeRequestFixedHeader(encFixed, fixedPlainScratch[:0]) if err != nil { return nil, total, err } - encVariable := ibuf.GetBytes(int(fixedHeader.Length) + method.TagSize) - defer ibuf.PutBytes(encVariable) + encVariable := internal.GetBytes(int(fixedHeader.Length) + method.TagSize) + defer internal.PutBytes(encVariable) n, err = io.ReadFull(src, encVariable) total += int64(n) if err != nil { return nil, total, err } - variablePlainScratch := ibuf.GetBytes(int(fixedHeader.Length)) - defer ibuf.PutBytes(variablePlainScratch) + variablePlainScratch := internal.GetBytes(int(fixedHeader.Length)) + defer internal.PutBytes(variablePlainScratch) variableHeader, err := requestCipher.DecodeRequestVariableHeader(encVariable, variablePlainScratch[:0]) if err != nil { return nil, total, err @@ -229,8 +229,8 @@ func WriteTCPResponseStart(dst io.Writer, method Method, psk, responseSalt []byt return nil, 0, err } - headerPlainScratch := ibuf.GetBytes(header.EncodedLen()) - defer ibuf.PutBytes(headerPlainScratch) + headerPlainScratch := internal.GetBytes(header.EncodedLen()) + defer internal.PutBytes(headerPlainScratch) var stackBuf [tcpResponseStartStackBufSize]byte out := stackBuf[:0] @@ -261,8 +261,8 @@ func ReadTCPResponseStart(src io.Reader, method Method, psk, expectedRequestSalt return nil, 0, fmt.Errorf("invalid request salt length: got %d, want %d", len(expectedRequestSalt), method.SaltSize) } - responseSaltBuf := ibuf.GetBytes(method.SaltSize) - defer ibuf.PutBytes(responseSaltBuf) + responseSaltBuf := internal.GetBytes(method.SaltSize) + defer internal.PutBytes(responseSaltBuf) n, err := io.ReadFull(src, responseSaltBuf) total += int64(n) if err != nil { @@ -275,31 +275,31 @@ func ReadTCPResponseStart(src io.Reader, method Method, psk, expectedRequestSalt } encHeaderLen := 1 + 8 + method.SaltSize + 2 + method.TagSize - encHeader := ibuf.GetBytes(encHeaderLen) - defer ibuf.PutBytes(encHeader) + encHeader := internal.GetBytes(encHeaderLen) + defer internal.PutBytes(encHeader) n, err = io.ReadFull(src, encHeader) total += int64(n) if err != nil { return nil, total, err } - plainHeaderScratch := ibuf.GetBytes(1 + 8 + method.SaltSize + 2) - defer ibuf.PutBytes(plainHeaderScratch) + plainHeaderScratch := internal.GetBytes(1 + 8 + method.SaltSize + 2) + defer internal.PutBytes(plainHeaderScratch) header, err := responseCipher.DecodeResponseHeader(encHeader, plainHeaderScratch[:0]) if err != nil { return nil, total, err } - encPayloadBuf := ibuf.GetBytes(responseCipher.EncryptedPayloadLength(int(header.Length))) - defer ibuf.PutBytes(encPayloadBuf) + encPayloadBuf := internal.GetBytes(responseCipher.EncryptedPayloadLength(int(header.Length))) + defer internal.PutBytes(encPayloadBuf) n, err = io.ReadFull(src, encPayloadBuf) total += int64(n) if err != nil { return nil, total, err } - payloadScratch := ibuf.GetBytes(int(header.Length)) - defer ibuf.PutBytes(payloadScratch) + payloadScratch := internal.GetBytes(int(header.Length)) + defer internal.PutBytes(payloadScratch) initialPayload, err := responseCipher.DecodeChunkPayloadTo(payloadScratch[:0], encPayloadBuf) if err != nil { return nil, total, err From 7aa03413c0ae7a8a9d9e95e4409513fddd69b6ac Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 23:11:29 +0300 Subject: [PATCH 21/25] refactor --- shadowsocks/consts.go | 30 ------------------- shadowsocks/tcp_proto.go | 33 +++++++++++++++++++++ shadowsocks/{tcp_stream.go => tcp_start.go} | 0 3 files changed, 33 insertions(+), 30 deletions(-) create mode 100644 shadowsocks/tcp_proto.go rename shadowsocks/{tcp_stream.go => tcp_start.go} (100%) diff --git a/shadowsocks/consts.go b/shadowsocks/consts.go index 3f7dead..a01c107 100644 --- a/shadowsocks/consts.go +++ b/shadowsocks/consts.go @@ -1,7 +1,5 @@ package shadowsocks -import "errors" - // Encryption method constants for Shadowsocks AEAD-2022. const ( Method2022Blake3AES128GCM = "2022-blake3-aes-128-gcm" @@ -15,31 +13,3 @@ const ( AddrTypeDomain = 0x03 AddrTypeIPv6 = 0x04 ) - -// TCP header types for Shadowsocks 2022 stream protocol. -const ( - TCPHeaderTypeClientStream = 0x00 - TCPHeaderTypeServerStream = 0x01 -) - -const ( - TcpRequestFixedHeaderLen = 1 + 8 + 2 - TcpResponseFixedBaseLen = 1 + 8 + 2 -) - -const ( - AeadNonceSize = 12 - AeadTagSize = 16 -) - -const TcpChunkLengthLen = 2 - -// Common validation and decode errors for Shadowsocks TCP headers. -var ( - ErrInvalidTCPHeaderType = errors.New("invalid TCP header type") // res and req - ErrInvalidTCPPaddingLength = errors.New("invalid TCP padding length") // req - ErrMissingTCPHeaderData = errors.New("missing TCP header data") // req - ErrShortTCPHeader = errors.New("short TCP header") // res and req - ErrMissingTCPResponseSalt = errors.New("missing TCP response salt") // res - ErrInvalidTCPResponseSaltLen = errors.New("invalid TCP response salt length") // res -) diff --git a/shadowsocks/tcp_proto.go b/shadowsocks/tcp_proto.go new file mode 100644 index 0000000..2ec7153 --- /dev/null +++ b/shadowsocks/tcp_proto.go @@ -0,0 +1,33 @@ +package shadowsocks + +import ( + "errors" +) + +// TCP header types for Shadowsocks 2022 stream protocol. +const ( + TCPHeaderTypeClientStream = 0x00 + TCPHeaderTypeServerStream = 0x01 +) + +const ( + TcpRequestFixedHeaderLen = 1 + 8 + 2 + TcpResponseFixedBaseLen = 1 + 8 + 2 +) + +const ( + AeadNonceSize = 12 + AeadTagSize = 16 +) + +const TcpChunkLengthLen = 2 + +// Common validation and decode errors for Shadowsocks TCP headers. +var ( + ErrInvalidTCPHeaderType = errors.New("invalid TCP header type") // res and req + ErrInvalidTCPPaddingLength = errors.New("invalid TCP padding length") // req + ErrMissingTCPHeaderData = errors.New("missing TCP header data") // req + ErrShortTCPHeader = errors.New("short TCP header") // res and req + ErrMissingTCPResponseSalt = errors.New("missing TCP response salt") // res + ErrInvalidTCPResponseSaltLen = errors.New("invalid TCP response salt length") // res +) diff --git a/shadowsocks/tcp_stream.go b/shadowsocks/tcp_start.go similarity index 100% rename from shadowsocks/tcp_stream.go rename to shadowsocks/tcp_start.go From f7845eddccf2f8ec4d4c0beebbb7b428029156b6 Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 23:22:46 +0300 Subject: [PATCH 22/25] rand padding --- shadowsocks/dialer.go | 17 ++++++++++++++- shadowsocks/rand.go | 50 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 shadowsocks/rand.go diff --git a/shadowsocks/dialer.go b/shadowsocks/dialer.go index b80ff56..f105fe1 100644 --- a/shadowsocks/dialer.go +++ b/shadowsocks/dialer.go @@ -7,6 +7,7 @@ import ( "net/url" "time" + "github.com/33TU/socks/internal" socksnet "github.com/33TU/socks/net" ) @@ -153,7 +154,21 @@ func (d *Dialer) DialConnContext(ctx context.Context, conn net.Conn, network, ad cleanup := bindConnToContext(ctx, conn) defer cleanup() - ssConn, err := NewClientTCPConn(conn, method, psk, target, []byte{0}, nil) + paddingLen, err := RandomInt(1, 64) // make configurable later + if err != nil { + conn.Close() + return nil, err + } + + padding := internal.GetBytes(paddingLen) + defer internal.PutBytes(padding) + + if err := FillRandomBytes(padding); err != nil { + conn.Close() + return nil, err + } + + ssConn, err := NewClientTCPConn(conn, method, psk, target, padding, nil) if err != nil { conn.Close() return nil, err diff --git a/shadowsocks/rand.go b/shadowsocks/rand.go new file mode 100644 index 0000000..ed0db16 --- /dev/null +++ b/shadowsocks/rand.go @@ -0,0 +1,50 @@ +package shadowsocks + +import ( + "crypto/rand" + "encoding/binary" + "fmt" +) + +// FillRandomBytes fills dst with cryptographically secure random bytes. +func FillRandomBytes(dst []byte) error { + if len(dst) == 0 { + return nil + } + n, err := rand.Read(dst) + if err != nil { + return err + } + if n != len(dst) { + return fmt.Errorf("short random read: got %d, want %d", n, len(dst)) + } + return nil +} + +// RandomInt returns a random integer in the inclusive range [min, max]. +func RandomInt(min, max int) (int, error) { + if min > max { + return 0, fmt.Errorf("invalid random range: min %d > max %d", min, max) + } + if min == max { + return min, nil + } + + span := uint64(max - min + 1) + if span == 0 { + return 0, fmt.Errorf("invalid random range") + } + + var buf [8]byte + limit := ^uint64(0) - (^uint64(0) % span) + + for { + if err := FillRandomBytes(buf[:]); err != nil { + return 0, err + } + v := binary.BigEndian.Uint64(buf[:]) + if v < limit { + return min + int(v%span), nil + } + } +} From 87dc7e734d0479772d8cf07c361baea0d4d03ee0 Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 23:24:51 +0300 Subject: [PATCH 23/25] simplify RandomInt --- shadowsocks/rand.go | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/shadowsocks/rand.go b/shadowsocks/rand.go index ed0db16..a06a28f 100644 --- a/shadowsocks/rand.go +++ b/shadowsocks/rand.go @@ -2,8 +2,8 @@ package shadowsocks import ( "crypto/rand" - "encoding/binary" "fmt" + "math/big" ) // FillRandomBytes fills dst with cryptographically secure random bytes. @@ -30,21 +30,9 @@ func RandomInt(min, max int) (int, error) { return min, nil } - span := uint64(max - min + 1) - if span == 0 { - return 0, fmt.Errorf("invalid random range") - } - - var buf [8]byte - limit := ^uint64(0) - (^uint64(0) % span) - - for { - if err := FillRandomBytes(buf[:]); err != nil { - return 0, err - } - v := binary.BigEndian.Uint64(buf[:]) - if v < limit { - return min + int(v%span), nil - } + n, err := rand.Int(rand.Reader, big.NewInt(int64(max-min+1))) + if err != nil { + return 0, err } + return min + int(n.Int64()), nil } From 0fdc8d60bd9eeb39e86efcc695c7796d3cd45f49 Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 23:46:31 +0300 Subject: [PATCH 24/25] optimize tcp chunk and conn --- shadowsocks/tcp_chunk_io.go | 58 +++++++++++++++++++++--------------- shadowsocks/tcp_conn.go | 44 ++++++++++++++------------- shadowsocks/tcp_conn_test.go | 10 +++---- 3 files changed, 63 insertions(+), 49 deletions(-) diff --git a/shadowsocks/tcp_chunk_io.go b/shadowsocks/tcp_chunk_io.go index fede3b6..fcf7be4 100644 --- a/shadowsocks/tcp_chunk_io.go +++ b/shadowsocks/tcp_chunk_io.go @@ -3,13 +3,15 @@ package shadowsocks import ( "fmt" "io" - - ibuf "github.com/33TU/socks/internal" ) // TCPChunkReader reads encrypted Shadowsocks 2022 TCP chunks. type TCPChunkReader struct { Cipher *TCPStreamCipher + + encLenBuf []byte + encPayloadBuf []byte + lenScratch [TcpChunkLengthLen]byte } // Init initializes the chunk reader for a TCP stream cipher. @@ -22,6 +24,15 @@ func (r *TCPChunkReader) Init(c *TCPStreamCipher) error { } r.Cipher = c + + encLenSize := c.EncryptedChunkLength() + if cap(r.encLenBuf) < encLenSize { + r.encLenBuf = make([]byte, encLenSize) + } else { + r.encLenBuf = r.encLenBuf[:encLenSize] + } + + r.encPayloadBuf = r.encPayloadBuf[:0] return nil } @@ -46,33 +57,31 @@ func (r *TCPChunkReader) ReadChunkTo(dst []byte, src io.Reader) ([]byte, int64, var total int64 - encLenBuf := ibuf.GetBytes(r.Cipher.EncryptedChunkLength()) - defer ibuf.PutBytes(encLenBuf) - - n, err := io.ReadFull(src, encLenBuf) + n, err := io.ReadFull(src, r.encLenBuf) total += int64(n) if err != nil { return nil, total, err } - lenScratch := ibuf.GetBytes(TcpChunkLengthLen) - defer ibuf.PutBytes(lenScratch) - - payloadLen, err := r.Cipher.DecodeChunkLength(encLenBuf, lenScratch[:0]) + payloadLen, err := r.Cipher.DecodeChunkLength(r.encLenBuf, r.lenScratch[:0]) if err != nil { return nil, total, err } - encPayloadBuf := ibuf.GetBytes(r.Cipher.EncryptedPayloadLength(int(payloadLen))) - defer ibuf.PutBytes(encPayloadBuf) + encPayloadLen := r.Cipher.EncryptedPayloadLength(int(payloadLen)) + if cap(r.encPayloadBuf) < encPayloadLen { + r.encPayloadBuf = make([]byte, encPayloadLen) + } else { + r.encPayloadBuf = r.encPayloadBuf[:encPayloadLen] + } - n, err = io.ReadFull(src, encPayloadBuf) + n, err = io.ReadFull(src, r.encPayloadBuf) total += int64(n) if err != nil { return nil, total, err } - dst, err = r.Cipher.DecodeChunkPayloadTo(dst, encPayloadBuf) + dst, err = r.Cipher.DecodeChunkPayloadTo(dst, r.encPayloadBuf) if err != nil { return nil, total, err } @@ -83,6 +92,7 @@ func (r *TCPChunkReader) ReadChunkTo(dst []byte, src io.Reader) ([]byte, int64, // TCPChunkWriter writes encrypted Shadowsocks 2022 TCP chunks. type TCPChunkWriter struct { Cipher *TCPStreamCipher + outBuf []byte } // Init initializes the chunk writer for a TCP stream cipher. @@ -95,6 +105,7 @@ func (w *TCPChunkWriter) Init(c *TCPStreamCipher) error { } w.Cipher = c + w.outBuf = w.outBuf[:0] return nil } @@ -119,25 +130,24 @@ func (w *TCPChunkWriter) WriteChunk(dst io.Writer, payload []byte) (int64, error return 0, fmt.Errorf("payload too large: got %d, max %d", len(payload), 0xFFFF) } - out := ibuf.GetBytes( - w.Cipher.EncryptedChunkLength() + - w.Cipher.EncryptedPayloadLength(len(payload)), - ) - defer ibuf.PutBytes(out) - - out = out[:0] + need := w.Cipher.EncryptedChunkLength() + w.Cipher.EncryptedPayloadLength(len(payload)) + if cap(w.outBuf) < need { + w.outBuf = make([]byte, 0, need) + } else { + w.outBuf = w.outBuf[:0] + } var err error - out, err = w.Cipher.EncodeChunkLengthTo(out, uint16(len(payload))) + w.outBuf, err = w.Cipher.EncodeChunkLengthTo(w.outBuf, uint16(len(payload))) if err != nil { return 0, err } - out, err = w.Cipher.EncodeChunkPayloadTo(out, payload) + w.outBuf, err = w.Cipher.EncodeChunkPayloadTo(w.outBuf, payload) if err != nil { return 0, err } - n, err := dst.Write(out) + n, err := dst.Write(w.outBuf) return int64(n), err } diff --git a/shadowsocks/tcp_conn.go b/shadowsocks/tcp_conn.go index 490bdfe..80ca4c4 100644 --- a/shadowsocks/tcp_conn.go +++ b/shadowsocks/tcp_conn.go @@ -6,12 +6,13 @@ import ( "time" ) -type TcpConn struct { +type TCPConn struct { net.Conn - Reader TCPChunkReader - Writer TCPChunkWriter - readBuf []byte + Reader TCPChunkReader + Writer TCPChunkWriter + readBuf []byte + chunkBuf []byte responseMethod Method responsePSK []byte @@ -25,7 +26,7 @@ func NewClientTCPConn( target Addr, padding []byte, initialPayload []byte, -) (*TcpConn, error) { +) (*TCPConn, error) { if conn == nil { return nil, fmt.Errorf("nil net.Conn") } @@ -63,7 +64,7 @@ func NewClientTCPConn( return nil, err } - return &TcpConn{ + return &TCPConn{ Conn: conn, Writer: writer, responseMethod: method, @@ -72,7 +73,7 @@ func NewClientTCPConn( }, nil } -func NewServerTCPConn(conn net.Conn, method Method, psk []byte) (*TcpConn, *ParsedTCPRequestStart, error) { +func NewServerTCPConn(conn net.Conn, method Method, psk []byte) (*TCPConn, *ParsedTCPRequestStart, error) { if conn == nil { return nil, nil, fmt.Errorf("nil net.Conn") } @@ -93,21 +94,22 @@ func NewServerTCPConn(conn net.Conn, method Method, psk []byte) (*TcpConn, *Pars return nil, nil, err } - c := &TcpConn{ + c := &TCPConn{ Conn: conn, Reader: reader, } if len(reqStart.Header.InitialData) > 0 { - c.readBuf = append([]byte(nil), reqStart.Header.InitialData...) + c.chunkBuf = append(c.chunkBuf[:0], reqStart.Header.InitialData...) + c.readBuf = c.chunkBuf } return c, reqStart, nil } -func (c *TcpConn) InitResponse(method Method, psk []byte, requestSalt []byte, initialPayload []byte) error { +func (c *TCPConn) InitResponse(method Method, psk []byte, requestSalt []byte, initialPayload []byte) error { if c == nil { - return fmt.Errorf("nil TcpConn") + return fmt.Errorf("nil TCPConn") } if c.Conn == nil { return fmt.Errorf("nil net.Conn") @@ -143,9 +145,9 @@ func (c *TcpConn) InitResponse(method Method, psk []byte, requestSalt []byte, in return c.Writer.Init(responseCipher) } -func (c *TcpConn) ensureClientResponseReady() error { +func (c *TCPConn) ensureClientResponseReady() error { if c == nil { - return fmt.Errorf("nil TcpConn") + return fmt.Errorf("nil TCPConn") } if c.Conn == nil { return fmt.Errorf("nil net.Conn") @@ -173,18 +175,19 @@ func (c *TcpConn) ensureClientResponseReady() error { } if len(respStart.InitialPayload) > 0 { - c.readBuf = append(c.readBuf[:0], respStart.InitialPayload...) + c.chunkBuf = append(c.chunkBuf[:0], respStart.InitialPayload...) + c.readBuf = c.chunkBuf } return nil } -func (c *TcpConn) Read(p []byte) (int, error) { +func (c *TCPConn) Read(p []byte) (int, error) { if len(p) == 0 { return 0, nil } if c == nil { - return 0, fmt.Errorf("nil TcpConn") + return 0, fmt.Errorf("nil TCPConn") } if c.Reader.Cipher == nil { if err := c.ensureClientResponseReady(); err != nil { @@ -196,10 +199,11 @@ func (c *TcpConn) Read(p []byte) (int, error) { } if len(c.readBuf) == 0 { - buf, _, err := c.Reader.ReadChunkTo(nil, c.Conn) + buf, _, err := c.Reader.ReadChunkTo(c.chunkBuf[:0], c.Conn) if err != nil { return 0, err } + c.chunkBuf = buf c.readBuf = buf } @@ -208,12 +212,12 @@ func (c *TcpConn) Read(p []byte) (int, error) { return n, nil } -func (c *TcpConn) Write(p []byte) (int, error) { +func (c *TCPConn) Write(p []byte) (int, error) { if len(p) == 0 { return 0, nil } if c == nil { - return 0, fmt.Errorf("nil TcpConn") + return 0, fmt.Errorf("nil TCPConn") } if c.Writer.Cipher == nil { return 0, fmt.Errorf("TCP writer not initialized") @@ -231,4 +235,4 @@ func (c *TcpConn) Write(p []byte) (int, error) { return written, nil } -var _ net.Conn = (*TcpConn)(nil) +var _ net.Conn = (*TCPConn)(nil) diff --git a/shadowsocks/tcp_conn_test.go b/shadowsocks/tcp_conn_test.go index 5037c48..03261a4 100644 --- a/shadowsocks/tcp_conn_test.go +++ b/shadowsocks/tcp_conn_test.go @@ -72,7 +72,7 @@ func TestTCPConn_Read(t *testing.T) { t.Fatalf("reader.Init() error = %v", err) } - c := &shadowsocks.TcpConn{ + c := &shadowsocks.TCPConn{ Conn: clientConn, Reader: reader, Writer: writer, @@ -119,7 +119,7 @@ func TestTCPConn_Read_PartialBuffered(t *testing.T) { t.Fatalf("reader.Init() error = %v", err) } - c := &shadowsocks.TcpConn{ + c := &shadowsocks.TCPConn{ Conn: clientConn, Reader: reader, Writer: writer, @@ -178,7 +178,7 @@ func TestTCPConn_Write(t *testing.T) { t.Fatalf("reader.Init() error = %v", err) } - c := &shadowsocks.TcpConn{ + c := &shadowsocks.TCPConn{ Conn: clientConn, Reader: reader, Writer: writer, @@ -234,7 +234,7 @@ func TestTCPConn_Write_Empty(t *testing.T) { t.Fatalf("reader.Init() error = %v", err) } - c := &shadowsocks.TcpConn{ + c := &shadowsocks.TCPConn{ Conn: clientConn, Reader: reader, Writer: writer, @@ -268,7 +268,7 @@ func TestTCPConn_Write_SplitsLargePayload(t *testing.T) { t.Fatalf("reader.Init() error = %v", err) } - c := &shadowsocks.TcpConn{ + c := &shadowsocks.TCPConn{ Conn: clientConn, Reader: reader, Writer: writer, From aa0ead8bac57cdfa2aafab22e31af1b397ceff89 Mon Sep 17 00:00:00 2001 From: 33TU Date: Sun, 19 Apr 2026 23:59:26 +0300 Subject: [PATCH 25/25] simplify tcp conn and tcp chunk --- shadowsocks/dialer_test.go | 4 +- shadowsocks/tcp_chunk_io.go | 104 +++++++++++++++++++---- shadowsocks/tcp_chunk_io_test.go | 138 ++++++++++++++++++++++--------- shadowsocks/tcp_conn.go | 54 +++--------- shadowsocks/tcp_conn_test.go | 56 ++++++++----- 5 files changed, 236 insertions(+), 120 deletions(-) diff --git a/shadowsocks/dialer_test.go b/shadowsocks/dialer_test.go index 5edadc5..5cfa6d5 100644 --- a/shadowsocks/dialer_test.go +++ b/shadowsocks/dialer_test.go @@ -387,12 +387,12 @@ func TestDialer_DialContext_Success(t *testing.T) { } var reader shadowsocks.TCPChunkReader - if err := reader.Init(reqStart.Cipher); err != nil { + if err := reader.Init(reqStart.Cipher, c); err != nil { t.Errorf("server: reader Init() error = %v", err) return } - payload, _, err := reader.ReadChunkTo(nil, c) + payload, _, err := reader.ReadChunkTo(nil) if err != nil { t.Errorf("server: ReadChunkTo() error = %v", err) return diff --git a/shadowsocks/tcp_chunk_io.go b/shadowsocks/tcp_chunk_io.go index fcf7be4..2505cc5 100644 --- a/shadowsocks/tcp_chunk_io.go +++ b/shadowsocks/tcp_chunk_io.go @@ -5,25 +5,34 @@ import ( "io" ) -// TCPChunkReader reads encrypted Shadowsocks 2022 TCP chunks. +// TCPChunkReader reads encrypted Shadowsocks 2022 TCP chunks and exposes a +// stream-style io.Reader view over the decrypted payload. type TCPChunkReader struct { Cipher *TCPStreamCipher + Src io.Reader encLenBuf []byte encPayloadBuf []byte lenScratch [TcpChunkLengthLen]byte + + chunkBuf []byte + readBuf []byte } -// Init initializes the chunk reader for a TCP stream cipher. -func (r *TCPChunkReader) Init(c *TCPStreamCipher) error { +// Init initializes the chunk reader for a TCP stream cipher and source. +func (r *TCPChunkReader) Init(c *TCPStreamCipher, src io.Reader) error { if c == nil { return fmt.Errorf("nil TCP stream cipher") } + if src == nil { + return fmt.Errorf("nil TCP chunk source") + } if err := c.Validate(); err != nil { return err } r.Cipher = c + r.Src = src encLenSize := c.EncryptedChunkLength() if cap(r.encLenBuf) < encLenSize { @@ -33,6 +42,9 @@ func (r *TCPChunkReader) Init(c *TCPStreamCipher) error { } r.encPayloadBuf = r.encPayloadBuf[:0] + r.chunkBuf = r.chunkBuf[:0] + r.readBuf = r.readBuf[:0] + return nil } @@ -44,20 +56,23 @@ func (r *TCPChunkReader) Validate() error { if r.Cipher == nil { return fmt.Errorf("missing TCP stream cipher") } + if r.Src == nil { + return fmt.Errorf("missing TCP chunk source") + } return r.Cipher.Validate() } -// ReadChunkTo reads a full encrypted TCP chunk from src, decrypts it, and -// appends the plaintext payload into dst. It returns the resulting slice and -// total bytes read. -func (r *TCPChunkReader) ReadChunkTo(dst []byte, src io.Reader) ([]byte, int64, error) { +// ReadChunkTo reads one full encrypted TCP chunk from the underlying source, +// decrypts it, and appends the plaintext payload into dst. It returns the +// resulting slice and total bytes read from the underlying source. +func (r *TCPChunkReader) ReadChunkTo(dst []byte) ([]byte, int64, error) { if err := r.Validate(); err != nil { return nil, 0, err } var total int64 - n, err := io.ReadFull(src, r.encLenBuf) + n, err := io.ReadFull(r.Src, r.encLenBuf) total += int64(n) if err != nil { return nil, total, err @@ -75,7 +90,7 @@ func (r *TCPChunkReader) ReadChunkTo(dst []byte, src io.Reader) ([]byte, int64, r.encPayloadBuf = r.encPayloadBuf[:encPayloadLen] } - n, err = io.ReadFull(src, r.encPayloadBuf) + n, err = io.ReadFull(r.Src, r.encPayloadBuf) total += int64(n) if err != nil { return nil, total, err @@ -89,22 +104,53 @@ func (r *TCPChunkReader) ReadChunkTo(dst []byte, src io.Reader) ([]byte, int64, return dst, total, nil } -// TCPChunkWriter writes encrypted Shadowsocks 2022 TCP chunks. +// Read implements io.Reader over the decrypted TCP stream. +func (r *TCPChunkReader) Read(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + if err := r.Validate(); err != nil { + return 0, err + } + + if len(r.readBuf) == 0 { + buf, _, err := r.ReadChunkTo(r.chunkBuf[:0]) + if err != nil { + return 0, err + } + r.chunkBuf = buf + r.readBuf = buf + } + + n := copy(p, r.readBuf) + r.readBuf = r.readBuf[n:] + return n, nil +} + +var _ io.Reader = (*TCPChunkReader)(nil) + +// TCPChunkWriter writes encrypted Shadowsocks 2022 TCP chunks and exposes a +// stream-style io.Writer interface over plaintext payload. type TCPChunkWriter struct { Cipher *TCPStreamCipher + Dst io.Writer outBuf []byte } -// Init initializes the chunk writer for a TCP stream cipher. -func (w *TCPChunkWriter) Init(c *TCPStreamCipher) error { +// Init initializes the chunk writer for a TCP stream cipher and destination. +func (w *TCPChunkWriter) Init(c *TCPStreamCipher, dst io.Writer) error { if c == nil { return fmt.Errorf("nil TCP stream cipher") } + if dst == nil { + return fmt.Errorf("nil TCP chunk destination") + } if err := c.Validate(); err != nil { return err } w.Cipher = c + w.Dst = dst w.outBuf = w.outBuf[:0] return nil } @@ -117,12 +163,15 @@ func (w *TCPChunkWriter) Validate() error { if w.Cipher == nil { return fmt.Errorf("missing TCP stream cipher") } + if w.Dst == nil { + return fmt.Errorf("missing TCP chunk destination") + } return w.Cipher.Validate() } -// WriteChunk writes a full encrypted TCP chunk to dst and returns the total -// bytes written. -func (w *TCPChunkWriter) WriteChunk(dst io.Writer, payload []byte) (int64, error) { +// WriteChunk writes one full encrypted TCP chunk to the underlying destination +// and returns the total bytes written to the underlying writer. +func (w *TCPChunkWriter) WriteChunk(payload []byte) (int64, error) { if err := w.Validate(); err != nil { return 0, err } @@ -148,6 +197,29 @@ func (w *TCPChunkWriter) WriteChunk(dst io.Writer, payload []byte) (int64, error return 0, err } - n, err := dst.Write(w.outBuf) + n, err := w.Dst.Write(w.outBuf) return int64(n), err } + +// Write implements io.Writer over the plaintext TCP stream. +func (w *TCPChunkWriter) Write(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + if err := w.Validate(); err != nil { + return 0, err + } + + written := 0 + for len(p) > 0 { + nn := min(len(p), 0xFFFF) + if _, err := w.WriteChunk(p[:nn]); err != nil { + return written, err + } + written += nn + p = p[nn:] + } + return written, nil +} + +var _ io.Writer = (*TCPChunkWriter)(nil) diff --git a/shadowsocks/tcp_chunk_io_test.go b/shadowsocks/tcp_chunk_io_test.go index c887714..af0141d 100644 --- a/shadowsocks/tcp_chunk_io_test.go +++ b/shadowsocks/tcp_chunk_io_test.go @@ -62,7 +62,7 @@ func TestTCPChunkReader_Init_Validate(t *testing.T) { t.Parallel() var r shadowsocks.TCPChunkReader - if err := r.Init(dec); err != nil { + if err := r.Init(dec, bytes.NewReader(nil)); err != nil { t.Fatalf("Init() error = %v", err) } if err := r.Validate(); err != nil { @@ -74,7 +74,7 @@ func TestTCPChunkReader_Init_Validate(t *testing.T) { t.Parallel() var r shadowsocks.TCPChunkReader - err := r.Init(nil) + err := r.Init(nil, bytes.NewReader(nil)) if err == nil { t.Fatal("expected error, got nil") } @@ -83,6 +83,19 @@ func TestTCPChunkReader_Init_Validate(t *testing.T) { } }) + t.Run("nil source", func(t *testing.T) { + t.Parallel() + + var r shadowsocks.TCPChunkReader + err := r.Init(dec, nil) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "nil TCP chunk source") { + t.Fatalf("unexpected error: %v", err) + } + }) + t.Run("nil reader validate", func(t *testing.T) { t.Parallel() @@ -99,7 +112,7 @@ func TestTCPChunkReader_Init_Validate(t *testing.T) { t.Run("missing cipher", func(t *testing.T) { t.Parallel() - r := &shadowsocks.TCPChunkReader{} + r := &shadowsocks.TCPChunkReader{Src: bytes.NewReader(nil)} err := r.Validate() if err == nil { t.Fatal("expected error, got nil") @@ -108,6 +121,19 @@ func TestTCPChunkReader_Init_Validate(t *testing.T) { t.Fatalf("unexpected error: %v", err) } }) + + t.Run("missing source", func(t *testing.T) { + t.Parallel() + + r := &shadowsocks.TCPChunkReader{Cipher: dec} + err := r.Validate() + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "missing TCP chunk source") { + t.Fatalf("unexpected error: %v", err) + } + }) } func TestTCPChunkWriter_Init_Validate(t *testing.T) { @@ -119,7 +145,7 @@ func TestTCPChunkWriter_Init_Validate(t *testing.T) { t.Parallel() var w shadowsocks.TCPChunkWriter - if err := w.Init(enc); err != nil { + if err := w.Init(enc, io.Discard); err != nil { t.Fatalf("Init() error = %v", err) } if err := w.Validate(); err != nil { @@ -131,7 +157,7 @@ func TestTCPChunkWriter_Init_Validate(t *testing.T) { t.Parallel() var w shadowsocks.TCPChunkWriter - err := w.Init(nil) + err := w.Init(nil, io.Discard) if err == nil { t.Fatal("expected error, got nil") } @@ -140,6 +166,19 @@ func TestTCPChunkWriter_Init_Validate(t *testing.T) { } }) + t.Run("nil destination", func(t *testing.T) { + t.Parallel() + + var w shadowsocks.TCPChunkWriter + err := w.Init(enc, nil) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "nil TCP chunk destination") { + t.Fatalf("unexpected error: %v", err) + } + }) + t.Run("nil writer validate", func(t *testing.T) { t.Parallel() @@ -156,7 +195,7 @@ func TestTCPChunkWriter_Init_Validate(t *testing.T) { t.Run("missing cipher", func(t *testing.T) { t.Parallel() - w := &shadowsocks.TCPChunkWriter{} + w := &shadowsocks.TCPChunkWriter{Dst: io.Discard} err := w.Validate() if err == nil { t.Fatal("expected error, got nil") @@ -165,6 +204,19 @@ func TestTCPChunkWriter_Init_Validate(t *testing.T) { t.Fatalf("unexpected error: %v", err) } }) + + t.Run("missing destination", func(t *testing.T) { + t.Parallel() + + w := &shadowsocks.TCPChunkWriter{Cipher: enc} + err := w.Validate() + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "missing TCP chunk destination") { + t.Fatalf("unexpected error: %v", err) + } + }) } func TestTCPChunkWriterReader_RoundTrip(t *testing.T) { @@ -172,20 +224,21 @@ func TestTCPChunkWriterReader_RoundTrip(t *testing.T) { enc, dec := newTCPChunkCipherPair(t) + var buf bytes.Buffer + var w shadowsocks.TCPChunkWriter - if err := w.Init(enc); err != nil { + if err := w.Init(enc, &buf); err != nil { t.Fatalf("writer Init() error = %v", err) } var r shadowsocks.TCPChunkReader - if err := r.Init(dec); err != nil { + if err := r.Init(dec, &buf); err != nil { t.Fatalf("reader Init() error = %v", err) } payload := []byte("hello chunk") - var buf bytes.Buffer - nw, err := w.WriteChunk(&buf, payload) + nw, err := w.WriteChunk(payload) if err != nil { t.Fatalf("WriteChunk() error = %v", err) } @@ -193,15 +246,15 @@ func TestTCPChunkWriterReader_RoundTrip(t *testing.T) { t.Fatalf("WriteChunk() wrote %d bytes, buffer has %d", nw, buf.Len()) } - got, nr, err := r.ReadChunkTo(nil, &buf) + got, nr, err := r.ReadChunkTo(nil) if err != nil { - t.Fatalf("ReadChunk() error = %v", err) + t.Fatalf("ReadChunkTo() error = %v", err) } if nr != nw { - t.Fatalf("ReadChunk() read %d bytes, want %d", nr, nw) + t.Fatalf("ReadChunkTo() read %d bytes, want %d", nr, nw) } if !bytes.Equal(got, payload) { - t.Fatalf("ReadChunk() = %q, want %q", got, payload) + t.Fatalf("ReadChunkTo() = %q, want %q", got, payload) } } @@ -210,25 +263,26 @@ func TestTCPChunkReader_ReadChunkTo_AppendsToDst(t *testing.T) { enc, dec := newTCPChunkCipherPair(t) + var buf bytes.Buffer + var w shadowsocks.TCPChunkWriter - if err := w.Init(enc); err != nil { + if err := w.Init(enc, &buf); err != nil { t.Fatalf("writer Init() error = %v", err) } var r shadowsocks.TCPChunkReader - if err := r.Init(dec); err != nil { + if err := r.Init(dec, &buf); err != nil { t.Fatalf("reader Init() error = %v", err) } payload := []byte("payload") prefix := []byte("prefix:") - var buf bytes.Buffer - if _, err := w.WriteChunk(&buf, payload); err != nil { + if _, err := w.WriteChunk(payload); err != nil { t.Fatalf("WriteChunk() error = %v", err) } - got, _, err := r.ReadChunkTo(append([]byte(nil), prefix...), &buf) + got, _, err := r.ReadChunkTo(append([]byte(nil), prefix...)) if err != nil { t.Fatalf("ReadChunkTo() error = %v", err) } @@ -245,12 +299,12 @@ func TestTCPChunkWriter_WriteChunk_TooLarge(t *testing.T) { enc, _ := newTCPChunkCipherPair(t) var w shadowsocks.TCPChunkWriter - if err := w.Init(enc); err != nil { + if err := w.Init(enc, io.Discard); err != nil { t.Fatalf("Init() error = %v", err) } payload := make([]byte, 0x10000) - _, err := w.WriteChunk(io.Discard, payload) + _, err := w.WriteChunk(payload) if err == nil { t.Fatal("expected error, got nil") } @@ -264,19 +318,19 @@ func TestTCPChunkReader_ReadChunk_ShortLengthRead(t *testing.T) { _, dec := newTCPChunkCipherPair(t) + short := bytes.NewReader([]byte{1, 2, 3}) + var r shadowsocks.TCPChunkReader - if err := r.Init(dec); err != nil { + if err := r.Init(dec, short); err != nil { t.Fatalf("Init() error = %v", err) } - short := bytes.NewReader([]byte{1, 2, 3}) - - _, n, err := r.ReadChunkTo(nil, short) + _, n, err := r.ReadChunkTo(nil) if err == nil { t.Fatal("expected error, got nil") } if n != 3 { - t.Fatalf("ReadChunk() read %d bytes, want 3", n) + t.Fatalf("ReadChunkTo() read %d bytes, want 3", n) } if err != io.EOF && err != io.ErrUnexpectedEOF { t.Fatalf("unexpected error: %v", err) @@ -288,19 +342,15 @@ func TestTCPChunkReader_ReadChunk_ShortPayloadRead(t *testing.T) { enc, dec := newTCPChunkCipherPair(t) + var buf bytes.Buffer + var w shadowsocks.TCPChunkWriter - if err := w.Init(enc); err != nil { + if err := w.Init(enc, &buf); err != nil { t.Fatalf("writer Init() error = %v", err) } - var r shadowsocks.TCPChunkReader - if err := r.Init(dec); err != nil { - t.Fatalf("reader Init() error = %v", err) - } - payload := []byte("hello payload") - var buf bytes.Buffer - nw, err := w.WriteChunk(&buf, payload) + nw, err := w.WriteChunk(payload) if err != nil { t.Fatalf("WriteChunk() error = %v", err) } @@ -308,12 +358,17 @@ func TestTCPChunkReader_ReadChunk_ShortPayloadRead(t *testing.T) { wire := buf.Bytes() shortWire := wire[:len(wire)-2] - _, nr, err := r.ReadChunkTo(nil, bytes.NewReader(shortWire)) + var r shadowsocks.TCPChunkReader + if err := r.Init(dec, bytes.NewReader(shortWire)); err != nil { + t.Fatalf("reader Init() error = %v", err) + } + + _, nr, err := r.ReadChunkTo(nil) if err == nil { t.Fatal("expected error, got nil") } if nr != nw-2 { - t.Fatalf("ReadChunk() read %d bytes, want %d", nr, nw-2) + t.Fatalf("ReadChunkTo() read %d bytes, want %d", nr, nw-2) } if err != io.EOF && err != io.ErrUnexpectedEOF { t.Fatalf("unexpected error: %v", err) @@ -340,22 +395,23 @@ func TestTCPChunkReader_ReadChunk_WrongCipherFails(t *testing.T) { t.Fatalf("NewTCPStreamCipherFromPSK() error = %v", err) } + var buf bytes.Buffer + var w shadowsocks.TCPChunkWriter - if err := w.Init(enc); err != nil { + if err := w.Init(enc, &buf); err != nil { t.Fatalf("writer Init() error = %v", err) } var r shadowsocks.TCPChunkReader - if err := r.Init(wrongDec); err != nil { + if err := r.Init(wrongDec, &buf); err != nil { t.Fatalf("reader Init() error = %v", err) } - var buf bytes.Buffer - if _, err := w.WriteChunk(&buf, []byte("hello")); err != nil { + if _, err := w.WriteChunk([]byte("hello")); err != nil { t.Fatalf("WriteChunk() error = %v", err) } - _, _, err = r.ReadChunkTo(nil, &buf) + _, _, err = r.ReadChunkTo(nil) if err == nil { t.Fatal("expected decrypt error, got nil") } diff --git a/shadowsocks/tcp_conn.go b/shadowsocks/tcp_conn.go index 80ca4c4..c2da6da 100644 --- a/shadowsocks/tcp_conn.go +++ b/shadowsocks/tcp_conn.go @@ -9,10 +9,8 @@ import ( type TCPConn struct { net.Conn - Reader TCPChunkReader - Writer TCPChunkWriter - readBuf []byte - chunkBuf []byte + Reader TCPChunkReader + Writer TCPChunkWriter responseMethod Method responsePSK []byte @@ -60,7 +58,7 @@ func NewClientTCPConn( } var writer TCPChunkWriter - if err := writer.Init(requestCipher); err != nil { + if err := writer.Init(requestCipher, conn); err != nil { return nil, err } @@ -90,7 +88,7 @@ func NewServerTCPConn(conn net.Conn, method Method, psk []byte) (*TCPConn, *Pars } var reader TCPChunkReader - if err := reader.Init(reqStart.Cipher); err != nil { + if err := reader.Init(reqStart.Cipher, conn); err != nil { return nil, nil, err } @@ -100,8 +98,8 @@ func NewServerTCPConn(conn net.Conn, method Method, psk []byte) (*TCPConn, *Pars } if len(reqStart.Header.InitialData) > 0 { - c.chunkBuf = append(c.chunkBuf[:0], reqStart.Header.InitialData...) - c.readBuf = c.chunkBuf + c.Reader.chunkBuf = append(c.Reader.chunkBuf[:0], reqStart.Header.InitialData...) + c.Reader.readBuf = c.Reader.chunkBuf } return c, reqStart, nil @@ -142,7 +140,7 @@ func (c *TCPConn) InitResponse(method Method, psk []byte, requestSalt []byte, in return err } - return c.Writer.Init(responseCipher) + return c.Writer.Init(responseCipher, c.Conn) } func (c *TCPConn) ensureClientResponseReady() error { @@ -170,13 +168,13 @@ func (c *TCPConn) ensureClientResponseReady() error { return err } - if err := c.Reader.Init(respStart.Cipher); err != nil { + if err := c.Reader.Init(respStart.Cipher, c.Conn); err != nil { return err } if len(respStart.InitialPayload) > 0 { - c.chunkBuf = append(c.chunkBuf[:0], respStart.InitialPayload...) - c.readBuf = c.chunkBuf + c.Reader.chunkBuf = append(c.Reader.chunkBuf[:0], respStart.InitialPayload...) + c.Reader.readBuf = c.Reader.chunkBuf } return nil @@ -194,22 +192,7 @@ func (c *TCPConn) Read(p []byte) (int, error) { return 0, err } } - if c.Reader.Cipher == nil { - return 0, fmt.Errorf("TCP reader not initialized") - } - - if len(c.readBuf) == 0 { - buf, _, err := c.Reader.ReadChunkTo(c.chunkBuf[:0], c.Conn) - if err != nil { - return 0, err - } - c.chunkBuf = buf - c.readBuf = buf - } - - n := copy(p, c.readBuf) - c.readBuf = c.readBuf[n:] - return n, nil + return c.Reader.Read(p) } func (c *TCPConn) Write(p []byte) (int, error) { @@ -219,20 +202,7 @@ func (c *TCPConn) Write(p []byte) (int, error) { if c == nil { return 0, fmt.Errorf("nil TCPConn") } - if c.Writer.Cipher == nil { - return 0, fmt.Errorf("TCP writer not initialized") - } - - written := 0 - for len(p) > 0 { - nn := min(len(p), 0xFFFF) - if _, err := c.Writer.WriteChunk(c.Conn, p[:nn]); err != nil { - return written, err - } - written += nn - p = p[nn:] - } - return written, nil + return c.Writer.Write(p) } var _ net.Conn = (*TCPConn)(nil) diff --git a/shadowsocks/tcp_conn_test.go b/shadowsocks/tcp_conn_test.go index 03261a4..ab64749 100644 --- a/shadowsocks/tcp_conn_test.go +++ b/shadowsocks/tcp_conn_test.go @@ -63,26 +63,26 @@ func TestTCPConn_Read(t *testing.T) { enc, dec := newTCPConnCipherPair(t) var writer shadowsocks.TCPChunkWriter - if err := writer.Init(enc); err != nil { + if err := writer.Init(enc, serverConn); err != nil { t.Fatalf("writer.Init() error = %v", err) } var reader shadowsocks.TCPChunkReader - if err := reader.Init(dec); err != nil { + if err := reader.Init(dec, clientConn); err != nil { t.Fatalf("reader.Init() error = %v", err) } c := &shadowsocks.TCPConn{ Conn: clientConn, Reader: reader, - Writer: writer, + Writer: readerlessWriter(enc, clientConn), } want := []byte("hello world") go func() { defer serverConn.Close() - if _, err := writer.WriteChunk(serverConn, want); err != nil { + if _, err := writer.WriteChunk(want); err != nil { t.Errorf("WriteChunk() error = %v", err) } }() @@ -110,26 +110,26 @@ func TestTCPConn_Read_PartialBuffered(t *testing.T) { enc, dec := newTCPConnCipherPair(t) var writer shadowsocks.TCPChunkWriter - if err := writer.Init(enc); err != nil { + if err := writer.Init(enc, serverConn); err != nil { t.Fatalf("writer.Init() error = %v", err) } var reader shadowsocks.TCPChunkReader - if err := reader.Init(dec); err != nil { + if err := reader.Init(dec, clientConn); err != nil { t.Fatalf("reader.Init() error = %v", err) } c := &shadowsocks.TCPConn{ Conn: clientConn, Reader: reader, - Writer: writer, + Writer: readerlessWriter(enc, clientConn), } want := []byte("hello world") go func() { defer serverConn.Close() - if _, err := writer.WriteChunk(serverConn, want); err != nil { + if _, err := writer.WriteChunk(want); err != nil { t.Errorf("WriteChunk() error = %v", err) } }() @@ -169,18 +169,18 @@ func TestTCPConn_Write(t *testing.T) { enc, dec := newTCPConnCipherPair(t) var writer shadowsocks.TCPChunkWriter - if err := writer.Init(enc); err != nil { + if err := writer.Init(enc, clientConn); err != nil { t.Fatalf("writer.Init() error = %v", err) } var reader shadowsocks.TCPChunkReader - if err := reader.Init(dec); err != nil { + if err := reader.Init(dec, serverConn); err != nil { t.Fatalf("reader.Init() error = %v", err) } c := &shadowsocks.TCPConn{ Conn: clientConn, - Reader: reader, + Reader: writerlessReader(dec, clientConn), Writer: writer, } @@ -190,7 +190,7 @@ func TestTCPConn_Write(t *testing.T) { go func() { defer serverConn.Close() - got, _, err := reader.ReadChunkTo(nil, serverConn) + got, _, err := reader.ReadChunkTo(nil) if err != nil { errCh <- err return @@ -225,12 +225,12 @@ func TestTCPConn_Write_Empty(t *testing.T) { enc, dec := newTCPConnCipherPair(t) var writer shadowsocks.TCPChunkWriter - if err := writer.Init(enc); err != nil { + if err := writer.Init(enc, clientConn); err != nil { t.Fatalf("writer.Init() error = %v", err) } var reader shadowsocks.TCPChunkReader - if err := reader.Init(dec); err != nil { + if err := reader.Init(dec, clientConn); err != nil { t.Fatalf("reader.Init() error = %v", err) } @@ -259,18 +259,18 @@ func TestTCPConn_Write_SplitsLargePayload(t *testing.T) { enc, dec := newTCPConnCipherPair(t) var writer shadowsocks.TCPChunkWriter - if err := writer.Init(enc); err != nil { + if err := writer.Init(enc, clientConn); err != nil { t.Fatalf("writer.Init() error = %v", err) } var reader shadowsocks.TCPChunkReader - if err := reader.Init(dec); err != nil { + if err := reader.Init(dec, serverConn); err != nil { t.Fatalf("reader.Init() error = %v", err) } c := &shadowsocks.TCPConn{ Conn: clientConn, - Reader: reader, + Reader: writerlessReader(dec, clientConn), Writer: writer, } @@ -282,14 +282,14 @@ func TestTCPConn_Write_SplitsLargePayload(t *testing.T) { var got []byte - part1, _, err := reader.ReadChunkTo(nil, serverConn) + part1, _, err := reader.ReadChunkTo(nil) if err != nil { done <- err return } got = append(got, part1...) - part2, _, err := reader.ReadChunkTo(nil, serverConn) + part2, _, err := reader.ReadChunkTo(nil) if err != nil { done <- err return @@ -315,3 +315,21 @@ func TestTCPConn_Write_SplitsLargePayload(t *testing.T) { t.Fatalf("server read error = %v", err) } } + +// helpers for constructing a TCPConn in tests where only one side is actually used + +func readerlessWriter(enc *shadowsocks.TCPStreamCipher, dst net.Conn) shadowsocks.TCPChunkWriter { + var w shadowsocks.TCPChunkWriter + if err := w.Init(enc, dst); err != nil { + panic(err) + } + return w +} + +func writerlessReader(dec *shadowsocks.TCPStreamCipher, src net.Conn) shadowsocks.TCPChunkReader { + var r shadowsocks.TCPChunkReader + if err := r.Init(dec, src); err != nil { + panic(err) + } + return r +}