From e59e6dd5cbaf6b0df8678fd39bf3f8f479a63d66 Mon Sep 17 00:00:00 2001 From: Minis <285644962+Minis233@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:49:30 +0800 Subject: [PATCH 01/15] feat: add Snell v4/v5 client outbound Add an unofficial Snell v4/v5 outbound for exclave-core, covering TCP CONNECT, UDP-over-TCP, optional http/tls obfs, and connection reuse. Protocol code is adapted from missuo/opensnell (GPL-3.0) and vendored under proxy/snell/internal so the module stays self-contained. Snell v6 is intentionally not supported (deployment-unique framing). --- infra/conf/v4/snell.go | 38 ++ infra/conf/v4/v2ray.go | 1 + main/distro/all/all.go | 1 + proxy/snell/NOTICE | 5 + proxy/snell/config.pb.go | 176 ++++++++ proxy/snell/config.proto | 25 ++ proxy/snell/errors.generated.go | 9 + proxy/snell/internal/obfs/http/client.go | 91 ++++ proxy/snell/internal/obfs/obfs.go | 28 ++ proxy/snell/internal/obfs/tls/client.go | 152 +++++++ proxy/snell/internal/obfs/tls/common.go | 47 +++ proxy/snell/internal/pool/alloc.go | 59 +++ proxy/snell/internal/pool/pool.go | 14 + proxy/snell/internal/snell/cipher.go | 27 ++ proxy/snell/internal/snell/client.go | 173 ++++++++ proxy/snell/internal/snell/conn.go | 150 +++++++ proxy/snell/internal/snell/pool.go | 176 ++++++++ proxy/snell/internal/snell/protocol.go | 73 ++++ proxy/snell/internal/snell/udp.go | 274 ++++++++++++ proxy/snell/internal/snell/v4.go | 507 +++++++++++++++++++++++ proxy/snell/internal/socks5/addr.go | 123 ++++++ proxy/snell/outbound.go | 221 ++++++++++ proxy/snell/snell.go | 3 + 23 files changed, 2373 insertions(+) create mode 100644 infra/conf/v4/snell.go create mode 100644 proxy/snell/NOTICE create mode 100644 proxy/snell/config.pb.go create mode 100644 proxy/snell/config.proto create mode 100644 proxy/snell/errors.generated.go create mode 100644 proxy/snell/internal/obfs/http/client.go create mode 100644 proxy/snell/internal/obfs/obfs.go create mode 100644 proxy/snell/internal/obfs/tls/client.go create mode 100644 proxy/snell/internal/obfs/tls/common.go create mode 100644 proxy/snell/internal/pool/alloc.go create mode 100644 proxy/snell/internal/pool/pool.go create mode 100644 proxy/snell/internal/snell/cipher.go create mode 100644 proxy/snell/internal/snell/client.go create mode 100644 proxy/snell/internal/snell/conn.go create mode 100644 proxy/snell/internal/snell/pool.go create mode 100644 proxy/snell/internal/snell/protocol.go create mode 100644 proxy/snell/internal/snell/udp.go create mode 100644 proxy/snell/internal/snell/v4.go create mode 100644 proxy/snell/internal/socks5/addr.go create mode 100644 proxy/snell/outbound.go create mode 100644 proxy/snell/snell.go diff --git a/infra/conf/v4/snell.go b/infra/conf/v4/snell.go new file mode 100644 index 00000000000..a9a6d4938f3 --- /dev/null +++ b/infra/conf/v4/snell.go @@ -0,0 +1,38 @@ +package v4 + +import ( + "github.com/golang/protobuf/proto" + + "github.com/exclavenetwork/exclave-core/v5/infra/conf/cfgcommon" + "github.com/exclavenetwork/exclave-core/v5/proxy/snell" +) + +type SnellClientConfig struct { + Address *cfgcommon.Address `json:"address"` + Port uint16 `json:"port"` + PSK string `json:"psk"` + Obfs string `json:"obfs"` + Version uint32 `json:"version"` + Reuse bool `json:"reuse"` +} + +func (c *SnellClientConfig) Build() (proto.Message, error) { + if c.Address == nil { + return nil, newError("missing server address") + } + if c.PSK == "" { + return nil, newError("missing psk") + } + version := c.Version + if version == 0 { + version = 4 + } + return &snell.ClientConfig{ + Address: c.Address.Build(), + Port: uint32(c.Port), + Psk: c.PSK, + Obfs: c.Obfs, + Version: version, + Reuse: c.Reuse, + }, nil +} diff --git a/infra/conf/v4/v2ray.go b/infra/conf/v4/v2ray.go index cd347184abb..aab85b74afe 100644 --- a/infra/conf/v4/v2ray.go +++ b/infra/conf/v4/v2ray.go @@ -62,6 +62,7 @@ var ( "tuic": func() interface{} { return new(TuicClientConfig) }, "juicity": func() interface{} { return new(JuicityClientConfig) }, "mieru": func() interface{} { return new(MieruClientConfig) }, + "snell": func() interface{} { return new(SnellClientConfig) }, "trusttunnel": func() interface{} { return new(TrustTunnelClientConfig) }, }, "protocol", "settings") ) diff --git a/main/distro/all/all.go b/main/distro/all/all.go index 45bb2ff7100..b2d1220a08d 100644 --- a/main/distro/all/all.go +++ b/main/distro/all/all.go @@ -64,6 +64,7 @@ import ( _ "github.com/exclavenetwork/exclave-core/v5/proxy/hysteria2" _ "github.com/exclavenetwork/exclave-core/v5/proxy/juicity" _ "github.com/exclavenetwork/exclave-core/v5/proxy/mieru" + _ "github.com/exclavenetwork/exclave-core/v5/proxy/snell" _ "github.com/exclavenetwork/exclave-core/v5/proxy/tuic" _ "github.com/exclavenetwork/exclave-core/v5/proxy/wireguard" diff --git a/proxy/snell/NOTICE b/proxy/snell/NOTICE new file mode 100644 index 00000000000..6aea36dde81 --- /dev/null +++ b/proxy/snell/NOTICE @@ -0,0 +1,5 @@ +Snell client protocol implementation adapted from: + https://github.com/missuo/opensnell (GPL-3.0-or-later) +which itself ports address/obfs pieces from Dreamacro/clash and icpz/open-snell. + +Snell is a protocol designed by the Surge team. This is an unofficial client. diff --git a/proxy/snell/config.pb.go b/proxy/snell/config.pb.go new file mode 100644 index 00000000000..65d92213892 --- /dev/null +++ b/proxy/snell/config.pb.go @@ -0,0 +1,176 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v4.24.4 +// source: proxy/snell/config.proto + +package snell + +import ( + net "github.com/exclavenetwork/exclave-core/v5/common/net" + _ "github.com/exclavenetwork/exclave-core/v5/common/protoext" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ClientConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Address *net.IPOrDomain `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` + Psk string `protobuf:"bytes,3,opt,name=psk,proto3" json:"psk,omitempty"` + // "off" | "http" | "tls" + Obfs string `protobuf:"bytes,4,opt,name=obfs,proto3" json:"obfs,omitempty"` + // 4 or 5 (wire-compatible for TCP / UDP-over-TCP) + Version uint32 `protobuf:"varint,5,opt,name=version,proto3" json:"version,omitempty"` + // enable CommandConnectV2 connection reuse + Reuse bool `protobuf:"varint,6,opt,name=reuse,proto3" json:"reuse,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientConfig) Reset() { + *x = ClientConfig{} + mi := &file_proxy_snell_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientConfig) ProtoMessage() {} + +func (x *ClientConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_snell_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientConfig.ProtoReflect.Descriptor instead. +func (*ClientConfig) Descriptor() ([]byte, []int) { + return file_proxy_snell_config_proto_rawDescGZIP(), []int{0} +} + +func (x *ClientConfig) GetAddress() *net.IPOrDomain { + if x != nil { + return x.Address + } + return nil +} + +func (x *ClientConfig) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *ClientConfig) GetPsk() string { + if x != nil { + return x.Psk + } + return "" +} + +func (x *ClientConfig) GetObfs() string { + if x != nil { + return x.Obfs + } + return "" +} + +func (x *ClientConfig) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *ClientConfig) GetReuse() bool { + if x != nil { + return x.Reuse + } + return false +} + +var File_proxy_snell_config_proto protoreflect.FileDescriptor + +const file_proxy_snell_config_proto_rawDesc = "" + + "\n" + + "\x18proxy/snell/config.proto\x12\x18exclave.core.proxy.snell\x1a common/protoext/extensions.proto\x1a\x18common/net/address.proto\"\xce\x01\n" + + "\fClientConfig\x12=\n" + + "\aaddress\x18\x01 \x01(\v2#.exclave.core.common.net.IPOrDomainR\aaddress\x12\x12\n" + + "\x04port\x18\x02 \x01(\rR\x04port\x12\x10\n" + + "\x03psk\x18\x03 \x01(\tR\x03psk\x12\x12\n" + + "\x04obfs\x18\x04 \x01(\tR\x04obfs\x12\x18\n" + + "\aversion\x18\x05 \x01(\rR\aversion\x12\x14\n" + + "\x05reuse\x18\x06 \x01(\bR\x05reuse:\x15\x82\xb5\x18\x11\n" + + "\boutbound\x12\x05snellB\x88\x01\n" + + "2com.github.exclavenetwork.exclave.core.proxy.snellP\x01Z5github.com/exclavenetwork/exclave-core/v5/proxy/snell\xaa\x02\x18Exclave.Core.Proxy.Snellb\x06proto3" + +var ( + file_proxy_snell_config_proto_rawDescOnce sync.Once + file_proxy_snell_config_proto_rawDescData []byte +) + +func file_proxy_snell_config_proto_rawDescGZIP() []byte { + file_proxy_snell_config_proto_rawDescOnce.Do(func() { + file_proxy_snell_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_snell_config_proto_rawDesc), len(file_proxy_snell_config_proto_rawDesc))) + }) + return file_proxy_snell_config_proto_rawDescData +} + +var file_proxy_snell_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_proxy_snell_config_proto_goTypes = []any{ + (*ClientConfig)(nil), // 0: exclave.core.proxy.snell.ClientConfig + (*net.IPOrDomain)(nil), // 1: exclave.core.common.net.IPOrDomain +} +var file_proxy_snell_config_proto_depIdxs = []int32{ + 1, // 0: exclave.core.proxy.snell.ClientConfig.address:type_name -> exclave.core.common.net.IPOrDomain + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_proxy_snell_config_proto_init() } +func file_proxy_snell_config_proto_init() { + if File_proxy_snell_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_snell_config_proto_rawDesc), len(file_proxy_snell_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_snell_config_proto_goTypes, + DependencyIndexes: file_proxy_snell_config_proto_depIdxs, + MessageInfos: file_proxy_snell_config_proto_msgTypes, + }.Build() + File_proxy_snell_config_proto = out.File + file_proxy_snell_config_proto_goTypes = nil + file_proxy_snell_config_proto_depIdxs = nil +} diff --git a/proxy/snell/config.proto b/proxy/snell/config.proto new file mode 100644 index 00000000000..d082450a135 --- /dev/null +++ b/proxy/snell/config.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package exclave.core.proxy.snell; +option csharp_namespace = "Exclave.Core.Proxy.Snell"; +option go_package = "github.com/exclavenetwork/exclave-core/v5/proxy/snell"; +option java_package = "com.github.exclavenetwork.exclave.core.proxy.snell"; +option java_multiple_files = true; + +import "common/protoext/extensions.proto"; +import "common/net/address.proto"; + +message ClientConfig { + option (exclave.core.common.protoext.message_opt).type = "outbound"; + option (exclave.core.common.protoext.message_opt).short_name = "snell"; + + exclave.core.common.net.IPOrDomain address = 1; + uint32 port = 2; + string psk = 3; + // "off" | "http" | "tls" + string obfs = 4; + // 4 or 5 (wire-compatible for TCP / UDP-over-TCP) + uint32 version = 5; + // enable CommandConnectV2 connection reuse + bool reuse = 6; +} diff --git a/proxy/snell/errors.generated.go b/proxy/snell/errors.generated.go new file mode 100644 index 00000000000..8ce20d7b51e --- /dev/null +++ b/proxy/snell/errors.generated.go @@ -0,0 +1,9 @@ +package snell + +import "github.com/exclavenetwork/exclave-core/v5/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/proxy/snell/internal/obfs/http/client.go b/proxy/snell/internal/obfs/http/client.go new file mode 100644 index 00000000000..711eedbf07b --- /dev/null +++ b/proxy/snell/internal/obfs/http/client.go @@ -0,0 +1,91 @@ +/* + * This file is part of opensnell. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package http + +import ( + "bufio" + "bytes" + crand "crypto/rand" + "encoding/base64" + "fmt" + "io" + "math/rand/v2" + "net" + "net/http" +) + +type HTTPObfsClient struct { + net.Conn + host string + port string + bio *bufio.Reader + buf []byte + offset int + firstRequest bool + firstResponse bool +} + +func (ho *HTTPObfsClient) Read(b []byte) (int, error) { + if ho.buf != nil { + n := copy(b, ho.buf[ho.offset:]) + ho.offset += n + if ho.offset == len(ho.buf) { + ho.offset = 0 + ho.buf = nil + } + return n, nil + } + + if ho.firstResponse { + bio := bufio.NewReader(ho.Conn) + resp, err := http.ReadResponse(bio, nil) + if err != nil { + return 0, err + } + buf, err := io.ReadAll(resp.Body) + if err != nil { + return 0, err + } + n := copy(b, buf) + if n < len(buf) { + ho.buf = buf + ho.offset = n + } + _ = resp.Body.Close() + ho.bio = bio + ho.firstResponse = false + return n, nil + } + return ho.bio.Read(b) +} + +func (ho *HTTPObfsClient) Write(b []byte) (int, error) { + if ho.firstRequest { + randBytes := make([]byte, 16) + _, _ = crand.Read(randBytes) + req, _ := http.NewRequest("GET", fmt.Sprintf("http://%s/", ho.host), bytes.NewBuffer(b)) + req.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", rand.IntN(54), rand.IntN(2))) + req.Header.Set("Upgrade", "websocket") + req.Header.Set("Connection", "Upgrade") + req.Host = fmt.Sprintf("%s:%s", ho.host, ho.port) + req.Header.Set("Sec-WebSocket-Key", base64.URLEncoding.EncodeToString(randBytes)) + req.ContentLength = int64(len(b)) + err := req.Write(ho.Conn) + ho.firstRequest = false + return len(b), err + } + return ho.Conn.Write(b) +} + +func NewHTTPObfsClient(conn net.Conn, host string, port string) net.Conn { + return &HTTPObfsClient{ + Conn: conn, + firstRequest: true, + firstResponse: true, + host: host, + port: port, + } +} diff --git a/proxy/snell/internal/obfs/obfs.go b/proxy/snell/internal/obfs/obfs.go new file mode 100644 index 00000000000..c3fd8fb3153 --- /dev/null +++ b/proxy/snell/internal/obfs/obfs.go @@ -0,0 +1,28 @@ +/* + * Based on opensnell (GPL-3.0-or-later). + * Client-side obfuscation wrapper for Snell. + */ +package obfs + +import ( + "fmt" + "net" + + "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/obfs/http" + "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/obfs/tls" +) + +// NewClient wraps conn with the requested obfuscation mode. +// mode: "off"/"none"/"" | "http" | "tls" +func NewClient(conn net.Conn, server, port, mode string) (net.Conn, error) { + switch mode { + case "tls": + return tls.NewTLSObfsClient(conn, server), nil + case "http": + return http.NewHTTPObfsClient(conn, server, port), nil + case "none", "off", "": + return conn, nil + default: + return nil, fmt.Errorf("invalid snell obfs type %q", mode) + } +} diff --git a/proxy/snell/internal/obfs/tls/client.go b/proxy/snell/internal/obfs/tls/client.go new file mode 100644 index 00000000000..8d0a30a2a8d --- /dev/null +++ b/proxy/snell/internal/obfs/tls/client.go @@ -0,0 +1,152 @@ +/* + * This file is part of opensnell. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package tls + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "io" + "net" + "time" +) + +type TLSObfsClient struct { + net.Conn + server string + remain int + firstRequest bool + firstResponse bool +} + +func (to *TLSObfsClient) read(b []byte, discardN int) (int, error) { + r, n, err := readBlock(to.Conn, b, discardN) + to.remain = r + return n, err +} + +func (to *TLSObfsClient) Read(b []byte) (int, error) { + if to.remain > 0 { + length := to.remain + if length > len(b) { + length = len(b) + } + n, err := io.ReadFull(to.Conn, b[:length]) + to.remain -= n + return n, err + } + + if to.firstResponse { + // ServerHello + ChangeCipherSpec + Finished framing yields 105 leading + // bytes that the client peer just discards. + to.firstResponse = false + return to.read(b, 105) + } + return to.read(b, 3) +} + +func (to *TLSObfsClient) Write(b []byte) (int, error) { + for i := 0; i < len(b); i += chunkSize { + end := i + chunkSize + if end > len(b) { + end = len(b) + } + if n, err := to.write(b[i:end]); err != nil { + return n, err + } + } + return len(b), nil +} + +func (to *TLSObfsClient) write(b []byte) (int, error) { + if to.firstRequest { + helloMsg := makeClientHelloMsg(b, to.server) + _, err := to.Conn.Write(helloMsg) + to.firstRequest = false + return len(b), err + } + + buf := bufferPool.Get().(*bytes.Buffer) + buf.Reset() + defer bufferPool.Put(buf) + + buf.Write([]byte{0x17, 0x03, 0x03}) + _ = binary.Write(buf, binary.BigEndian, uint16(len(b))) + if _, err := to.Conn.Write(buf.Bytes()); err != nil { + return 0, err + } + return to.Conn.Write(b) +} + +func NewTLSObfsClient(conn net.Conn, server string) net.Conn { + return &TLSObfsClient{ + Conn: conn, + server: server, + firstRequest: true, + firstResponse: true, + } +} + +func makeClientHelloMsg(data []byte, server string) []byte { + random := make([]byte, 28) + sessionID := make([]byte, 32) + _, _ = rand.Read(random) + _, _ = rand.Read(sessionID) + + buf := bufferPool.Get().(*bytes.Buffer) + buf.Reset() + defer bufferPool.Put(buf) + + buf.WriteByte(22) + buf.Write([]byte{0x03, 0x01}) + length := uint16(212 + len(data) + len(server)) + buf.WriteByte(byte(length >> 8)) + buf.WriteByte(byte(length & 0xff)) + + buf.WriteByte(1) + buf.WriteByte(0) + _ = binary.Write(buf, binary.BigEndian, uint16(208+len(data)+len(server))) + buf.Write([]byte{0x03, 0x03}) + + _ = binary.Write(buf, binary.BigEndian, uint32(time.Now().Unix())) + buf.Write(random) + buf.WriteByte(32) + buf.Write(sessionID) + + buf.Write([]byte{0x00, 0x38}) + buf.Write([]byte{ + 0xc0, 0x2c, 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f, + 0x00, 0x9e, 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, 0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a, + 0xc0, 0x14, 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, 0x00, 0x9d, 0x00, 0x9c, 0x00, 0x3d, + 0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0xff, + }) + buf.Write([]byte{0x01, 0x00}) + + _ = binary.Write(buf, binary.BigEndian, uint16(79+len(data)+len(server))) + + buf.Write([]byte{0x00, 0x23}) + _ = binary.Write(buf, binary.BigEndian, uint16(len(data))) + buf.Write(data) + + buf.Write([]byte{0x00, 0x00}) + _ = binary.Write(buf, binary.BigEndian, uint16(len(server)+5)) + _ = binary.Write(buf, binary.BigEndian, uint16(len(server)+3)) + buf.WriteByte(0) + _ = binary.Write(buf, binary.BigEndian, uint16(len(server))) + buf.Write([]byte(server)) + + buf.Write([]byte{0x00, 0x0b, 0x00, 0x04, 0x03, 0x01, 0x00, 0x02}) + buf.Write([]byte{0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18}) + buf.Write([]byte{ + 0x00, 0x0d, 0x00, 0x20, 0x00, 0x1e, 0x06, 0x01, 0x06, 0x02, 0x06, 0x03, 0x05, + 0x01, 0x05, 0x02, 0x05, 0x03, 0x04, 0x01, 0x04, 0x02, 0x04, 0x03, 0x03, 0x01, + 0x03, 0x02, 0x03, 0x03, 0x02, 0x01, 0x02, 0x02, 0x02, 0x03, + }) + buf.Write([]byte{0x00, 0x16, 0x00, 0x00}) + buf.Write([]byte{0x00, 0x17, 0x00, 0x00}) + + return buf.Bytes() +} diff --git a/proxy/snell/internal/obfs/tls/common.go b/proxy/snell/internal/obfs/tls/common.go new file mode 100644 index 00000000000..ffa05e33343 --- /dev/null +++ b/proxy/snell/internal/obfs/tls/common.go @@ -0,0 +1,47 @@ +/* + * This file is part of opensnell. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package tls + +import ( + "bytes" + "io" + "net" + "sync" + + p "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/pool" +) + +const chunkSize = 16 * 1024 + +var bufferPool = sync.Pool{New: func() any { return &bytes.Buffer{} }} + +// readBlock reads `skipSize` bytes followed by a 2-byte big-endian length +// and that many payload bytes. When the payload exceeds len(b), the unread +// remainder is returned so the caller can drain it on the next call. +func readBlock(c net.Conn, b []byte, skipSize int) (remain, n int, err error) { + if skipSize > 0 { + buf := p.Get(skipSize) + _, err = io.ReadFull(c, buf) + _ = p.Put(buf) + if err != nil { + return + } + } + + sizeBuf := make([]byte, 2) + if _, err = io.ReadFull(c, sizeBuf); err != nil { + return + } + + length := (int(sizeBuf[0]) << 8) | int(sizeBuf[1]) + if length > len(b) { + n, err = c.Read(b) + remain = length - n + return + } + n, err = io.ReadFull(c, b[:length]) + return +} diff --git a/proxy/snell/internal/pool/alloc.go b/proxy/snell/internal/pool/alloc.go new file mode 100644 index 00000000000..71c96137fed --- /dev/null +++ b/proxy/snell/internal/pool/alloc.go @@ -0,0 +1,59 @@ +package pool + +// Borrowed from https://github.com/Dreamacro/clash/common/pool. + +import ( + "errors" + "math/bits" + "sync" +) + +var defaultAllocator *Allocator + +func init() { + defaultAllocator = NewAllocator() +} + +// Allocator hands out []byte from sync.Pool buckets, keyed by power-of-two +// capacity. Memory fragmentation is bounded at 50% by always returning a +// buffer from the next-larger bucket if the requested size is not exactly +// a power of two. +type Allocator struct { + buffers []sync.Pool +} + +func NewAllocator() *Allocator { + alloc := new(Allocator) + alloc.buffers = make([]sync.Pool, 17) // 1B -> 64K + for k := range alloc.buffers { + i := k + alloc.buffers[k].New = func() any { + return make([]byte, 1< 65536 { + return nil + } + b := msb(size) + if size == 1< 65536 || cap(buf) != 1< 255 { + return errors.New("snell: host name too long") + } + buf := make([]byte, 0, 5+len(host)+2) + buf = append(buf, HeaderVersion) + if reuse { + buf = append(buf, CommandConnectV2) + } else { + buf = append(buf, CommandConnect) + } + buf = append(buf, 0) // empty client ID + buf = append(buf, byte(len(host))) + buf = append(buf, host...) + buf = binary.BigEndian.AppendUint16(buf, port) + _, err := conn.Write(buf) + return err +} + +// WriteUDPHeader emits a UDP-ASSOCIATE request header. v4/v5 only. +func WriteUDPHeader(conn net.Conn) error { + _, err := conn.Write([]byte{HeaderVersion, CommandUDP, 0x00}) + return err +} + +// writeZeroChunk sends an empty payload frame, signaling half-close to the +// peer. +func writeZeroChunk(conn net.Conn) error { + _, err := conn.Write(nil) + return err +} + +// HalfClose sends a zero chunk and resets the reply state so the same Snell +// can be reused for a subsequent request (only valid after a successful +// reuse-mode CONNECT). +func HalfClose(conn net.Conn) error { + if err := writeZeroChunk(conn); err != nil { + return err + } + if s, ok := conn.(*Snell); ok { + s.reply = false + } + return nil +} + +// StreamConn turns a raw transport (TCP, obfs-wrapped TCP, ...) into a +// Snell client connection. The returned value is ready for WriteHeader. +func StreamConn(conn net.Conn, psk []byte) *Snell { + return &Snell{Conn: newV4Conn(conn, psk)} +} + +// ServerStreamConn is StreamConn for the server side: the reply state is +// pre-set so a subsequent Read returns the client's first request bytes. +func ServerStreamConn(conn net.Conn, psk []byte) *Snell { + s := StreamConn(conn, psk) + s.reply = true + return s +} + +// packetFrameWriter is implemented by the v4 transport conn so that UDP +// datagrams can be sent as discrete frames with snell-specific padding. +type packetFrameWriter interface { + WritePacketFrame(b []byte) (int, error) +} + +// WritePacketFrame writes b as a single snell frame on the underlying +// transport, falling back to a plain Write if the transport does not +// support framed writes. +func (s *Snell) WritePacketFrame(b []byte) (int, error) { + if fw, ok := s.Conn.(packetFrameWriter); ok { + return fw.WritePacketFrame(b) + } + return s.Conn.Write(b) +} diff --git a/proxy/snell/internal/snell/pool.go b/proxy/snell/internal/snell/pool.go new file mode 100644 index 00000000000..a6b3f714cdd --- /dev/null +++ b/proxy/snell/internal/snell/pool.go @@ -0,0 +1,176 @@ +/* + * This file is part of opensnell. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package snell + +import ( + "context" + "errors" + "io" + "net" + "sync" + "time" +) + +// Pool is a tiny TCP-connection pool used by the client to reuse upstream +// Snell connections when the server negotiated reuse mode (V2 connect). +// +// Empirically the official Surge snell-server v5.0.1 closes a reuse-mode +// TCP connection after the second session on it (1 fresh CONNECT + 1 +// reuse). Putting back a conn that has already been reused once would +// hand the next caller a soon-to-be-dead socket, so we cap each pooled +// conn at MaxUsesPerConn lifetime sessions and discard beyond that. +type Pool struct { + factory func(ctx context.Context) (*Snell, error) + maxSize int + maxAge time.Duration + maxUsesPerConn int + mu sync.Mutex + items []pooledConn +} + +type pooledConn struct { + conn *Snell + expiresAt time.Time + uses int // number of CONNECT sessions served by this TCP so far +} + +func NewPool(factory func(ctx context.Context) (*Snell, error)) *Pool { + return &Pool{ + factory: factory, + maxSize: 10, + maxAge: 15 * time.Second, + maxUsesPerConn: 2, + } +} + +// GetContext returns an idle pooled connection wrapped so that Close +// returns it to the pool, or dials a fresh one. +func (p *Pool) GetContext(ctx context.Context) (net.Conn, error) { + if item := p.takeIdle(); item != nil { + pc := &PoolConn{Snell: item.conn, pool: p, uses: item.uses + 1} + return pc, nil + } + conn, err := p.factory(ctx) + if err != nil { + return nil, err + } + return &PoolConn{Snell: conn, pool: p, uses: 1}, nil +} + +func (p *Pool) takeIdle() *pooledConn { + p.mu.Lock() + defer p.mu.Unlock() + now := time.Now() + for len(p.items) > 0 { + item := p.items[len(p.items)-1] + p.items = p.items[:len(p.items)-1] + if now.Before(item.expiresAt) { + return &item + } + _ = item.conn.Close() + } + return nil +} + +func (p *Pool) put(conn *Snell, uses int) { + if p.maxUsesPerConn > 0 && uses >= p.maxUsesPerConn { + _ = conn.Close() + return + } + p.mu.Lock() + defer p.mu.Unlock() + if len(p.items) >= p.maxSize { + _ = conn.Close() + return + } + p.items = append(p.items, pooledConn{ + conn: conn, + expiresAt: time.Now().Add(p.maxAge), + uses: uses, + }) +} + +// PoolConn is a checked-out Snell connection that, on Close, gets returned +// to the pool after sending a zero chunk to half-close the upstream +// session. Read maps the snell zero-chunk EOF to io.EOF for the caller's +// convenience. `uses` tracks how many CONNECT sessions this TCP has served +// so far (1 for the first session). +type PoolConn struct { + *Snell + pool *Pool + uses int + closeWriteOnce sync.Once + closeWriteErr error + closeOnce sync.Once + closeErr error +} + +// Read converts the snell zero-chunk half-close signal into io.EOF so +// io.Copy-style relays terminate cleanly instead of spinning on (0, nil). +// Mirrors mihomo's PoolConn behavior. +func (pc *PoolConn) Read(b []byte) (int, error) { + n, err := pc.Snell.Read(b) + if errors.Is(err, ErrZeroChunk) { + return n, io.EOF + } + return n, err +} + +func (pc *PoolConn) Write(b []byte) (int, error) { + return pc.Snell.Write(b) +} + +func (pc *PoolConn) CloseWrite() error { + pc.closeWriteOnce.Do(func() { + pc.closeWriteErr = writeZeroChunk(pc.Snell) + }) + return pc.closeWriteErr +} + +func (pc *PoolConn) Close() error { + pc.closeOnce.Do(func() { + if err := pc.CloseWrite(); err != nil { + pc.closeErr = err + _ = pc.Snell.Close() + return + } + + // If the relay terminated because the SOCKS5 client closed first + // (typical for short HTTP/1 responses), the server may still have + // pending data — most importantly its half-close zero chunk — + // queued in the TCP receive buffer. Reusing this conn without + // draining would surface that stale zero chunk on the next read + // and break the next session. Drain until we observe the + // server's zero chunk or hit a short timeout; only then put back. + if !pc.drainPendingForReuse() { + _ = pc.Snell.Close() + return + } + _ = pc.Snell.Conn.SetReadDeadline(time.Time{}) + pc.Snell.reply = false + pc.pool.put(pc.Snell, pc.uses) + }) + return pc.closeErr +} + +// drainPendingForReuse reads remaining frames from the snell stream until +// the server's zero-chunk half-close is observed, with a short deadline. +// Returns true if the conn is in a clean state suitable for pool reuse. +func (pc *PoolConn) drainPendingForReuse() bool { + if err := pc.Snell.Conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond)); err != nil { + return false + } + scratch := make([]byte, 4096) + for { + _, err := pc.Snell.Read(scratch) + if errors.Is(err, ErrZeroChunk) { + return true + } + if err != nil { + return false + } + } +} diff --git a/proxy/snell/internal/snell/protocol.go b/proxy/snell/internal/snell/protocol.go new file mode 100644 index 00000000000..e4ae376140d --- /dev/null +++ b/proxy/snell/internal/snell/protocol.go @@ -0,0 +1,73 @@ +/* + * This file is part of opensnell. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +// Package snell implements the Snell v4/v5 wire protocol used by Surge. +// +// Only v4 and v5 are supported; v5 currently behaves identically to v4 on +// the wire (the version byte in the request header is 0x01 regardless of +// the AEAD frame "version field" being 4). Older Snell v1/v2/v3 are not +// supported and live in the sibling opensnellv1-2 project. +package snell + +import "errors" + +// Version constants. The byte that appears as the first byte of every +// Snell request header is HeaderVersion, regardless of v4 vs v5. The "4" +// inside each AEAD frame header indicates the AEAD frame format. +const ( + Version4 = 4 + Version5 = 5 + + // HeaderVersion is the byte sent as the first byte of every Snell + // request from the client. It has always been 0x01 since v1. + HeaderVersion byte = 1 +) + +// Commands sent by the client. +const ( + CommandPing byte = 0 + CommandConnect byte = 1 + CommandConnectV2 byte = 5 // reuse-capable TCP connect + CommandUDP byte = 6 +) + +// CommandUDPForward is the inner UDP command byte that appears as the +// first byte of each UDP-over-TCP request frame. +const CommandUDPForward byte = 1 + +// Server response codes. +const ( + ResponseTunnel byte = 0 + ResponsePong byte = 1 + ResponseError byte = 2 +) + +// MaxPayloadLength is the largest snell payload that may appear in a +// single frame (matches mihomo's `maxLength`). +const MaxPayloadLength = 0x3FFF + +// ErrZeroChunk is returned by Read when the peer signaled half-close by +// emitting an empty payload frame. The caller should treat this as EOF. +var ErrZeroChunk = errors.New("snell: zero chunk") + +// AppError is an application-layer error carrying the error code returned +// by the snell peer. +type AppError struct { + Code byte + Message string +} + +func (e *AppError) Error() string { + return e.Message +} + +// NewAppError constructs an AppError. Empty messages get a placeholder so +// the AppError can still be propagated through `errors.As`. +func NewAppError(code byte, msg string) error { + if msg == "" { + msg = "snell app error" + } + return &AppError{Code: code, Message: msg} +} diff --git a/proxy/snell/internal/snell/udp.go b/proxy/snell/internal/snell/udp.go new file mode 100644 index 00000000000..ddd1d39056f --- /dev/null +++ b/proxy/snell/internal/snell/udp.go @@ -0,0 +1,274 @@ +/* + * This file is part of opensnell. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package snell + +import ( + "encoding/binary" + "errors" + "io" + "net" + "net/netip" + "sync" + + "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/socks5" + "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/pool" +) + +// UDPRequest is one parsed UDP-over-TCP request frame coming from the client. +type UDPRequest struct { + Host string + IP netip.Addr + Port uint16 + Payload []byte +} + +// ParseUDPRequest decodes one snell-format UDP request frame. The frame +// payload starts with the UDP forward command byte (0x01) followed by +// either a domain name or an IPv4/IPv6 address. +func ParseUDPRequest(packet []byte) (UDPRequest, error) { + if len(packet) < 2 || packet[0] != CommandUDPForward { + return UDPRequest{}, errors.New("snell: invalid UDP request") + } + if hostLen := int(packet[1]); hostLen != 0 { + if len(packet) <= 2+hostLen+2 { + return UDPRequest{}, errors.New("snell: invalid UDP domain request") + } + offset := 2 + hostLen + return UDPRequest{ + Host: string(packet[2:offset]), + Port: binary.BigEndian.Uint16(packet[offset : offset+2]), + Payload: packet[offset+2:], + }, nil + } + if len(packet) < 3 { + return UDPRequest{}, errors.New("snell: invalid UDP IP request") + } + switch packet[2] { + case 0x04: + if len(packet) < 3+net.IPv4len+2 { + return UDPRequest{}, errors.New("snell: invalid UDP IPv4 request") + } + offset := 3 + net.IPv4len + ip, _ := netip.AddrFromSlice(packet[3:offset]) + return UDPRequest{ + IP: ip.Unmap(), + Port: binary.BigEndian.Uint16(packet[offset : offset+2]), + Payload: packet[offset+2:], + }, nil + case 0x06: + if len(packet) < 3+net.IPv6len+2 { + return UDPRequest{}, errors.New("snell: invalid UDP IPv6 request") + } + offset := 3 + net.IPv6len + ip, _ := netip.AddrFromSlice(packet[3:offset]) + return UDPRequest{ + IP: ip.Unmap(), + Port: binary.BigEndian.Uint16(packet[offset : offset+2]), + Payload: packet[offset+2:], + }, nil + default: + return UDPRequest{}, errors.New("snell: invalid UDP address type") + } +} + +// udpHeaderLength returns the encoded UDP request header length for a +// given SOCKS5 address slice. +func udpHeaderLength(socksAddr []byte) int { + if len(socksAddr) == 0 { + return MaxPayloadLength + 1 + } + switch socksAddr[0] { + case socks5.AtypDomainName: + if len(socksAddr) < 2 { + return MaxPayloadLength + 1 + } + return 1 + 1 + int(socksAddr[1]) + 2 + case socks5.AtypIPv4: + return 1 + 2 + net.IPv4len + 2 + case socks5.AtypIPv6: + return 1 + 2 + net.IPv6len + 2 + default: + return MaxPayloadLength + 1 + } +} + +func writeUDPRequest(w io.Writer, socksAddr, payload []byte) (int, error) { + buf := make([]byte, 0, 1+len(socksAddr)+len(payload)) + buf = append(buf, CommandUDPForward) + switch socksAddr[0] { + case socks5.AtypDomainName: + hostLen := int(socksAddr[1]) + if len(socksAddr) < 1+1+hostLen+2 { + return 0, errors.New("snell UDP address invalid") + } + buf = append(buf, socksAddr[1:1+1+hostLen+2]...) + case socks5.AtypIPv4: + if len(socksAddr) < 1+net.IPv4len+2 { + return 0, errors.New("snell UDP address invalid") + } + buf = append(buf, 0x00, 0x04) + buf = append(buf, socksAddr[1:1+net.IPv4len+2]...) + case socks5.AtypIPv6: + if len(socksAddr) < 1+net.IPv6len+2 { + return 0, errors.New("snell UDP address invalid") + } + buf = append(buf, 0x00, 0x06) + buf = append(buf, socksAddr[1:1+net.IPv6len+2]...) + default: + return 0, errors.New("snell UDP address invalid") + } + buf = append(buf, payload...) + + if fw, ok := w.(packetFrameWriter); ok { + if _, err := fw.WritePacketFrame(buf); err != nil { + return 0, err + } + return len(payload), nil + } + if _, err := w.Write(buf); err != nil { + return 0, err + } + return len(payload), nil +} + +// WritePacket writes one UDP datagram in snell-request format. The +// destination address is encoded inline with the payload. +func WritePacket(w io.Writer, socksAddr, payload []byte) (int, error) { + maxPayload := MaxPayloadLength - udpHeaderLength(socksAddr) + if maxPayload <= 0 { + return 0, errors.New("snell UDP address too large") + } + if len(payload) > maxPayload { + return 0, errors.New("snell UDP payload too large") + } + return writeUDPRequest(w, socksAddr, payload) +} + +// WritePacketResponse writes one UDP datagram in snell-response format +// (server -> client). The response header uses a slightly different +// address encoding than the request header. +func WritePacketResponse(w io.Writer, addr net.Addr, payload []byte) (int, error) { + socksAddr := socks5.ParseAddrToSocksAddr(addr) + if len(socksAddr) == 0 { + return 0, errors.New("snell UDP response address invalid") + } + buf := make([]byte, 0, 1+len(socksAddr)+len(payload)) + switch socksAddr[0] { + case socks5.AtypIPv4: + if len(socksAddr) < 1+net.IPv4len+2 { + return 0, errors.New("snell UDP response address invalid") + } + buf = append(buf, 0x04) + buf = append(buf, socksAddr[1:1+net.IPv4len+2]...) + case socks5.AtypIPv6: + if len(socksAddr) < 1+net.IPv6len+2 { + return 0, errors.New("snell UDP response address invalid") + } + buf = append(buf, 0x06) + buf = append(buf, socksAddr[1:1+net.IPv6len+2]...) + default: + return 0, errors.New("snell UDP response address invalid") + } + buf = append(buf, payload...) + + if fw, ok := w.(packetFrameWriter); ok { + if _, err := fw.WritePacketFrame(buf); err != nil { + return 0, err + } + return len(payload), nil + } + if _, err := w.Write(buf); err != nil { + return 0, err + } + return len(payload), nil +} + +// ReadPacketResponse reads one server->client UDP response frame from r, +// returning the source address embedded in the frame and the payload bytes +// copied into out. +func ReadPacketResponse(r io.Reader, out []byte) (net.Addr, int, error) { + buf := pool.Get(MaxPayloadLength + 1) + defer func() { _ = pool.Put(buf) }() + + n, err := r.Read(buf) + if err != nil { + return nil, 0, err + } + if n < 1 { + return nil, 0, errors.New("insufficient UDP length") + } + + headLen := 1 + switch buf[0] { + case 0x04: + headLen += net.IPv4len + 2 + if n < headLen { + return nil, 0, errors.New("insufficient UDP length") + } + buf[0] = socks5.AtypIPv4 + case 0x06: + headLen += net.IPv6len + 2 + if n < headLen { + return nil, 0, errors.New("insufficient UDP length") + } + buf[0] = socks5.AtypIPv6 + default: + return nil, 0, errors.New("ip version invalid") + } + + addr := socks5.SplitAddr(buf[:headLen]) + if addr == nil { + return nil, 0, errors.New("remote address invalid") + } + uaddr := addr.UDPAddr() + if uaddr == nil { + return nil, 0, errors.New("parse addr error") + } + + length := len(out) + if n-headLen < length { + length = n - headLen + } + copy(out, buf[headLen:headLen+length]) + return uaddr, length, nil +} + +// PacketConn adapts a Snell stream into a net.PacketConn so that the +// client can shuttle UDP datagrams across. +func PacketConn(conn net.Conn) net.PacketConn { + return &packetConn{Conn: conn} +} + +type packetConn struct { + net.Conn + rMux sync.Mutex + wMux sync.Mutex +} + +func (pc *packetConn) WritePacketFrame(b []byte) (int, error) { + if s, ok := pc.Conn.(*Snell); ok { + if fw, ok := s.Conn.(packetFrameWriter); ok { + return fw.WritePacketFrame(b) + } + } + return pc.Conn.Write(b) +} + +func (pc *packetConn) WriteTo(b []byte, addr net.Addr) (int, error) { + pc.wMux.Lock() + defer pc.wMux.Unlock() + return WritePacket(pc, socks5.ParseAddrToSocksAddr(addr), b) +} + +func (pc *packetConn) ReadFrom(b []byte) (int, net.Addr, error) { + pc.rMux.Lock() + defer pc.rMux.Unlock() + addr, n, err := ReadPacketResponse(pc.Conn, b) + if err != nil { + return 0, nil, err + } + return n, addr, nil +} diff --git a/proxy/snell/internal/snell/v4.go b/proxy/snell/internal/snell/v4.go new file mode 100644 index 00000000000..246ea35285c --- /dev/null +++ b/proxy/snell/internal/snell/v4.go @@ -0,0 +1,507 @@ +/* + * This file is part of opensnell. + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Port of mihomo's transport/snell/v4.go. + */ + +package snell + +import ( + "bufio" + "crypto/cipher" + crand "crypto/rand" + "encoding/binary" + "errors" + "io" + "math" + "math/big" + "math/bits" + "net" + "sync" + "time" +) + +const ( + v4SaltSize = 16 + v4NonceSize = 12 + v4HeaderPlainSize = 7 + v4HeaderCipherSize = v4HeaderPlainSize + 16 + v4FrameSize = 1460 + v4InitialPaddingMin = 0x100 + v4InitialPaddingSpan = 0x100 +) + +// v4Conn wraps a TCP-like net.Conn with the Snell v4 AEAD frame format. +// Reader and writer initialize lazily on the first Read/Write — the salt is +// emitted as part of the first written frame and consumed on the first read. +type v4Conn struct { + net.Conn + psk []byte + r *v4Reader + w *v4Writer +} + +func newV4Conn(conn net.Conn, psk []byte) *v4Conn { + return &v4Conn{Conn: conn, psk: psk} +} + +func (c *v4Conn) initReader() error { + salt := make([]byte, v4SaltSize) + if _, err := io.ReadFull(c.Conn, salt); err != nil { + return err + } + aead, err := v4AEAD(c.psk, salt) + if err != nil { + return err + } + // Buffered read on top of the (possibly obfs-wrapped) TCP conn. Each + // snell frame requires two ReadFulls — one for the 23-byte AEAD'd + // header, one for padding+payload — so without buffering we'd do two + // recv() syscalls per frame on the hot path. At ~1.5 KB/frame and + // a 10 MB transfer that's ~14k syscalls vs ~160 with a 64 KB pull. + // Fewer syscalls also lets Linux's delayed-ACK kick in, eliminating + // the empty-ACK pressure we observed against the official server. + c.r = &v4Reader{Reader: bufio.NewReaderSize(c.Conn, 64*1024), aead: aead} + return nil +} + +func (c *v4Conn) initWriter() error { + w, err := newV4Writer(c.Conn, c.psk) + if err != nil { + return err + } + c.w = w + return nil +} + +func (c *v4Conn) Read(b []byte) (int, error) { + if c.r == nil { + if err := c.initReader(); err != nil { + return 0, err + } + } + return c.r.Read(b) +} + +func (c *v4Conn) Write(b []byte) (int, error) { + if c.w == nil { + if err := c.initWriter(); err != nil { + return 0, err + } + } + return c.w.Write(b) +} + +// WritePacketFrame writes b as a single snell frame, used for UDP-over-TCP +// where datagram boundaries must be preserved. +func (c *v4Conn) WritePacketFrame(b []byte) (int, error) { + if len(b) > MaxPayloadLength { + return 0, errors.New("snell v4 frame too large") + } + if c.w == nil { + if err := c.initWriter(); err != nil { + return 0, err + } + } + c.w.mux.Lock() + defer c.w.mux.Unlock() + if err := c.w.writeFrame(b, c.w.nextFramePaddingLength(len(b))); err != nil { + return 0, err + } + return len(b), nil +} + +// WriteTo / ReadFrom give the standard library copy fast paths something +// to work with. +func (c *v4Conn) WriteTo(w io.Writer) (int64, error) { + if c.r == nil { + if err := c.initReader(); err != nil { + return 0, err + } + } + var written int64 + buf := make([]byte, MaxPayloadLength) + for { + n, err := c.r.Read(buf) + if n > 0 { + nw, ew := w.Write(buf[:n]) + written += int64(nw) + if ew != nil { + return written, ew + } + if nw != n { + return written, io.ErrShortWrite + } + } + if err != nil { + if err == io.EOF { + err = nil + } + return written, err + } + } +} + +func (c *v4Conn) ReadFrom(r io.Reader) (int64, error) { + if c.w == nil { + if err := c.initWriter(); err != nil { + return 0, err + } + } + var read int64 + buf := make([]byte, MaxPayloadLength) + for { + n, err := r.Read(buf) + if n > 0 { + read += int64(n) + if _, ew := c.w.Write(buf[:n]); ew != nil { + return read, ew + } + } + if err != nil { + if err == io.EOF { + err = nil + } + return read, err + } + } +} + +func v4AEAD(psk, salt []byte) (cipher.AEAD, error) { + return aesGCM(snellKDF(psk, salt, 16)) +} + +type v4Reader struct { + io.Reader + aead cipher.AEAD + nonce [v4NonceSize]byte + buf []byte + mux sync.Mutex +} + +func (r *v4Reader) Read(b []byte) (int, error) { + r.mux.Lock() + defer r.mux.Unlock() + + if len(r.buf) == 0 { + payload, err := r.readFrame() + if err != nil { + return 0, err + } + r.buf = payload + } + n := copy(b, r.buf) + r.buf = r.buf[n:] + return n, nil +} + +func (r *v4Reader) readFrame() ([]byte, error) { + headerCipher := make([]byte, v4HeaderCipherSize) + if _, err := io.ReadFull(r.Reader, headerCipher); err != nil { + return nil, err + } + + header, err := r.aead.Open(headerCipher[:0], r.nonce[:], headerCipher, nil) + incrementV4Nonce(r.nonce[:]) + if err != nil { + return nil, err + } + if len(header) != v4HeaderPlainSize || header[0] != 4 { + return nil, errors.New("snell v4 invalid frame header") + } + + paddingLength := int(binary.BigEndian.Uint16(header[3:5])) + payloadLength := int(binary.BigEndian.Uint16(header[5:7])) + if payloadLength == 0 { + if paddingLength != 0 { + return nil, errors.New("snell v4 zero chunk with padding") + } + return nil, ErrZeroChunk + } + if payloadLength > MaxPayloadLength || paddingLength > MaxPayloadLength { + return nil, errors.New("snell v4 frame too large") + } + + payloadCipherLength := payloadLength + r.aead.Overhead() + frame := make([]byte, paddingLength+payloadCipherLength) + if _, err := io.ReadFull(r.Reader, frame); err != nil { + return nil, err + } + if paddingLength > 0 { + swapPadding(frame[:paddingLength], frame[paddingLength:]) + } + payloadCipher := frame[paddingLength:] + payload, err := r.aead.Open(payloadCipher[:0], r.nonce[:], payloadCipher, nil) + incrementV4Nonce(r.nonce[:]) + if err != nil { + return nil, err + } + return payload, nil +} + +type v4Writer struct { + io.Writer + aead cipher.AEAD + nonce [v4NonceSize]byte + salt [v4SaltSize]byte + saltSent bool + initialPaddingLength uint16 + payloadLimit uint16 + lastWrite time.Time + mux sync.Mutex +} + +func newV4Writer(w io.Writer, psk []byte) (*v4Writer, error) { + var salt [v4SaltSize]byte + if _, err := io.ReadFull(crand.Reader, salt[:]); err != nil { + return nil, err + } + aead, err := v4AEAD(psk, salt[:]) + if err != nil { + return nil, err + } + paddingDelta, err := cryptoRandomInt(v4InitialPaddingSpan) + if err != nil { + return nil, err + } + return &v4Writer{ + Writer: w, + aead: aead, + salt: salt, + initialPaddingLength: uint16(v4InitialPaddingMin + paddingDelta), + }, nil +} + +func (w *v4Writer) Write(b []byte) (int, error) { + w.mux.Lock() + defer w.mux.Unlock() + + if len(b) == 0 { + return 0, w.writeFrame(nil, 0) + } + + written := 0 + for written < len(b) { + payloadLimit := int(w.nextPayloadLimit()) + if payloadLimit <= 0 || payloadLimit > MaxPayloadLength { + payloadLimit = MaxPayloadLength + } + end := written + payloadLimit + if end > len(b) { + end = len(b) + } + paddingLength := w.nextFramePaddingLength(end - written) + if err := w.writeFrame(b[written:end], paddingLength); err != nil { + return written, err + } + written = end + } + return written, nil +} + +// nextPayloadLimit mirrors the reference implementation's frame-size +// ramp-up: the very first frame is small (to make the salt+padding+payload +// fit in a single MTU), then each subsequent frame in a "burst" can be +// larger until it saturates at MaxPayloadLength. +func (w *v4Writer) nextPayloadLimit() uint16 { + now := time.Now() + var payloadLimit uint16 + switch { + case w.lastWrite.IsZero(): + payloadLimit = v4FrameSize - 55 - w.initialPaddingLength + case now.Sub(w.lastWrite) > 30*time.Second: + payloadLimit = v4FrameSize - 39 + default: + payloadLimit = w.payloadLimit + } + w.lastWrite = now + + if payloadLimit <= MaxPayloadLength-1 { + next := int(payloadLimit) + v4FrameSize - 39 + if next > MaxPayloadLength { + next = MaxPayloadLength + } + w.payloadLimit = uint16(next) + } else { + w.payloadLimit = MaxPayloadLength + } + return payloadLimit +} + +func (w *v4Writer) nextFramePaddingLength(payloadLength int) int { + if w.saltSent || payloadLength == 0 { + return 0 + } + return int(w.initialPaddingLength) +} + +func (w *v4Writer) writeFrame(payload []byte, paddingLength int) error { + if len(payload) > MaxPayloadLength || paddingLength > MaxPayloadLength { + return errors.New("snell v4 frame too large") + } + if len(payload) == 0 && paddingLength != 0 { + return errors.New("snell v4 zero chunk with padding") + } + + header := make([]byte, v4HeaderPlainSize) + header[0] = 4 + binary.BigEndian.PutUint16(header[3:5], uint16(paddingLength)) + binary.BigEndian.PutUint16(header[5:7], uint16(len(payload))) + + headerCipher := w.aead.Seal(nil, w.nonce[:], header, nil) + incrementV4Nonce(w.nonce[:]) + + var payloadCipher []byte + if len(payload) > 0 { + payloadCipher = w.aead.Seal(nil, w.nonce[:], payload, nil) + incrementV4Nonce(w.nonce[:]) + } + + frameLength := len(headerCipher) + paddingLength + len(payloadCipher) + if !w.saltSent { + frameLength += v4SaltSize + } + frame := make([]byte, 0, frameLength) + if !w.saltSent { + frame = append(frame, w.salt[:]...) + w.saltSent = true + } + frame = append(frame, headerCipher...) + if paddingLength > 0 { + padding, err := makeV4Padding(payloadCipher, paddingLength) + if err != nil { + return err + } + swapPadding(padding, payloadCipher) + frame = append(frame, padding...) + } + frame = append(frame, payloadCipher...) + return writeFull(w.Writer, frame) +} + +// swapPadding interleaves the even-indexed bytes of `padding` with those of +// `payloadCipher`. The receiver does the same swap to recover both streams. +func swapPadding(padding, payloadCipher []byte) { + limit := len(padding) + if len(payloadCipher) < limit { + limit = len(payloadCipher) + } + for i := 0; i < limit; i += 2 { + padding[i], payloadCipher[i] = payloadCipher[i], padding[i] + } +} + +// makeV4Padding picks padding bytes such that the bit-count ratio of the +// resulting on-the-wire data stays inside a "natural" band — defeats the +// trivial entropy fingerprint of AEAD-only traffic. +func makeV4Padding(payloadCipher []byte, paddingLength int) ([]byte, error) { + if paddingLength <= 0 { + return nil, nil + } + payloadOnes := countV4PayloadOnes(payloadCipher) + payloadZeros := 8*len(payloadCipher) - payloadOnes + if payloadZeros <= 0 { + return makeV4RandomPadding(paddingLength) + } + + ratio := float64(payloadOnes) / float64(payloadZeros) + if ratio <= 0.5 || ratio >= 1.6 { + return makeV4RandomPadding(paddingLength) + } + + targetRatioBase := 1.6 + if payloadZeros < payloadOnes { + targetRatioBase = 0.4 + } + jitter, err := randomUnitFloat64() + if err != nil { + return nil, err + } + targetRatio := targetRatioBase + jitter/10 + totalBits := 8 * (paddingLength + len(payloadCipher)) + targetOnes := int(float64(totalBits)*(targetRatio/(targetRatio+1)) - float64(payloadOnes)) + if targetOnes < 0 || targetOnes > 8*paddingLength { + return makeV4RandomPadding(paddingLength) + } + return makeV4BitCountPadding(paddingLength, targetOnes) +} + +func countV4PayloadOnes(payloadCipher []byte) int { + limit := len(payloadCipher) &^ 3 + ones := 0 + for _, b := range payloadCipher[:limit] { + ones += bits.OnesCount8(b) + } + return ones +} + +func makeV4RandomPadding(length int) ([]byte, error) { + padding := make([]byte, length) + _, err := io.ReadFull(crand.Reader, padding) + return padding, err +} + +func makeV4BitCountPadding(length, oneBits int) ([]byte, error) { + totalBits := 8 * length + if oneBits < 0 || oneBits > totalBits { + return nil, errors.New("snell v4 invalid padding bit count") + } + bitset := make([]byte, totalBits) + for i := 0; i < oneBits; i++ { + bitset[i] = 1 + } + for i := totalBits - 1; i > 0; i-- { + j, err := cryptoRandomInt(i + 1) + if err != nil { + return nil, err + } + bitset[i], bitset[j] = bitset[j], bitset[i] + } + padding := make([]byte, length) + for i, bit := range bitset { + if bit == 1 { + padding[i/8] |= 1 << uint(i%8) + } + } + return padding, nil +} + +func cryptoRandomInt(max int) (int, error) { + n, err := crand.Int(crand.Reader, big.NewInt(int64(max))) + if err != nil { + return 0, err + } + return int(n.Int64()), nil +} + +func randomUnitFloat64() (float64, error) { + n, err := crand.Int(crand.Reader, big.NewInt(1<<53)) + if err != nil { + return 0, err + } + return float64(n.Int64()) / math.Exp2(53), nil +} + +func writeFull(w io.Writer, p []byte) error { + for len(p) > 0 { + n, err := w.Write(p) + if err != nil { + return err + } + if n == 0 { + return io.ErrShortWrite + } + p = p[n:] + } + return nil +} + +func incrementV4Nonce(nonce []byte) { + for i := range nonce { + nonce[i]++ + if nonce[i] != 0 { + return + } + } +} diff --git a/proxy/snell/internal/socks5/addr.go b/proxy/snell/internal/socks5/addr.go new file mode 100644 index 00000000000..80ddefcba64 --- /dev/null +++ b/proxy/snell/internal/socks5/addr.go @@ -0,0 +1,123 @@ +// Address helpers for Snell UDP framing (subset of SOCKS5 address encoding). +package socks5 + +import ( + "encoding/binary" + "net" + "strconv" +) + +const ( + AtypIPv4 = 1 + AtypDomainName = 3 + AtypIPv6 = 4 + MaxAddrLen = 1 + 1 + 255 + 2 +) + +type Addr []byte + +func (a Addr) UDPAddr() *net.UDPAddr { + if len(a) == 0 { + return nil + } + switch a[0] { + case AtypIPv4: + return &net.UDPAddr{ + IP: net.IP(a[1 : 1+net.IPv4len]), + Port: int(binary.BigEndian.Uint16(a[1+net.IPv4len:])), + } + case AtypIPv6: + return &net.UDPAddr{ + IP: net.IP(a[1 : 1+net.IPv6len]), + Port: int(binary.BigEndian.Uint16(a[1+net.IPv6len:])), + } + } + return nil +} + +func SplitAddr(b []byte) Addr { + addrLen := 1 + if len(b) < addrLen { + return nil + } + switch b[0] { + case AtypDomainName: + if len(b) < 2 { + return nil + } + addrLen = 1 + 1 + int(b[1]) + 2 + case AtypIPv4: + addrLen = 1 + net.IPv4len + 2 + case AtypIPv6: + addrLen = 1 + net.IPv6len + 2 + default: + return nil + } + if len(b) < addrLen { + return nil + } + return b[:addrLen] +} + +func ParseAddr(s string) Addr { + var addr Addr + host, port, err := net.SplitHostPort(s) + if err != nil { + return nil + } + if ip := net.ParseIP(host); ip != nil { + if ip4 := ip.To4(); ip4 != nil { + addr = make([]byte, 1+net.IPv4len+2) + addr[0] = AtypIPv4 + copy(addr[1:], ip4) + } else { + addr = make([]byte, 1+net.IPv6len+2) + addr[0] = AtypIPv6 + copy(addr[1:], ip) + } + } else { + if len(host) > 255 { + return nil + } + addr = make([]byte, 1+1+len(host)+2) + addr[0] = AtypDomainName + addr[1] = byte(len(host)) + copy(addr[2:], host) + } + portnum, err := strconv.ParseUint(port, 10, 16) + if err != nil { + return nil + } + binary.BigEndian.PutUint16(addr[len(addr)-2:], uint16(portnum)) + return addr +} + +func ParseAddrToSocksAddr(addr net.Addr) Addr { + var hostip net.IP + var port int + switch a := addr.(type) { + case *net.UDPAddr: + hostip = a.IP + port = a.Port + case *net.TCPAddr: + hostip = a.IP + port = a.Port + default: + return ParseAddr(addr.String()) + } + if ip4 := hostip.To4(); ip4 != nil { + parsed := make([]byte, 1+net.IPv4len+2) + parsed[0] = AtypIPv4 + copy(parsed[1:], ip4) + binary.BigEndian.PutUint16(parsed[1+net.IPv4len:], uint16(port)) + return parsed + } + if ip6 := hostip.To16(); ip6 != nil { + parsed := make([]byte, 1+net.IPv6len+2) + parsed[0] = AtypIPv6 + copy(parsed[1:], ip6) + binary.BigEndian.PutUint16(parsed[1+net.IPv6len:], uint16(port)) + return parsed + } + return nil +} diff --git a/proxy/snell/outbound.go b/proxy/snell/outbound.go new file mode 100644 index 00000000000..c3708f3359e --- /dev/null +++ b/proxy/snell/outbound.go @@ -0,0 +1,221 @@ +package snell + +import ( + "context" + "net" + "strconv" + "sync" + + snellclient "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/snell" + "github.com/sagernet/sing/common/bufio" + + core "github.com/exclavenetwork/exclave-core/v5" + "github.com/exclavenetwork/exclave-core/v5/app/proxyman/outbound" + "github.com/exclavenetwork/exclave-core/v5/common" + "github.com/exclavenetwork/exclave-core/v5/common/buf" + "github.com/exclavenetwork/exclave-core/v5/common/bytespool" + v2net "github.com/exclavenetwork/exclave-core/v5/common/net" + "github.com/exclavenetwork/exclave-core/v5/common/session" + "github.com/exclavenetwork/exclave-core/v5/common/singbridge" + "github.com/exclavenetwork/exclave-core/v5/proxy" + "github.com/exclavenetwork/exclave-core/v5/transport" + "github.com/exclavenetwork/exclave-core/v5/transport/internet" +) + +func init() { + common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewClient(ctx, config.(*ClientConfig)) + })) +} + +// Outbound implements a Snell v4/v5 client outbound. +type Outbound struct { + serverAddr v2net.Destination + psk string + obfs string + version int + reuse bool + client *snellclient.Client + clientAccess sync.Mutex +} + +func NewClient(ctx context.Context, config *ClientConfig) (*Outbound, error) { + if config.Address == nil { + return nil, newError("missing server address") + } + if config.Psk == "" { + return nil, newError("missing psk") + } + version := int(config.Version) + if version == 0 { + version = 4 + } + if version != 4 && version != 5 { + return nil, newError("unsupported snell version ", version) + } + obfs := config.Obfs + if obfs == "" { + obfs = "off" + } + return &Outbound{ + serverAddr: v2net.Destination{ + Address: config.Address.AsAddress(), + Port: v2net.Port(config.Port), + Network: v2net.Network_TCP, + }, + psk: config.Psk, + obfs: obfs, + version: version, + reuse: config.Reuse, + }, nil +} + +func (o *Outbound) getClient(dialer internet.Dialer) (*snellclient.Client, error) { + o.clientAccess.Lock() + defer o.clientAccess.Unlock() + if o.client != nil { + return o.client, nil + } + handler, ok := dialer.(*outbound.Handler) + if !ok { + panic("dialer is not *outbound.Handler") + } + if handler.MuxEnabled() { + return nil, newError("mux enabled") + } + if handler.TransportLayerEnabled() { + return nil, newError("transport layer enabled") + } + if streamSettings := handler.StreamSettings(); streamSettings != nil && streamSettings.SecurityType != "" { + // Snell carries its own AEAD; stream TLS is not used (obfs=tls is a fake handshake). + return nil, newError("tls/security enabled on streamSettings; snell uses built-in AEAD (use obfs=tls for fake TLS)") + } + + client, err := snellclient.NewClient(snellclient.ClientOptions{ + Server: o.serverAddr.NetAddr(), + PSK: o.psk, + Obfs: o.obfs, + Version: o.version, + Reuse: o.reuse, + Dialer: func(ctx context.Context, network, address string) (net.Conn, error) { + return dialer.Dial(ctx, o.serverAddr) + }, + }) + if err != nil { + return nil, err + } + o.client = client + return client, nil +} + +func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + client, err := o.getClient(dialer) + if err != nil { + return err + } + + ob := session.OutboundFromContext(ctx) + if ob == nil || !ob.Target.IsValid() { + return newError("target not specified") + } + destination := ob.Target + + newError("tunneling request to ", destination, " via snell://", o.serverAddr.NetAddr()).WriteToLog(session.ExportIDToError(ctx)) + + detachedCtx := core.ToBackgroundDetachedContext(ctx) + + if destination.Network == v2net.Network_TCP { + host := destinationAddressHost(destination) + port := uint16(destination.Port) + serverConn, err := client.DialTCP(detachedCtx, host, port) + if err != nil { + return err + } + + var firstPayload []byte + if reader, ok := link.Reader.(buf.TimeoutReader); ok { + if mb, _ := reader.ReadMultiBufferTimeout(proxy.FirstPayloadTimeout); mb != nil { + length := mb.Len() + firstPayload = bytespool.Alloc(length) + mb, _ = buf.SplitBytes(mb, firstPayload) + firstPayload = firstPayload[:length] + buf.ReleaseMulti(mb) + } + } + if len(firstPayload) > 0 { + _, err = serverConn.Write(firstPayload) + bytespool.Free(firstPayload) + if err != nil { + _ = serverConn.Close() + return singbridge.ReturnError(err) + } + } + + return singbridge.ReturnError(bufio.CopyConn(detachedCtx, singbridge.NewPipeConnWrapper(link), serverConn)) + } + + // UDP-over-TCP + pc, err := client.ListenPacket(detachedCtx) + if err != nil { + return err + } + target := destinationToNetAddr(destination) + wrapped := &fixedUDPPacketConn{PacketConn: pc, target: target} + return singbridge.ReturnError(bufio.CopyPacketConn( + detachedCtx, + singbridge.NewPacketConnWrapper(link, destination), + wrapped, + )) +} + +func destinationAddressHost(d v2net.Destination) string { + if d.Address.Family().IsDomain() { + return d.Address.Domain() + } + return d.Address.IP().String() +} + +func destinationToNetAddr(d v2net.Destination) net.Addr { + host := destinationAddressHost(d) + port := int(d.Port) + if d.Address.Family().IsDomain() { + return &domainAddr{host: host, port: port} + } + return &net.UDPAddr{IP: d.Address.IP(), Port: port} +} + +type domainAddr struct { + host string + port int +} + +func (a *domainAddr) Network() string { return "udp" } +func (a *domainAddr) String() string { + return net.JoinHostPort(a.host, strconv.Itoa(a.port)) +} + +type fixedUDPPacketConn struct { + net.PacketConn + target net.Addr +} + +func (c *fixedUDPPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) { + if addr == nil { + addr = c.target + } + return c.PacketConn.WriteTo(p, addr) +} + +func (o *Outbound) InterfaceUpdate() { + _ = o.Close() +} + +func (o *Outbound) Close() error { + o.clientAccess.Lock() + if o.client != nil { + _ = o.client.Close() + o.client = nil + } + o.clientAccess.Unlock() + return nil +} diff --git a/proxy/snell/snell.go b/proxy/snell/snell.go new file mode 100644 index 00000000000..dc671207f68 --- /dev/null +++ b/proxy/snell/snell.go @@ -0,0 +1,3 @@ +package snell + +//go:generate go run github.com/exclavenetwork/exclave-core/v5/common/errors/errorgen From e31e11529ca0c4e9379ccd6279a2b41cc0485bb9 Mon Sep 17 00:00:00 2001 From: Minis <285644962+Minis233@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:13:00 +0800 Subject: [PATCH 02/15] fix(snell): wrap UDP PacketConn with bufio.NewPacketConn --- proxy/snell/outbound.go | 38 ++------------------------------------ 1 file changed, 2 insertions(+), 36 deletions(-) diff --git a/proxy/snell/outbound.go b/proxy/snell/outbound.go index c3708f3359e..43af4471ac0 100644 --- a/proxy/snell/outbound.go +++ b/proxy/snell/outbound.go @@ -3,7 +3,6 @@ package snell import ( "context" "net" - "strconv" "sync" snellclient "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/snell" @@ -154,17 +153,15 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int return singbridge.ReturnError(bufio.CopyConn(detachedCtx, singbridge.NewPipeConnWrapper(link), serverConn)) } - // UDP-over-TCP + // UDP-over-TCP: wrap std net.PacketConn as sing network.PacketConn pc, err := client.ListenPacket(detachedCtx) if err != nil { return err } - target := destinationToNetAddr(destination) - wrapped := &fixedUDPPacketConn{PacketConn: pc, target: target} return singbridge.ReturnError(bufio.CopyPacketConn( detachedCtx, singbridge.NewPacketConnWrapper(link, destination), - wrapped, + bufio.NewPacketConn(pc), )) } @@ -175,37 +172,6 @@ func destinationAddressHost(d v2net.Destination) string { return d.Address.IP().String() } -func destinationToNetAddr(d v2net.Destination) net.Addr { - host := destinationAddressHost(d) - port := int(d.Port) - if d.Address.Family().IsDomain() { - return &domainAddr{host: host, port: port} - } - return &net.UDPAddr{IP: d.Address.IP(), Port: port} -} - -type domainAddr struct { - host string - port int -} - -func (a *domainAddr) Network() string { return "udp" } -func (a *domainAddr) String() string { - return net.JoinHostPort(a.host, strconv.Itoa(a.port)) -} - -type fixedUDPPacketConn struct { - net.PacketConn - target net.Addr -} - -func (c *fixedUDPPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) { - if addr == nil { - addr = c.target - } - return c.PacketConn.WriteTo(p, addr) -} - func (o *Outbound) InterfaceUpdate() { _ = o.Close() } From 671cbf960e1ff2f07466c8e47b792b7f836f0df7 Mon Sep 17 00:00:00 2001 From: Minis <285644962+Minis233@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:18:44 +0800 Subject: [PATCH 03/15] =?UTF-8?q?feat(snell):=20support=20v3=E2=80=93v6=20?= =?UTF-8?q?via=20SagerNet/sing-snell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the OpenSnell-based v4/v5-only stack with sagernet/sing-snell, which implements Snell v6 (ChaCha20-Poly1305 AEAD, deployment-unique framing, default/unshaped/unsafe-raw modes) as well as v3–v5. Config adds obfsHost and mode fields for v6. --- go.mod | 1 + infra/conf/v4/snell.go | 28 +- proxy/snell/NOTICE | 8 +- proxy/snell/config.pb.go | 32 +- proxy/snell/config.proto | 10 +- proxy/snell/internal/obfs/http/client.go | 91 ---- proxy/snell/internal/obfs/obfs.go | 28 -- proxy/snell/internal/obfs/tls/client.go | 152 ------- proxy/snell/internal/obfs/tls/common.go | 47 --- proxy/snell/internal/pool/alloc.go | 59 --- proxy/snell/internal/pool/pool.go | 14 - proxy/snell/internal/snell/cipher.go | 27 -- proxy/snell/internal/snell/client.go | 173 -------- proxy/snell/internal/snell/conn.go | 150 ------- proxy/snell/internal/snell/pool.go | 176 -------- proxy/snell/internal/snell/protocol.go | 73 ---- proxy/snell/internal/snell/udp.go | 274 ------------ proxy/snell/internal/snell/v4.go | 507 ----------------------- proxy/snell/internal/socks5/addr.go | 123 ------ proxy/snell/outbound.go | 124 ++++-- 20 files changed, 138 insertions(+), 1959 deletions(-) delete mode 100644 proxy/snell/internal/obfs/http/client.go delete mode 100644 proxy/snell/internal/obfs/obfs.go delete mode 100644 proxy/snell/internal/obfs/tls/client.go delete mode 100644 proxy/snell/internal/obfs/tls/common.go delete mode 100644 proxy/snell/internal/pool/alloc.go delete mode 100644 proxy/snell/internal/pool/pool.go delete mode 100644 proxy/snell/internal/snell/cipher.go delete mode 100644 proxy/snell/internal/snell/client.go delete mode 100644 proxy/snell/internal/snell/conn.go delete mode 100644 proxy/snell/internal/snell/pool.go delete mode 100644 proxy/snell/internal/snell/protocol.go delete mode 100644 proxy/snell/internal/snell/udp.go delete mode 100644 proxy/snell/internal/snell/v4.go delete mode 100644 proxy/snell/internal/socks5/addr.go diff --git a/go.mod b/go.mod index 21adde4fe29..def784bbed1 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/sagernet/sing-quic v0.6.3 github.com/sagernet/sing-shadowsocks v0.2.9 github.com/sagernet/sing-shadowsocks2 v0.2.2 + github.com/sagernet/sing-snell v0.0.0-20260709045721-90e5a65e1f85 github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 github.com/stretchr/testify v1.11.1 github.com/v2fly/BrowserBridge v0.0.0-20210430233438-0570fc1d7d08 diff --git a/infra/conf/v4/snell.go b/infra/conf/v4/snell.go index a9a6d4938f3..ff66f863c57 100644 --- a/infra/conf/v4/snell.go +++ b/infra/conf/v4/snell.go @@ -8,12 +8,14 @@ import ( ) type SnellClientConfig struct { - Address *cfgcommon.Address `json:"address"` - Port uint16 `json:"port"` - PSK string `json:"psk"` - Obfs string `json:"obfs"` - Version uint32 `json:"version"` - Reuse bool `json:"reuse"` + Address *cfgcommon.Address `json:"address"` + Port uint16 `json:"port"` + PSK string `json:"psk"` + Obfs string `json:"obfs"` + ObfsHost string `json:"obfsHost"` + Version uint32 `json:"version"` + Reuse bool `json:"reuse"` + Mode string `json:"mode"` } func (c *SnellClientConfig) Build() (proto.Message, error) { @@ -28,11 +30,13 @@ func (c *SnellClientConfig) Build() (proto.Message, error) { version = 4 } return &snell.ClientConfig{ - Address: c.Address.Build(), - Port: uint32(c.Port), - Psk: c.PSK, - Obfs: c.Obfs, - Version: version, - Reuse: c.Reuse, + Address: c.Address.Build(), + Port: uint32(c.Port), + Psk: c.PSK, + Obfs: c.Obfs, + ObfsHost: c.ObfsHost, + Version: version, + Reuse: c.Reuse, + Mode: c.Mode, }, nil } diff --git a/proxy/snell/NOTICE b/proxy/snell/NOTICE index 6aea36dde81..d8fd443c22b 100644 --- a/proxy/snell/NOTICE +++ b/proxy/snell/NOTICE @@ -1,5 +1,3 @@ -Snell client protocol implementation adapted from: - https://github.com/missuo/opensnell (GPL-3.0-or-later) -which itself ports address/obfs pieces from Dreamacro/clash and icpz/open-snell. - -Snell is a protocol designed by the Surge team. This is an unofficial client. +Snell outbound uses https://github.com/SagerNet/sing-snell (GPL-3.0) +which implements Snell v3–v6 including official v6 deployment-unique framing. +Snell is a protocol designed by the Surge team. diff --git a/proxy/snell/config.pb.go b/proxy/snell/config.pb.go index 65d92213892..1ffb9f8f5ee 100644 --- a/proxy/snell/config.pb.go +++ b/proxy/snell/config.pb.go @@ -28,12 +28,16 @@ type ClientConfig struct { Address *net.IPOrDomain `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` Psk string `protobuf:"bytes,3,opt,name=psk,proto3" json:"psk,omitempty"` - // "off" | "http" | "tls" + // none | http | tls (also accept off) Obfs string `protobuf:"bytes,4,opt,name=obfs,proto3" json:"obfs,omitempty"` - // 4 or 5 (wire-compatible for TCP / UDP-over-TCP) + // 1-6 (legacy 1-2 disabled in client; 3-5 via snellv4, 6 via snellv6) Version uint32 `protobuf:"varint,5,opt,name=version,proto3" json:"version,omitempty"` - // enable CommandConnectV2 connection reuse - Reuse bool `protobuf:"varint,6,opt,name=reuse,proto3" json:"reuse,omitempty"` + // enable connection reuse (v4+) + Reuse bool `protobuf:"varint,6,opt,name=reuse,proto3" json:"reuse,omitempty"` + // optional obfs host (default bing.com / cloudfront.net per mode) + ObfsHost string `protobuf:"bytes,7,opt,name=obfs_host,json=obfsHost,proto3" json:"obfs_host,omitempty"` + // v6 only: default | unshaped | unsafe-raw + Mode string `protobuf:"bytes,8,opt,name=mode,proto3" json:"mode,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -110,18 +114,34 @@ func (x *ClientConfig) GetReuse() bool { return false } +func (x *ClientConfig) GetObfsHost() string { + if x != nil { + return x.ObfsHost + } + return "" +} + +func (x *ClientConfig) GetMode() string { + if x != nil { + return x.Mode + } + return "" +} + var File_proxy_snell_config_proto protoreflect.FileDescriptor const file_proxy_snell_config_proto_rawDesc = "" + "\n" + - "\x18proxy/snell/config.proto\x12\x18exclave.core.proxy.snell\x1a common/protoext/extensions.proto\x1a\x18common/net/address.proto\"\xce\x01\n" + + "\x18proxy/snell/config.proto\x12\x18exclave.core.proxy.snell\x1a common/protoext/extensions.proto\x1a\x18common/net/address.proto\"\xff\x01\n" + "\fClientConfig\x12=\n" + "\aaddress\x18\x01 \x01(\v2#.exclave.core.common.net.IPOrDomainR\aaddress\x12\x12\n" + "\x04port\x18\x02 \x01(\rR\x04port\x12\x10\n" + "\x03psk\x18\x03 \x01(\tR\x03psk\x12\x12\n" + "\x04obfs\x18\x04 \x01(\tR\x04obfs\x12\x18\n" + "\aversion\x18\x05 \x01(\rR\aversion\x12\x14\n" + - "\x05reuse\x18\x06 \x01(\bR\x05reuse:\x15\x82\xb5\x18\x11\n" + + "\x05reuse\x18\x06 \x01(\bR\x05reuse\x12\x1b\n" + + "\tobfs_host\x18\a \x01(\tR\bobfsHost\x12\x12\n" + + "\x04mode\x18\b \x01(\tR\x04mode:\x15\x82\xb5\x18\x11\n" + "\boutbound\x12\x05snellB\x88\x01\n" + "2com.github.exclavenetwork.exclave.core.proxy.snellP\x01Z5github.com/exclavenetwork/exclave-core/v5/proxy/snell\xaa\x02\x18Exclave.Core.Proxy.Snellb\x06proto3" diff --git a/proxy/snell/config.proto b/proxy/snell/config.proto index d082450a135..b9cfdd32eca 100644 --- a/proxy/snell/config.proto +++ b/proxy/snell/config.proto @@ -16,10 +16,14 @@ message ClientConfig { exclave.core.common.net.IPOrDomain address = 1; uint32 port = 2; string psk = 3; - // "off" | "http" | "tls" + // none | http | tls (also accept off) string obfs = 4; - // 4 or 5 (wire-compatible for TCP / UDP-over-TCP) + // 1-6 (legacy 1-2 disabled in client; 3-5 via snellv4, 6 via snellv6) uint32 version = 5; - // enable CommandConnectV2 connection reuse + // enable connection reuse (v4+) bool reuse = 6; + // optional obfs host (default bing.com / cloudfront.net per mode) + string obfs_host = 7; + // v6 only: default | unshaped | unsafe-raw + string mode = 8; } diff --git a/proxy/snell/internal/obfs/http/client.go b/proxy/snell/internal/obfs/http/client.go deleted file mode 100644 index 711eedbf07b..00000000000 --- a/proxy/snell/internal/obfs/http/client.go +++ /dev/null @@ -1,91 +0,0 @@ -/* - * This file is part of opensnell. - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package http - -import ( - "bufio" - "bytes" - crand "crypto/rand" - "encoding/base64" - "fmt" - "io" - "math/rand/v2" - "net" - "net/http" -) - -type HTTPObfsClient struct { - net.Conn - host string - port string - bio *bufio.Reader - buf []byte - offset int - firstRequest bool - firstResponse bool -} - -func (ho *HTTPObfsClient) Read(b []byte) (int, error) { - if ho.buf != nil { - n := copy(b, ho.buf[ho.offset:]) - ho.offset += n - if ho.offset == len(ho.buf) { - ho.offset = 0 - ho.buf = nil - } - return n, nil - } - - if ho.firstResponse { - bio := bufio.NewReader(ho.Conn) - resp, err := http.ReadResponse(bio, nil) - if err != nil { - return 0, err - } - buf, err := io.ReadAll(resp.Body) - if err != nil { - return 0, err - } - n := copy(b, buf) - if n < len(buf) { - ho.buf = buf - ho.offset = n - } - _ = resp.Body.Close() - ho.bio = bio - ho.firstResponse = false - return n, nil - } - return ho.bio.Read(b) -} - -func (ho *HTTPObfsClient) Write(b []byte) (int, error) { - if ho.firstRequest { - randBytes := make([]byte, 16) - _, _ = crand.Read(randBytes) - req, _ := http.NewRequest("GET", fmt.Sprintf("http://%s/", ho.host), bytes.NewBuffer(b)) - req.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", rand.IntN(54), rand.IntN(2))) - req.Header.Set("Upgrade", "websocket") - req.Header.Set("Connection", "Upgrade") - req.Host = fmt.Sprintf("%s:%s", ho.host, ho.port) - req.Header.Set("Sec-WebSocket-Key", base64.URLEncoding.EncodeToString(randBytes)) - req.ContentLength = int64(len(b)) - err := req.Write(ho.Conn) - ho.firstRequest = false - return len(b), err - } - return ho.Conn.Write(b) -} - -func NewHTTPObfsClient(conn net.Conn, host string, port string) net.Conn { - return &HTTPObfsClient{ - Conn: conn, - firstRequest: true, - firstResponse: true, - host: host, - port: port, - } -} diff --git a/proxy/snell/internal/obfs/obfs.go b/proxy/snell/internal/obfs/obfs.go deleted file mode 100644 index c3fd8fb3153..00000000000 --- a/proxy/snell/internal/obfs/obfs.go +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Based on opensnell (GPL-3.0-or-later). - * Client-side obfuscation wrapper for Snell. - */ -package obfs - -import ( - "fmt" - "net" - - "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/obfs/http" - "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/obfs/tls" -) - -// NewClient wraps conn with the requested obfuscation mode. -// mode: "off"/"none"/"" | "http" | "tls" -func NewClient(conn net.Conn, server, port, mode string) (net.Conn, error) { - switch mode { - case "tls": - return tls.NewTLSObfsClient(conn, server), nil - case "http": - return http.NewHTTPObfsClient(conn, server, port), nil - case "none", "off", "": - return conn, nil - default: - return nil, fmt.Errorf("invalid snell obfs type %q", mode) - } -} diff --git a/proxy/snell/internal/obfs/tls/client.go b/proxy/snell/internal/obfs/tls/client.go deleted file mode 100644 index 8d0a30a2a8d..00000000000 --- a/proxy/snell/internal/obfs/tls/client.go +++ /dev/null @@ -1,152 +0,0 @@ -/* - * This file is part of opensnell. - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package tls - -import ( - "bytes" - "crypto/rand" - "encoding/binary" - "io" - "net" - "time" -) - -type TLSObfsClient struct { - net.Conn - server string - remain int - firstRequest bool - firstResponse bool -} - -func (to *TLSObfsClient) read(b []byte, discardN int) (int, error) { - r, n, err := readBlock(to.Conn, b, discardN) - to.remain = r - return n, err -} - -func (to *TLSObfsClient) Read(b []byte) (int, error) { - if to.remain > 0 { - length := to.remain - if length > len(b) { - length = len(b) - } - n, err := io.ReadFull(to.Conn, b[:length]) - to.remain -= n - return n, err - } - - if to.firstResponse { - // ServerHello + ChangeCipherSpec + Finished framing yields 105 leading - // bytes that the client peer just discards. - to.firstResponse = false - return to.read(b, 105) - } - return to.read(b, 3) -} - -func (to *TLSObfsClient) Write(b []byte) (int, error) { - for i := 0; i < len(b); i += chunkSize { - end := i + chunkSize - if end > len(b) { - end = len(b) - } - if n, err := to.write(b[i:end]); err != nil { - return n, err - } - } - return len(b), nil -} - -func (to *TLSObfsClient) write(b []byte) (int, error) { - if to.firstRequest { - helloMsg := makeClientHelloMsg(b, to.server) - _, err := to.Conn.Write(helloMsg) - to.firstRequest = false - return len(b), err - } - - buf := bufferPool.Get().(*bytes.Buffer) - buf.Reset() - defer bufferPool.Put(buf) - - buf.Write([]byte{0x17, 0x03, 0x03}) - _ = binary.Write(buf, binary.BigEndian, uint16(len(b))) - if _, err := to.Conn.Write(buf.Bytes()); err != nil { - return 0, err - } - return to.Conn.Write(b) -} - -func NewTLSObfsClient(conn net.Conn, server string) net.Conn { - return &TLSObfsClient{ - Conn: conn, - server: server, - firstRequest: true, - firstResponse: true, - } -} - -func makeClientHelloMsg(data []byte, server string) []byte { - random := make([]byte, 28) - sessionID := make([]byte, 32) - _, _ = rand.Read(random) - _, _ = rand.Read(sessionID) - - buf := bufferPool.Get().(*bytes.Buffer) - buf.Reset() - defer bufferPool.Put(buf) - - buf.WriteByte(22) - buf.Write([]byte{0x03, 0x01}) - length := uint16(212 + len(data) + len(server)) - buf.WriteByte(byte(length >> 8)) - buf.WriteByte(byte(length & 0xff)) - - buf.WriteByte(1) - buf.WriteByte(0) - _ = binary.Write(buf, binary.BigEndian, uint16(208+len(data)+len(server))) - buf.Write([]byte{0x03, 0x03}) - - _ = binary.Write(buf, binary.BigEndian, uint32(time.Now().Unix())) - buf.Write(random) - buf.WriteByte(32) - buf.Write(sessionID) - - buf.Write([]byte{0x00, 0x38}) - buf.Write([]byte{ - 0xc0, 0x2c, 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f, - 0x00, 0x9e, 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, 0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a, - 0xc0, 0x14, 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, 0x00, 0x9d, 0x00, 0x9c, 0x00, 0x3d, - 0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0xff, - }) - buf.Write([]byte{0x01, 0x00}) - - _ = binary.Write(buf, binary.BigEndian, uint16(79+len(data)+len(server))) - - buf.Write([]byte{0x00, 0x23}) - _ = binary.Write(buf, binary.BigEndian, uint16(len(data))) - buf.Write(data) - - buf.Write([]byte{0x00, 0x00}) - _ = binary.Write(buf, binary.BigEndian, uint16(len(server)+5)) - _ = binary.Write(buf, binary.BigEndian, uint16(len(server)+3)) - buf.WriteByte(0) - _ = binary.Write(buf, binary.BigEndian, uint16(len(server))) - buf.Write([]byte(server)) - - buf.Write([]byte{0x00, 0x0b, 0x00, 0x04, 0x03, 0x01, 0x00, 0x02}) - buf.Write([]byte{0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18}) - buf.Write([]byte{ - 0x00, 0x0d, 0x00, 0x20, 0x00, 0x1e, 0x06, 0x01, 0x06, 0x02, 0x06, 0x03, 0x05, - 0x01, 0x05, 0x02, 0x05, 0x03, 0x04, 0x01, 0x04, 0x02, 0x04, 0x03, 0x03, 0x01, - 0x03, 0x02, 0x03, 0x03, 0x02, 0x01, 0x02, 0x02, 0x02, 0x03, - }) - buf.Write([]byte{0x00, 0x16, 0x00, 0x00}) - buf.Write([]byte{0x00, 0x17, 0x00, 0x00}) - - return buf.Bytes() -} diff --git a/proxy/snell/internal/obfs/tls/common.go b/proxy/snell/internal/obfs/tls/common.go deleted file mode 100644 index ffa05e33343..00000000000 --- a/proxy/snell/internal/obfs/tls/common.go +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This file is part of opensnell. - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package tls - -import ( - "bytes" - "io" - "net" - "sync" - - p "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/pool" -) - -const chunkSize = 16 * 1024 - -var bufferPool = sync.Pool{New: func() any { return &bytes.Buffer{} }} - -// readBlock reads `skipSize` bytes followed by a 2-byte big-endian length -// and that many payload bytes. When the payload exceeds len(b), the unread -// remainder is returned so the caller can drain it on the next call. -func readBlock(c net.Conn, b []byte, skipSize int) (remain, n int, err error) { - if skipSize > 0 { - buf := p.Get(skipSize) - _, err = io.ReadFull(c, buf) - _ = p.Put(buf) - if err != nil { - return - } - } - - sizeBuf := make([]byte, 2) - if _, err = io.ReadFull(c, sizeBuf); err != nil { - return - } - - length := (int(sizeBuf[0]) << 8) | int(sizeBuf[1]) - if length > len(b) { - n, err = c.Read(b) - remain = length - n - return - } - n, err = io.ReadFull(c, b[:length]) - return -} diff --git a/proxy/snell/internal/pool/alloc.go b/proxy/snell/internal/pool/alloc.go deleted file mode 100644 index 71c96137fed..00000000000 --- a/proxy/snell/internal/pool/alloc.go +++ /dev/null @@ -1,59 +0,0 @@ -package pool - -// Borrowed from https://github.com/Dreamacro/clash/common/pool. - -import ( - "errors" - "math/bits" - "sync" -) - -var defaultAllocator *Allocator - -func init() { - defaultAllocator = NewAllocator() -} - -// Allocator hands out []byte from sync.Pool buckets, keyed by power-of-two -// capacity. Memory fragmentation is bounded at 50% by always returning a -// buffer from the next-larger bucket if the requested size is not exactly -// a power of two. -type Allocator struct { - buffers []sync.Pool -} - -func NewAllocator() *Allocator { - alloc := new(Allocator) - alloc.buffers = make([]sync.Pool, 17) // 1B -> 64K - for k := range alloc.buffers { - i := k - alloc.buffers[k].New = func() any { - return make([]byte, 1< 65536 { - return nil - } - b := msb(size) - if size == 1< 65536 || cap(buf) != 1< 255 { - return errors.New("snell: host name too long") - } - buf := make([]byte, 0, 5+len(host)+2) - buf = append(buf, HeaderVersion) - if reuse { - buf = append(buf, CommandConnectV2) - } else { - buf = append(buf, CommandConnect) - } - buf = append(buf, 0) // empty client ID - buf = append(buf, byte(len(host))) - buf = append(buf, host...) - buf = binary.BigEndian.AppendUint16(buf, port) - _, err := conn.Write(buf) - return err -} - -// WriteUDPHeader emits a UDP-ASSOCIATE request header. v4/v5 only. -func WriteUDPHeader(conn net.Conn) error { - _, err := conn.Write([]byte{HeaderVersion, CommandUDP, 0x00}) - return err -} - -// writeZeroChunk sends an empty payload frame, signaling half-close to the -// peer. -func writeZeroChunk(conn net.Conn) error { - _, err := conn.Write(nil) - return err -} - -// HalfClose sends a zero chunk and resets the reply state so the same Snell -// can be reused for a subsequent request (only valid after a successful -// reuse-mode CONNECT). -func HalfClose(conn net.Conn) error { - if err := writeZeroChunk(conn); err != nil { - return err - } - if s, ok := conn.(*Snell); ok { - s.reply = false - } - return nil -} - -// StreamConn turns a raw transport (TCP, obfs-wrapped TCP, ...) into a -// Snell client connection. The returned value is ready for WriteHeader. -func StreamConn(conn net.Conn, psk []byte) *Snell { - return &Snell{Conn: newV4Conn(conn, psk)} -} - -// ServerStreamConn is StreamConn for the server side: the reply state is -// pre-set so a subsequent Read returns the client's first request bytes. -func ServerStreamConn(conn net.Conn, psk []byte) *Snell { - s := StreamConn(conn, psk) - s.reply = true - return s -} - -// packetFrameWriter is implemented by the v4 transport conn so that UDP -// datagrams can be sent as discrete frames with snell-specific padding. -type packetFrameWriter interface { - WritePacketFrame(b []byte) (int, error) -} - -// WritePacketFrame writes b as a single snell frame on the underlying -// transport, falling back to a plain Write if the transport does not -// support framed writes. -func (s *Snell) WritePacketFrame(b []byte) (int, error) { - if fw, ok := s.Conn.(packetFrameWriter); ok { - return fw.WritePacketFrame(b) - } - return s.Conn.Write(b) -} diff --git a/proxy/snell/internal/snell/pool.go b/proxy/snell/internal/snell/pool.go deleted file mode 100644 index a6b3f714cdd..00000000000 --- a/proxy/snell/internal/snell/pool.go +++ /dev/null @@ -1,176 +0,0 @@ -/* - * This file is part of opensnell. - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package snell - -import ( - "context" - "errors" - "io" - "net" - "sync" - "time" -) - -// Pool is a tiny TCP-connection pool used by the client to reuse upstream -// Snell connections when the server negotiated reuse mode (V2 connect). -// -// Empirically the official Surge snell-server v5.0.1 closes a reuse-mode -// TCP connection after the second session on it (1 fresh CONNECT + 1 -// reuse). Putting back a conn that has already been reused once would -// hand the next caller a soon-to-be-dead socket, so we cap each pooled -// conn at MaxUsesPerConn lifetime sessions and discard beyond that. -type Pool struct { - factory func(ctx context.Context) (*Snell, error) - maxSize int - maxAge time.Duration - maxUsesPerConn int - mu sync.Mutex - items []pooledConn -} - -type pooledConn struct { - conn *Snell - expiresAt time.Time - uses int // number of CONNECT sessions served by this TCP so far -} - -func NewPool(factory func(ctx context.Context) (*Snell, error)) *Pool { - return &Pool{ - factory: factory, - maxSize: 10, - maxAge: 15 * time.Second, - maxUsesPerConn: 2, - } -} - -// GetContext returns an idle pooled connection wrapped so that Close -// returns it to the pool, or dials a fresh one. -func (p *Pool) GetContext(ctx context.Context) (net.Conn, error) { - if item := p.takeIdle(); item != nil { - pc := &PoolConn{Snell: item.conn, pool: p, uses: item.uses + 1} - return pc, nil - } - conn, err := p.factory(ctx) - if err != nil { - return nil, err - } - return &PoolConn{Snell: conn, pool: p, uses: 1}, nil -} - -func (p *Pool) takeIdle() *pooledConn { - p.mu.Lock() - defer p.mu.Unlock() - now := time.Now() - for len(p.items) > 0 { - item := p.items[len(p.items)-1] - p.items = p.items[:len(p.items)-1] - if now.Before(item.expiresAt) { - return &item - } - _ = item.conn.Close() - } - return nil -} - -func (p *Pool) put(conn *Snell, uses int) { - if p.maxUsesPerConn > 0 && uses >= p.maxUsesPerConn { - _ = conn.Close() - return - } - p.mu.Lock() - defer p.mu.Unlock() - if len(p.items) >= p.maxSize { - _ = conn.Close() - return - } - p.items = append(p.items, pooledConn{ - conn: conn, - expiresAt: time.Now().Add(p.maxAge), - uses: uses, - }) -} - -// PoolConn is a checked-out Snell connection that, on Close, gets returned -// to the pool after sending a zero chunk to half-close the upstream -// session. Read maps the snell zero-chunk EOF to io.EOF for the caller's -// convenience. `uses` tracks how many CONNECT sessions this TCP has served -// so far (1 for the first session). -type PoolConn struct { - *Snell - pool *Pool - uses int - closeWriteOnce sync.Once - closeWriteErr error - closeOnce sync.Once - closeErr error -} - -// Read converts the snell zero-chunk half-close signal into io.EOF so -// io.Copy-style relays terminate cleanly instead of spinning on (0, nil). -// Mirrors mihomo's PoolConn behavior. -func (pc *PoolConn) Read(b []byte) (int, error) { - n, err := pc.Snell.Read(b) - if errors.Is(err, ErrZeroChunk) { - return n, io.EOF - } - return n, err -} - -func (pc *PoolConn) Write(b []byte) (int, error) { - return pc.Snell.Write(b) -} - -func (pc *PoolConn) CloseWrite() error { - pc.closeWriteOnce.Do(func() { - pc.closeWriteErr = writeZeroChunk(pc.Snell) - }) - return pc.closeWriteErr -} - -func (pc *PoolConn) Close() error { - pc.closeOnce.Do(func() { - if err := pc.CloseWrite(); err != nil { - pc.closeErr = err - _ = pc.Snell.Close() - return - } - - // If the relay terminated because the SOCKS5 client closed first - // (typical for short HTTP/1 responses), the server may still have - // pending data — most importantly its half-close zero chunk — - // queued in the TCP receive buffer. Reusing this conn without - // draining would surface that stale zero chunk on the next read - // and break the next session. Drain until we observe the - // server's zero chunk or hit a short timeout; only then put back. - if !pc.drainPendingForReuse() { - _ = pc.Snell.Close() - return - } - _ = pc.Snell.Conn.SetReadDeadline(time.Time{}) - pc.Snell.reply = false - pc.pool.put(pc.Snell, pc.uses) - }) - return pc.closeErr -} - -// drainPendingForReuse reads remaining frames from the snell stream until -// the server's zero-chunk half-close is observed, with a short deadline. -// Returns true if the conn is in a clean state suitable for pool reuse. -func (pc *PoolConn) drainPendingForReuse() bool { - if err := pc.Snell.Conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond)); err != nil { - return false - } - scratch := make([]byte, 4096) - for { - _, err := pc.Snell.Read(scratch) - if errors.Is(err, ErrZeroChunk) { - return true - } - if err != nil { - return false - } - } -} diff --git a/proxy/snell/internal/snell/protocol.go b/proxy/snell/internal/snell/protocol.go deleted file mode 100644 index e4ae376140d..00000000000 --- a/proxy/snell/internal/snell/protocol.go +++ /dev/null @@ -1,73 +0,0 @@ -/* - * This file is part of opensnell. - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -// Package snell implements the Snell v4/v5 wire protocol used by Surge. -// -// Only v4 and v5 are supported; v5 currently behaves identically to v4 on -// the wire (the version byte in the request header is 0x01 regardless of -// the AEAD frame "version field" being 4). Older Snell v1/v2/v3 are not -// supported and live in the sibling opensnellv1-2 project. -package snell - -import "errors" - -// Version constants. The byte that appears as the first byte of every -// Snell request header is HeaderVersion, regardless of v4 vs v5. The "4" -// inside each AEAD frame header indicates the AEAD frame format. -const ( - Version4 = 4 - Version5 = 5 - - // HeaderVersion is the byte sent as the first byte of every Snell - // request from the client. It has always been 0x01 since v1. - HeaderVersion byte = 1 -) - -// Commands sent by the client. -const ( - CommandPing byte = 0 - CommandConnect byte = 1 - CommandConnectV2 byte = 5 // reuse-capable TCP connect - CommandUDP byte = 6 -) - -// CommandUDPForward is the inner UDP command byte that appears as the -// first byte of each UDP-over-TCP request frame. -const CommandUDPForward byte = 1 - -// Server response codes. -const ( - ResponseTunnel byte = 0 - ResponsePong byte = 1 - ResponseError byte = 2 -) - -// MaxPayloadLength is the largest snell payload that may appear in a -// single frame (matches mihomo's `maxLength`). -const MaxPayloadLength = 0x3FFF - -// ErrZeroChunk is returned by Read when the peer signaled half-close by -// emitting an empty payload frame. The caller should treat this as EOF. -var ErrZeroChunk = errors.New("snell: zero chunk") - -// AppError is an application-layer error carrying the error code returned -// by the snell peer. -type AppError struct { - Code byte - Message string -} - -func (e *AppError) Error() string { - return e.Message -} - -// NewAppError constructs an AppError. Empty messages get a placeholder so -// the AppError can still be propagated through `errors.As`. -func NewAppError(code byte, msg string) error { - if msg == "" { - msg = "snell app error" - } - return &AppError{Code: code, Message: msg} -} diff --git a/proxy/snell/internal/snell/udp.go b/proxy/snell/internal/snell/udp.go deleted file mode 100644 index ddd1d39056f..00000000000 --- a/proxy/snell/internal/snell/udp.go +++ /dev/null @@ -1,274 +0,0 @@ -/* - * This file is part of opensnell. - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package snell - -import ( - "encoding/binary" - "errors" - "io" - "net" - "net/netip" - "sync" - - "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/socks5" - "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/pool" -) - -// UDPRequest is one parsed UDP-over-TCP request frame coming from the client. -type UDPRequest struct { - Host string - IP netip.Addr - Port uint16 - Payload []byte -} - -// ParseUDPRequest decodes one snell-format UDP request frame. The frame -// payload starts with the UDP forward command byte (0x01) followed by -// either a domain name or an IPv4/IPv6 address. -func ParseUDPRequest(packet []byte) (UDPRequest, error) { - if len(packet) < 2 || packet[0] != CommandUDPForward { - return UDPRequest{}, errors.New("snell: invalid UDP request") - } - if hostLen := int(packet[1]); hostLen != 0 { - if len(packet) <= 2+hostLen+2 { - return UDPRequest{}, errors.New("snell: invalid UDP domain request") - } - offset := 2 + hostLen - return UDPRequest{ - Host: string(packet[2:offset]), - Port: binary.BigEndian.Uint16(packet[offset : offset+2]), - Payload: packet[offset+2:], - }, nil - } - if len(packet) < 3 { - return UDPRequest{}, errors.New("snell: invalid UDP IP request") - } - switch packet[2] { - case 0x04: - if len(packet) < 3+net.IPv4len+2 { - return UDPRequest{}, errors.New("snell: invalid UDP IPv4 request") - } - offset := 3 + net.IPv4len - ip, _ := netip.AddrFromSlice(packet[3:offset]) - return UDPRequest{ - IP: ip.Unmap(), - Port: binary.BigEndian.Uint16(packet[offset : offset+2]), - Payload: packet[offset+2:], - }, nil - case 0x06: - if len(packet) < 3+net.IPv6len+2 { - return UDPRequest{}, errors.New("snell: invalid UDP IPv6 request") - } - offset := 3 + net.IPv6len - ip, _ := netip.AddrFromSlice(packet[3:offset]) - return UDPRequest{ - IP: ip.Unmap(), - Port: binary.BigEndian.Uint16(packet[offset : offset+2]), - Payload: packet[offset+2:], - }, nil - default: - return UDPRequest{}, errors.New("snell: invalid UDP address type") - } -} - -// udpHeaderLength returns the encoded UDP request header length for a -// given SOCKS5 address slice. -func udpHeaderLength(socksAddr []byte) int { - if len(socksAddr) == 0 { - return MaxPayloadLength + 1 - } - switch socksAddr[0] { - case socks5.AtypDomainName: - if len(socksAddr) < 2 { - return MaxPayloadLength + 1 - } - return 1 + 1 + int(socksAddr[1]) + 2 - case socks5.AtypIPv4: - return 1 + 2 + net.IPv4len + 2 - case socks5.AtypIPv6: - return 1 + 2 + net.IPv6len + 2 - default: - return MaxPayloadLength + 1 - } -} - -func writeUDPRequest(w io.Writer, socksAddr, payload []byte) (int, error) { - buf := make([]byte, 0, 1+len(socksAddr)+len(payload)) - buf = append(buf, CommandUDPForward) - switch socksAddr[0] { - case socks5.AtypDomainName: - hostLen := int(socksAddr[1]) - if len(socksAddr) < 1+1+hostLen+2 { - return 0, errors.New("snell UDP address invalid") - } - buf = append(buf, socksAddr[1:1+1+hostLen+2]...) - case socks5.AtypIPv4: - if len(socksAddr) < 1+net.IPv4len+2 { - return 0, errors.New("snell UDP address invalid") - } - buf = append(buf, 0x00, 0x04) - buf = append(buf, socksAddr[1:1+net.IPv4len+2]...) - case socks5.AtypIPv6: - if len(socksAddr) < 1+net.IPv6len+2 { - return 0, errors.New("snell UDP address invalid") - } - buf = append(buf, 0x00, 0x06) - buf = append(buf, socksAddr[1:1+net.IPv6len+2]...) - default: - return 0, errors.New("snell UDP address invalid") - } - buf = append(buf, payload...) - - if fw, ok := w.(packetFrameWriter); ok { - if _, err := fw.WritePacketFrame(buf); err != nil { - return 0, err - } - return len(payload), nil - } - if _, err := w.Write(buf); err != nil { - return 0, err - } - return len(payload), nil -} - -// WritePacket writes one UDP datagram in snell-request format. The -// destination address is encoded inline with the payload. -func WritePacket(w io.Writer, socksAddr, payload []byte) (int, error) { - maxPayload := MaxPayloadLength - udpHeaderLength(socksAddr) - if maxPayload <= 0 { - return 0, errors.New("snell UDP address too large") - } - if len(payload) > maxPayload { - return 0, errors.New("snell UDP payload too large") - } - return writeUDPRequest(w, socksAddr, payload) -} - -// WritePacketResponse writes one UDP datagram in snell-response format -// (server -> client). The response header uses a slightly different -// address encoding than the request header. -func WritePacketResponse(w io.Writer, addr net.Addr, payload []byte) (int, error) { - socksAddr := socks5.ParseAddrToSocksAddr(addr) - if len(socksAddr) == 0 { - return 0, errors.New("snell UDP response address invalid") - } - buf := make([]byte, 0, 1+len(socksAddr)+len(payload)) - switch socksAddr[0] { - case socks5.AtypIPv4: - if len(socksAddr) < 1+net.IPv4len+2 { - return 0, errors.New("snell UDP response address invalid") - } - buf = append(buf, 0x04) - buf = append(buf, socksAddr[1:1+net.IPv4len+2]...) - case socks5.AtypIPv6: - if len(socksAddr) < 1+net.IPv6len+2 { - return 0, errors.New("snell UDP response address invalid") - } - buf = append(buf, 0x06) - buf = append(buf, socksAddr[1:1+net.IPv6len+2]...) - default: - return 0, errors.New("snell UDP response address invalid") - } - buf = append(buf, payload...) - - if fw, ok := w.(packetFrameWriter); ok { - if _, err := fw.WritePacketFrame(buf); err != nil { - return 0, err - } - return len(payload), nil - } - if _, err := w.Write(buf); err != nil { - return 0, err - } - return len(payload), nil -} - -// ReadPacketResponse reads one server->client UDP response frame from r, -// returning the source address embedded in the frame and the payload bytes -// copied into out. -func ReadPacketResponse(r io.Reader, out []byte) (net.Addr, int, error) { - buf := pool.Get(MaxPayloadLength + 1) - defer func() { _ = pool.Put(buf) }() - - n, err := r.Read(buf) - if err != nil { - return nil, 0, err - } - if n < 1 { - return nil, 0, errors.New("insufficient UDP length") - } - - headLen := 1 - switch buf[0] { - case 0x04: - headLen += net.IPv4len + 2 - if n < headLen { - return nil, 0, errors.New("insufficient UDP length") - } - buf[0] = socks5.AtypIPv4 - case 0x06: - headLen += net.IPv6len + 2 - if n < headLen { - return nil, 0, errors.New("insufficient UDP length") - } - buf[0] = socks5.AtypIPv6 - default: - return nil, 0, errors.New("ip version invalid") - } - - addr := socks5.SplitAddr(buf[:headLen]) - if addr == nil { - return nil, 0, errors.New("remote address invalid") - } - uaddr := addr.UDPAddr() - if uaddr == nil { - return nil, 0, errors.New("parse addr error") - } - - length := len(out) - if n-headLen < length { - length = n - headLen - } - copy(out, buf[headLen:headLen+length]) - return uaddr, length, nil -} - -// PacketConn adapts a Snell stream into a net.PacketConn so that the -// client can shuttle UDP datagrams across. -func PacketConn(conn net.Conn) net.PacketConn { - return &packetConn{Conn: conn} -} - -type packetConn struct { - net.Conn - rMux sync.Mutex - wMux sync.Mutex -} - -func (pc *packetConn) WritePacketFrame(b []byte) (int, error) { - if s, ok := pc.Conn.(*Snell); ok { - if fw, ok := s.Conn.(packetFrameWriter); ok { - return fw.WritePacketFrame(b) - } - } - return pc.Conn.Write(b) -} - -func (pc *packetConn) WriteTo(b []byte, addr net.Addr) (int, error) { - pc.wMux.Lock() - defer pc.wMux.Unlock() - return WritePacket(pc, socks5.ParseAddrToSocksAddr(addr), b) -} - -func (pc *packetConn) ReadFrom(b []byte) (int, net.Addr, error) { - pc.rMux.Lock() - defer pc.rMux.Unlock() - addr, n, err := ReadPacketResponse(pc.Conn, b) - if err != nil { - return 0, nil, err - } - return n, addr, nil -} diff --git a/proxy/snell/internal/snell/v4.go b/proxy/snell/internal/snell/v4.go deleted file mode 100644 index 246ea35285c..00000000000 --- a/proxy/snell/internal/snell/v4.go +++ /dev/null @@ -1,507 +0,0 @@ -/* - * This file is part of opensnell. - * SPDX-License-Identifier: GPL-3.0-or-later - * - * Port of mihomo's transport/snell/v4.go. - */ - -package snell - -import ( - "bufio" - "crypto/cipher" - crand "crypto/rand" - "encoding/binary" - "errors" - "io" - "math" - "math/big" - "math/bits" - "net" - "sync" - "time" -) - -const ( - v4SaltSize = 16 - v4NonceSize = 12 - v4HeaderPlainSize = 7 - v4HeaderCipherSize = v4HeaderPlainSize + 16 - v4FrameSize = 1460 - v4InitialPaddingMin = 0x100 - v4InitialPaddingSpan = 0x100 -) - -// v4Conn wraps a TCP-like net.Conn with the Snell v4 AEAD frame format. -// Reader and writer initialize lazily on the first Read/Write — the salt is -// emitted as part of the first written frame and consumed on the first read. -type v4Conn struct { - net.Conn - psk []byte - r *v4Reader - w *v4Writer -} - -func newV4Conn(conn net.Conn, psk []byte) *v4Conn { - return &v4Conn{Conn: conn, psk: psk} -} - -func (c *v4Conn) initReader() error { - salt := make([]byte, v4SaltSize) - if _, err := io.ReadFull(c.Conn, salt); err != nil { - return err - } - aead, err := v4AEAD(c.psk, salt) - if err != nil { - return err - } - // Buffered read on top of the (possibly obfs-wrapped) TCP conn. Each - // snell frame requires two ReadFulls — one for the 23-byte AEAD'd - // header, one for padding+payload — so without buffering we'd do two - // recv() syscalls per frame on the hot path. At ~1.5 KB/frame and - // a 10 MB transfer that's ~14k syscalls vs ~160 with a 64 KB pull. - // Fewer syscalls also lets Linux's delayed-ACK kick in, eliminating - // the empty-ACK pressure we observed against the official server. - c.r = &v4Reader{Reader: bufio.NewReaderSize(c.Conn, 64*1024), aead: aead} - return nil -} - -func (c *v4Conn) initWriter() error { - w, err := newV4Writer(c.Conn, c.psk) - if err != nil { - return err - } - c.w = w - return nil -} - -func (c *v4Conn) Read(b []byte) (int, error) { - if c.r == nil { - if err := c.initReader(); err != nil { - return 0, err - } - } - return c.r.Read(b) -} - -func (c *v4Conn) Write(b []byte) (int, error) { - if c.w == nil { - if err := c.initWriter(); err != nil { - return 0, err - } - } - return c.w.Write(b) -} - -// WritePacketFrame writes b as a single snell frame, used for UDP-over-TCP -// where datagram boundaries must be preserved. -func (c *v4Conn) WritePacketFrame(b []byte) (int, error) { - if len(b) > MaxPayloadLength { - return 0, errors.New("snell v4 frame too large") - } - if c.w == nil { - if err := c.initWriter(); err != nil { - return 0, err - } - } - c.w.mux.Lock() - defer c.w.mux.Unlock() - if err := c.w.writeFrame(b, c.w.nextFramePaddingLength(len(b))); err != nil { - return 0, err - } - return len(b), nil -} - -// WriteTo / ReadFrom give the standard library copy fast paths something -// to work with. -func (c *v4Conn) WriteTo(w io.Writer) (int64, error) { - if c.r == nil { - if err := c.initReader(); err != nil { - return 0, err - } - } - var written int64 - buf := make([]byte, MaxPayloadLength) - for { - n, err := c.r.Read(buf) - if n > 0 { - nw, ew := w.Write(buf[:n]) - written += int64(nw) - if ew != nil { - return written, ew - } - if nw != n { - return written, io.ErrShortWrite - } - } - if err != nil { - if err == io.EOF { - err = nil - } - return written, err - } - } -} - -func (c *v4Conn) ReadFrom(r io.Reader) (int64, error) { - if c.w == nil { - if err := c.initWriter(); err != nil { - return 0, err - } - } - var read int64 - buf := make([]byte, MaxPayloadLength) - for { - n, err := r.Read(buf) - if n > 0 { - read += int64(n) - if _, ew := c.w.Write(buf[:n]); ew != nil { - return read, ew - } - } - if err != nil { - if err == io.EOF { - err = nil - } - return read, err - } - } -} - -func v4AEAD(psk, salt []byte) (cipher.AEAD, error) { - return aesGCM(snellKDF(psk, salt, 16)) -} - -type v4Reader struct { - io.Reader - aead cipher.AEAD - nonce [v4NonceSize]byte - buf []byte - mux sync.Mutex -} - -func (r *v4Reader) Read(b []byte) (int, error) { - r.mux.Lock() - defer r.mux.Unlock() - - if len(r.buf) == 0 { - payload, err := r.readFrame() - if err != nil { - return 0, err - } - r.buf = payload - } - n := copy(b, r.buf) - r.buf = r.buf[n:] - return n, nil -} - -func (r *v4Reader) readFrame() ([]byte, error) { - headerCipher := make([]byte, v4HeaderCipherSize) - if _, err := io.ReadFull(r.Reader, headerCipher); err != nil { - return nil, err - } - - header, err := r.aead.Open(headerCipher[:0], r.nonce[:], headerCipher, nil) - incrementV4Nonce(r.nonce[:]) - if err != nil { - return nil, err - } - if len(header) != v4HeaderPlainSize || header[0] != 4 { - return nil, errors.New("snell v4 invalid frame header") - } - - paddingLength := int(binary.BigEndian.Uint16(header[3:5])) - payloadLength := int(binary.BigEndian.Uint16(header[5:7])) - if payloadLength == 0 { - if paddingLength != 0 { - return nil, errors.New("snell v4 zero chunk with padding") - } - return nil, ErrZeroChunk - } - if payloadLength > MaxPayloadLength || paddingLength > MaxPayloadLength { - return nil, errors.New("snell v4 frame too large") - } - - payloadCipherLength := payloadLength + r.aead.Overhead() - frame := make([]byte, paddingLength+payloadCipherLength) - if _, err := io.ReadFull(r.Reader, frame); err != nil { - return nil, err - } - if paddingLength > 0 { - swapPadding(frame[:paddingLength], frame[paddingLength:]) - } - payloadCipher := frame[paddingLength:] - payload, err := r.aead.Open(payloadCipher[:0], r.nonce[:], payloadCipher, nil) - incrementV4Nonce(r.nonce[:]) - if err != nil { - return nil, err - } - return payload, nil -} - -type v4Writer struct { - io.Writer - aead cipher.AEAD - nonce [v4NonceSize]byte - salt [v4SaltSize]byte - saltSent bool - initialPaddingLength uint16 - payloadLimit uint16 - lastWrite time.Time - mux sync.Mutex -} - -func newV4Writer(w io.Writer, psk []byte) (*v4Writer, error) { - var salt [v4SaltSize]byte - if _, err := io.ReadFull(crand.Reader, salt[:]); err != nil { - return nil, err - } - aead, err := v4AEAD(psk, salt[:]) - if err != nil { - return nil, err - } - paddingDelta, err := cryptoRandomInt(v4InitialPaddingSpan) - if err != nil { - return nil, err - } - return &v4Writer{ - Writer: w, - aead: aead, - salt: salt, - initialPaddingLength: uint16(v4InitialPaddingMin + paddingDelta), - }, nil -} - -func (w *v4Writer) Write(b []byte) (int, error) { - w.mux.Lock() - defer w.mux.Unlock() - - if len(b) == 0 { - return 0, w.writeFrame(nil, 0) - } - - written := 0 - for written < len(b) { - payloadLimit := int(w.nextPayloadLimit()) - if payloadLimit <= 0 || payloadLimit > MaxPayloadLength { - payloadLimit = MaxPayloadLength - } - end := written + payloadLimit - if end > len(b) { - end = len(b) - } - paddingLength := w.nextFramePaddingLength(end - written) - if err := w.writeFrame(b[written:end], paddingLength); err != nil { - return written, err - } - written = end - } - return written, nil -} - -// nextPayloadLimit mirrors the reference implementation's frame-size -// ramp-up: the very first frame is small (to make the salt+padding+payload -// fit in a single MTU), then each subsequent frame in a "burst" can be -// larger until it saturates at MaxPayloadLength. -func (w *v4Writer) nextPayloadLimit() uint16 { - now := time.Now() - var payloadLimit uint16 - switch { - case w.lastWrite.IsZero(): - payloadLimit = v4FrameSize - 55 - w.initialPaddingLength - case now.Sub(w.lastWrite) > 30*time.Second: - payloadLimit = v4FrameSize - 39 - default: - payloadLimit = w.payloadLimit - } - w.lastWrite = now - - if payloadLimit <= MaxPayloadLength-1 { - next := int(payloadLimit) + v4FrameSize - 39 - if next > MaxPayloadLength { - next = MaxPayloadLength - } - w.payloadLimit = uint16(next) - } else { - w.payloadLimit = MaxPayloadLength - } - return payloadLimit -} - -func (w *v4Writer) nextFramePaddingLength(payloadLength int) int { - if w.saltSent || payloadLength == 0 { - return 0 - } - return int(w.initialPaddingLength) -} - -func (w *v4Writer) writeFrame(payload []byte, paddingLength int) error { - if len(payload) > MaxPayloadLength || paddingLength > MaxPayloadLength { - return errors.New("snell v4 frame too large") - } - if len(payload) == 0 && paddingLength != 0 { - return errors.New("snell v4 zero chunk with padding") - } - - header := make([]byte, v4HeaderPlainSize) - header[0] = 4 - binary.BigEndian.PutUint16(header[3:5], uint16(paddingLength)) - binary.BigEndian.PutUint16(header[5:7], uint16(len(payload))) - - headerCipher := w.aead.Seal(nil, w.nonce[:], header, nil) - incrementV4Nonce(w.nonce[:]) - - var payloadCipher []byte - if len(payload) > 0 { - payloadCipher = w.aead.Seal(nil, w.nonce[:], payload, nil) - incrementV4Nonce(w.nonce[:]) - } - - frameLength := len(headerCipher) + paddingLength + len(payloadCipher) - if !w.saltSent { - frameLength += v4SaltSize - } - frame := make([]byte, 0, frameLength) - if !w.saltSent { - frame = append(frame, w.salt[:]...) - w.saltSent = true - } - frame = append(frame, headerCipher...) - if paddingLength > 0 { - padding, err := makeV4Padding(payloadCipher, paddingLength) - if err != nil { - return err - } - swapPadding(padding, payloadCipher) - frame = append(frame, padding...) - } - frame = append(frame, payloadCipher...) - return writeFull(w.Writer, frame) -} - -// swapPadding interleaves the even-indexed bytes of `padding` with those of -// `payloadCipher`. The receiver does the same swap to recover both streams. -func swapPadding(padding, payloadCipher []byte) { - limit := len(padding) - if len(payloadCipher) < limit { - limit = len(payloadCipher) - } - for i := 0; i < limit; i += 2 { - padding[i], payloadCipher[i] = payloadCipher[i], padding[i] - } -} - -// makeV4Padding picks padding bytes such that the bit-count ratio of the -// resulting on-the-wire data stays inside a "natural" band — defeats the -// trivial entropy fingerprint of AEAD-only traffic. -func makeV4Padding(payloadCipher []byte, paddingLength int) ([]byte, error) { - if paddingLength <= 0 { - return nil, nil - } - payloadOnes := countV4PayloadOnes(payloadCipher) - payloadZeros := 8*len(payloadCipher) - payloadOnes - if payloadZeros <= 0 { - return makeV4RandomPadding(paddingLength) - } - - ratio := float64(payloadOnes) / float64(payloadZeros) - if ratio <= 0.5 || ratio >= 1.6 { - return makeV4RandomPadding(paddingLength) - } - - targetRatioBase := 1.6 - if payloadZeros < payloadOnes { - targetRatioBase = 0.4 - } - jitter, err := randomUnitFloat64() - if err != nil { - return nil, err - } - targetRatio := targetRatioBase + jitter/10 - totalBits := 8 * (paddingLength + len(payloadCipher)) - targetOnes := int(float64(totalBits)*(targetRatio/(targetRatio+1)) - float64(payloadOnes)) - if targetOnes < 0 || targetOnes > 8*paddingLength { - return makeV4RandomPadding(paddingLength) - } - return makeV4BitCountPadding(paddingLength, targetOnes) -} - -func countV4PayloadOnes(payloadCipher []byte) int { - limit := len(payloadCipher) &^ 3 - ones := 0 - for _, b := range payloadCipher[:limit] { - ones += bits.OnesCount8(b) - } - return ones -} - -func makeV4RandomPadding(length int) ([]byte, error) { - padding := make([]byte, length) - _, err := io.ReadFull(crand.Reader, padding) - return padding, err -} - -func makeV4BitCountPadding(length, oneBits int) ([]byte, error) { - totalBits := 8 * length - if oneBits < 0 || oneBits > totalBits { - return nil, errors.New("snell v4 invalid padding bit count") - } - bitset := make([]byte, totalBits) - for i := 0; i < oneBits; i++ { - bitset[i] = 1 - } - for i := totalBits - 1; i > 0; i-- { - j, err := cryptoRandomInt(i + 1) - if err != nil { - return nil, err - } - bitset[i], bitset[j] = bitset[j], bitset[i] - } - padding := make([]byte, length) - for i, bit := range bitset { - if bit == 1 { - padding[i/8] |= 1 << uint(i%8) - } - } - return padding, nil -} - -func cryptoRandomInt(max int) (int, error) { - n, err := crand.Int(crand.Reader, big.NewInt(int64(max))) - if err != nil { - return 0, err - } - return int(n.Int64()), nil -} - -func randomUnitFloat64() (float64, error) { - n, err := crand.Int(crand.Reader, big.NewInt(1<<53)) - if err != nil { - return 0, err - } - return float64(n.Int64()) / math.Exp2(53), nil -} - -func writeFull(w io.Writer, p []byte) error { - for len(p) > 0 { - n, err := w.Write(p) - if err != nil { - return err - } - if n == 0 { - return io.ErrShortWrite - } - p = p[n:] - } - return nil -} - -func incrementV4Nonce(nonce []byte) { - for i := range nonce { - nonce[i]++ - if nonce[i] != 0 { - return - } - } -} diff --git a/proxy/snell/internal/socks5/addr.go b/proxy/snell/internal/socks5/addr.go deleted file mode 100644 index 80ddefcba64..00000000000 --- a/proxy/snell/internal/socks5/addr.go +++ /dev/null @@ -1,123 +0,0 @@ -// Address helpers for Snell UDP framing (subset of SOCKS5 address encoding). -package socks5 - -import ( - "encoding/binary" - "net" - "strconv" -) - -const ( - AtypIPv4 = 1 - AtypDomainName = 3 - AtypIPv6 = 4 - MaxAddrLen = 1 + 1 + 255 + 2 -) - -type Addr []byte - -func (a Addr) UDPAddr() *net.UDPAddr { - if len(a) == 0 { - return nil - } - switch a[0] { - case AtypIPv4: - return &net.UDPAddr{ - IP: net.IP(a[1 : 1+net.IPv4len]), - Port: int(binary.BigEndian.Uint16(a[1+net.IPv4len:])), - } - case AtypIPv6: - return &net.UDPAddr{ - IP: net.IP(a[1 : 1+net.IPv6len]), - Port: int(binary.BigEndian.Uint16(a[1+net.IPv6len:])), - } - } - return nil -} - -func SplitAddr(b []byte) Addr { - addrLen := 1 - if len(b) < addrLen { - return nil - } - switch b[0] { - case AtypDomainName: - if len(b) < 2 { - return nil - } - addrLen = 1 + 1 + int(b[1]) + 2 - case AtypIPv4: - addrLen = 1 + net.IPv4len + 2 - case AtypIPv6: - addrLen = 1 + net.IPv6len + 2 - default: - return nil - } - if len(b) < addrLen { - return nil - } - return b[:addrLen] -} - -func ParseAddr(s string) Addr { - var addr Addr - host, port, err := net.SplitHostPort(s) - if err != nil { - return nil - } - if ip := net.ParseIP(host); ip != nil { - if ip4 := ip.To4(); ip4 != nil { - addr = make([]byte, 1+net.IPv4len+2) - addr[0] = AtypIPv4 - copy(addr[1:], ip4) - } else { - addr = make([]byte, 1+net.IPv6len+2) - addr[0] = AtypIPv6 - copy(addr[1:], ip) - } - } else { - if len(host) > 255 { - return nil - } - addr = make([]byte, 1+1+len(host)+2) - addr[0] = AtypDomainName - addr[1] = byte(len(host)) - copy(addr[2:], host) - } - portnum, err := strconv.ParseUint(port, 10, 16) - if err != nil { - return nil - } - binary.BigEndian.PutUint16(addr[len(addr)-2:], uint16(portnum)) - return addr -} - -func ParseAddrToSocksAddr(addr net.Addr) Addr { - var hostip net.IP - var port int - switch a := addr.(type) { - case *net.UDPAddr: - hostip = a.IP - port = a.Port - case *net.TCPAddr: - hostip = a.IP - port = a.Port - default: - return ParseAddr(addr.String()) - } - if ip4 := hostip.To4(); ip4 != nil { - parsed := make([]byte, 1+net.IPv4len+2) - parsed[0] = AtypIPv4 - copy(parsed[1:], ip4) - binary.BigEndian.PutUint16(parsed[1+net.IPv4len:], uint16(port)) - return parsed - } - if ip6 := hostip.To16(); ip6 != nil { - parsed := make([]byte, 1+net.IPv6len+2) - parsed[0] = AtypIPv6 - copy(parsed[1:], ip6) - binary.BigEndian.PutUint16(parsed[1+net.IPv6len:], uint16(port)) - return parsed - } - return nil -} diff --git a/proxy/snell/outbound.go b/proxy/snell/outbound.go index 43af4471ac0..310af2d4187 100644 --- a/proxy/snell/outbound.go +++ b/proxy/snell/outbound.go @@ -3,10 +3,15 @@ package snell import ( "context" "net" + "strings" "sync" - snellclient "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/snell" + "github.com/sagernet/sing-snell" + "github.com/sagernet/sing-snell/snellv4" + "github.com/sagernet/sing-snell/snellv6" "github.com/sagernet/sing/common/bufio" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" core "github.com/exclavenetwork/exclave-core/v5" "github.com/exclavenetwork/exclave-core/v5/app/proxyman/outbound" @@ -27,14 +32,23 @@ func init() { })) } -// Outbound implements a Snell v4/v5 client outbound. +// snellClient is the subset used by the outbound for v4 and v6 clients. +type snellClient interface { + DialContext(ctx context.Context, destination M.Socksaddr) (net.Conn, error) + ListenPacket(ctx context.Context) (N.NetPacketConn, error) + Close() error +} + +// Outbound implements Snell v3–v6 client outbound (via SagerNet sing-snell). type Outbound struct { serverAddr v2net.Destination psk string obfs string + obfsHost string version int reuse bool - client *snellclient.Client + mode string + client snellClient clientAccess sync.Mutex } @@ -49,12 +63,16 @@ func NewClient(ctx context.Context, config *ClientConfig) (*Outbound, error) { if version == 0 { version = 4 } - if version != 4 && version != 5 { - return nil, newError("unsupported snell version ", version) + if version < 3 || version > 6 { + return nil, newError("unsupported snell version ", version, " (want 3-6)") } - obfs := config.Obfs - if obfs == "" { - obfs = "off" + obfs := strings.ToLower(config.Obfs) + switch obfs { + case "", "off", "none": + obfs = "none" + case "http", "tls": + default: + return nil, newError("invalid snell obfs ", config.Obfs) } return &Outbound{ serverAddr: v2net.Destination{ @@ -62,14 +80,16 @@ func NewClient(ctx context.Context, config *ClientConfig) (*Outbound, error) { Port: v2net.Port(config.Port), Network: v2net.Network_TCP, }, - psk: config.Psk, - obfs: obfs, - version: version, - reuse: config.Reuse, + psk: config.Psk, + obfs: obfs, + obfsHost: config.ObfsHost, + version: version, + reuse: config.Reuse, + mode: config.Mode, }, nil } -func (o *Outbound) getClient(dialer internet.Dialer) (*snellclient.Client, error) { +func (o *Outbound) getClient(dialer internet.Dialer) (snellClient, error) { o.clientAccess.Lock() defer o.clientAccess.Unlock() if o.client != nil { @@ -86,23 +106,57 @@ func (o *Outbound) getClient(dialer internet.Dialer) (*snellclient.Client, error return nil, newError("transport layer enabled") } if streamSettings := handler.StreamSettings(); streamSettings != nil && streamSettings.SecurityType != "" { - // Snell carries its own AEAD; stream TLS is not used (obfs=tls is a fake handshake). - return nil, newError("tls/security enabled on streamSettings; snell uses built-in AEAD (use obfs=tls for fake TLS)") - } - - client, err := snellclient.NewClient(snellclient.ClientOptions{ - Server: o.serverAddr.NetAddr(), - PSK: o.psk, - Obfs: o.obfs, - Version: o.version, - Reuse: o.reuse, - Dialer: func(ctx context.Context, network, address string) (net.Conn, error) { - return dialer.Dial(ctx, o.serverAddr) - }, - }) + return nil, newError("tls/security enabled on streamSettings; snell uses built-in crypto (use obfs=tls for fake TLS)") + } + + obfsMode, err := snell.ParseObfsMode(o.obfs) if err != nil { return nil, err } + obfsHost := o.obfsHost + if obfsHost == "" { + switch obfsMode { + case snell.ObfsModeTLS: + obfsHost = snell.DefaultTLSObfsHost + default: + obfsHost = snell.DefaultObfsHost + } + } + obfsCfg := snell.ObfsConfig{Mode: obfsMode, Host: obfsHost} + sdialer := singbridge.NewDialerWrapper(dialer) + server := M.ParseSocksaddr(o.serverAddr.NetAddr()) + psk := []byte(o.psk) + + var client snellClient + if o.version == 6 { + mode, err := snellv6.ParseMode(o.mode) + if err != nil { + return nil, err + } + client, err = snellv6.NewClient(snellv6.ClientOptions{ + Dialer: sdialer, + Server: server, + PSK: psk, + Obfs: obfsCfg, + Mode: mode, + Reuse: o.reuse, + }) + if err != nil { + return nil, err + } + } else { + client, err = snellv4.NewClient(snellv4.ClientOptions{ + Dialer: sdialer, + Server: server, + PSK: psk, + Version: o.version, + Obfs: obfsCfg, + Reuse: o.reuse, + }) + if err != nil { + return nil, err + } + } o.client = client return client, nil } @@ -119,14 +173,14 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int } destination := ob.Target - newError("tunneling request to ", destination, " via snell://", o.serverAddr.NetAddr()).WriteToLog(session.ExportIDToError(ctx)) + newError("tunneling request to ", destination, " via snell://", o.serverAddr.NetAddr(), + " (v", o.version, ")").WriteToLog(session.ExportIDToError(ctx)) detachedCtx := core.ToBackgroundDetachedContext(ctx) + target := singbridge.ToSocksaddr(destination) if destination.Network == v2net.Network_TCP { - host := destinationAddressHost(destination) - port := uint16(destination.Port) - serverConn, err := client.DialTCP(detachedCtx, host, port) + serverConn, err := client.DialContext(detachedCtx, target) if err != nil { return err } @@ -153,7 +207,6 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int return singbridge.ReturnError(bufio.CopyConn(detachedCtx, singbridge.NewPipeConnWrapper(link), serverConn)) } - // UDP-over-TCP: wrap std net.PacketConn as sing network.PacketConn pc, err := client.ListenPacket(detachedCtx) if err != nil { return err @@ -165,13 +218,6 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int )) } -func destinationAddressHost(d v2net.Destination) string { - if d.Address.Family().IsDomain() { - return d.Address.Domain() - } - return d.Address.IP().String() -} - func (o *Outbound) InterfaceUpdate() { _ = o.Close() } From c324014fab6f4727a950bc21aa7b1b0331f85287 Mon Sep 17 00:00:00 2001 From: Minis <285644962+Minis233@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:18:10 +0800 Subject: [PATCH 04/15] fix(snell): vendor sing-snell and pin sing v0.8.11 Remote sagernet/sing-snell pulled sing v0.8.12 which conflicts with exclave-core's utls (HandshakeTimeout). Vendor sing-snell under proxy/snell/sing-snell and require sing v0.8.11 for a consistent build. --- go.mod | 4 +- proxy/snell/NOTICE | 6 +- .../sing-snell/.github/update_dependencies.sh | 5 + .../sing-snell/.github/workflows/lint.yml | 30 + .../sing-snell/.github/workflows/test.yml | 34 + proxy/snell/sing-snell/.gitignore | 6 + proxy/snell/sing-snell/LICENSE | 14 + proxy/snell/sing-snell/Makefile | 18 + proxy/snell/sing-snell/README.md | 17 + proxy/snell/sing-snell/address.go | 97 ++ proxy/snell/sing-snell/crypto.go | 31 + proxy/snell/sing-snell/go.mod | 10 + proxy/snell/sing-snell/go.sum | 14 + .../snell/sing-snell/internal/reuse/reuse.go | 274 ++++++ proxy/snell/sing-snell/obfs.go | 685 ++++++++++++++ proxy/snell/sing-snell/request.go | 97 ++ proxy/snell/sing-snell/snell.go | 83 ++ proxy/snell/sing-snell/snellv4/client.go | 333 +++++++ proxy/snell/sing-snell/snellv4/packet.go | 272 ++++++ proxy/snell/sing-snell/snellv4/record.go | 830 +++++++++++++++++ proxy/snell/sing-snell/snellv4/reuse.go | 462 ++++++++++ proxy/snell/sing-snell/snellv5/packet.go | 289 ++++++ proxy/snell/sing-snell/snellv5/record.go | 790 ++++++++++++++++ proxy/snell/sing-snell/snellv5/reuse.go | 482 ++++++++++ proxy/snell/sing-snell/snellv5/server.go | 695 ++++++++++++++ proxy/snell/sing-snell/snellv6/client.go | 359 ++++++++ proxy/snell/sing-snell/snellv6/handshake.go | 216 +++++ proxy/snell/sing-snell/snellv6/mixing.go | 54 ++ proxy/snell/sing-snell/snellv6/mode.go | 41 + proxy/snell/sing-snell/snellv6/packet.go | 479 ++++++++++ proxy/snell/sing-snell/snellv6/profile.go | 522 +++++++++++ proxy/snell/sing-snell/snellv6/record.go | 642 +++++++++++++ proxy/snell/sing-snell/snellv6/reuse.go | 853 ++++++++++++++++++ proxy/snell/sing-snell/snellv6/salt.go | 56 ++ proxy/snell/sing-snell/snellv6/server.go | 460 ++++++++++ proxy/snell/sing-snell/snellv6/shaped.go | 489 ++++++++++ proxy/snell/sing-snell/test/docker_test.go | 210 +++++ proxy/snell/sing-snell/test/go.mod | 46 + proxy/snell/sing-snell/test/go.sum | 112 +++ proxy/snell/sing-snell/test/helper_test.go | 53 ++ proxy/snell/sing-snell/test/loopback_test.go | 44 + .../sing-snell/test/reuse_helpers_test.go | 297 ++++++ .../sing-snell/test/tls_obfs_loopback_test.go | 240 +++++ proxy/snell/sing-snell/test/v4_test.go | 369 ++++++++ proxy/snell/sing-snell/test/v5_test.go | 610 +++++++++++++ proxy/snell/sing-snell/test/v6_test.go | 415 +++++++++ 46 files changed, 12141 insertions(+), 4 deletions(-) create mode 100644 proxy/snell/sing-snell/.github/update_dependencies.sh create mode 100644 proxy/snell/sing-snell/.github/workflows/lint.yml create mode 100644 proxy/snell/sing-snell/.github/workflows/test.yml create mode 100644 proxy/snell/sing-snell/.gitignore create mode 100644 proxy/snell/sing-snell/LICENSE create mode 100644 proxy/snell/sing-snell/Makefile create mode 100644 proxy/snell/sing-snell/README.md create mode 100644 proxy/snell/sing-snell/address.go create mode 100644 proxy/snell/sing-snell/crypto.go create mode 100644 proxy/snell/sing-snell/go.mod create mode 100644 proxy/snell/sing-snell/go.sum create mode 100644 proxy/snell/sing-snell/internal/reuse/reuse.go create mode 100644 proxy/snell/sing-snell/obfs.go create mode 100644 proxy/snell/sing-snell/request.go create mode 100644 proxy/snell/sing-snell/snell.go create mode 100644 proxy/snell/sing-snell/snellv4/client.go create mode 100644 proxy/snell/sing-snell/snellv4/packet.go create mode 100644 proxy/snell/sing-snell/snellv4/record.go create mode 100644 proxy/snell/sing-snell/snellv4/reuse.go create mode 100644 proxy/snell/sing-snell/snellv5/packet.go create mode 100644 proxy/snell/sing-snell/snellv5/record.go create mode 100644 proxy/snell/sing-snell/snellv5/reuse.go create mode 100644 proxy/snell/sing-snell/snellv5/server.go create mode 100644 proxy/snell/sing-snell/snellv6/client.go create mode 100644 proxy/snell/sing-snell/snellv6/handshake.go create mode 100644 proxy/snell/sing-snell/snellv6/mixing.go create mode 100644 proxy/snell/sing-snell/snellv6/mode.go create mode 100644 proxy/snell/sing-snell/snellv6/packet.go create mode 100644 proxy/snell/sing-snell/snellv6/profile.go create mode 100644 proxy/snell/sing-snell/snellv6/record.go create mode 100644 proxy/snell/sing-snell/snellv6/reuse.go create mode 100644 proxy/snell/sing-snell/snellv6/salt.go create mode 100644 proxy/snell/sing-snell/snellv6/server.go create mode 100644 proxy/snell/sing-snell/snellv6/shaped.go create mode 100644 proxy/snell/sing-snell/test/docker_test.go create mode 100644 proxy/snell/sing-snell/test/go.mod create mode 100644 proxy/snell/sing-snell/test/go.sum create mode 100644 proxy/snell/sing-snell/test/helper_test.go create mode 100644 proxy/snell/sing-snell/test/loopback_test.go create mode 100644 proxy/snell/sing-snell/test/reuse_helpers_test.go create mode 100644 proxy/snell/sing-snell/test/tls_obfs_loopback_test.go create mode 100644 proxy/snell/sing-snell/test/v4_test.go create mode 100644 proxy/snell/sing-snell/test/v5_test.go create mode 100644 proxy/snell/sing-snell/test/v6_test.go diff --git a/go.mod b/go.mod index def784bbed1..e65b3060abe 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/sagernet/sing-quic v0.6.3 github.com/sagernet/sing-shadowsocks v0.2.9 github.com/sagernet/sing-shadowsocks2 v0.2.2 - github.com/sagernet/sing-snell v0.0.0-20260709045721-90e5a65e1f85 + github.com/sagernet/sing-snell v0.0.0 github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 github.com/stretchr/testify v1.11.1 github.com/v2fly/BrowserBridge v0.0.0-20210430233438-0570fc1d7d08 @@ -72,3 +72,5 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/sagernet/sing-snell => ./proxy/snell/sing-snell diff --git a/proxy/snell/NOTICE b/proxy/snell/NOTICE index d8fd443c22b..6d68a8b697e 100644 --- a/proxy/snell/NOTICE +++ b/proxy/snell/NOTICE @@ -1,3 +1,3 @@ -Snell outbound uses https://github.com/SagerNet/sing-snell (GPL-3.0) -which implements Snell v3–v6 including official v6 deployment-unique framing. -Snell is a protocol designed by the Surge team. +Snell outbound vendors https://github.com/SagerNet/sing-snell (GPL-3.0) +with sing dependency pinned to v0.8.11 for exclave-core compatibility. +Supports Snell v3–v6. diff --git a/proxy/snell/sing-snell/.github/update_dependencies.sh b/proxy/snell/sing-snell/.github/update_dependencies.sh new file mode 100644 index 00000000000..4702ddfe018 --- /dev/null +++ b/proxy/snell/sing-snell/.github/update_dependencies.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +PROJECTS=$(dirname "$0")/../.. +go get -x github.com/sagernet/$1@$(git -C $PROJECTS/$1 rev-parse HEAD) +go mod tidy diff --git a/proxy/snell/sing-snell/.github/workflows/lint.yml b/proxy/snell/sing-snell/.github/workflows/lint.yml new file mode 100644 index 00000000000..87b6f104cf7 --- /dev/null +++ b/proxy/snell/sing-snell/.github/workflows/lint.yml @@ -0,0 +1,30 @@ +name: lint + +on: + pull_request: + push: + workflow_dispatch: + +permissions: + contents: read + +jobs: + lint: + name: lint + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version: '1.26' + cache: true + + - name: Install golangci-lint + run: make lint_install + + - name: Run golangci-lint + run: make lint diff --git a/proxy/snell/sing-snell/.github/workflows/test.yml b/proxy/snell/sing-snell/.github/workflows/test.yml new file mode 100644 index 00000000000..4396d2593d2 --- /dev/null +++ b/proxy/snell/sing-snell/.github/workflows/test.yml @@ -0,0 +1,34 @@ +name: test + +on: + pull_request: + push: + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + name: test + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + go-version: + - '~1.26' + - '~1.25' + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version: ${{ matrix.go-version }} + cache: true + + - name: Run unit tests + run: make test \ No newline at end of file diff --git a/proxy/snell/sing-snell/.gitignore b/proxy/snell/sing-snell/.gitignore new file mode 100644 index 00000000000..89444706f85 --- /dev/null +++ b/proxy/snell/sing-snell/.gitignore @@ -0,0 +1,6 @@ +/.idea/ +/vendor/ +.DS_Store +/CLAUDE.md +/.claude/ +/testenv/ diff --git a/proxy/snell/sing-snell/LICENSE b/proxy/snell/sing-snell/LICENSE new file mode 100644 index 00000000000..3e3e29e359d --- /dev/null +++ b/proxy/snell/sing-snell/LICENSE @@ -0,0 +1,14 @@ +Copyright (C) 2022 by nekohasekai + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . \ No newline at end of file diff --git a/proxy/snell/sing-snell/Makefile b/proxy/snell/sing-snell/Makefile new file mode 100644 index 00000000000..fe99adeab1b --- /dev/null +++ b/proxy/snell/sing-snell/Makefile @@ -0,0 +1,18 @@ +.PHONY: fmt lint lint_install test + +fmt: + @golangci-lint fmt + +lint: + GOOS=linux golangci-lint run ./... + GOOS=android golangci-lint run ./... + GOOS=windows golangci-lint run ./... + GOOS=darwin golangci-lint run ./... +# GOOS=freebsd golangci-lint run ./... + +lint_install: + go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest + +test: + go test ./... + go -C test test -v -count=1 -timeout 15m ./... diff --git a/proxy/snell/sing-snell/README.md b/proxy/snell/sing-snell/README.md new file mode 100644 index 00000000000..76f3efcdff7 --- /dev/null +++ b/proxy/snell/sing-snell/README.md @@ -0,0 +1,17 @@ +# sing-snell + +[Surge snell](https://kb.nssurge.com/surge-knowledge-base/release-notes/snell) implemented in Go. + +## Features + +* All complete features except the v5 QUIC proxy. +* Behavior as consistent with the official implementation as possible. +* Performance at least on par with the official implementation. + +## Why + +Surge believes that being closed-source and not proliferated can keep this protocol +covert, but this is already impossible in 2026. Considering that snell still has +advantages that other random-traffic protocols do not possess, such as multiplexing +support with complete TCP semantics and traffic-characteristic diversity, we made this +instead of reinventing the wheel. diff --git a/proxy/snell/sing-snell/address.go b/proxy/snell/sing-snell/address.go new file mode 100644 index 00000000000..1a93fb74349 --- /dev/null +++ b/proxy/snell/sing-snell/address.go @@ -0,0 +1,97 @@ +package snell + +import ( + "encoding/binary" + "io" + + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" +) + +var udpIPSerializer = M.NewSerializer( + M.AddressFamilyByte(AddressTypeIPv4, M.AddressFamilyIPv4), + M.AddressFamilyByte(AddressTypeIPv6, M.AddressFamilyIPv6), +) + +// Surge 6.7.0 (11520): SNConnectorV4::targetHandshakeData: CONNECT targets are host strings even for literal IPs. +func WriteConnectAddress(buffer *buf.Buffer, destination M.Socksaddr) error { + host := destination.AddrString() + if len(host) > 255 { + return E.New("snell: host too long: ", host) + } + err := M.WriteSocksString(buffer, host) + if err != nil { + return err + } + binary.BigEndian.PutUint16(buffer.Extend(2), destination.Port) + return nil +} + +func ReadConnectAddress(reader io.Reader) (M.Socksaddr, error) { + host, err := M.ReadSockString(reader) + if err != nil { + return M.Socksaddr{}, err + } + var portBytes [2]byte + _, err = io.ReadFull(reader, portBytes[:]) + if err != nil { + return M.Socksaddr{}, err + } + return M.ParseSocksaddrHostPort(host, binary.BigEndian.Uint16(portBytes[:])), nil +} + +func WriteUDPRequestAddress(buffer *buf.Buffer, destination M.Socksaddr) error { + // Surge 6.7.0 (11520): SGUDPConnectorSnellV4::sendData:toHostname:port:metadata: writes 0x00 before literal IP UDP targets. + if destination.IsIP() { + err := buffer.WriteByte(0x00) + if err != nil { + return err + } + return udpIPSerializer.WriteAddrPort(buffer, destination.Unwrap()) + } else { + host := destination.Fqdn + if len(host) == 0 || len(host) > 255 { + return E.New("snell: invalid udp host: ", host) + } + err := M.WriteSocksString(buffer, host) + if err != nil { + return err + } + } + binary.BigEndian.PutUint16(buffer.Extend(2), destination.Port) + return nil +} + +func ReadUDPRequestAddress(buffer *buf.Buffer) (M.Socksaddr, error) { + first, err := buffer.ReadByte() + if err != nil { + return M.Socksaddr{}, err + } + if first != 0x00 { + host, hostErr := buffer.ReadBytes(int(first)) + if hostErr != nil { + return M.Socksaddr{}, hostErr + } + port, portErr := buffer.ReadBytes(2) + if portErr != nil { + return M.Socksaddr{}, portErr + } + return M.ParseSocksaddrHostPort(string(host), binary.BigEndian.Uint16(port)), nil + } + return udpIPSerializer.ReadAddrPort(buffer) +} + +// snell-server v6.0.0b4: FUN_001431c0: UDP response sources are emitted as literal IPs only. +func WriteUDPResponseAddress(buffer *buf.Buffer, source M.Socksaddr) error { + source = source.Unwrap() + addr := source.Addr + if !addr.IsValid() { + return E.New("snell: udp response source is not an ip: ", source) + } + return udpIPSerializer.WriteAddrPort(buffer, source) +} + +func ReadUDPResponseAddress(buffer *buf.Buffer) (M.Socksaddr, error) { + return udpIPSerializer.ReadAddrPort(buffer) +} diff --git a/proxy/snell/sing-snell/crypto.go b/proxy/snell/sing-snell/crypto.go new file mode 100644 index 00000000000..0c4ca5c56cf --- /dev/null +++ b/proxy/snell/sing-snell/crypto.go @@ -0,0 +1,31 @@ +package snell + +import ( + "crypto/aes" + "crypto/cipher" + + "golang.org/x/crypto/argon2" +) + +// Surge 6.7.0 (11520): FUN_1000123d0: AEAD keys are derived with these Argon2id parameters. +func DeriveKey(psk []byte, salt []byte) []byte { + return argon2.IDKey(psk, salt, 3, 8, 1, 32)[:16] +} + +func NewAEAD(key []byte) (cipher.AEAD, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + return cipher.NewGCM(block) +} + +// Surge 6.7.0 (11520): FUN_100012944: nonce is u64le(counter) || 0x00000000. +func IncreaseNonce(nonce []byte) { + for index := range nonce { + nonce[index]++ + if nonce[index] != 0 { + return + } + } +} diff --git a/proxy/snell/sing-snell/go.mod b/proxy/snell/sing-snell/go.mod new file mode 100644 index 00000000000..e7415788e7c --- /dev/null +++ b/proxy/snell/sing-snell/go.mod @@ -0,0 +1,10 @@ +module github.com/sagernet/sing-snell + +go 1.24.0 + +require ( + github.com/sagernet/sing v0.8.11 + golang.org/x/crypto v0.42.0 +) + +require golang.org/x/sys v0.41.0 // indirect diff --git a/proxy/snell/sing-snell/go.sum b/proxy/snell/sing-snell/go.sum new file mode 100644 index 00000000000..fcd58aedf99 --- /dev/null +++ b/proxy/snell/sing-snell/go.sum @@ -0,0 +1,14 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d h1:BhsQU0Iug1tU4xR52cjm8Sc+LBo+KwdyLTRn3ie9moo= +github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/proxy/snell/sing-snell/internal/reuse/reuse.go b/proxy/snell/sing-snell/internal/reuse/reuse.go new file mode 100644 index 00000000000..28c783cf3fd --- /dev/null +++ b/proxy/snell/sing-snell/internal/reuse/reuse.go @@ -0,0 +1,274 @@ +package reuse + +import ( + "io" + "sync" + "sync/atomic" + "time" + + snell "github.com/sagernet/sing-snell" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/x/list" +) + +const ( + PoolSize = 10 + PoolMaxAge = 180 * time.Second + PoolTimerInterval = 20 * time.Second + // Surge 6.7.0 (11520): SNConnectorV4::readServerEOFIfNotInReadState: closes a waiting connector after 0x80001 bytes are read after client EOF. + WaitingDiscardLimit = 0x80001 + // snell-server v5.0.1: FUN_0013f020: writes ReplyError code 0x65 with message "Remote EOF" when the server side closes before replying. + RemoteEOFCode = 0x65 + RemoteEOFMessage = "Remote EOF" +) + +type State uint8 + +const ( + StateReady State = iota + StateActive + StateWaiting + StateClosed +) + +type Session interface { + io.Closer + ReuseState() *atomic.Uint32 +} + +type poolEntry[S Session] struct { + session S + time time.Time +} + +type Pool[S Session] struct { + access sync.Mutex + closed bool + entries list.List[*poolEntry[S]] + stop chan struct{} + ticking bool + drainWg sync.WaitGroup +} + +func (p *Pool[S]) Init() { + p.access.Lock() + if p.stop == nil { + p.entries.Init() + p.stop = make(chan struct{}) + } + p.access.Unlock() +} + +func (p *Pool[S]) IsClosed() bool { + p.access.Lock() + closed := p.closed + p.access.Unlock() + return closed +} + +func (p *Pool[S]) Take() (session S, found bool, closed bool) { + p.access.Lock() + closed = p.closed + var expired []S + now := time.Now() + if !closed { + for element := p.entries.Front(); element != nil; { + next := element.Next() + entry := element.Value + state := State(entry.session.ReuseState().Load()) + if state == StateClosed { + p.entries.Remove(element) + element = next + continue + } + if now.Sub(entry.time) > PoolMaxAge { + p.entries.Remove(element) + expired = append(expired, entry.session) + element = next + continue + } + if !found && state == StateReady && entry.session.ReuseState().CompareAndSwap(uint32(StateReady), uint32(StateActive)) { + session = entry.session + found = true + p.entries.Remove(element) + element = next + continue + } + element = next + } + } + p.access.Unlock() + for _, expiredSession := range expired { + expiredSession.Close() + } + return +} + +func (p *Pool[S]) MoveToPool(session S, state State, drain bool) bool { + p.access.Lock() + closed := p.closed + var expired []S + now := time.Now() + if !closed { + for element := p.entries.Front(); element != nil; { + next := element.Next() + entry := element.Value + entryState := State(entry.session.ReuseState().Load()) + if entryState == StateClosed { + p.entries.Remove(element) + element = next + continue + } + if now.Sub(entry.time) > PoolMaxAge { + p.entries.Remove(element) + expired = append(expired, entry.session) + element = next + continue + } + element = next + } + } + var added bool + if !closed && p.stop != nil && p.entries.Len() < PoolSize { + p.entries.PushBack(&poolEntry[S]{session: session, time: now}) + if state == StateWaiting && drain { + p.drainWg.Add(1) + } + added = true + if !p.ticking { + p.ticking = true + go p.ExpireLoop() + } + } + p.access.Unlock() + for _, expiredSession := range expired { + expiredSession.Close() + } + if !added { + session.Close() + } + return added +} + +func (p *Pool[S]) ExpireLoop() { + ticker := time.NewTicker(PoolTimerInterval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + var expired []S + now := time.Now() + p.access.Lock() + if p.closed { + p.ticking = false + p.access.Unlock() + return + } + for element := p.entries.Front(); element != nil; { + next := element.Next() + entry := element.Value + state := State(entry.session.ReuseState().Load()) + if state == StateClosed { + p.entries.Remove(element) + element = next + continue + } + if now.Sub(entry.time) > PoolMaxAge { + p.entries.Remove(element) + expired = append(expired, entry.session) + element = next + continue + } + element = next + } + if p.entries.Len() == 0 { + p.ticking = false + p.access.Unlock() + for _, expiredSession := range expired { + expiredSession.Close() + } + return + } + p.access.Unlock() + for _, expiredSession := range expired { + expiredSession.Close() + } + case <-p.stop: + return + } + } +} + +func (p *Pool[S]) DrainDone() { + p.drainWg.Done() +} + +func (p *Pool[S]) Close() error { + p.access.Lock() + if p.closed { + p.access.Unlock() + return nil + } + p.closed = true + var sessions []S + for element := p.entries.Front(); element != nil; element = element.Next() { + sessions = append(sessions, element.Value.session) + } + p.entries.Init() + if p.stop != nil { + close(p.stop) + } + p.access.Unlock() + var closeErr error + for _, session := range sessions { + closeErr = E.Errors(closeErr, session.Close()) + } + p.drainWg.Wait() + return closeErr +} + +type RecordReader interface { + N.ExtendedReader + N.ReadWaiter + ReadRecord() (*buf.Buffer, error) + NextRecord() (*buf.Buffer, error) + SetCache(*buf.Buffer) + ReleaseCache() +} + +type RecordWriter interface { + N.ExtendedWriter + N.VectorisedWriteCreator + CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter + CreatePacketVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter + WritePacketBuffer(buffer *buf.Buffer) error + WriteZeroChunk() error + FrontHeadroom() int + RearHeadroom() int + WriterMTU() int + Upstream() any +} + +func ParseReply(record *buf.Buffer) (*buf.Buffer, error) { + reply, err := record.ReadByte() + if err != nil { + record.Release() + return nil, err + } + switch reply { + case snell.ReplyTunnel: + if record.IsEmpty() { + record.Release() + return nil, nil + } + return record, nil + case snell.ReplyError: + defer record.Release() + return nil, snell.ReadServerError(record) + default: + record.Release() + return nil, E.Extend(snell.ErrUnexpectedReply, reply) + } +} diff --git a/proxy/snell/sing-snell/obfs.go b/proxy/snell/sing-snell/obfs.go new file mode 100644 index 00000000000..729251d9f66 --- /dev/null +++ b/proxy/snell/sing-snell/obfs.go @@ -0,0 +1,685 @@ +package snell + +import ( + "bufio" + "crypto/rand" + "encoding/base64" + "encoding/binary" + "fmt" + "io" + "math/big" + "net" + "strings" + "sync" + "time" + + E "github.com/sagernet/sing/common/exceptions" +) + +type ObfsMode int + +const ( + ObfsModeNone ObfsMode = iota + ObfsModeHTTP + ObfsModeTLS +) + +const ( + // Surge 6.7.0 (11520): SGObfsHelper::initWithPolicy: uses these legacy obfs default hosts. + DefaultObfsHost = "bing.com" + DefaultTLSObfsHost = "cloudfront.net" +) + +const ( + // Surge 6.7.0 (11520): SGObfsHelperTLS::encodeObfsData: embeds at most 0x400 bytes in the ClientHello and chunks later records at 0x4000. + tlsObfsFirstClientPayloadLen = 0x400 + tlsObfsRecordPayloadLen = 0x4000 +) + +var ( + httpObfsClientFingerprintOnce sync.Once + httpObfsClientFingerprintErr error + httpObfsClientKey string + httpObfsClientUserAgent string + + httpObfsServerResponseOnce sync.Once + httpObfsServerResponseErr error + httpObfsServerResponse []byte +) + +type ObfsConfig struct { + Mode ObfsMode + Host string +} + +func ParseObfsMode(name string) (ObfsMode, error) { + switch strings.ToLower(name) { + case "", "none": + return ObfsModeNone, nil + case "http": + return ObfsModeHTTP, nil + case "tls": + return ObfsModeTLS, nil + default: + return 0, E.New("snell: unknown obfs mode: ", name) + } +} + +func (m ObfsMode) String() string { + switch m { + case ObfsModeNone: + return "none" + case ObfsModeHTTP: + return "http" + case ObfsModeTLS: + return "tls" + default: + panic("snell: invalid obfs mode") + } +} + +func (c ObfsConfig) ClientConn(conn net.Conn) net.Conn { + switch c.Mode { + case ObfsModeNone: + return conn + case ObfsModeHTTP: + return &httpObfsClientConn{Conn: conn, config: c} + case ObfsModeTLS: + return &tlsObfsClientConn{Conn: conn, config: c, firstResponse: true} + default: + panic("snell: invalid obfs mode") + } +} + +func (c ObfsConfig) ServerConn(conn net.Conn) net.Conn { + switch c.Mode { + case ObfsModeNone: + return conn + case ObfsModeHTTP: + return &httpObfsServerConn{Conn: conn, config: c} + case ObfsModeTLS: + return &tlsObfsServerConn{Conn: conn, config: c, firstRequest: true, firstResponse: true} + default: + panic("snell: invalid obfs mode") + } +} + +func (c ObfsConfig) WriteClientRequest(w io.Writer) error { + return c.WriteClientRequestWithPayload(w, nil) +} + +func (c ObfsConfig) WriteClientRequestWithPayload(w io.Writer, payload []byte) error { + switch c.Mode { + case ObfsModeHTTP: + httpObfsClientFingerprintOnce.Do(func() { + var keyBytes [16]byte + _, readErr := io.ReadFull(rand.Reader, keyBytes[:]) + if readErr != nil { + httpObfsClientFingerprintErr = E.Cause(readErr, "generate http obfs websocket key") + return + } + osMinorDelta, randomErr := rand.Int(rand.Reader, big.NewInt(6)) + if randomErr != nil { + httpObfsClientFingerprintErr = E.Cause(randomErr, "generate http obfs user agent") + return + } + firefoxVersionDelta, randomErr := rand.Int(rand.Reader, big.NewInt(43)) + if randomErr != nil { + httpObfsClientFingerprintErr = E.Cause(randomErr, "generate http obfs user agent") + return + } + httpObfsClientKey = base64.StdEncoding.EncodeToString(keyBytes[:]) + // Surge 6.7.0 (11520): SNObfsHelperHTTP init creates one Firefox-style user agent per helper class lifetime. + httpObfsClientUserAgent = fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.%d; rv:64.0) Gecko/20100101 Firefox/%d.0", int(osMinorDelta.Int64())+9, int(firefoxVersionDelta.Int64())+22) + }) + if httpObfsClientFingerprintErr != nil { + return httpObfsClientFingerprintErr + } + host := c.Host + if host == "" { + host = DefaultObfsHost + } + var request []byte + if len(payload) == 0 { + request = fmt.Appendf(request, "GET / HTTP/1.1\r\nHost: %s\r\nUser-Agent: %s\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: %s\r\n\r\n", host, httpObfsClientUserAgent, httpObfsClientKey) + } else { + request = fmt.Appendf(request, "GET / HTTP/1.1\r\nHost: %s\r\nUser-Agent: %s\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nContent-Length: %d\r\nSec-WebSocket-Key: %s\r\n\r\n", host, httpObfsClientUserAgent, len(payload), httpObfsClientKey) + request = append(request, payload...) + } + _, err := w.Write(request) + if err != nil { + return E.Cause(err, "write http obfs request") + } + return nil + case ObfsModeTLS: + host := c.Host + if host == "" { + host = DefaultTLSObfsHost + } + firstPayload := payload + if len(firstPayload) > tlsObfsFirstClientPayloadLen { + firstPayload = payload[:tlsObfsFirstClientPayloadLen] + } + random := make([]byte, 28) + _, err := io.ReadFull(rand.Reader, random) + if err != nil { + return err + } + sessionID := make([]byte, 32) + _, err = io.ReadFull(rand.Reader, sessionID) + if err != nil { + return err + } + // Surge 6.7.0 (11520): SGObfsHelperTLS::encodeObfsData: sends the first Snell record in the ClientHello + // session_ticket extension and caps that embedded payload at 0x400 bytes. + out := make([]byte, 0, 0xd9+len(firstPayload)+len(host)) + out = append(out, 0x16, 0x03, 0x01) + out = binary.BigEndian.AppendUint16(out, uint16(212+len(firstPayload)+len(host))) + out = append(out, 0x01, 0x00) + out = binary.BigEndian.AppendUint16(out, uint16(208+len(firstPayload)+len(host))) + out = append(out, 0x03, 0x03) + out = binary.BigEndian.AppendUint32(out, uint32(time.Now().Unix())) + out = append(out, random...) + out = append(out, 0x20) + out = append(out, sessionID...) + out = binary.BigEndian.AppendUint16(out, 0x0038) + out = append(out, + 0xc0, 0x2c, 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f, + 0x00, 0x9e, 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, 0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a, + 0xc0, 0x14, 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, 0x00, 0x9d, 0x00, 0x9c, 0x00, 0x3d, + 0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0xff, + ) + out = append(out, 0x01, 0x00) + out = binary.BigEndian.AppendUint16(out, uint16(79+len(firstPayload)+len(host))) + out = binary.BigEndian.AppendUint16(out, 0x0023) + out = binary.BigEndian.AppendUint16(out, uint16(len(firstPayload))) + out = append(out, firstPayload...) + out = binary.BigEndian.AppendUint16(out, 0x0000) + out = binary.BigEndian.AppendUint16(out, uint16(len(host)+5)) + out = binary.BigEndian.AppendUint16(out, uint16(len(host)+3)) + out = append(out, 0x00) + out = binary.BigEndian.AppendUint16(out, uint16(len(host))) + out = append(out, host...) + out = append(out, + 0x00, 0x0b, 0x00, 0x04, 0x03, 0x01, 0x00, 0x02, + 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18, + 0x00, 0x0d, 0x00, 0x20, 0x00, 0x1e, 0x06, 0x01, 0x06, 0x02, 0x06, 0x03, 0x05, + 0x01, 0x05, 0x02, 0x05, 0x03, 0x04, 0x01, 0x04, 0x02, 0x04, 0x03, 0x03, 0x01, + 0x03, 0x02, 0x03, 0x03, 0x02, 0x01, 0x02, 0x02, 0x02, 0x03, + 0x00, 0x16, 0x00, 0x00, + 0x00, 0x17, 0x00, 0x00, + ) + _, err = w.Write(out) + if err != nil { + return E.Cause(err, "write tls obfs request") + } + for payload = payload[len(firstPayload):]; len(payload) > 0; { + payloadLen := min(len(payload), tlsObfsRecordPayloadLen) + record := make([]byte, 0, 5+payloadLen) + record = append(record, 0x17, 0x03, 0x03) + record = binary.BigEndian.AppendUint16(record, uint16(payloadLen)) + record = append(record, payload[:payloadLen]...) + _, err = w.Write(record) + if err != nil { + return E.Cause(err, "write tls obfs request payload") + } + payload = payload[payloadLen:] + } + return nil + case ObfsModeNone: + if len(payload) == 0 { + return nil + } + _, err := w.Write(payload) + if err != nil { + return E.Cause(err, "write obfs payload") + } + return nil + default: + panic("snell: invalid obfs mode") + } +} + +func (c ObfsConfig) ReadClientResponse(r io.Reader) (io.Reader, error) { + switch c.Mode { + case ObfsModeNone, ObfsModeTLS: + return r, nil + case ObfsModeHTTP: + reader := bufio.NewReader(r) + terminator := [4]byte{'\r', '\n', '\r', '\n'} + matched := 0 + for { + value, err := reader.ReadByte() + if err != nil { + return nil, E.Cause(err, "read http obfs response") + } + if value == terminator[matched] { + matched++ + if matched == len(terminator) { + return reader, nil + } + continue + } + if value == terminator[0] { + matched = 1 + } else { + matched = 0 + } + } + default: + panic("snell: invalid obfs mode") + } +} + +func (c ObfsConfig) ReadServerRequest(r io.Reader) (io.Reader, error) { + switch c.Mode { + case ObfsModeNone, ObfsModeTLS: + return r, nil + case ObfsModeHTTP: + reader := bufio.NewReader(r) + terminator := [4]byte{'\r', '\n', '\r', '\n'} + matched := 0 + for { + value, err := reader.ReadByte() + if err != nil { + return nil, E.Cause(err, "read http obfs request") + } + if value == terminator[matched] { + matched++ + if matched == len(terminator) { + return reader, nil + } + continue + } + if value == terminator[0] { + matched = 1 + } else { + matched = 0 + } + } + default: + panic("snell: invalid obfs mode") + } +} + +func (c ObfsConfig) WriteServerResponse(w io.Writer) error { + return c.WriteServerResponseWithPayload(w, nil) +} + +func (c ObfsConfig) WriteServerResponseWithPayload(w io.Writer, payload []byte) error { + switch c.Mode { + case ObfsModeHTTP: + httpObfsServerResponseOnce.Do(func() { + var acceptBytes [16]byte + _, readErr := io.ReadFull(rand.Reader, acceptBytes[:]) + if readErr != nil { + httpObfsServerResponseErr = E.Cause(readErr, "generate http obfs accept") + return + } + versionMinorDelta, randomErr := rand.Int(rand.Reader, big.NewInt(14)) + if randomErr != nil { + httpObfsServerResponseErr = E.Cause(randomErr, "generate http obfs server version") + return + } + versionPatchDelta, randomErr := rand.Int(rand.Reader, big.NewInt(12)) + if randomErr != nil { + httpObfsServerResponseErr = E.Cause(randomErr, "generate http obfs server version") + return + } + accept := base64.StdEncoding.EncodeToString(acceptBytes[:]) + // Surge 6.7.0 (11520): SNObfsHelperServer initializes and reuses one fake 101 response. + httpObfsServerResponse = fmt.Appendf(nil, "HTTP/1.1 101 Switching Protocols\r\nServer: nginx/1.%d.%d\r\nDate: %s\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: %s\r\n\r\n", int(versionMinorDelta.Int64()), int(versionPatchDelta.Int64()), time.Now().Format("Mon, 02 Jan 2006 15:04:05 GMT"), accept) + }) + if httpObfsServerResponseErr != nil { + return httpObfsServerResponseErr + } + response := append([]byte(nil), httpObfsServerResponse...) + if len(payload) > 0 { + response = append(response, payload...) + } + _, err := w.Write(response) + if err != nil { + return E.Cause(err, "write http obfs response") + } + return nil + case ObfsModeTLS: + random := make([]byte, 28) + _, err := io.ReadFull(rand.Reader, random) + if err != nil { + return err + } + sessionID := make([]byte, 32) + _, err = io.ReadFull(rand.Reader, sessionID) + if err != nil { + return err + } + firstPayload := payload + if len(firstPayload) > tlsObfsRecordPayloadLen { + firstPayload = payload[:tlsObfsRecordPayloadLen] + } + // Surge 6.7.0 (11520): SGObfsHelperTLS::decodeObfsData: expects this fake ServerHello, a CCS record, + // then the first Snell payload in a TLS handshake record. + out := make([]byte, 0, 107+len(firstPayload)) + out = append(out, 0x16, 0x03, 0x01) + out = binary.BigEndian.AppendUint16(out, 91) + out = append(out, 0x02, 0x00, 0x00, 0x57, 0x03, 0x03) + out = binary.BigEndian.AppendUint32(out, uint32(time.Now().Unix())) + out = append(out, random...) + out = append(out, 0x20) + out = append(out, sessionID...) + out = append(out, + 0xcc, 0xa8, 0x00, + 0x00, 0x00, + 0xff, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x17, 0x00, 0x00, + 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, + 0x14, 0x03, 0x03, 0x00, 0x01, 0x01, + 0x16, 0x03, 0x03, + ) + out = binary.BigEndian.AppendUint16(out, uint16(len(firstPayload))) + out = append(out, firstPayload...) + _, err = w.Write(out) + if err != nil { + return E.Cause(err, "write tls obfs response") + } + for payload = payload[len(firstPayload):]; len(payload) > 0; { + payloadLen := min(len(payload), tlsObfsRecordPayloadLen) + record := make([]byte, 0, 5+payloadLen) + record = append(record, 0x17, 0x03, 0x03) + record = binary.BigEndian.AppendUint16(record, uint16(payloadLen)) + record = append(record, payload[:payloadLen]...) + _, err = w.Write(record) + if err != nil { + return E.Cause(err, "write tls obfs response payload") + } + payload = payload[payloadLen:] + } + return nil + case ObfsModeNone: + if len(payload) == 0 { + return nil + } + _, err := w.Write(payload) + if err != nil { + return E.Cause(err, "write obfs payload") + } + return nil + default: + panic("snell: invalid obfs mode") + } +} + +type httpObfsClientConn struct { + net.Conn + config ObfsConfig + + readAccess sync.Mutex + writeAccess sync.Mutex + reader io.Reader + wrote bool +} + +func (c *httpObfsClientConn) Read(p []byte) (int, error) { + c.readAccess.Lock() + if c.reader == nil { + reader, err := c.config.ReadClientResponse(c.Conn) + if err != nil { + c.readAccess.Unlock() + return 0, err + } + c.reader = reader + } + reader := c.reader + c.readAccess.Unlock() + return reader.Read(p) +} + +func (c *httpObfsClientConn) Write(p []byte) (int, error) { + c.writeAccess.Lock() + defer c.writeAccess.Unlock() + if !c.wrote { + c.wrote = true + err := c.config.WriteClientRequestWithPayload(c.Conn, p) + if err != nil { + return 0, err + } + return len(p), nil + } + return c.Conn.Write(p) +} + +type httpObfsServerConn struct { + net.Conn + config ObfsConfig + + readAccess sync.Mutex + writeAccess sync.Mutex + reader io.Reader + wrote bool +} + +func (c *httpObfsServerConn) Read(p []byte) (int, error) { + c.readAccess.Lock() + if c.reader == nil { + reader, err := c.config.ReadServerRequest(c.Conn) + if err != nil { + c.readAccess.Unlock() + return 0, err + } + c.reader = reader + } + reader := c.reader + c.readAccess.Unlock() + return reader.Read(p) +} + +func (c *httpObfsServerConn) Write(p []byte) (int, error) { + c.writeAccess.Lock() + defer c.writeAccess.Unlock() + if !c.wrote { + c.wrote = true + err := c.config.WriteServerResponseWithPayload(c.Conn, p) + if err != nil { + return 0, err + } + return len(p), nil + } + return c.Conn.Write(p) +} + +type tlsObfsClientConn struct { + net.Conn + config ObfsConfig + + readAccess sync.Mutex + writeAccess sync.Mutex + readRemaining int + firstResponse bool + wrote bool +} + +func (c *tlsObfsClientConn) Read(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + c.readAccess.Lock() + defer c.readAccess.Unlock() + if c.readRemaining > 0 { + payloadLen := min(c.readRemaining, len(p)) + n, err := io.ReadFull(c.Conn, p[:payloadLen]) + c.readRemaining -= n + return n, err + } + if c.firstResponse { + c.firstResponse = false + return c.readRecordPayload(p, 105) + } + return c.readRecordPayload(p, 3) +} + +func (c *tlsObfsClientConn) readRecordPayload(p []byte, discardLen int) (int, error) { + _, err := io.CopyN(io.Discard, c.Conn, int64(discardLen)) + if err != nil { + return 0, err + } + var lengthBytes [2]byte + _, err = io.ReadFull(c.Conn, lengthBytes[:]) + if err != nil { + return 0, err + } + payloadLen := int(binary.BigEndian.Uint16(lengthBytes[:])) + if payloadLen == 0 { + return 0, nil + } + if payloadLen > len(p) { + n, err := io.ReadFull(c.Conn, p) + c.readRemaining = payloadLen - n + return n, err + } + return io.ReadFull(c.Conn, p[:payloadLen]) +} + +func (c *tlsObfsClientConn) Write(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + c.writeAccess.Lock() + defer c.writeAccess.Unlock() + written := 0 + if !c.wrote { + c.wrote = true + firstPayload := p + if len(firstPayload) > tlsObfsFirstClientPayloadLen { + firstPayload = p[:tlsObfsFirstClientPayloadLen] + } + err := c.config.WriteClientRequestWithPayload(c.Conn, firstPayload) + if err != nil { + return 0, err + } + written += len(firstPayload) + p = p[len(firstPayload):] + } + for len(p) > 0 { + payloadLen := min(len(p), tlsObfsRecordPayloadLen) + record := make([]byte, 0, 5+payloadLen) + record = append(record, 0x17, 0x03, 0x03) + record = binary.BigEndian.AppendUint16(record, uint16(payloadLen)) + record = append(record, p[:payloadLen]...) + _, err := c.Conn.Write(record) + if err != nil { + return written, E.Cause(err, "write tls obfs payload") + } + written += payloadLen + p = p[payloadLen:] + } + return written, nil +} + +type tlsObfsServerConn struct { + net.Conn + config ObfsConfig + + readAccess sync.Mutex + writeAccess sync.Mutex + readRemaining int + firstRequest bool + sessionTicketDone bool + firstResponse bool +} + +func (c *tlsObfsServerConn) Read(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + c.readAccess.Lock() + defer c.readAccess.Unlock() + if c.readRemaining > 0 { + payloadLen := min(c.readRemaining, len(p)) + n, err := io.ReadFull(c.Conn, p[:payloadLen]) + c.readRemaining -= n + return n, err + } + if c.firstRequest { + c.firstRequest = false + return c.readRecordPayload(p, 9*16-4) + } + if !c.sessionTicketDone { + c.sessionTicketDone = true + _, err := io.CopyN(io.Discard, c.Conn, 7) + if err != nil { + return 0, err + } + var lengthBytes [2]byte + _, err = io.ReadFull(c.Conn, lengthBytes[:]) + if err != nil { + return 0, err + } + _, err = io.CopyN(io.Discard, c.Conn, int64(binary.BigEndian.Uint16(lengthBytes[:]))) + if err != nil { + return 0, err + } + _, err = io.CopyN(io.Discard, c.Conn, 4*16+2) + if err != nil { + return 0, err + } + } + return c.readRecordPayload(p, 3) +} + +func (c *tlsObfsServerConn) readRecordPayload(p []byte, discardLen int) (int, error) { + _, err := io.CopyN(io.Discard, c.Conn, int64(discardLen)) + if err != nil { + return 0, err + } + var lengthBytes [2]byte + _, err = io.ReadFull(c.Conn, lengthBytes[:]) + if err != nil { + return 0, err + } + payloadLen := int(binary.BigEndian.Uint16(lengthBytes[:])) + if payloadLen == 0 { + return 0, nil + } + if payloadLen > len(p) { + n, err := io.ReadFull(c.Conn, p) + c.readRemaining = payloadLen - n + return n, err + } + return io.ReadFull(c.Conn, p[:payloadLen]) +} + +func (c *tlsObfsServerConn) Write(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + c.writeAccess.Lock() + defer c.writeAccess.Unlock() + written := 0 + if c.firstResponse { + c.firstResponse = false + firstPayload := p + if len(firstPayload) > tlsObfsRecordPayloadLen { + firstPayload = p[:tlsObfsRecordPayloadLen] + } + err := c.config.WriteServerResponseWithPayload(c.Conn, firstPayload) + if err != nil { + return 0, err + } + written += len(firstPayload) + p = p[len(firstPayload):] + } + for len(p) > 0 { + payloadLen := min(len(p), tlsObfsRecordPayloadLen) + record := make([]byte, 0, 5+payloadLen) + record = append(record, 0x17, 0x03, 0x03) + record = binary.BigEndian.AppendUint16(record, uint16(payloadLen)) + record = append(record, p[:payloadLen]...) + _, err := c.Conn.Write(record) + if err != nil { + return written, E.Cause(err, "write tls obfs payload") + } + written += payloadLen + p = p[payloadLen:] + } + return written, nil +} diff --git a/proxy/snell/sing-snell/request.go b/proxy/snell/sing-snell/request.go new file mode 100644 index 00000000000..f4eadf736a4 --- /dev/null +++ b/proxy/snell/sing-snell/request.go @@ -0,0 +1,97 @@ +package snell + +import ( + "io" + + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" +) + +type Request struct { + Command byte + ClientID []byte + Destination M.Socksaddr +} + +func (r Request) Write(buffer *buf.Buffer) error { + if len(r.ClientID) > 255 { + return E.New("snell: client id too long") + } + _, err := buffer.Write([]byte{RequestVersion, r.Command, byte(len(r.ClientID))}) + if err != nil { + return err + } + if len(r.ClientID) > 0 { + _, err = buffer.Write(r.ClientID) + if err != nil { + return err + } + } + switch r.Command { + case CommandConnect, CommandConnectV2: + return WriteConnectAddress(buffer, r.Destination) + case CommandPing, CommandUDP: + return nil + default: + return E.Extend(ErrUnsupportedCommand, r.Command) + } +} + +func (r Request) Len() int { + requestLen := 3 + len(r.ClientID) + switch r.Command { + case CommandConnect, CommandConnectV2: + return requestLen + 1 + len(r.Destination.AddrString()) + 2 + default: + return requestLen + } +} + +func ReadRequest(reader io.Reader) (Request, error) { + prefix := make([]byte, 3) + _, err := io.ReadFull(reader, prefix) + if err != nil { + return Request{}, err + } + if prefix[0] != RequestVersion { + return Request{}, E.Extend(ErrBadVersion, "request version ", prefix[0]) + } + clientIDLen := int(prefix[2]) + request := Request{Command: prefix[1]} + if clientIDLen > 0 { + request.ClientID = make([]byte, clientIDLen) + _, err = io.ReadFull(reader, request.ClientID) + if err != nil { + return Request{}, err + } + } + switch request.Command { + case CommandConnect, CommandConnectV2: + request.Destination, err = ReadConnectAddress(reader) + if err != nil { + return Request{}, err + } + case CommandPing, CommandUDP: + default: + return Request{}, E.Extend(ErrUnsupportedCommand, request.Command) + } + return request, nil +} + +func ReadServerError(record *buf.Buffer) error { + errorCode, err := record.ReadByte() + if err != nil { + return err + } + message, err := M.ReadSockString(record) + if err != nil { + return err + } + // Surge 6.7.0 (11520): SNConnectorV4::socket:didReadData: rejects server-error replies unless the frame is + // exactly code, message length, and message bytes. + if !record.IsEmpty() { + return E.New("snell: server error reply has trailing data") + } + return E.New("snell: server error ", errorCode, ": ", message) +} diff --git a/proxy/snell/sing-snell/snell.go b/proxy/snell/sing-snell/snell.go new file mode 100644 index 00000000000..eb6eb12aed1 --- /dev/null +++ b/proxy/snell/sing-snell/snell.go @@ -0,0 +1,83 @@ +package snell + +import ( + "context" + "net" + + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +const ( + HeaderVersion = 0x04 + HeaderPlainLen = 7 + AEADTagLen = 16 + HeaderCipherLen = HeaderPlainLen + AEADTagLen + SaltLen = 16 + NonceLen = 12 + MaxPayloadLen = 0x3fff +) + +const RequestVersion = 0x01 + +const ( + CommandPing = 0x00 + CommandConnect = 0x01 + CommandConnectV2 = 0x05 + CommandUDP = 0x06 +) + +const ( + ReplyTunnel = 0x00 + ReplyPong = 0x01 + ReplyError = 0x02 +) + +const ( + UDPCommandForward = 0x01 + AddressTypeIPv4 = 0x04 + AddressTypeIPv6 = 0x06 +) + +var ( + ErrBadVersion = E.New("snell: bad header version") + ErrReservedNonZero = E.New("snell: reserved header octet is non-zero") + ErrPayloadTooLarge = E.New("snell: payload length exceeds maximum") + ErrUnsupportedCommand = E.New("snell: unsupported command") + ErrUnexpectedReply = E.New("snell: unexpected reply opcode") + ErrMissingPSK = E.New("snell: missing pre-shared key") + ErrNoUsers = E.New("snell: no users") + ErrBadUserKey = E.New("snell: bad user key") + ErrDuplicateUserKey = E.New("snell: duplicate user key") +) + +type Method interface { + DialConn(conn net.Conn, destination M.Socksaddr) (net.Conn, error) + DialEarlyConn(conn net.Conn, destination M.Socksaddr) net.Conn + DialPacketConn(conn net.Conn) (N.NetPacketConn, error) +} + +type Handler interface { + N.TCPConnectionHandlerEx + N.UDPConnectionHandlerEx +} + +type Service interface { + NewConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error +} + +// snell-server v6.0.0b4: FUN_00141bc0: malformed handshakes are torn down abruptly. +type ServerError struct { + net.Conn + Source M.Socksaddr + Cause error +} + +func (e *ServerError) Unwrap() error { + return e.Cause +} + +func (e *ServerError) Error() string { + return "snell: serve " + e.Source.String() + ": " + e.Cause.Error() +} diff --git a/proxy/snell/sing-snell/snellv4/client.go b/proxy/snell/sing-snell/snellv4/client.go new file mode 100644 index 00000000000..961a4d59da0 --- /dev/null +++ b/proxy/snell/sing-snell/snellv4/client.go @@ -0,0 +1,333 @@ +package snellv4 + +import ( + "net" + "sync" + + snell "github.com/sagernet/sing-snell" + "github.com/sagernet/sing-snell/internal/reuse" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type Client struct { + psk []byte + userKey []byte + reuse bool + obfs snell.ObfsConfig + dialer N.Dialer + server M.Socksaddr + + pool reuse.Pool[*reuseSession] +} + +type ClientOptions struct { + PSK []byte + UserKey []byte + Reuse bool + ObfsMode snell.ObfsMode + ObfsHost string + Dialer N.Dialer + Server M.Socksaddr +} + +func NewClient(options ClientOptions) (*Client, error) { + if len(options.PSK) == 0 { + return nil, snell.ErrMissingPSK + } + if len(options.UserKey) > 255 { + return nil, E.New("snell: user key too long") + } + switch options.ObfsMode { + case snell.ObfsModeNone, snell.ObfsModeHTTP, snell.ObfsModeTLS: + default: + return nil, E.New("snell: unknown obfs mode: ", int(options.ObfsMode)) + } + client := &Client{ + psk: options.PSK, + userKey: options.UserKey, + reuse: options.Reuse, + obfs: snell.ObfsConfig{Mode: options.ObfsMode, Host: options.ObfsHost}, + dialer: options.Dialer, + server: options.Server, + } + if options.Reuse { + client.pool.Init() + } + return client, nil +} + +func (c *Client) DialConn(conn net.Conn, destination M.Socksaddr) (net.Conn, error) { + clientConn := &clientConn{client: c, Conn: c.obfs.ClientConn(conn), destination: destination} + return clientConn, clientConn.writeRequest(nil) +} + +func (c *Client) DialEarlyConn(conn net.Conn, destination M.Socksaddr) net.Conn { + return &clientConn{client: c, Conn: c.obfs.ClientConn(conn), destination: destination} +} + +func (c *Client) DialPacketConn(conn net.Conn) (N.NetPacketConn, error) { + return bufio.NewNetPacketConn(&clientPacketConn{Conn: c.obfs.ClientConn(conn), client: c}), nil +} + +var _ snell.Method = (*Client)(nil) + +type clientConn struct { + net.Conn + client *Client + destination M.Socksaddr + + access sync.Mutex + reader *reader + writer *writer + readWaitOptions N.ReadWaitOptions + closeWriteOnce sync.Once + closeWriteErr error +} + +func (c *clientConn) writeRequest(payload []byte) error { + // Surge 6.7.0 (11520): SNConnectorV4::targetHandshakeData: writes command 5 for v4/v5 TCP + // handshakes even when connector reuse is disabled. + requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: c.client.userKey, Destination: c.destination} + request := buf.NewSize(requestPayload.Len() + len(payload)) + err := requestPayload.Write(request) + if err != nil { + request.Release() + return err + } + if len(payload) > 0 { + common.Must1(request.Write(payload)) + } + defer request.Release() + + recordWriter := &writer{ + upstream: c.Conn, + psk: c.client.psk, + } + _, err = recordWriter.Write(request.Bytes()) + if err != nil { + return E.Cause(err, "write request") + } + c.writer = recordWriter + return nil +} + +func (c *clientConn) writeRequestBuffer(buffer *buf.Buffer) error { + requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: c.client.userKey, Destination: c.destination} + request := buf.With(buffer.ExtendHeader(requestPayload.Len())) + err := requestPayload.Write(request) + if err != nil { + buffer.Release() + return err + } + recordWriter := &writer{ + upstream: c.Conn, + psk: c.client.psk, + } + err = recordWriter.WriteBuffer(buffer) + if err != nil { + return E.Cause(err, "write request") + } + c.writer = recordWriter + return nil +} + +func (c *clientConn) readResponse() error { + if c.reader != nil { + return nil + } + recordReader := &reader{upstream: c.Conn, psk: c.client.psk} + recordReader.InitializeReadWaiter(c.readWaitOptions) + record, err := recordReader.ReadRecord() + if err != nil { + return E.Cause(err, "read reply") + } + cached, err := reuse.ParseReply(record) + if err != nil { + return err + } + if cached != nil { + recordReader.SetCache(cached) + } + c.reader = recordReader + return nil +} + +func (c *clientConn) Read(p []byte) (int, error) { + err := c.readResponse() + if err != nil { + return 0, err + } + return c.reader.Read(p) +} + +func (c *clientConn) ReadBuffer(buffer *buf.Buffer) error { + err := c.readResponse() + if err != nil { + return err + } + return c.reader.ReadBuffer(buffer) +} + +func (c *clientConn) Write(p []byte) (int, error) { + if c.writer != nil { + return c.writer.Write(p) + } + c.access.Lock() + if c.writer != nil { + c.access.Unlock() + return c.writer.Write(p) + } + defer c.access.Unlock() + err := c.writeRequest(p) + if err != nil { + return 0, err + } + return len(p), nil +} + +func (c *clientConn) WriteBuffer(buffer *buf.Buffer) error { + if c.writer != nil { + return c.writer.WriteBuffer(buffer) + } + c.access.Lock() + if c.writer != nil { + c.access.Unlock() + return c.writer.WriteBuffer(buffer) + } + defer c.access.Unlock() + return c.writeRequestBuffer(buffer) +} + +func (c *clientConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + upstreamWriter, created := bufio.CreateVectorisedWriter(c.Conn) + if !created { + return nil, false + } + return &clientVectorisedWriter{conn: c, upstream: upstreamWriter}, true +} + +type clientVectorisedWriter struct { + conn *clientConn + upstream N.VectorisedWriter +} + +func (w *clientVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { + conn := w.conn + if conn.writer != nil { + return conn.writer.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) + } + conn.access.Lock() + if conn.writer != nil { + recordWriter := conn.writer + conn.access.Unlock() + return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) + } + for index, buffer := range buffers { + if buffer.IsEmpty() { + buffer.Release() + continue + } + err := conn.writeRequestBuffer(buffer) + if err != nil { + conn.access.Unlock() + buf.ReleaseMulti(buffers[index+1:]) + return err + } + if index+1 < len(buffers) { + recordWriter := conn.writer + conn.access.Unlock() + return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers[index+1:]) + } + conn.access.Unlock() + return nil + } + conn.access.Unlock() + return nil +} + +func (c *clientConn) CloseWrite() error { + c.closeWriteOnce.Do(func() { + c.access.Lock() + defer c.access.Unlock() + if c.writer == nil { + c.closeWriteErr = c.writeRequest(nil) + if c.closeWriteErr != nil { + return + } + } + c.closeWriteErr = c.writer.WriteZeroChunk() + }) + return c.closeWriteErr +} + +func (c *clientConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + c.readWaitOptions = options + if c.reader != nil { + c.reader.InitializeReadWaiter(options) + } + return false +} + +func (c *clientConn) WaitReadBuffer() (*buf.Buffer, error) { + err := c.readResponse() + if err != nil { + return nil, err + } + return c.reader.WaitReadBuffer() +} + +func (c *clientConn) CreateReadWaiter() (N.ReadWaiter, bool) { + return c, true +} + +func (c *clientConn) FrontHeadroom() int { + if c.writer != nil { + return c.writer.FrontHeadroom() + } + requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: c.client.userKey, Destination: c.destination} + return requestPayload.Len() + snell.SaltLen + snell.HeaderCipherLen + maxInitialPaddingLen +} + +func (c *clientConn) RearHeadroom() int { + return snell.AEADTagLen +} + +func (c *clientConn) WriterMTU() int { + return maxPayload +} + +func (c *clientConn) NeedHandshakeForRead() bool { + return c.reader == nil +} + +func (c *clientConn) NeedHandshakeForWrite() bool { + return c.writer == nil +} + +func (c *clientConn) NeedAdditionalReadDeadline() bool { + return c.reader == nil +} + +func (c *clientConn) Upstream() any { + return c.Conn +} + +func (c *clientConn) RemoteAddr() net.Addr { + return c.destination.TCPAddr() +} + +var ( + _ N.ExtendedConn = (*clientConn)(nil) + _ N.ReadWaiter = (*clientConn)(nil) + _ N.ReadWaitCreator = (*clientConn)(nil) + _ N.VectorisedWriteCreator = (*clientConn)(nil) + _ N.VectorisedWriter = (*clientVectorisedWriter)(nil) + _ N.EarlyReader = (*clientConn)(nil) + _ N.EarlyWriter = (*clientConn)(nil) + _ N.WriteCloser = (*clientConn)(nil) +) diff --git a/proxy/snell/sing-snell/snellv4/packet.go b/proxy/snell/sing-snell/snellv4/packet.go new file mode 100644 index 00000000000..28c717e7c66 --- /dev/null +++ b/proxy/snell/sing-snell/snellv4/packet.go @@ -0,0 +1,272 @@ +package snellv4 + +import ( + "io" + "net" + "os" + "sync" + + snell "github.com/sagernet/sing-snell" + "github.com/sagernet/sing-snell/internal/reuse" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +const ( + maxUDPRequestHeaderLen = 1 + 1 + 255 + 2 + minUDPPayloadLimit = frameSize - resetRecordOverhead +) + +type clientPacketConn struct { + net.Conn + client *Client + + writeAccess sync.Mutex + writer *writer + + readAccess sync.Mutex + reader *reader + readWaitOptions N.ReadWaitOptions +} + +func (c *clientPacketConn) udpRequestAddrLen(destination M.Socksaddr) int { + if destination.IsIP() { + if destination.Unwrap().Addr.Is4() { + return 1 + 1 + 4 + 2 + } + return 1 + 1 + 16 + 2 + } + return 1 + len(destination.Fqdn) + 2 +} + +func (c *clientPacketConn) writeRequest() error { + c.writeAccess.Lock() + defer c.writeAccess.Unlock() + if c.writer != nil { + return nil + } + recordWriter := &writer{ + upstream: c.Conn, + psk: c.client.psk, + } + requestPayload := snell.Request{Command: snell.CommandUDP, ClientID: c.client.userKey} + request := buf.NewSize(requestPayload.Len()) + err := requestPayload.Write(request) + if err != nil { + request.Release() + return err + } + _, err = recordWriter.Write(request.Bytes()) + request.Release() + if err != nil { + return E.Cause(err, "write udp request") + } + c.writer = recordWriter + return nil +} + +func (c *clientPacketConn) readReply() (*reader, error) { + c.readAccess.Lock() + defer c.readAccess.Unlock() + if c.reader != nil { + return c.reader, nil + } + recordReader := &reader{upstream: c.Conn, psk: c.client.psk} + recordReader.InitializeReadWaiter(c.readWaitOptions) + record, err := recordReader.ReadRecord() + if err != nil { + return nil, E.Cause(err, "read udp reply") + } + cached, err := reuse.ParseReply(record) + if err != nil { + return nil, err + } + if cached != nil { + recordReader.SetCache(cached) + } + c.reader = recordReader + return recordReader, nil +} + +func (c *clientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { + err := c.writeRequest() + if err != nil { + buffer.Release() + return err + } + _, err = c.readReply() + if err != nil { + buffer.Release() + return err + } + header := buf.With(buffer.ExtendHeader(1 + c.udpRequestAddrLen(destination))) + common.Must(header.WriteByte(snell.UDPCommandForward)) + err = snell.WriteUDPRequestAddress(header, destination) + if err != nil { + buffer.Release() + return err + } + if buffer.Len() > maxPayload { + buffer.Release() + return snell.ErrPayloadTooLarge + } + return c.writer.WritePacketBuffer(buffer) +} + +func (c *clientPacketConn) CreatePacketBatchWriter() (N.PacketBatchWriter, bool) { + upstreamWriter, created := bufio.CreateVectorisedWriter(c.Conn) + if !created { + return nil, false + } + return &clientPacketBatchWriter{conn: c, upstream: upstreamWriter}, true +} + +type clientPacketBatchWriter struct { + conn *clientPacketConn + upstream N.VectorisedWriter + + access sync.Mutex + writer N.VectorisedWriter +} + +func (w *clientPacketBatchWriter) WritePacketBatch(buffers []*buf.Buffer, destinations []M.Socksaddr) error { + if len(buffers) == 0 || len(buffers) != len(destinations) { + buf.ReleaseMulti(buffers) + return os.ErrInvalid + } + w.access.Lock() + if w.writer == nil { + err := w.conn.writeRequest() + if err != nil { + w.access.Unlock() + buf.ReleaseMulti(buffers) + return err + } + _, err = w.conn.readReply() + if err != nil { + w.access.Unlock() + buf.ReleaseMulti(buffers) + return err + } + w.writer = w.conn.writer.CreatePacketVectorisedWriterFor(w.upstream) + } + recordWriter := w.writer + w.access.Unlock() + for index, buffer := range buffers { + header := buf.With(buffer.ExtendHeader(1 + w.conn.udpRequestAddrLen(destinations[index]))) + common.Must(header.WriteByte(snell.UDPCommandForward)) + err := snell.WriteUDPRequestAddress(header, destinations[index]) + if err != nil { + buf.ReleaseMulti(buffers) + return err + } + } + return recordWriter.WriteVectorised(buffers) +} + +func (c *clientPacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) { + recordReader, err := c.readReply() + if err != nil { + return M.Socksaddr{}, err + } + for { + record, err := recordReader.NextRecord() + if err != nil { + return M.Socksaddr{}, err + } + if record.Len() < 8 { + record.Release() + return M.Socksaddr{}, E.New("snell: udp response too short") + } + // Surge 6.7.0 (11520): SGUDPConnectorSnellV4::socket:didReadData: silently ignores UDP + // response frames whose address type is neither IPv4 nor IPv6. + addressType := record.Bytes()[0] + if addressType != snell.AddressTypeIPv4 && addressType != snell.AddressTypeIPv6 { + record.Release() + continue + } + destination, err := snell.ReadUDPResponseAddress(record) + if err != nil { + record.Release() + return M.Socksaddr{}, err + } + if record.Len() > buffer.FreeLen() { + record.Release() + return M.Socksaddr{}, io.ErrShortBuffer + } + _, err = buffer.Write(record.Bytes()) + record.Release() + if err != nil { + return M.Socksaddr{}, err + } + return destination, nil + } +} + +func (c *clientPacketConn) FrontHeadroom() int { + return snell.HeaderCipherLen + maxUDPRequestHeaderLen +} + +func (c *clientPacketConn) RearHeadroom() int { + return snell.AEADTagLen +} + +func (c *clientPacketConn) WriterMTU() int { + return minUDPPayloadLimit - maxUDPRequestHeaderLen +} + +func (c *clientPacketConn) Upstream() any { + return c.Conn +} + +func (c *clientPacketConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + c.readAccess.Lock() + defer c.readAccess.Unlock() + c.readWaitOptions = options + if c.reader != nil { + c.reader.InitializeReadWaiter(options) + } + return false +} + +func (c *clientPacketConn) WaitReadPacket() (*buf.Buffer, M.Socksaddr, error) { + recordReader, err := c.readReply() + if err != nil { + return nil, M.Socksaddr{}, err + } + for { + record, err := recordReader.WaitReadBuffer() + if err != nil { + return nil, M.Socksaddr{}, err + } + if record.Len() < 8 { + record.Release() + return nil, M.Socksaddr{}, E.New("snell: udp response too short") + } + // Surge 6.7.0 (11520): SGUDPConnectorSnellV4::socket:didReadData: silently ignores UDP + // response frames whose address type is neither IPv4 nor IPv6. + addressType := record.Bytes()[0] + if addressType != snell.AddressTypeIPv4 && addressType != snell.AddressTypeIPv6 { + record.Release() + continue + } + destination, err := snell.ReadUDPResponseAddress(record) + if err != nil { + record.Release() + return nil, M.Socksaddr{}, err + } + return record, destination, nil + } +} + +var ( + _ N.PacketConn = (*clientPacketConn)(nil) + _ N.PacketReadWaiter = (*clientPacketConn)(nil) + _ N.PacketBatchWriteCreator = (*clientPacketConn)(nil) + _ N.WriterWithMTU = (*clientPacketConn)(nil) + _ N.PacketBatchWriter = (*clientPacketBatchWriter)(nil) +) diff --git a/proxy/snell/sing-snell/snellv4/record.go b/proxy/snell/sing-snell/snellv4/record.go new file mode 100644 index 00000000000..018003656d6 --- /dev/null +++ b/proxy/snell/sing-snell/snellv4/record.go @@ -0,0 +1,830 @@ +package snellv4 + +import ( + standardbufio "bufio" + "crypto/cipher" + "crypto/rand" + "encoding/binary" + "io" + "math/big" + "math/bits" + "sync" + "time" + + snell "github.com/sagernet/sing-snell" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" +) + +const ( + // Surge 6.7.0 (11520): SNConnectorEngineAdapterV4::encryptData:outData: uses these v4 frame sizing constants. + maxPayload = snell.MaxPayloadLen + frameSize = 1460 + firstRecordOverhead = 55 + resetRecordOverhead = 39 + initialPaddingMin = 0x100 + initialPaddingSpan = 0x100 + maxInitialPaddingLen = initialPaddingMin + initialPaddingSpan - 1 + // Surge 6.7.0 (11520): FUN_100012944 compares time(0) deltas against 0x1f. + framePayloadResetInterval = 31 +) + +type reader struct { + upstream io.Reader + bufferedUpstream *standardbufio.Reader + psk []byte + cipher cipher.AEAD + nonce []byte + cache *buf.Buffer + readWaitOptions N.ReadWaitOptions +} + +func (r *reader) initialize() error { + if r.cipher != nil { + return nil + } + if r.bufferedUpstream == nil { + r.bufferedUpstream = standardbufio.NewReader(r.upstream) + } + salt := make([]byte, snell.SaltLen) + _, err := io.ReadFull(r.bufferedUpstream, salt) + if err != nil { + return err + } + aead, err := snell.NewAEAD(snell.DeriveKey(r.psk, salt)) + if err != nil { + return err + } + r.cipher = aead + r.nonce = make([]byte, snell.NonceLen) + return nil +} + +func (r *reader) ReadRecord() (*buf.Buffer, error) { + err := r.initialize() + if err != nil { + return nil, err + } + headerCipher := buf.NewSize(snell.HeaderCipherLen) + _, err = headerCipher.ReadFullFrom(r.bufferedUpstream, snell.HeaderCipherLen) + if err != nil { + headerCipher.Release() + return nil, err + } + _, err = r.cipher.Open(headerCipher.Index(0), r.nonce, headerCipher.Bytes(), nil) + if err != nil { + headerCipher.Release() + return nil, E.Cause(err, "open record header") + } + snell.IncreaseNonce(r.nonce) + header := headerCipher.To(snell.HeaderPlainLen) + if header[0] != snell.HeaderVersion { + headerCipher.Release() + return nil, E.Extend(snell.ErrBadVersion, header[0]) + } + paddingLen := int(binary.BigEndian.Uint16(header[3:5])) + payloadLen := int(binary.BigEndian.Uint16(header[5:7])) + headerCipher.Release() + + if payloadLen == 0 { + // Surge 6.7.0 (11520): SNConnectorEngineAdapterV4::decryptData:outData: + // returns "ZERO CHUNK with remaining data." when the EOF record leaves + // bytes in the internal decrypt input buffer. + if r.bufferedUpstream.Buffered() > 0 { + return nil, E.New("snell: zero chunk has trailing data") + } + return nil, io.EOF + } + + var padding *buf.Buffer + if paddingLen > 0 { + padding = buf.NewSize(paddingLen) + _, err = padding.ReadFullFrom(r.bufferedUpstream, paddingLen) + if err != nil { + padding.Release() + return nil, err + } + } + body := r.readWaitOptions.NewBufferSize(payloadLen + snell.AEADTagLen) + _, err = body.ReadFullFrom(r.bufferedUpstream, payloadLen+snell.AEADTagLen) + if err != nil { + if padding != nil { + padding.Release() + } + body.Release() + return nil, err + } + if padding != nil { + paddingBytes := padding.Bytes() + payloadCipher := body.Bytes() + limit := min(len(paddingBytes), len(payloadCipher)) + for index := 0; index < limit; index += 2 { + paddingBytes[index], payloadCipher[index] = payloadCipher[index], paddingBytes[index] + } + padding.Release() + } + payloadCipher := body.Bytes() + _, err = r.cipher.Open(payloadCipher[:0], r.nonce, payloadCipher, nil) + if err != nil { + body.Release() + return nil, E.Cause(err, "open record payload") + } + snell.IncreaseNonce(r.nonce) + body.Truncate(payloadLen) + r.readWaitOptions.PostReturn(body) + return body, nil +} + +func (r *reader) NextRecord() (*buf.Buffer, error) { + record := r.cache + if record != nil { + r.cache = nil + if record.IsEmpty() { + record.Release() + } else { + return record, nil + } + } + return r.ReadRecord() +} + +func (r *reader) SetCache(cache *buf.Buffer) { + r.cache = cache +} + +func (r *reader) ReleaseCache() { + if r.cache != nil { + r.cache.Release() + r.cache = nil + } +} + +func (r *reader) Read(p []byte) (n int, err error) { + for { + if r.cache != nil { + if r.cache.IsEmpty() { + r.cache.Release() + r.cache = nil + } else { + n = copy(p, r.cache.Bytes()) + r.cache.Advance(n) + return + } + } + r.cache, err = r.ReadRecord() + if err != nil { + return + } + } +} + +func (r *reader) ReadBuffer(buffer *buf.Buffer) error { + for { + if r.cache != nil { + if r.cache.IsEmpty() { + r.cache.Release() + r.cache = nil + } else { + n, _ := buffer.Write(r.cache.Bytes()) + r.cache.Advance(n) + return nil + } + } + var err error + r.cache, err = r.ReadRecord() + if err != nil { + return err + } + } +} + +func (r *reader) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + r.readWaitOptions = options + return false +} + +func (r *reader) WaitReadBuffer() (buffer *buf.Buffer, err error) { + for { + if r.cache == nil { + return r.ReadRecord() + } + buffer = r.cache + r.cache = nil + if buffer.IsEmpty() { + buffer.Release() + continue + } + if buffer.Start() >= r.readWaitOptions.FrontHeadroom && buffer.FreeLen() >= r.readWaitOptions.RearHeadroom { + return + } + buffer = r.readWaitOptions.Copy(buffer) + return buffer, nil + } +} + +func (r *reader) Upstream() any { + return r.upstream +} + +type writer struct { + upstream io.Writer + psk []byte + cipher cipher.AEAD + nonce []byte + salt []byte + saltSent bool + initialPaddingLen int + payloadLimit int + lastWriteUnix int64 + access sync.Mutex +} + +func (w *writer) initialize() error { + if w.cipher != nil { + return nil + } + salt := make([]byte, snell.SaltLen) + _, err := io.ReadFull(rand.Reader, salt) + if err != nil { + return err + } + aead, err := snell.NewAEAD(snell.DeriveKey(w.psk, salt)) + if err != nil { + return err + } + paddingDelta, err := rand.Int(rand.Reader, big.NewInt(initialPaddingSpan)) + if err != nil { + return err + } + w.cipher = aead + w.nonce = make([]byte, snell.NonceLen) + w.salt = salt + w.initialPaddingLen = initialPaddingMin + int(paddingDelta.Int64()) + return nil +} + +func (w *writer) payloadLimitFor(nowUnix int64) int { + if !w.saltSent { + return frameSize - firstRecordOverhead - w.initialPaddingLen + } + if w.lastWriteUnix != 0 && nowUnix-w.lastWriteUnix < framePayloadResetInterval { + if w.payloadLimit > 0 { + return w.payloadLimit + } + return frameSize - resetRecordOverhead + } + return frameSize - resetRecordOverhead +} + +func (w *writer) advancePayloadLimit(payloadLimit int, nowUnix int64) { + w.lastWriteUnix = nowUnix + if payloadLimit <= maxPayload-1 { + nextPayloadLimit := payloadLimit + frameSize - resetRecordOverhead + w.payloadLimit = min(nextPayloadLimit, maxPayload) + } else { + w.payloadLimit = maxPayload + } +} + +func (w *writer) framePaddingLen(payloadLen int) int { + if w.saltSent || payloadLen == 0 { + return 0 + } + return w.initialPaddingLen +} + +func (w *writer) writeBytesLocked(p []byte) (n int, err error) { + err = w.initialize() + if err != nil { + return 0, err + } + nowUnix := time.Now().Unix() + payloadLimit := w.payloadLimitFor(nowUnix) + if payloadLimit <= 0 || payloadLimit > maxPayload { + panic("snell: invalid v4 payload limit") + } + // Surge 6.7.0 (11520): SNConnectorEngineAdapterV4::encryptData:outData: updates the next payload limit once for + // each encryptData call, splits that call using the same limit, and returns the salt plus every + // record in one output buffer that the caller writes to the socket once. + w.advancePayloadLimit(payloadLimit, nowUnix) + recordCount := (len(p) + payloadLimit - 1) / payloadLimit + totalLen := recordCount*(snell.HeaderCipherLen+snell.AEADTagLen) + len(p) + if !w.saltSent { + totalLen += snell.SaltLen + w.initialPaddingLen + } + output := buf.NewSize(totalLen) + defer output.Release() + for data := p; len(data) > 0; { + recordLen := min(len(data), payloadLimit) + err = w.sealRecordLocked(output, data[:recordLen], w.framePaddingLen(recordLen)) + if err != nil { + return 0, err + } + data = data[recordLen:] + } + _, err = w.upstream.Write(output.Bytes()) + if err != nil { + return 0, err + } + return len(p), nil +} + +func (w *writer) sealRecordLocked(output *buf.Buffer, payload []byte, paddingLen int) error { + if len(payload) > maxPayload || paddingLen > maxPayload { + panic("snell: v4 record exceeds maximum") + } + if len(payload) == 0 && paddingLen != 0 { + panic("snell: zero-length v4 record carries padding") + } + if !w.saltSent { + common.Must1(output.Write(w.salt)) + w.saltSent = true + } + header := output.Extend(snell.HeaderCipherLen) + header[0] = snell.HeaderVersion + header[1] = 0 + header[2] = 0 + binary.BigEndian.PutUint16(header[3:5], uint16(paddingLen)) + binary.BigEndian.PutUint16(header[5:7], uint16(len(payload))) + w.cipher.Seal(header[:0], w.nonce, header[:snell.HeaderPlainLen], nil) + snell.IncreaseNonce(w.nonce) + padding := output.Extend(paddingLen) + if len(payload) > 0 { + payloadCipher := output.Extend(len(payload) + snell.AEADTagLen) + copy(payloadCipher, payload) + w.cipher.Seal(payloadCipher[:0], w.nonce, payloadCipher[:len(payload)], nil) + snell.IncreaseNonce(w.nonce) + if paddingLen > 0 { + err := w.fillPadding(padding, payloadCipher) + if err != nil { + return err + } + limit := min(len(padding), len(payloadCipher)) + for index := 0; index < limit; index += 2 { + padding[index], payloadCipher[index] = payloadCipher[index], padding[index] + } + } + } + return nil +} + +func (w *writer) makeSliceRecordLocked(payload []byte, paddingLen int) (*buf.Buffer, error) { + err := w.initialize() + if err != nil { + return nil, err + } + saltLen := 0 + if !w.saltSent { + saltLen = snell.SaltLen + } + payloadCipherLen := 0 + if len(payload) > 0 { + payloadCipherLen = len(payload) + snell.AEADTagLen + } + out := buf.NewSize(saltLen + snell.HeaderCipherLen + paddingLen + payloadCipherLen) + err = w.sealRecordLocked(out, payload, paddingLen) + if err != nil { + out.Release() + return nil, err + } + return out, nil +} + +func (w *writer) writeSliceRecordLocked(payload []byte, paddingLen int) error { + out, err := w.makeSliceRecordLocked(payload, paddingLen) + if err != nil { + return err + } + return w.writeRecordLocked(out) +} + +func (w *writer) writeRecordLocked(record *buf.Buffer) error { + defer record.Release() + return common.Error(w.upstream.Write(record.Bytes())) +} + +func (w *writer) randomInt31() (int, error) { + var randomBytes [4]byte + _, err := io.ReadFull(rand.Reader, randomBytes[:]) + if err != nil { + return 0, err + } + return int(binary.LittleEndian.Uint32(randomBytes[:]) & 0x7fffffff), nil +} + +func (w *writer) makeBufferRecordLocked(buffer *buf.Buffer, paddingLen int) (*buf.Buffer, error) { + err := w.initialize() + if err != nil { + return nil, err + } + dataLen := buffer.Len() + if dataLen > maxPayload || paddingLen > maxPayload { + panic("snell: v4 record exceeds maximum") + } + if dataLen == 0 && paddingLen != 0 { + panic("snell: zero-length v4 record carries padding") + } + saltLen := 0 + if !w.saltSent { + saltLen = snell.SaltLen + } + frontLen := saltLen + snell.HeaderCipherLen + paddingLen + prefix := buffer.ExtendHeader(frontLen) + if saltLen > 0 { + copy(prefix[:saltLen], w.salt) + w.saltSent = true + } + header := prefix[saltLen : saltLen+snell.HeaderCipherLen] + header[0] = snell.HeaderVersion + header[1] = 0 + header[2] = 0 + binary.BigEndian.PutUint16(header[3:5], uint16(paddingLen)) + binary.BigEndian.PutUint16(header[5:7], uint16(dataLen)) + w.cipher.Seal(header[:0], w.nonce, header[:snell.HeaderPlainLen], nil) + snell.IncreaseNonce(w.nonce) + if dataLen > 0 { + payloadCipher := buffer.From(frontLen) + buffer.Extend(snell.AEADTagLen) + w.cipher.Seal(payloadCipher[:0], w.nonce, payloadCipher[:dataLen], nil) + snell.IncreaseNonce(w.nonce) + if paddingLen > 0 { + padding := prefix[saltLen+snell.HeaderCipherLen:] + payloadCipher = buffer.From(frontLen) + err = w.fillPadding(padding, payloadCipher) + if err != nil { + return nil, err + } + limit := min(len(padding), len(payloadCipher)) + for index := 0; index < limit; index += 2 { + padding[index], payloadCipher[index] = payloadCipher[index], padding[index] + } + } + } + return buffer, nil +} + +func (w *writer) writeBufferRecordLocked(buffer *buf.Buffer, paddingLen int) error { + record, err := w.makeBufferRecordLocked(buffer, paddingLen) + if err != nil { + return err + } + return w.writeRecordLocked(record) +} + +func (w *writer) fillPadding(padding []byte, payloadCipher []byte) error { + if len(padding) == 0 { + return nil + } + payloadOnes := 0 + // Surge 6.7.0 (11520): FUN_100012b24 counts ones only over complete + // 32-bit chunks, but computes zeros against the full payload cipher length. + payloadCountLen := len(payloadCipher) &^ 3 + for _, payloadByte := range payloadCipher[:payloadCountLen] { + payloadOnes += bits.OnesCount8(payloadByte) + } + payloadZeros := 8*len(payloadCipher) - payloadOnes + if payloadZeros <= 0 { + _, err := io.ReadFull(rand.Reader, padding) + return err + } + + ratio := float64(payloadOnes) / float64(payloadZeros) + if ratio <= 0.5 || ratio >= 1.6 { + _, err := io.ReadFull(rand.Reader, padding) + return err + } + + targetRatioBase := 1.6 + if payloadZeros < payloadOnes { + targetRatioBase = 0.4 + } + // Surge 6.7.0 (11520): FUN_100012944 derives the target padding ratio as + // base + rand()/2147483647.0/10, using the process rand() 31-bit range. + randomValue, err := w.randomInt31() + if err != nil { + return err + } + targetRatio := targetRatioBase + float64(randomValue)/2147483647.0/10 + totalBits := 8 * (len(padding) + len(payloadCipher)) + targetOnes := int(float64(totalBits)*(targetRatio/(targetRatio+1)) - float64(payloadOnes)) + if targetOnes < 0 || targetOnes > 8*len(padding) { + _, err = io.ReadFull(rand.Reader, padding) + return err + } + return w.fillPaddingWithBitCount(padding, targetOnes) +} + +func (w *writer) fillPaddingWithBitCount(padding []byte, oneBits int) error { + totalBits := 8 * len(padding) + if oneBits < 0 || oneBits > totalBits { + panic("snell: invalid padding bit count") + } + bitset := make([]byte, totalBits) + for index := range oneBits { + bitset[index] = 1 + } + // Surge 6.7.0 (11520): FUN_100012eb8 shuffles from the front and maps + // rand() with division by RAND_MAX/remaining+1, preserving its modulo bias. + position := 0 + remaining := totalBits + for remaining != 1 { + randomValue, err := w.randomInt31() + if err != nil { + return err + } + divisor := 0 + if remaining != 0 { + divisor = 0x7fffffff / remaining + } + swapIndex := position + randomValue/(divisor+1) + bitset[swapIndex], bitset[position] = bitset[position], bitset[swapIndex] + position++ + remaining-- + } + clear(padding) + for index, bit := range bitset { + if bit == 1 { + padding[index/8] |= 1 << uint(index%8) + } + } + return nil +} + +func (w *writer) Write(p []byte) (n int, err error) { + w.access.Lock() + defer w.access.Unlock() + if len(p) == 0 { + err = w.initialize() + if err != nil { + return 0, err + } + nowUnix := time.Now().Unix() + payloadLimit := w.payloadLimitFor(nowUnix) + if payloadLimit <= 0 || payloadLimit > maxPayload { + panic("snell: invalid v4 payload limit") + } + // Surge 6.7.0 (11520): FUN_100012944 advances the growth window before + // calling FUN_100012b24 even when encrypting a zero-length EOF record. + w.advancePayloadLimit(payloadLimit, nowUnix) + return 0, w.writeSliceRecordLocked(nil, 0) + } + return w.writeBytesLocked(p) +} + +func (w *writer) WriteZeroChunk() error { + w.access.Lock() + defer w.access.Unlock() + err := w.initialize() + if err != nil { + return err + } + nowUnix := time.Now().Unix() + payloadLimit := w.payloadLimitFor(nowUnix) + if payloadLimit <= 0 || payloadLimit > maxPayload { + panic("snell: invalid v4 payload limit") + } + // Surge 6.7.0 (11520): FUN_100012944 advances the growth window before + // calling FUN_100012b24 even when encrypting a zero-length EOF record. + w.advancePayloadLimit(payloadLimit, nowUnix) + return w.writeSliceRecordLocked(nil, 0) +} + +func (w *writer) WriteBuffer(buffer *buf.Buffer) error { + defer buffer.Release() + dataLen := buffer.Len() + if dataLen == 0 { + return nil + } + w.access.Lock() + defer w.access.Unlock() + err := w.initialize() + if err != nil { + return err + } + nowUnix := time.Now().Unix() + payloadLimit := w.payloadLimitFor(nowUnix) + if payloadLimit <= 0 || payloadLimit > maxPayload { + panic("snell: invalid v4 payload limit") + } + if dataLen > payloadLimit { + _, err = w.writeBytesLocked(buffer.Bytes()) + return err + } + w.advancePayloadLimit(payloadLimit, nowUnix) + paddingLen := w.framePaddingLen(dataLen) + return w.writeBufferRecordLocked(buffer, paddingLen) +} + +func (w *writer) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + upstreamWriter, created := bufio.CreateVectorisedWriter(w.upstream) + if !created { + return nil, false + } + return w.CreateVectorisedWriterFor(upstreamWriter), true +} + +func (w *writer) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { + return &vectorisedWriter{writer: w, upstream: upstream} +} + +type vectorisedWriter struct { + writer *writer + upstream N.VectorisedWriter +} + +func (w *vectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { + payloadLen := buf.LenMulti(buffers) + if payloadLen == 0 { + buf.ReleaseMulti(buffers) + return nil + } + var records []*buf.Buffer + defer func() { + buf.ReleaseMulti(records) + }() + recordWriter := w.writer + recordWriter.access.Lock() + defer recordWriter.access.Unlock() + err := recordWriter.initialize() + if err != nil { + buf.ReleaseMulti(buffers) + return err + } + nowUnix := time.Now().Unix() + payloadLimit := recordWriter.payloadLimitFor(nowUnix) + if payloadLimit <= 0 || payloadLimit > maxPayload { + panic("snell: invalid v4 payload limit") + } + // Surge 6.7.0 (11520): SNConnectorEngineAdapterV4::encryptData:outData: + // advances the growth window once per encrypt call and splits that call + // using the same payload limit. + recordWriter.advancePayloadLimit(payloadLimit, nowUnix) + index := 0 + for remainingPayload := payloadLen; remainingPayload > 0; { + for buffers[index].IsEmpty() { + buffers[index].Release() + index++ + } + recordLen := min(remainingPayload, payloadLimit) + paddingLen := recordWriter.framePaddingLen(recordLen) + buffer := buffers[index] + if buffer.Len() == recordLen { + record, err := recordWriter.makeBufferRecordLocked(buffer, paddingLen) + if err != nil { + buffer.Release() + buf.ReleaseMulti(buffers[index+1:]) + return err + } + records = append(records, record) + index++ + } else { + payload := make([]byte, recordLen) + for copiedLen := 0; copiedLen < recordLen; { + buffer = buffers[index] + if buffer.IsEmpty() { + buffer.Release() + index++ + continue + } + copyLen := min(recordLen-copiedLen, buffer.Len()) + copy(payload[copiedLen:], buffer.Bytes()[:copyLen]) + buffer.Advance(copyLen) + copiedLen += copyLen + if buffer.IsEmpty() { + buffer.Release() + index++ + } + } + record, err := recordWriter.makeSliceRecordLocked(payload, paddingLen) + if err != nil { + buf.ReleaseMulti(buffers[index:]) + return err + } + records = append(records, record) + } + remainingPayload -= recordLen + } + buf.ReleaseMulti(buffers[index:]) + flushRecords := records + records = nil + return w.upstream.WriteVectorised(flushRecords) +} + +func (w *writer) CreatePacketVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { + return &packetVectorisedWriter{writer: w, upstream: upstream} +} + +type packetVectorisedWriter struct { + writer *writer + upstream N.VectorisedWriter +} + +func (w *packetVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { + var records []*buf.Buffer + defer func() { + buf.ReleaseMulti(records) + }() + recordWriter := w.writer + recordWriter.access.Lock() + defer recordWriter.access.Unlock() + err := recordWriter.initialize() + if err != nil { + buf.ReleaseMulti(buffers) + return err + } + for index, buffer := range buffers { + if buffer.IsEmpty() { + buffer.Release() + continue + } + nowUnix := time.Now().Unix() + payloadLimit := recordWriter.payloadLimitFor(nowUnix) + if payloadLimit <= 0 || payloadLimit > maxPayload { + panic("snell: invalid v4 payload limit") + } + dataLen := buffer.Len() + if dataLen > payloadLimit { + buffer.Release() + buf.ReleaseMulti(buffers[index+1:]) + return snell.ErrPayloadTooLarge + } + recordWriter.advancePayloadLimit(payloadLimit, nowUnix) + paddingLen := recordWriter.framePaddingLen(dataLen) + record, err := recordWriter.makeBufferRecordLocked(buffer, paddingLen) + if err != nil { + buffer.Release() + buf.ReleaseMulti(buffers[index+1:]) + return err + } + records = append(records, record) + } + if len(records) == 0 { + return nil + } + flushRecords := records + records = nil + return w.upstream.WriteVectorised(flushRecords) +} + +func (w *writer) WritePacketBuffer(buffer *buf.Buffer) error { + defer buffer.Release() + dataLen := buffer.Len() + w.access.Lock() + defer w.access.Unlock() + err := w.initialize() + if err != nil { + return err + } + nowUnix := time.Now().Unix() + payloadLimit := w.payloadLimitFor(nowUnix) + if payloadLimit <= 0 || payloadLimit > maxPayload { + panic("snell: invalid v4 payload limit") + } + if dataLen > payloadLimit { + return snell.ErrPayloadTooLarge + } + w.advancePayloadLimit(payloadLimit, nowUnix) + paddingLen := w.framePaddingLen(dataLen) + frontHeadroom := snell.HeaderCipherLen + paddingLen + if !w.saltSent { + frontHeadroom += snell.SaltLen + } + if buffer.Start() < frontHeadroom || buffer.FreeLen() < snell.AEADTagLen { + return w.writeSliceRecordLocked(buffer.Bytes(), paddingLen) + } + return w.writeBufferRecordLocked(buffer, paddingLen) +} + +func (w *writer) FrontHeadroom() int { + if w.saltSent { + return snell.HeaderCipherLen + } + return snell.SaltLen + snell.HeaderCipherLen + maxInitialPaddingLen +} + +func (w *writer) RearHeadroom() int { + return snell.AEADTagLen +} + +func (w *writer) WriterMTU() int { + return maxPayload +} + +func (w *writer) Upstream() any { + return w.upstream +} + +var ( + _ N.ExtendedReader = (*reader)(nil) + _ N.ReadWaiter = (*reader)(nil) + _ N.ExtendedWriter = (*writer)(nil) + _ N.VectorisedWriteCreator = (*writer)(nil) + _ N.VectorisedWriter = (*vectorisedWriter)(nil) + _ N.VectorisedWriter = (*packetVectorisedWriter)(nil) + _ N.FrontHeadroom = (*writer)(nil) + _ N.RearHeadroom = (*writer)(nil) + _ N.WriterWithMTU = (*writer)(nil) +) diff --git a/proxy/snell/sing-snell/snellv4/reuse.go b/proxy/snell/sing-snell/snellv4/reuse.go new file mode 100644 index 00000000000..8d780a70842 --- /dev/null +++ b/proxy/snell/sing-snell/snellv4/reuse.go @@ -0,0 +1,462 @@ +package snellv4 + +import ( + "context" + "errors" + "io" + "net" + "sync" + "sync/atomic" + "time" + + snell "github.com/sagernet/sing-snell" + "github.com/sagernet/sing-snell/internal/reuse" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func (c *Client) DialContext(ctx context.Context, destination M.Socksaddr) (net.Conn, error) { + if c.reuse { + session, err := c.reuseSession(ctx) + if err != nil { + return nil, err + } + conn, err := session.DialConn(destination) + if err != nil { + session.Close() + return nil, err + } + return conn, nil + } + if c.pool.IsClosed() { + return nil, net.ErrClosed + } + if c.dialer == nil { + return nil, E.New("snell: missing dialer") + } + conn, err := c.dialer.DialContext(ctx, N.NetworkTCP, c.server) + if err != nil { + return nil, err + } + proxyConn, err := c.DialConn(conn, destination) + if err != nil { + conn.Close() + return nil, err + } + return proxyConn, nil +} + +func (c *Client) reuseSession(ctx context.Context) (*reuseSession, error) { + session, found, closed := c.pool.Take() + if closed { + return nil, net.ErrClosed + } + if c.dialer == nil { + return nil, E.New("snell: missing dialer") + } + if found { + return session, nil + } + conn, err := c.dialer.DialContext(ctx, N.NetworkTCP, c.server) + if err != nil { + return nil, err + } + if c.pool.IsClosed() { + conn.Close() + return nil, net.ErrClosed + } + session = c.newReuseSession(conn) + session.state.Store(uint32(reuse.StateActive)) + return session, nil +} + +func (c *Client) Close() error { + return c.pool.Close() +} + +type reuseSession struct { + net.Conn + client *Client + + state atomic.Uint32 + reader *reader + writer *writer +} + +func (c *Client) newReuseSession(conn net.Conn) *reuseSession { + return &reuseSession{Conn: c.obfs.ClientConn(conn), client: c} +} + +func (s *reuseSession) ReuseState() *atomic.Uint32 { + return &s.state +} + +func (s *reuseSession) DialConn(destination M.Socksaddr) (net.Conn, error) { + state := reuse.State(s.state.Load()) + if state == reuse.StateClosed { + return nil, net.ErrClosed + } + if state != reuse.StateActive { + return nil, E.New("snell: reuse session is busy") + } + + requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: s.client.userKey, Destination: destination} + request := buf.NewSize(requestPayload.Len()) + err := requestPayload.Write(request) + if err != nil { + request.Release() + s.Release(false) + return nil, err + } + if s.writer == nil { + s.writer = &writer{ + upstream: s.Conn, + psk: s.client.psk, + } + } + _, err = s.writer.Write(request.Bytes()) + request.Release() + if err != nil { + s.Release(false) + return nil, E.Cause(err, "write request") + } + return &reuseConn{Conn: s.Conn, session: s, destination: destination}, nil +} + +func (s *reuseSession) Release(reusable bool) { + if !reusable { + s.Close() + return + } + if !s.state.CompareAndSwap(uint32(reuse.StateActive), uint32(reuse.StateReady)) { + if reuse.State(s.state.Load()) != reuse.StateClosed { + s.Close() + } + return + } + s.client.pool.MoveToPool(s, reuse.StateReady, false) +} + +func (s *reuseSession) Close() error { + if reuse.State(s.state.Swap(uint32(reuse.StateClosed))) == reuse.StateClosed { + return nil + } + return s.Conn.Close() +} + +func (s *reuseSession) startDrain() { + if !s.state.CompareAndSwap(uint32(reuse.StateActive), uint32(reuse.StateWaiting)) { + if reuse.State(s.state.Load()) != reuse.StateClosed { + s.Close() + } + return + } + if !s.client.pool.MoveToPool(s, reuse.StateWaiting, true) { + return + } + go s.drain() +} + +func (s *reuseSession) drain() { + defer s.client.pool.DrainDone() + var discarded int + for { + record, err := s.reader.NextRecord() + if errors.Is(err, io.EOF) { + s.state.CompareAndSwap(uint32(reuse.StateWaiting), uint32(reuse.StateReady)) + return + } + if err != nil { + s.Close() + return + } + // Surge 6.7.0 (11520): SNConnectorV4::socket:didReadData: discards data + // received while waiting for server EOF and closes after 0x80001 bytes. + discarded += record.Len() + record.Release() + if discarded >= reuse.WaitingDiscardLimit { + s.Close() + return + } + } +} + +type reuseConn struct { + net.Conn + session *reuseSession + destination M.Socksaddr + + closeWriteOnce sync.Once + closeWriteErr error + closeOnce sync.Once + closeErr error + replyRead atomic.Bool + readWaitOptions N.ReadWaitOptions + closed atomic.Bool + readActionCount atomic.Int32 + readClosed atomic.Bool +} + +func (c *reuseConn) readResponse() error { + if c.replyRead.Load() { + return nil + } + if c.session.reader == nil { + c.session.reader = &reader{upstream: c.session.Conn, psk: c.session.client.psk} + c.session.reader.InitializeReadWaiter(c.readWaitOptions) + } + record, err := c.session.reader.ReadRecord() + if err != nil { + return E.Cause(err, "read reply") + } + cached, err := reuse.ParseReply(record) + if err != nil { + return err + } + if cached != nil { + c.session.reader.SetCache(cached) + } + c.replyRead.Store(true) + return nil +} + +func (c *reuseConn) Read(p []byte) (int, error) { + if c.closed.Load() { + return 0, net.ErrClosed + } + c.readActionCount.Add(1) + defer c.readActionCount.Add(-1) + if c.closed.Load() { + return 0, net.ErrClosed + } + err := c.readResponse() + if err != nil { + return 0, err + } + var discarded int + for { + n, err := c.session.reader.Read(p) + if c.closed.Load() { + discarded += n + if errors.Is(err, io.EOF) { + c.readClosed.Store(true) + c.session.state.CompareAndSwap(uint32(reuse.StateWaiting), uint32(reuse.StateReady)) + return 0, err + } + if err != nil { + c.session.Close() + return 0, err + } + if discarded >= reuse.WaitingDiscardLimit { + c.session.Close() + return 0, E.New("snell: read too much data after client EOF") + } + continue + } + if errors.Is(err, io.EOF) { + c.readClosed.Store(true) + } + return n, err + } +} + +func (c *reuseConn) ReadBuffer(buffer *buf.Buffer) error { + if c.closed.Load() { + return net.ErrClosed + } + c.readActionCount.Add(1) + defer c.readActionCount.Add(-1) + if c.closed.Load() { + return net.ErrClosed + } + err := c.readResponse() + if err != nil { + return err + } + var discarded int + for { + bufferLen := buffer.Len() + err = c.session.reader.ReadBuffer(buffer) + if c.closed.Load() { + discarded += buffer.Len() - bufferLen + buffer.Truncate(bufferLen) + if errors.Is(err, io.EOF) { + c.readClosed.Store(true) + c.session.state.CompareAndSwap(uint32(reuse.StateWaiting), uint32(reuse.StateReady)) + return err + } + if err != nil { + c.session.Close() + return err + } + if discarded >= reuse.WaitingDiscardLimit { + c.session.Close() + return E.New("snell: read too much data after client EOF") + } + continue + } + if errors.Is(err, io.EOF) { + c.readClosed.Store(true) + } + return err + } +} + +func (c *reuseConn) Write(p []byte) (int, error) { + return c.session.writer.Write(p) +} + +func (c *reuseConn) WriteBuffer(buffer *buf.Buffer) error { + return c.session.writer.WriteBuffer(buffer) +} + +func (c *reuseConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + return c.session.writer.CreateVectorisedWriter() +} + +func (c *reuseConn) CloseWrite() error { + c.closeWriteOnce.Do(func() { + c.closeWriteErr = c.session.writer.WriteZeroChunk() + }) + return c.closeWriteErr +} + +func (c *reuseConn) Close() error { + c.closeOnce.Do(func() { + c.closed.Store(true) + if !c.replyRead.Load() { + c.session.Release(false) + return + } + c.closeErr = c.CloseWrite() + if c.closeErr != nil { + c.session.Release(false) + return + } + c.session.Conn.SetReadDeadline(time.Time{}) + c.session.Conn.SetWriteDeadline(time.Time{}) + if c.readClosed.Load() { + c.session.Release(true) + return + } + // Surge 6.7.0 (11520): SNConnectorV4::readServerEOFIfNotInReadState: starts waiting-state EOF + // reads when _socketReadActionCounter is 0. + if c.readActionCount.Load() > 0 { + if !c.session.state.CompareAndSwap(uint32(reuse.StateActive), uint32(reuse.StateWaiting)) { + if reuse.State(c.session.state.Load()) != reuse.StateClosed { + c.session.Close() + } + return + } + poolState := reuse.StateWaiting + if c.readClosed.Load() { + c.session.state.CompareAndSwap(uint32(reuse.StateWaiting), uint32(reuse.StateReady)) + poolState = reuse.StateReady + } + if !c.session.client.pool.MoveToPool(c.session, poolState, false) { + return + } + return + } + c.session.startDrain() + }) + return c.closeErr +} + +func (c *reuseConn) CreateReadWaiter() (N.ReadWaiter, bool) { + return &reuseReadWaiter{conn: c}, true +} + +type reuseReadWaiter struct { + conn *reuseConn +} + +func (w *reuseReadWaiter) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + w.conn.readWaitOptions = options + if w.conn.session.reader != nil { + w.conn.session.reader.InitializeReadWaiter(options) + } + return false +} + +func (w *reuseReadWaiter) WaitReadBuffer() (*buf.Buffer, error) { + if w.conn.closed.Load() { + return nil, net.ErrClosed + } + w.conn.readActionCount.Add(1) + defer w.conn.readActionCount.Add(-1) + if w.conn.closed.Load() { + return nil, net.ErrClosed + } + err := w.conn.readResponse() + if err != nil { + return nil, err + } + var discarded int + for { + buffer, err := w.conn.session.reader.WaitReadBuffer() + if w.conn.closed.Load() { + if buffer != nil { + discarded += buffer.Len() + buffer.Release() + } + if errors.Is(err, io.EOF) { + w.conn.readClosed.Store(true) + w.conn.session.state.CompareAndSwap(uint32(reuse.StateWaiting), uint32(reuse.StateReady)) + return nil, err + } + if err != nil { + w.conn.session.Close() + return nil, err + } + if discarded >= reuse.WaitingDiscardLimit { + w.conn.session.Close() + return nil, E.New("snell: read too much data after client EOF") + } + continue + } + if errors.Is(err, io.EOF) { + w.conn.readClosed.Store(true) + } + return buffer, err + } +} + +func (c *reuseConn) FrontHeadroom() int { + return c.session.writer.FrontHeadroom() +} + +func (c *reuseConn) RearHeadroom() int { + return snell.AEADTagLen +} + +func (c *reuseConn) WriterMTU() int { + return maxPayload +} + +func (c *reuseConn) NeedHandshakeForRead() bool { + return !c.replyRead.Load() +} + +func (c *reuseConn) NeedAdditionalReadDeadline() bool { + return !c.replyRead.Load() +} + +func (c *reuseConn) Upstream() any { + return c.session.Conn +} + +func (c *reuseConn) RemoteAddr() net.Addr { + return c.destination.TCPAddr() +} + +var ( + _ N.ExtendedConn = (*reuseConn)(nil) + _ N.ReadWaitCreator = (*reuseConn)(nil) + _ N.VectorisedWriteCreator = (*reuseConn)(nil) + _ N.EarlyReader = (*reuseConn)(nil) + _ N.WriteCloser = (*reuseConn)(nil) + _ N.ReadWaiter = (*reuseReadWaiter)(nil) +) diff --git a/proxy/snell/sing-snell/snellv5/packet.go b/proxy/snell/sing-snell/snellv5/packet.go new file mode 100644 index 00000000000..4bcc1d18563 --- /dev/null +++ b/proxy/snell/sing-snell/snellv5/packet.go @@ -0,0 +1,289 @@ +package snellv5 + +import ( + "crypto/rand" + "encoding/binary" + "io" + "net" + "net/netip" + "os" + "sync" + + snell "github.com/sagernet/sing-snell" + "github.com/sagernet/sing-snell/internal/reuse" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +const ( + maxUDPResponseHeaderLen = 1 + 16 + 2 + minUDPPayloadLimit = framePayloadStep +) + +type serverPacketConn struct { + net.Conn + service *Service + reader reuse.RecordReader + readWaitOptions N.ReadWaitOptions + + writeAccess sync.Mutex + writer reuse.RecordWriter +} + +func (c *serverPacketConn) responseAddrLen(source M.Socksaddr) int { + if source.Unwrap().Addr.Is4() { + return 1 + 4 + 2 + } + return 1 + 16 + 2 +} + +func (c *serverPacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) { + record, err := c.reader.NextRecord() + if err != nil { + return M.Socksaddr{}, err + } + command, err := record.ReadByte() + if err != nil { + record.Release() + return M.Socksaddr{}, err + } + if command != snell.UDPCommandForward { + record.Release() + return M.Socksaddr{}, E.Extend(snell.ErrUnsupportedCommand, "udp ", command) + } + destination, err := c.readRequestPacketDestination(record) + if err != nil { + record.Release() + return M.Socksaddr{}, err + } + if record.Len() > buffer.FreeLen() { + record.Release() + return M.Socksaddr{}, io.ErrShortBuffer + } + _, err = buffer.Write(record.Bytes()) + record.Release() + if err != nil { + return M.Socksaddr{}, err + } + return destination, nil +} + +func (c *serverPacketConn) readRequestPacketDestination(record *buf.Buffer) (M.Socksaddr, error) { + first, err := record.ReadByte() + if err != nil { + return M.Socksaddr{}, err + } + if first != 0x00 { + var host []byte + host, err = record.ReadBytes(int(first)) + if err != nil { + return M.Socksaddr{}, err + } + var portBytes []byte + portBytes, err = record.ReadBytes(2) + if err != nil { + return M.Socksaddr{}, err + } + return M.ParseSocksaddrHostPort(string(host), binary.BigEndian.Uint16(portBytes)), nil + } + addressFamily, err := record.ReadByte() + if err != nil { + return M.Socksaddr{}, err + } + switch addressFamily { + case snell.AddressTypeIPv4: + var addressBytes []byte + addressBytes, err = record.ReadBytes(4) + if err != nil { + return M.Socksaddr{}, err + } + var portBytes []byte + portBytes, err = record.ReadBytes(2) + if err != nil { + return M.Socksaddr{}, err + } + var address [4]byte + copy(address[:], addressBytes) + return M.SocksaddrFrom(netip.AddrFrom4(address), binary.BigEndian.Uint16(portBytes)), nil + case snell.AddressTypeIPv6: + var addressBytes []byte + addressBytes, err = record.ReadBytes(16) + if err != nil { + return M.Socksaddr{}, err + } + var portBytes []byte + portBytes, err = record.ReadBytes(2) + if err != nil { + return M.Socksaddr{}, err + } + var address [16]byte + copy(address[:], addressBytes) + return M.SocksaddrFrom(netip.AddrFrom16(address), binary.BigEndian.Uint16(portBytes)).Unwrap(), nil + default: + // snell-server v5.0.1: FUN_00140a50 consumes the 0x00 marker and the + // unknown address-family byte, then forwards the rest as payload with an + // empty destination instead of aborting the UDP tunnel. + return M.Socksaddr{}, nil + } +} + +func (c *serverPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { + c.writeAccess.Lock() + if c.writer == nil { + err := c.writeTunnelReply() + if err != nil { + c.writeAccess.Unlock() + buffer.Release() + return err + } + } + recordWriter := c.writer + c.writeAccess.Unlock() + header := buf.With(buffer.ExtendHeader(c.responseAddrLen(destination))) + err := snell.WriteUDPResponseAddress(header, destination) + if err != nil { + buffer.Release() + return err + } + if buffer.Len() > maxPayload { + buffer.Release() + return snell.ErrPayloadTooLarge + } + return recordWriter.WritePacketBuffer(buffer) +} + +func (c *serverPacketConn) CreatePacketBatchWriter() (N.PacketBatchWriter, bool) { + upstreamWriter, created := bufio.CreateVectorisedWriter(c.Conn) + if !created { + return nil, false + } + return &serverPacketBatchWriter{conn: c, upstream: upstreamWriter}, true +} + +type serverPacketBatchWriter struct { + conn *serverPacketConn + upstream N.VectorisedWriter + + access sync.Mutex + writer N.VectorisedWriter +} + +func (w *serverPacketBatchWriter) WritePacketBatch(buffers []*buf.Buffer, destinations []M.Socksaddr) error { + if len(buffers) == 0 || len(buffers) != len(destinations) { + buf.ReleaseMulti(buffers) + return os.ErrInvalid + } + w.access.Lock() + if w.writer == nil { + w.conn.writeAccess.Lock() + if w.conn.writer == nil { + err := w.conn.writeTunnelReply() + if err != nil { + w.conn.writeAccess.Unlock() + w.access.Unlock() + buf.ReleaseMulti(buffers) + return err + } + } + w.writer = w.conn.writer.CreatePacketVectorisedWriterFor(w.upstream) + w.conn.writeAccess.Unlock() + } + recordWriter := w.writer + w.access.Unlock() + for index, buffer := range buffers { + header := buf.With(buffer.ExtendHeader(w.conn.responseAddrLen(destinations[index]))) + err := snell.WriteUDPResponseAddress(header, destinations[index]) + if err != nil { + buf.ReleaseMulti(buffers) + return err + } + } + return recordWriter.WriteVectorised(buffers) +} + +func (c *serverPacketConn) writeTunnelReply() error { + reply := [1]byte{snell.ReplyTunnel} + if c.writer != nil { + _, err := c.writer.Write(reply[:]) + if err != nil { + return E.Cause(err, "write udp reply") + } + return nil + } + salt := make([]byte, snell.SaltLen) + _, err := io.ReadFull(rand.Reader, salt) + if err != nil { + return err + } + key := snell.DeriveKey(c.service.psk, salt) + aead, err := snell.NewAEAD(key) + if err != nil { + return err + } + nonce := make([]byte, snell.NonceLen) + recordWriter, err := newWriter(c.Conn, aead, nonce) + if err != nil { + return err + } + err = recordWriter.WriteFirst(salt, reply[:]) + if err != nil { + return E.Cause(err, "write udp reply") + } + c.writer = recordWriter + return nil +} + +func (c *serverPacketConn) FrontHeadroom() int { + return snell.HeaderCipherLen + maxUDPResponseHeaderLen +} + +func (c *serverPacketConn) RearHeadroom() int { + return snell.AEADTagLen +} + +func (c *serverPacketConn) WriterMTU() int { + return minUDPPayloadLimit - maxUDPResponseHeaderLen +} + +func (c *serverPacketConn) Upstream() any { + return c.Conn +} + +func (c *serverPacketConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + c.readWaitOptions = options + c.reader.InitializeReadWaiter(options) + return false +} + +func (c *serverPacketConn) WaitReadPacket() (*buf.Buffer, M.Socksaddr, error) { + record, err := c.reader.WaitReadBuffer() + if err != nil { + return nil, M.Socksaddr{}, err + } + command, err := record.ReadByte() + if err != nil { + record.Release() + return nil, M.Socksaddr{}, err + } + if command != snell.UDPCommandForward { + record.Release() + return nil, M.Socksaddr{}, E.Extend(snell.ErrUnsupportedCommand, "udp ", command) + } + destination, err := c.readRequestPacketDestination(record) + if err != nil { + record.Release() + return nil, M.Socksaddr{}, err + } + return record, destination, nil +} + +var ( + _ N.PacketConn = (*serverPacketConn)(nil) + _ N.PacketReadWaiter = (*serverPacketConn)(nil) + _ N.PacketBatchWriteCreator = (*serverPacketConn)(nil) + _ N.WriterWithMTU = (*serverPacketConn)(nil) + _ N.PacketBatchWriter = (*serverPacketBatchWriter)(nil) +) diff --git a/proxy/snell/sing-snell/snellv5/record.go b/proxy/snell/sing-snell/snellv5/record.go new file mode 100644 index 00000000000..251c2e5f13e --- /dev/null +++ b/proxy/snell/sing-snell/snellv5/record.go @@ -0,0 +1,790 @@ +package snellv5 + +import ( + "crypto/cipher" + "crypto/rand" + "encoding/binary" + "io" + "math/big" + "math/bits" + "sync" + "time" + + snell "github.com/sagernet/sing-snell" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" +) + +const ( + // snell-server v5.0.1: FUN_0013bbe0 calls getsockopt(TCP_MAXSEG) on the + // accepted client socket but, on the normal success path, passes 0x5b4 to + // FUN_00139080; FUN_00139670 then computes the first payload limit as + // frameSize - 0x37 - paddingLen, and uses frameSize - 0x27 for growth. + maxPayload = snell.MaxPayloadLen + frameSize = 0x5b4 + firstRecordOverhead = 0x37 + resetRecordOverhead = 0x27 + initialPaddingMin = 0x100 + initialPaddingSpan = 0x100 + maxInitialPaddingLen = initialPaddingMin + initialPaddingSpan - 1 + framePayloadStep = frameSize - resetRecordOverhead + framePayloadResetInterval = 31 +) + +type reader struct { + upstream io.Reader + cipher cipher.AEAD + nonce []byte + cache *buf.Buffer + readWaitOptions N.ReadWaitOptions +} + +func newReader(upstream io.Reader, aead cipher.AEAD, nonce []byte) *reader { + return &reader{upstream: upstream, cipher: aead, nonce: nonce} +} + +func (r *reader) ReadRecord() (*buf.Buffer, error) { + headerCipher := buf.NewSize(snell.HeaderCipherLen) + _, err := headerCipher.ReadFullFrom(r.upstream, snell.HeaderCipherLen) + if err != nil { + headerCipher.Release() + return nil, err + } + _, err = r.cipher.Open(headerCipher.Index(0), r.nonce, headerCipher.Bytes(), nil) + if err != nil { + headerCipher.Release() + return nil, E.Cause(err, "open record header") + } + snell.IncreaseNonce(r.nonce) + header := headerCipher.To(snell.HeaderPlainLen) + if header[0] != snell.HeaderVersion { + headerCipher.Release() + return nil, E.Extend(snell.ErrBadVersion, header[0]) + } + paddingLen := int(binary.BigEndian.Uint16(header[3:5])) + payloadLen := int(binary.BigEndian.Uint16(header[5:7])) + headerCipher.Release() + + if payloadLen == 0 { + return nil, io.EOF + } + + var padding *buf.Buffer + if paddingLen > 0 { + padding = buf.NewSize(paddingLen) + _, err = padding.ReadFullFrom(r.upstream, paddingLen) + if err != nil { + padding.Release() + return nil, err + } + } + body := r.readWaitOptions.NewBufferSize(payloadLen + snell.AEADTagLen) + _, err = body.ReadFullFrom(r.upstream, payloadLen+snell.AEADTagLen) + if err != nil { + if padding != nil { + padding.Release() + } + body.Release() + return nil, err + } + if padding != nil { + paddingBytes := padding.Bytes() + payloadCipher := body.Bytes() + limit := min(len(payloadCipher), len(paddingBytes)) + for index := 0; index < limit; index += 2 { + paddingBytes[index], payloadCipher[index] = payloadCipher[index], paddingBytes[index] + } + padding.Release() + } + payloadCipher := body.Bytes() + _, err = r.cipher.Open(payloadCipher[:0], r.nonce, payloadCipher, nil) + if err != nil { + body.Release() + return nil, E.Cause(err, "open record payload") + } + snell.IncreaseNonce(r.nonce) + body.Truncate(payloadLen) + r.readWaitOptions.PostReturn(body) + return body, nil +} + +func (r *reader) NextRecord() (*buf.Buffer, error) { + record := r.cache + if record != nil { + r.cache = nil + if record.IsEmpty() { + record.Release() + } else { + return record, nil + } + } + return r.ReadRecord() +} + +func (r *reader) SetCache(cache *buf.Buffer) { + r.cache = cache +} + +func (r *reader) ReleaseCache() { + if r.cache != nil { + r.cache.Release() + r.cache = nil + } +} + +func (r *reader) Read(p []byte) (n int, err error) { + for { + if r.cache != nil { + if r.cache.IsEmpty() { + r.cache.Release() + r.cache = nil + } else { + n = copy(p, r.cache.Bytes()) + r.cache.Advance(n) + return + } + } + r.cache, err = r.ReadRecord() + if err != nil { + return + } + } +} + +func (r *reader) ReadBuffer(buffer *buf.Buffer) error { + for { + if r.cache != nil { + if r.cache.IsEmpty() { + r.cache.Release() + r.cache = nil + } else { + n, _ := buffer.Write(r.cache.Bytes()) + r.cache.Advance(n) + return nil + } + } + var err error + r.cache, err = r.ReadRecord() + if err != nil { + return err + } + } +} + +func (r *reader) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + r.readWaitOptions = options + return false +} + +func (r *reader) WaitReadBuffer() (buffer *buf.Buffer, err error) { + for { + if r.cache == nil { + return r.ReadRecord() + } + buffer = r.cache + r.cache = nil + if buffer.IsEmpty() { + buffer.Release() + continue + } + if buffer.Start() >= r.readWaitOptions.FrontHeadroom && buffer.FreeLen() >= r.readWaitOptions.RearHeadroom { + return + } + buffer = r.readWaitOptions.Copy(buffer) + return buffer, nil + } +} + +func (r *reader) Upstream() any { + return r.upstream +} + +type writer struct { + upstream io.Writer + cipher cipher.AEAD + nonce []byte + access sync.Mutex + lastFrameUnix int64 + framePayloadLen int + initialPaddingLen int +} + +func newWriter(upstream io.Writer, aead cipher.AEAD, nonce []byte) (*writer, error) { + paddingDelta, err := rand.Int(rand.Reader, big.NewInt(initialPaddingSpan)) + if err != nil { + return nil, err + } + return &writer{ + upstream: upstream, + cipher: aead, + nonce: nonce, + initialPaddingLen: initialPaddingMin + int(paddingDelta.Int64()), + }, nil +} + +func (w *writer) sealHeader(header []byte, paddingLen int, payloadLen int) { + header[0] = snell.HeaderVersion + header[1] = 0 + header[2] = 0 + binary.BigEndian.PutUint16(header[3:5], uint16(paddingLen)) + binary.BigEndian.PutUint16(header[5:7], uint16(payloadLen)) + w.cipher.Seal(header[:0], w.nonce, header[:snell.HeaderPlainLen], nil) + snell.IncreaseNonce(w.nonce) +} + +func (w *writer) nextPayloadLimit() int { + now := time.Now().Unix() + var payloadLimit int + if w.lastFrameUnix == 0 { + payloadLimit = frameSize - firstRecordOverhead - w.initialPaddingLen + } else if now-w.lastFrameUnix < framePayloadResetInterval { + payloadLimit = w.framePayloadLen + if payloadLimit == 0 { + payloadLimit = framePayloadStep + } + } else { + payloadLimit = framePayloadStep + } + w.lastFrameUnix = now + w.markPayloadLimitUsed(payloadLimit) + return payloadLimit +} + +func (w *writer) markPayloadLimitUsed(payloadLimit int) { + nextPayloadLimit := min(payloadLimit+framePayloadStep, maxPayload) + w.framePayloadLen = nextPayloadLimit + if w.lastFrameUnix == 0 { + w.lastFrameUnix = time.Now().Unix() + } +} + +func (w *writer) randomInt31() (int, error) { + var randomBytes [4]byte + _, err := io.ReadFull(rand.Reader, randomBytes[:]) + if err != nil { + return 0, err + } + return int(binary.LittleEndian.Uint32(randomBytes[:]) & 0x7fffffff), nil +} + +func (w *writer) Write(p []byte) (n int, err error) { + w.access.Lock() + defer w.access.Unlock() + payloadLimit := w.nextPayloadLimit() + if len(p) == 0 { + return 0, nil + } + // snell-server v5.0.1: FUN_00139670 encrypts every record of one encrypt call + // into a single malloc'd output buffer that the caller writes to the socket once. + recordCount := (len(p) + payloadLimit - 1) / payloadLimit + output := buf.NewSize(recordCount*(snell.HeaderCipherLen+snell.AEADTagLen) + len(p)) + defer output.Release() + for data := p; len(data) > 0; { + recordLen := min(len(data), payloadLimit) + err = w.sealRecordLocked(output, data[:recordLen], 0) + if err != nil { + return 0, err + } + data = data[recordLen:] + } + _, err = w.upstream.Write(output.Bytes()) + if err != nil { + return 0, err + } + return len(p), nil +} + +func (w *writer) WriteFirst(salt []byte, payload []byte) error { + w.access.Lock() + defer w.access.Unlock() + // snell-server v5.0.1: FUN_0013f020 prefixes the response byte and then calls + // FUN_00139670 once; that call reuses one payload limit for every emitted chunk + // and returns the salt plus all records in one buffer written to the socket once. + payloadLimit := w.nextPayloadLimit() + if len(payload) == 0 { + record := buf.NewSize(len(salt) + snell.HeaderCipherLen) + common.Must1(record.Write(salt)) + w.sealHeader(record.Extend(snell.HeaderCipherLen), 0, 0) + err := common.Error(w.upstream.Write(record.Bytes())) + record.Release() + return err + } + recordCount := (len(payload) + payloadLimit - 1) / payloadLimit + output := buf.NewSize(len(salt) + w.initialPaddingLen + recordCount*(snell.HeaderCipherLen+snell.AEADTagLen) + len(payload)) + defer output.Release() + common.Must1(output.Write(salt)) + paddingLen := w.initialPaddingLen + for data := payload; len(data) > 0; { + recordLen := min(len(data), payloadLimit) + err := w.sealRecordLocked(output, data[:recordLen], paddingLen) + if err != nil { + return err + } + paddingLen = 0 + data = data[recordLen:] + } + return common.Error(w.upstream.Write(output.Bytes())) +} + +func (w *writer) WriteFirstVectorised(salt []byte, buffers []*buf.Buffer, upstream N.VectorisedWriter) error { + payloadLen := buf.LenMulti(buffers) + var records []*buf.Buffer + defer func() { + buf.ReleaseMulti(records) + }() + w.access.Lock() + defer w.access.Unlock() + payloadLimit := w.nextPayloadLimit() + if payloadLen == 0 { + buf.ReleaseMulti(buffers) + record := buf.NewSize(len(salt) + snell.HeaderCipherLen) + common.Must1(record.Write(salt)) + w.sealHeader(record.Extend(snell.HeaderCipherLen), 0, 0) + records = append(records, record) + flushRecords := records + records = nil + return upstream.WriteVectorised(flushRecords) + } + saltPrefix := salt + paddingLen := w.initialPaddingLen + index := 0 + // snell-server v5.0.1: FUN_00139670 advances the growth window once per + // encrypt call and splits that call using the same payload limit. + for remainingPayload := payloadLen; remainingPayload > 0; { + for buffers[index].IsEmpty() { + buffers[index].Release() + index++ + } + recordLen := min(remainingPayload, payloadLimit) + buffer := buffers[index] + if len(saltPrefix) == 0 && paddingLen == 0 && buffer.Len() == recordLen { + record, err := w.makeBufferRecordLocked(buffer, 0) + if err != nil { + buffer.Release() + buf.ReleaseMulti(buffers[index+1:]) + return err + } + records = append(records, record) + index++ + } else { + payload := make([]byte, recordLen) + for copiedLen := 0; copiedLen < recordLen; { + buffer = buffers[index] + if buffer.IsEmpty() { + buffer.Release() + index++ + continue + } + copyLen := min(recordLen-copiedLen, buffer.Len()) + copy(payload[copiedLen:], buffer.Bytes()[:copyLen]) + buffer.Advance(copyLen) + copiedLen += copyLen + if buffer.IsEmpty() { + buffer.Release() + index++ + } + } + record := buf.NewSize(len(saltPrefix) + snell.HeaderCipherLen + paddingLen + recordLen + snell.AEADTagLen) + common.Must1(record.Write(saltPrefix)) + err := w.sealRecordLocked(record, payload, paddingLen) + if err != nil { + record.Release() + buf.ReleaseMulti(buffers[index:]) + return err + } + records = append(records, record) + } + remainingPayload -= recordLen + if len(saltPrefix) > 0 { + saltPrefix = nil + } + paddingLen = 0 + } + buf.ReleaseMulti(buffers[index:]) + flushRecords := records + records = nil + return upstream.WriteVectorised(flushRecords) +} + +func (w *writer) sealRecordLocked(output *buf.Buffer, payload []byte, paddingLen int) error { + if len(payload) > maxPayload || paddingLen > maxPayload { + panic("snell: v5 record exceeds maximum") + } + if len(payload) == 0 && paddingLen != 0 { + panic("snell: zero-length v5 record carries padding") + } + w.sealHeader(output.Extend(snell.HeaderCipherLen), paddingLen, len(payload)) + padding := output.Extend(paddingLen) + payloadCipher := output.Extend(len(payload) + snell.AEADTagLen) + copy(payloadCipher, payload) + w.cipher.Seal(payloadCipher[:0], w.nonce, payloadCipher[:len(payload)], nil) + snell.IncreaseNonce(w.nonce) + if paddingLen > 0 { + err := w.fillPadding(padding, payloadCipher) + if err != nil { + return err + } + limit := min(len(padding), len(payloadCipher)) + for index := 0; index < limit; index += 2 { + padding[index], payloadCipher[index] = payloadCipher[index], padding[index] + } + } + return nil +} + +func (w *writer) makeBufferRecordLocked(buffer *buf.Buffer, paddingLen int) (*buf.Buffer, error) { + dataLen := buffer.Len() + if dataLen > maxPayload || paddingLen > maxPayload { + panic("snell: v5 record exceeds maximum") + } + if dataLen == 0 && paddingLen != 0 { + panic("snell: zero-length v5 record carries padding") + } + prefix := buffer.ExtendHeader(snell.HeaderCipherLen + paddingLen) + header := prefix[:snell.HeaderCipherLen] + w.sealHeader(header, paddingLen, dataLen) + payloadCipher := buffer.From(snell.HeaderCipherLen + paddingLen) + buffer.Extend(snell.AEADTagLen) + w.cipher.Seal(payloadCipher[:0], w.nonce, payloadCipher, nil) + payloadCipher = buffer.From(snell.HeaderCipherLen + paddingLen) + snell.IncreaseNonce(w.nonce) + if paddingLen > 0 { + padding := prefix[snell.HeaderCipherLen:] + err := w.fillPadding(padding, payloadCipher) + if err != nil { + return nil, err + } + limit := min(len(padding), len(payloadCipher)) + for index := 0; index < limit; index += 2 { + padding[index], payloadCipher[index] = payloadCipher[index], padding[index] + } + } + return buffer, nil +} + +func (w *writer) WriteBuffer(buffer *buf.Buffer) error { + defer buffer.Release() + dataLen := buffer.Len() + if dataLen == 0 { + return nil + } + w.access.Lock() + defer w.access.Unlock() + payloadLimit := w.nextPayloadLimit() + if dataLen > payloadLimit { + recordCount := (dataLen + payloadLimit - 1) / payloadLimit + output := buf.NewSize(recordCount*(snell.HeaderCipherLen+snell.AEADTagLen) + dataLen) + defer output.Release() + for data := buffer.Bytes(); len(data) > 0; { + recordLen := min(len(data), payloadLimit) + err := w.sealRecordLocked(output, data[:recordLen], 0) + if err != nil { + return err + } + data = data[recordLen:] + } + return common.Error(w.upstream.Write(output.Bytes())) + } + record, err := w.makeBufferRecordLocked(buffer, 0) + if err != nil { + return err + } + defer record.Release() + return common.Error(w.upstream.Write(record.Bytes())) +} + +func (w *writer) WritePacketBuffer(buffer *buf.Buffer) error { + dataLen := buffer.Len() + w.access.Lock() + defer w.access.Unlock() + now := time.Now().Unix() + var payloadLimit int + if w.lastFrameUnix == 0 { + payloadLimit = frameSize - firstRecordOverhead - w.initialPaddingLen + } else if now-w.lastFrameUnix < framePayloadResetInterval { + payloadLimit = w.framePayloadLen + if payloadLimit == 0 { + payloadLimit = framePayloadStep + } + } else { + payloadLimit = framePayloadStep + } + if dataLen > payloadLimit { + buffer.Release() + return snell.ErrPayloadTooLarge + } + w.lastFrameUnix = now + w.markPayloadLimitUsed(payloadLimit) + record, err := w.makeBufferRecordLocked(buffer, 0) + if err != nil { + buffer.Release() + return err + } + defer record.Release() + return common.Error(w.upstream.Write(record.Bytes())) +} + +func (w *writer) WriteZeroChunk() error { + buffer := buf.NewSize(snell.HeaderCipherLen) + w.access.Lock() + defer w.access.Unlock() + // snell-server v5.0.1: FUN_0013def0 calls FUN_00139670 even for an empty + // payload, so EOF records consume one growth-window step. + w.nextPayloadLimit() + w.sealHeader(buffer.Extend(snell.HeaderCipherLen), 0, 0) + err := common.Error(w.upstream.Write(buffer.Bytes())) + buffer.Release() + return err +} + +func (w *writer) fillPadding(padding []byte, payloadCipher []byte) error { + if len(padding) == 0 { + return nil + } + oneBits := 0 + // snell-server v5.0.1: FUN_00138af0 counts ones only over complete + // 32-bit chunks, but computes zeros against the full payload cipher length. + payloadCountLen := len(payloadCipher) &^ 3 + for _, payloadByte := range payloadCipher[:payloadCountLen] { + oneBits += bits.OnesCount8(payloadByte) + } + zeroBits := 8*len(payloadCipher) - oneBits + if zeroBits <= 0 { + _, err := io.ReadFull(rand.Reader, padding) + return err + } + + ratio := float64(oneBits) / float64(zeroBits) + if ratio <= 0.5 || ratio >= 1.6 { + _, err := io.ReadFull(rand.Reader, padding) + return err + } + + targetRatioBase := 1.6 + if zeroBits < oneBits { + targetRatioBase = 0.4 + } + // snell-server v5.0.1: FUN_00138af0 derives the target padding ratio as + // base + rand()/2147483647.0/10, using the process rand() 31-bit range. + randomValue, err := w.randomInt31() + if err != nil { + return err + } + targetRatio := targetRatioBase + float64(randomValue)/2147483647.0/10 + targetOnes := int(float64(8*(len(payloadCipher)+len(padding)))*targetRatio/(targetRatio+1) - float64(oneBits)) + if targetOnes < 0 || targetOnes > 8*len(padding) { + _, err = io.ReadFull(rand.Reader, padding) + return err + } + return w.fillPaddingWithBitCount(padding, targetOnes) +} + +func (w *writer) fillPaddingWithBitCount(padding []byte, oneBits int) error { + totalBits := 8 * len(padding) + if oneBits < 0 || oneBits > totalBits { + panic("snell: invalid padding bit count") + } + bitset := make([]byte, totalBits) + for index := range oneBits { + bitset[index] = 1 + } + // snell-server v5.0.1: FUN_00138af0 shuffles from the front and maps rand() + // with division by RAND_MAX/remaining+1, preserving its modulo bias. + position := 0 + remaining := totalBits + for remaining != 1 { + randomValue, err := w.randomInt31() + if err != nil { + return err + } + divisor := 0 + if remaining != 0 { + divisor = 0x7fffffff / remaining + } + swapIndex := position + randomValue/(divisor+1) + bitset[swapIndex], bitset[position] = bitset[position], bitset[swapIndex] + position++ + remaining-- + } + clear(padding) + for index, bit := range bitset { + if bit == 1 { + padding[index/8] |= 1 << uint(index%8) + } + } + return nil +} + +func (w *writer) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + upstreamWriter, created := bufio.CreateVectorisedWriter(w.upstream) + if !created { + return nil, false + } + return w.CreateVectorisedWriterFor(upstreamWriter), true +} + +func (w *writer) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { + return &vectorisedWriter{writer: w, upstream: upstream} +} + +type vectorisedWriter struct { + writer *writer + upstream N.VectorisedWriter +} + +func (w *vectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { + payloadLen := buf.LenMulti(buffers) + if payloadLen == 0 { + buf.ReleaseMulti(buffers) + return nil + } + var records []*buf.Buffer + defer func() { + buf.ReleaseMulti(records) + }() + recordWriter := w.writer + recordWriter.access.Lock() + defer recordWriter.access.Unlock() + // snell-server v5.0.1: FUN_00139670 advances the growth window once per + // encrypt call and splits that call using the same payload limit. + payloadLimit := recordWriter.nextPayloadLimit() + index := 0 + for remainingPayload := payloadLen; remainingPayload > 0; { + for buffers[index].IsEmpty() { + buffers[index].Release() + index++ + } + recordLen := min(remainingPayload, payloadLimit) + buffer := buffers[index] + if buffer.Len() == recordLen { + record, err := recordWriter.makeBufferRecordLocked(buffer, 0) + if err != nil { + buffer.Release() + buf.ReleaseMulti(buffers[index+1:]) + return err + } + records = append(records, record) + index++ + } else { + payload := make([]byte, recordLen) + for copiedLen := 0; copiedLen < recordLen; { + buffer = buffers[index] + if buffer.IsEmpty() { + buffer.Release() + index++ + continue + } + copyLen := min(recordLen-copiedLen, buffer.Len()) + copy(payload[copiedLen:], buffer.Bytes()[:copyLen]) + buffer.Advance(copyLen) + copiedLen += copyLen + if buffer.IsEmpty() { + buffer.Release() + index++ + } + } + record := buf.NewSize(snell.HeaderCipherLen + recordLen + snell.AEADTagLen) + err := recordWriter.sealRecordLocked(record, payload, 0) + if err != nil { + record.Release() + buf.ReleaseMulti(buffers[index:]) + return err + } + records = append(records, record) + } + remainingPayload -= recordLen + } + buf.ReleaseMulti(buffers[index:]) + flushRecords := records + records = nil + return w.upstream.WriteVectorised(flushRecords) +} + +func (w *writer) CreatePacketVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { + return &packetVectorisedWriter{writer: w, upstream: upstream} +} + +type packetVectorisedWriter struct { + writer *writer + upstream N.VectorisedWriter +} + +func (w *packetVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { + var records []*buf.Buffer + defer func() { + buf.ReleaseMulti(records) + }() + recordWriter := w.writer + recordWriter.access.Lock() + defer recordWriter.access.Unlock() + for index, buffer := range buffers { + dataLen := buffer.Len() + if dataLen == 0 { + buffer.Release() + continue + } + now := time.Now().Unix() + var payloadLimit int + if recordWriter.lastFrameUnix == 0 { + payloadLimit = frameSize - firstRecordOverhead - recordWriter.initialPaddingLen + } else if now-recordWriter.lastFrameUnix < framePayloadResetInterval { + payloadLimit = recordWriter.framePayloadLen + if payloadLimit == 0 { + payloadLimit = framePayloadStep + } + } else { + payloadLimit = framePayloadStep + } + if dataLen > payloadLimit { + buffer.Release() + buf.ReleaseMulti(buffers[index+1:]) + return snell.ErrPayloadTooLarge + } + recordWriter.lastFrameUnix = now + recordWriter.markPayloadLimitUsed(payloadLimit) + record, err := recordWriter.makeBufferRecordLocked(buffer, 0) + if err != nil { + buffer.Release() + buf.ReleaseMulti(buffers[index+1:]) + return err + } + records = append(records, record) + } + if len(records) == 0 { + return nil + } + flushRecords := records + records = nil + return w.upstream.WriteVectorised(flushRecords) +} + +func (w *writer) FrontHeadroom() int { + return snell.HeaderCipherLen +} + +func (w *writer) RearHeadroom() int { + return snell.AEADTagLen +} + +func (w *writer) WriterMTU() int { + return maxPayload +} + +func (w *writer) Upstream() any { + return w.upstream +} + +var ( + _ N.ExtendedReader = (*reader)(nil) + _ N.ReadWaiter = (*reader)(nil) + _ N.ExtendedWriter = (*writer)(nil) + _ N.VectorisedWriteCreator = (*writer)(nil) + _ N.VectorisedWriter = (*vectorisedWriter)(nil) + _ N.VectorisedWriter = (*packetVectorisedWriter)(nil) + _ N.FrontHeadroom = (*writer)(nil) + _ N.RearHeadroom = (*writer)(nil) + _ N.WriterWithMTU = (*writer)(nil) +) diff --git a/proxy/snell/sing-snell/snellv5/reuse.go b/proxy/snell/sing-snell/snellv5/reuse.go new file mode 100644 index 00000000000..21709588bee --- /dev/null +++ b/proxy/snell/sing-snell/snellv5/reuse.go @@ -0,0 +1,482 @@ +package snellv5 + +import ( + "context" + "crypto/rand" + "errors" + "io" + "net" + "sync" + "sync/atomic" + + snell "github.com/sagernet/sing-snell" + "github.com/sagernet/sing-snell/internal/reuse" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +const ( + remoteEOFCode = reuse.RemoteEOFCode + remoteEOFMessage = reuse.RemoteEOFMessage +) + +type serverReuseSession[U comparable] struct { + net.Conn + service *Service + multiService *MultiService[U] + reader reuse.RecordReader + writer reuse.RecordWriter +} + +func (s *serverReuseSession[U]) Serve(ctx context.Context, source M.Socksaddr, onClose N.CloseHandlerFunc, record *buf.Buffer, request snell.Request) (err error) { + callOnClose := true + if onClose != nil { + defer func() { + if callOnClose { + onClose(err) + } + }() + } + for { + requestCtx := ctx + switch request.Command { + case snell.CommandConnect, snell.CommandConnectV2: + if s.multiService != nil { + requestCtx, err = s.multiService.authenticate(ctx, request) + if err != nil { + record.Release() + s.Conn.Close() + return err + } + } + case snell.CommandUDP: + if s.multiService != nil { + requestCtx, err = s.multiService.authenticate(ctx, request) + if err != nil { + record.Release() + s.Conn.Close() + return err + } + } + record.Release() + packetConn := &serverPacketConn{Conn: s.Conn, service: s.service, reader: s.reader, writer: s.writer} + err = packetConn.writeTunnelReply() + if err != nil { + s.Conn.Close() + return err + } + s.writer = packetConn.writer + callOnClose = false + s.service.handler.NewPacketConnectionEx(requestCtx, packetConn, source, M.Socksaddr{}, onClose) + return nil + case snell.CommandPing: + record.Release() + pong := [1]byte{snell.ReplyPong} + if s.writer == nil { + err = s.service.writePong(s.Conn) + } else { + _, err = s.writer.Write(pong[:]) + } + if err != nil { + s.Conn.Close() + return err + } + s.Conn.Close() + return nil + default: + record.Release() + s.Conn.Close() + return E.Extend(snell.ErrUnsupportedCommand, request.Command) + } + serverConn := &serverReuseConn[U]{Conn: s.Conn, session: s, completion: make(chan struct{})} + if record.IsEmpty() { + record.Release() + } else { + s.reader.SetCache(record) + } + s.service.handler.NewConnectionEx(requestCtx, serverConn, source, request.Destination, serverConn.completeLogicalConnection) + select { + case <-serverConn.completion: + case <-requestCtx.Done(): + s.Conn.Close() + return requestCtx.Err() + } + err = serverConn.CloseWrite() + if err != nil { + s.Conn.Close() + return err + } + if serverConn.aborted.Load() { + return nil + } + if !serverConn.readClosed.Load() { + // snell-server v5.0.1: after writing the EOF packet, FUN_0013f630 keeps + // consuming client records (forwarding them into the still writable + // outgoing socket) until the zero chunk reaches FUN_0013f500 -> + // FUN_0013ddb0 -> FUN_0013dca0, which resets the tunnel stage so the + // next record is parsed as a request. The handler has already released + // the upstream, so discard instead, capped like the client waiting state. + var discarded int + for { + var drainRecord *buf.Buffer + drainRecord, err = s.reader.NextRecord() + if err != nil { + break + } + discarded += drainRecord.Len() + drainRecord.Release() + if discarded >= reuse.WaitingDiscardLimit { + s.Conn.Close() + return E.New("snell: read too much data after request end") + } + } + if !errors.Is(err, io.EOF) { + s.Conn.Close() + return E.Cause(err, "drain request") + } + } + record, err = s.reader.ReadRecord() + if err != nil { + if errors.Is(err, io.EOF) { + return nil + } + s.Conn.Close() + return E.Cause(err, "read request") + } + request, err = s.service.readRequest(record) + if err != nil { + record.Release() + s.Conn.Close() + return E.Cause(err, "decode request") + } + } +} + +type serverReuseConn[U comparable] struct { + net.Conn + session *serverReuseSession[U] + + access sync.Mutex + closeWriteOnce sync.Once + closeWriteErr error + closeOnce sync.Once + closeErr error + readClosed atomic.Bool + aborted atomic.Bool + replyWritten bool + completeOnce sync.Once + completion chan struct{} +} + +func (c *serverReuseConn[U]) writeFirstResponse(payload []byte) error { + salt := make([]byte, snell.SaltLen) + _, err := io.ReadFull(rand.Reader, salt) + if err != nil { + return err + } + key := snell.DeriveKey(c.session.service.psk, salt) + aead, err := snell.NewAEAD(key) + if err != nil { + return err + } + nonce := make([]byte, snell.NonceLen) + reply := buf.NewSize(1 + len(payload)) + common.Must(reply.WriteByte(snell.ReplyTunnel)) + common.Must1(reply.Write(payload)) + writer, err := newWriter(c.session.Conn, aead, nonce) + if err != nil { + reply.Release() + return err + } + err = writer.WriteFirst(salt, reply.Bytes()) + reply.Release() + if err != nil { + return E.Cause(err, "write reply") + } + c.session.writer = writer + c.replyWritten = true + return nil +} + +func (c *serverReuseConn[U]) writeResponse(payload []byte) error { + if c.session.writer == nil { + return c.writeFirstResponse(payload) + } + reply := buf.NewSize(1 + len(payload)) + common.Must(reply.WriteByte(snell.ReplyTunnel)) + common.Must1(reply.Write(payload)) + defer reply.Release() + _, err := c.session.writer.Write(reply.Bytes()) + if err != nil { + return E.Cause(err, "write reply") + } + c.replyWritten = true + return nil +} + +func (c *serverReuseConn[U]) writeResponseBuffer(buffer *buf.Buffer) error { + if c.session.writer != nil { + buffer.ExtendHeader(1)[0] = snell.ReplyTunnel + err := c.session.writer.WriteBuffer(buffer) + if err != nil { + return E.Cause(err, "write reply") + } + c.replyWritten = true + return nil + } + buffer.ExtendHeader(1)[0] = snell.ReplyTunnel + salt := make([]byte, snell.SaltLen) + _, err := io.ReadFull(rand.Reader, salt) + if err != nil { + buffer.Release() + return err + } + key := snell.DeriveKey(c.session.service.psk, salt) + aead, err := snell.NewAEAD(key) + if err != nil { + buffer.Release() + return err + } + nonce := make([]byte, snell.NonceLen) + writer, err := newWriter(c.session.Conn, aead, nonce) + if err != nil { + buffer.Release() + return err + } + err = writer.WriteFirst(salt, buffer.Bytes()) + buffer.Release() + if err != nil { + return E.Cause(err, "write reply") + } + c.session.writer = writer + c.replyWritten = true + return nil +} + +func (c *serverReuseConn[U]) writeErrorResponse(code byte, messageText string) error { + message := []byte(messageText) + reply := buf.NewSize(3 + len(message)) + common.Must(reply.WriteByte(snell.ReplyError)) + common.Must(reply.WriteByte(code)) + common.Must(reply.WriteByte(byte(len(message)))) + common.Must1(reply.Write(message)) + defer reply.Release() + if c.session.writer == nil { + salt := make([]byte, snell.SaltLen) + _, err := io.ReadFull(rand.Reader, salt) + if err != nil { + return err + } + key := snell.DeriveKey(c.session.service.psk, salt) + aead, err := snell.NewAEAD(key) + if err != nil { + return err + } + nonce := make([]byte, snell.NonceLen) + writer, err := newWriter(c.session.Conn, aead, nonce) + if err != nil { + return err + } + err = writer.WriteFirst(salt, reply.Bytes()) + if err != nil { + return E.Cause(err, "write error reply") + } + c.session.writer = writer + return nil + } + _, err := c.session.writer.Write(reply.Bytes()) + if err != nil { + return E.Cause(err, "write error reply") + } + return nil +} + +func (c *serverReuseConn[U]) Read(p []byte) (int, error) { + n, err := c.session.reader.Read(p) + if errors.Is(err, io.EOF) { + c.readClosed.Store(true) + } + return n, err +} + +func (c *serverReuseConn[U]) ReadBuffer(buffer *buf.Buffer) error { + err := c.session.reader.ReadBuffer(buffer) + if errors.Is(err, io.EOF) { + c.readClosed.Store(true) + } + return err +} + +func (c *serverReuseConn[U]) Write(p []byte) (int, error) { + c.access.Lock() + defer c.access.Unlock() + if c.replyWritten { + return c.session.writer.Write(p) + } + err := c.writeResponse(p) + if err != nil { + return 0, err + } + return len(p), nil +} + +func (c *serverReuseConn[U]) WriteBuffer(buffer *buf.Buffer) error { + c.access.Lock() + defer c.access.Unlock() + if c.replyWritten { + return c.session.writer.WriteBuffer(buffer) + } + return c.writeResponseBuffer(buffer) +} + +func (c *serverReuseConn[U]) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + upstreamWriter, created := bufio.CreateVectorisedWriter(c.session.Conn) + if !created { + return nil, false + } + return &serverReuseVectorisedWriter[U]{conn: c, upstream: upstreamWriter}, true +} + +type serverReuseVectorisedWriter[U comparable] struct { + conn *serverReuseConn[U] + upstream N.VectorisedWriter +} + +func (w *serverReuseVectorisedWriter[U]) WriteVectorised(buffers []*buf.Buffer) error { + conn := w.conn + conn.access.Lock() + if conn.replyWritten { + recordWriter := conn.session.writer + conn.access.Unlock() + return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) + } + for index, buffer := range buffers { + if buffer.IsEmpty() { + buffer.Release() + continue + } + buffer.ExtendHeader(1)[0] = snell.ReplyTunnel + if conn.session.writer != nil { + recordWriter := conn.session.writer + err := recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers[index:]) + if err != nil { + conn.access.Unlock() + return err + } + conn.replyWritten = true + conn.access.Unlock() + return nil + } + salt := make([]byte, snell.SaltLen) + _, err := io.ReadFull(rand.Reader, salt) + if err != nil { + conn.access.Unlock() + buf.ReleaseMulti(buffers[index:]) + return err + } + key := snell.DeriveKey(conn.session.service.psk, salt) + aead, err := snell.NewAEAD(key) + if err != nil { + conn.access.Unlock() + buf.ReleaseMulti(buffers[index:]) + return err + } + nonce := make([]byte, snell.NonceLen) + recordWriter, err := newWriter(conn.session.Conn, aead, nonce) + if err != nil { + conn.access.Unlock() + buf.ReleaseMulti(buffers[index:]) + return err + } + err = recordWriter.WriteFirstVectorised(salt, buffers[index:], w.upstream) + if err != nil { + conn.access.Unlock() + return E.Cause(err, "write reply") + } + conn.session.writer = recordWriter + conn.replyWritten = true + conn.access.Unlock() + return nil + } + conn.access.Unlock() + return nil +} + +func (c *serverReuseConn[U]) CloseWrite() error { + c.closeWriteOnce.Do(func() { + c.access.Lock() + defer c.access.Unlock() + // snell-server v5.0.1: FUN_0013f020 reports upstream EOF before any + // payload as ReplyError 0x65 "Remote EOF", then aborts the reusable tunnel. + if !c.replyWritten { + c.closeWriteErr = c.writeErrorResponse(remoteEOFCode, remoteEOFMessage) + if c.closeWriteErr == nil { + c.aborted.Store(true) + c.closeWriteErr = c.session.Conn.Close() + } + return + } + c.closeWriteErr = c.session.writer.WriteZeroChunk() + }) + return c.closeWriteErr +} + +func (c *serverReuseConn[U]) Close() error { + c.closeOnce.Do(func() { + c.closeErr = c.CloseWrite() + c.completeLogicalConnection(c.closeErr) + }) + return c.closeErr +} + +func (c *serverReuseConn[U]) completeLogicalConnection(error) { + c.completeOnce.Do(func() { + close(c.completion) + }) +} + +func (c *serverReuseConn[U]) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + return c.session.reader.InitializeReadWaiter(options) +} + +func (c *serverReuseConn[U]) WaitReadBuffer() (*buf.Buffer, error) { + buffer, err := c.session.reader.WaitReadBuffer() + if errors.Is(err, io.EOF) { + c.readClosed.Store(true) + } + return buffer, err +} + +func (c *serverReuseConn[U]) FrontHeadroom() int { + if c.replyWritten { + return snell.HeaderCipherLen + } + if c.session.writer != nil { + return 1 + snell.HeaderCipherLen + } + return 1 + snell.SaltLen + snell.HeaderCipherLen + maxInitialPaddingLen +} + +func (c *serverReuseConn[U]) RearHeadroom() int { + return snell.AEADTagLen +} + +func (c *serverReuseConn[U]) WriterMTU() int { + return maxPayload +} + +func (c *serverReuseConn[U]) Upstream() any { + return c.session.Conn +} + +var ( + _ N.ExtendedConn = (*serverReuseConn[struct{}])(nil) + _ N.ReadWaiter = (*serverReuseConn[struct{}])(nil) + _ N.VectorisedWriteCreator = (*serverReuseConn[struct{}])(nil) + _ N.VectorisedWriter = (*serverReuseVectorisedWriter[struct{}])(nil) + _ N.WriteCloser = (*serverReuseConn[struct{}])(nil) +) diff --git a/proxy/snell/sing-snell/snellv5/server.go b/proxy/snell/sing-snell/snellv5/server.go new file mode 100644 index 00000000000..3455262d05c --- /dev/null +++ b/proxy/snell/sing-snell/snellv5/server.go @@ -0,0 +1,695 @@ +package snellv5 + +import ( + "context" + "crypto/rand" + "encoding/binary" + "io" + "math" + "net" + "sync" + + snell "github.com/sagernet/sing-snell" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +const ( + // snell-server v5.0.1: FUN_0014c020 initializes two 500000-entry Bloom generations + // with false-positive probability 1e-10 for first-record salt replay detection. + saltReplayCacheGenerationSize = 500000 + saltReplayCacheFalsePositiveRate = 1e-10 + saltReplayCacheMurmurHashDefaultSeed = 0x9747b28c +) + +var errDuplicateSalt = E.New("snell: duplicated salt") + +const ( + nonReusableEOFCode = 0xff + nonReusableEOFMessage = "end of file" +) + +type saltReplayCache struct { + access sync.Mutex + current int + counts [2]int + filters [2]saltReplayBloom +} + +type saltReplayBloom struct { + bits []byte + bitCount uint32 + hashCount int +} + +func (b *saltReplayBloom) Initialize(expectedEntries int, falsePositiveRate float64) { + bitsPerEntry := -math.Log(falsePositiveRate) / (math.Ln2 * math.Ln2) + b.bitCount = uint32(float64(expectedEntries) * bitsPerEntry) + b.hashCount = int(math.Ceil(bitsPerEntry * math.Ln2)) + b.bits = make([]byte, (b.bitCount+7)/8) +} + +func (b *saltReplayBloom) Contains(data []byte) bool { + firstHash := b.MurmurHash(data, saltReplayCacheMurmurHashDefaultSeed) + hashStep := b.MurmurHash(data, firstHash) + hashValue := firstHash + for index := 0; index < b.hashCount; index++ { + bitIndex := hashValue % b.bitCount + if b.bits[bitIndex>>3]&(1<<(bitIndex&7)) == 0 { + return false + } + hashValue += hashStep + } + return true +} + +func (b *saltReplayBloom) Add(data []byte) { + firstHash := b.MurmurHash(data, saltReplayCacheMurmurHashDefaultSeed) + hashStep := b.MurmurHash(data, firstHash) + hashValue := firstHash + for index := 0; index < b.hashCount; index++ { + bitIndex := hashValue % b.bitCount + b.bits[bitIndex>>3] |= 1 << (bitIndex & 7) + hashValue += hashStep + } +} + +func (b *saltReplayBloom) Clear() { + clear(b.bits) +} + +func (b *saltReplayBloom) MurmurHash(data []byte, seed uint32) uint32 { + hashValue := seed ^ uint32(len(data)) + for len(data) >= 4 { + chunk := binary.LittleEndian.Uint32(data) + chunk *= 0x5bd1e995 + chunk ^= chunk >> 24 + chunk *= 0x5bd1e995 + hashValue *= 0x5bd1e995 + hashValue ^= chunk + data = data[4:] + } + switch len(data) { + case 3: + hashValue ^= uint32(data[2]) << 16 + fallthrough + case 2: + hashValue ^= uint32(data[1]) << 8 + fallthrough + case 1: + hashValue ^= uint32(data[0]) + hashValue *= 0x5bd1e995 + } + hashValue ^= hashValue >> 13 + hashValue *= 0x5bd1e995 + hashValue ^= hashValue >> 15 + return hashValue +} + +func (c *saltReplayCache) CheckAndAdd(salt []byte) bool { + c.access.Lock() + defer c.access.Unlock() + if c.filters[0].bits == nil { + c.filters[0].Initialize(saltReplayCacheGenerationSize, saltReplayCacheFalsePositiveRate) + c.filters[1].Initialize(saltReplayCacheGenerationSize, saltReplayCacheFalsePositiveRate) + } + if c.filters[0].Contains(salt) || c.filters[1].Contains(salt) { + return true + } + c.filters[c.current].Add(salt) + c.counts[c.current]++ + if c.counts[c.current] >= saltReplayCacheGenerationSize { + c.counts[c.current] = 0 + c.current ^= 1 + c.filters[c.current].Clear() + } + return false +} + +type Service struct { + psk []byte + obfs snell.ObfsConfig + handler snell.Handler + saltCache saltReplayCache +} + +type ServiceOptions struct { + PSK []byte + ObfsMode snell.ObfsMode + Handler snell.Handler +} + +func NewService(options ServiceOptions) (*Service, error) { + if len(options.PSK) == 0 { + return nil, snell.ErrMissingPSK + } + switch options.ObfsMode { + case snell.ObfsModeNone, snell.ObfsModeHTTP: + case snell.ObfsModeTLS: + default: + return nil, E.New("snell: unknown obfs mode: ", int(options.ObfsMode)) + } + return &Service{ + psk: options.PSK, + obfs: snell.ObfsConfig{Mode: options.ObfsMode}, + handler: options.Handler, + }, nil +} + +func (s *Service) NewConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error { + err := s.newConnection(ctx, conn, source, onClose) + if err != nil { + return &snell.ServerError{Conn: conn, Source: source, Cause: err} + } + return nil +} + +type MultiService[U comparable] struct { + *Service + users map[string]U +} + +func NewMultiService[U comparable](options ServiceOptions) (*MultiService[U], error) { + service, err := NewService(options) + if err != nil { + return nil, err + } + return &MultiService[U]{Service: service, users: make(map[string]U)}, nil +} + +func (s *MultiService[U]) UpdateUsers(users []U, userKeys [][]byte) error { + if len(users) != len(userKeys) { + return E.New("snell: user/key count mismatch") + } + if len(users) == 0 { + return snell.ErrNoUsers + } + userMap := make(map[string]U, len(users)) + for index, user := range users { + key := userKeys[index] + if len(key) == 0 { + return snell.ErrBadUserKey + } + if len(key) > 255 { + return E.New("snell: user key too long") + } + keyString := string(key) + if _, loaded := userMap[keyString]; loaded { + return snell.ErrDuplicateUserKey + } + userMap[keyString] = user + } + s.users = userMap + return nil +} + +func (s *MultiService[U]) NewConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error { + err := s.newConnection(ctx, conn, source, onClose) + if err != nil { + return &snell.ServerError{Conn: conn, Source: source, Cause: err} + } + return nil +} + +func (s *MultiService[U]) authenticate(ctx context.Context, request snell.Request) (context.Context, error) { + user, loaded := s.users[string(request.ClientID)] + if !loaded { + return nil, snell.ErrBadUserKey + } + return auth.ContextWithUser(ctx, user), nil +} + +func (s *Service) readRequest(record *buf.Buffer) (snell.Request, error) { + prefix := make([]byte, 3) + _, err := io.ReadFull(record, prefix) + if err != nil { + return snell.Request{}, err + } + if prefix[0] != snell.RequestVersion { + return snell.Request{}, E.Extend(snell.ErrBadVersion, "request version ", prefix[0]) + } + request := snell.Request{Command: prefix[1]} + // snell-server v5.0.1: FUN_0013f630 handles SN_COMMAND_PING after the 3-byte + // request prefix is read and aborts with PONG data, without consuming prefix[2] + // client-id bytes. + if request.Command == snell.CommandPing { + return request, nil + } + clientIDLen := int(prefix[2]) + if clientIDLen > 0 { + request.ClientID = make([]byte, clientIDLen) + _, err = io.ReadFull(record, request.ClientID) + if err != nil { + return snell.Request{}, err + } + } + switch request.Command { + case snell.CommandConnect, snell.CommandConnectV2: + request.Destination, err = snell.ReadConnectAddress(record) + if err != nil { + return snell.Request{}, err + } + case snell.CommandUDP: + default: + return snell.Request{}, E.Extend(snell.ErrUnsupportedCommand, request.Command) + } + return request, nil +} + +func (s *Service) newConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error { + conn = s.obfs.ServerConn(conn) + salt := make([]byte, snell.SaltLen) + _, err := io.ReadFull(conn, salt) + if err != nil { + return err + } + key := snell.DeriveKey(s.psk, salt) + aead, err := snell.NewAEAD(key) + if err != nil { + return err + } + r := newReader(conn, aead, make([]byte, snell.NonceLen)) + record, err := r.ReadRecord() + if err != nil { + return E.Cause(err, "read request") + } + // snell-server v5.0.1: initial request path: checks the first record salt after decryption + // and aborts with "Duplicated salt detected" when it was recently seen. + if s.saltCache.CheckAndAdd(salt) { + record.Release() + return errDuplicateSalt + } + request, err := s.readRequest(record) + if err != nil { + record.Release() + return E.Cause(err, "decode request") + } + + switch request.Command { + case snell.CommandConnect: + serverConn := &serverConn{Conn: conn, service: s, reader: r} + if record.IsEmpty() { + record.Release() + } else { + r.SetCache(record) + } + s.handler.NewConnectionEx(ctx, serverConn, source, request.Destination, onClose) + return nil + case snell.CommandConnectV2: + reuseSession := &serverReuseSession[struct{}]{Conn: conn, service: s, reader: r} + return reuseSession.Serve(ctx, source, onClose, record, request) + case snell.CommandUDP: + record.Release() + packetConn := &serverPacketConn{Conn: conn, service: s, reader: r} + err = packetConn.writeTunnelReply() + if err != nil { + return err + } + s.handler.NewPacketConnectionEx(ctx, packetConn, source, M.Socksaddr{}, onClose) + return nil + case snell.CommandPing: + record.Release() + err = s.writePong(conn) + closeErr := conn.Close() + if err != nil { + return err + } + return closeErr + default: + record.Release() + return E.Extend(snell.ErrUnsupportedCommand, request.Command) + } +} + +func (s *MultiService[U]) newConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error { + conn = s.obfs.ServerConn(conn) + salt := make([]byte, snell.SaltLen) + _, err := io.ReadFull(conn, salt) + if err != nil { + return err + } + key := snell.DeriveKey(s.psk, salt) + aead, err := snell.NewAEAD(key) + if err != nil { + return err + } + r := newReader(conn, aead, make([]byte, snell.NonceLen)) + record, err := r.ReadRecord() + if err != nil { + return E.Cause(err, "read request") + } + // snell-server v5.0.1: initial request path: checks the first record salt after decryption + // and aborts with "Duplicated salt detected" when it was recently seen. + if s.saltCache.CheckAndAdd(salt) { + record.Release() + return errDuplicateSalt + } + request, err := s.readRequest(record) + if err != nil { + record.Release() + return E.Cause(err, "decode request") + } + + switch request.Command { + case snell.CommandConnect: + requestCtx, err := s.authenticate(ctx, request) + if err != nil { + record.Release() + return err + } + serverConn := &serverConn{Conn: conn, service: s.Service, reader: r} + if record.IsEmpty() { + record.Release() + } else { + r.SetCache(record) + } + s.handler.NewConnectionEx(requestCtx, serverConn, source, request.Destination, onClose) + return nil + case snell.CommandConnectV2: + reuseSession := &serverReuseSession[U]{Conn: conn, service: s.Service, multiService: s, reader: r} + return reuseSession.Serve(ctx, source, onClose, record, request) + case snell.CommandUDP: + requestCtx, err := s.authenticate(ctx, request) + if err != nil { + record.Release() + return err + } + record.Release() + packetConn := &serverPacketConn{Conn: conn, service: s.Service, reader: r} + err = packetConn.writeTunnelReply() + if err != nil { + return err + } + s.handler.NewPacketConnectionEx(requestCtx, packetConn, source, M.Socksaddr{}, onClose) + return nil + case snell.CommandPing: + record.Release() + err = s.writePong(conn) + closeErr := conn.Close() + if err != nil { + return err + } + return closeErr + default: + record.Release() + return E.Extend(snell.ErrUnsupportedCommand, request.Command) + } +} + +var ( + _ snell.Service = (*Service)(nil) + _ snell.Service = (*MultiService[int])(nil) +) + +type serverConn struct { + net.Conn + service *Service + reader *reader + + access sync.Mutex + writer *writer + + closeWriteOnce sync.Once + closeWriteErr error + closeOnce sync.Once + closeErr error +} + +func (c *serverConn) writeResponse(payload []byte) error { + salt := make([]byte, snell.SaltLen) + _, err := io.ReadFull(rand.Reader, salt) + if err != nil { + return err + } + key := snell.DeriveKey(c.service.psk, salt) + aead, err := snell.NewAEAD(key) + if err != nil { + return err + } + nonce := make([]byte, snell.NonceLen) + w, err := newWriter(c.Conn, aead, nonce) + if err != nil { + return err + } + reply := buf.NewSize(1 + len(payload)) + common.Must(reply.WriteByte(snell.ReplyTunnel)) + common.Must1(reply.Write(payload)) + err = w.WriteFirst(salt, reply.Bytes()) + reply.Release() + if err != nil { + return E.Cause(err, "write reply") + } + c.writer = w + return nil +} + +func (c *serverConn) writeErrorResponse(code byte, messageText string) error { + message := []byte(messageText) + reply := buf.NewSize(3 + len(message)) + common.Must(reply.WriteByte(snell.ReplyError)) + common.Must(reply.WriteByte(code)) + common.Must(reply.WriteByte(byte(len(message)))) + common.Must1(reply.Write(message)) + defer reply.Release() + salt := make([]byte, snell.SaltLen) + _, err := io.ReadFull(rand.Reader, salt) + if err != nil { + return err + } + key := snell.DeriveKey(c.service.psk, salt) + aead, err := snell.NewAEAD(key) + if err != nil { + return err + } + nonce := make([]byte, snell.NonceLen) + w, err := newWriter(c.Conn, aead, nonce) + if err != nil { + return err + } + err = w.WriteFirst(salt, reply.Bytes()) + if err != nil { + return E.Cause(err, "write error reply") + } + c.writer = w + return nil +} + +func (c *serverConn) writeResponseBuffer(buffer *buf.Buffer) error { + buffer.ExtendHeader(1)[0] = snell.ReplyTunnel + salt := make([]byte, snell.SaltLen) + _, err := io.ReadFull(rand.Reader, salt) + if err != nil { + buffer.Release() + return err + } + key := snell.DeriveKey(c.service.psk, salt) + aead, err := snell.NewAEAD(key) + if err != nil { + buffer.Release() + return err + } + nonce := make([]byte, snell.NonceLen) + writer, err := newWriter(c.Conn, aead, nonce) + if err != nil { + buffer.Release() + return err + } + err = writer.WriteFirst(salt, buffer.Bytes()) + buffer.Release() + if err != nil { + return E.Cause(err, "write reply") + } + c.writer = writer + return nil +} + +func (c *serverConn) Read(p []byte) (int, error) { + return c.reader.Read(p) +} + +func (c *serverConn) ReadBuffer(buffer *buf.Buffer) error { + return c.reader.ReadBuffer(buffer) +} + +func (c *serverConn) Write(p []byte) (int, error) { + if c.writer != nil { + return c.writer.Write(p) + } + c.access.Lock() + if c.writer != nil { + c.access.Unlock() + return c.writer.Write(p) + } + defer c.access.Unlock() + err := c.writeResponse(p) + if err != nil { + return 0, err + } + return len(p), nil +} + +func (c *serverConn) WriteBuffer(buffer *buf.Buffer) error { + if c.writer != nil { + return c.writer.WriteBuffer(buffer) + } + c.access.Lock() + if c.writer != nil { + c.access.Unlock() + return c.writer.WriteBuffer(buffer) + } + defer c.access.Unlock() + return c.writeResponseBuffer(buffer) +} + +func (c *serverConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + upstreamWriter, created := bufio.CreateVectorisedWriter(c.Conn) + if !created { + return nil, false + } + return &serverVectorisedWriter{conn: c, upstream: upstreamWriter}, true +} + +type serverVectorisedWriter struct { + conn *serverConn + upstream N.VectorisedWriter +} + +func (w *serverVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { + conn := w.conn + if conn.writer != nil { + return conn.writer.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) + } + conn.access.Lock() + if conn.writer != nil { + recordWriter := conn.writer + conn.access.Unlock() + return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) + } + for index, buffer := range buffers { + if buffer.IsEmpty() { + buffer.Release() + continue + } + buffer.ExtendHeader(1)[0] = snell.ReplyTunnel + salt := make([]byte, snell.SaltLen) + _, err := io.ReadFull(rand.Reader, salt) + if err != nil { + conn.access.Unlock() + buf.ReleaseMulti(buffers[index:]) + return err + } + key := snell.DeriveKey(conn.service.psk, salt) + aead, err := snell.NewAEAD(key) + if err != nil { + conn.access.Unlock() + buf.ReleaseMulti(buffers[index:]) + return err + } + nonce := make([]byte, snell.NonceLen) + recordWriter, err := newWriter(conn.Conn, aead, nonce) + if err != nil { + conn.access.Unlock() + buf.ReleaseMulti(buffers[index:]) + return err + } + err = recordWriter.WriteFirstVectorised(salt, buffers[index:], w.upstream) + if err != nil { + conn.access.Unlock() + return E.Cause(err, "write reply") + } + conn.writer = recordWriter + conn.access.Unlock() + return nil + } + conn.access.Unlock() + return nil +} + +func (c *serverConn) CloseWrite() error { + c.closeWriteOnce.Do(func() { + c.access.Lock() + defer c.access.Unlock() + if c.writer == nil { + // snell-server v5.0.1: FUN_0013f020 uses the generic network-error + // path for command-1 EOF before the first response; UV_EOF maps to + // ReplyError 0xff "end of file". + c.closeWriteErr = c.writeErrorResponse(nonReusableEOFCode, nonReusableEOFMessage) + closeErr := c.Close() + if c.closeWriteErr == nil { + c.closeWriteErr = closeErr + } + return + } + // snell-server v5.0.1: FUN_0013f020 closes non-reusable command-1 tunnels + // on upstream EOF instead of writing a Snell zero chunk. + c.closeWriteErr = c.Close() + }) + return c.closeWriteErr +} + +func (c *serverConn) Close() error { + c.closeOnce.Do(func() { + c.closeErr = c.Conn.Close() + }) + return c.closeErr +} + +func (c *serverConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + return c.reader.InitializeReadWaiter(options) +} + +func (c *serverConn) WaitReadBuffer() (*buf.Buffer, error) { + return c.reader.WaitReadBuffer() +} + +func (c *serverConn) FrontHeadroom() int { + if c.writer == nil { + return 1 + snell.SaltLen + snell.HeaderCipherLen + maxInitialPaddingLen + } + return snell.HeaderCipherLen +} + +func (c *serverConn) RearHeadroom() int { + return snell.AEADTagLen +} + +func (c *serverConn) WriterMTU() int { + return maxPayload +} + +func (c *serverConn) Upstream() any { + return c.Conn +} + +func (s *Service) writePong(conn net.Conn) error { + salt := make([]byte, snell.SaltLen) + _, err := io.ReadFull(rand.Reader, salt) + if err != nil { + return err + } + key := snell.DeriveKey(s.psk, salt) + aead, err := snell.NewAEAD(key) + if err != nil { + return err + } + nonce := make([]byte, snell.NonceLen) + w, err := newWriter(conn, aead, nonce) + if err != nil { + return err + } + pong := [1]byte{snell.ReplyPong} + return w.WriteFirst(salt, pong[:]) +} + +var ( + _ N.ExtendedConn = (*serverConn)(nil) + _ N.ReadWaiter = (*serverConn)(nil) + _ N.VectorisedWriteCreator = (*serverConn)(nil) + _ N.VectorisedWriter = (*serverVectorisedWriter)(nil) + _ N.WriteCloser = (*serverConn)(nil) +) diff --git a/proxy/snell/sing-snell/snellv6/client.go b/proxy/snell/sing-snell/snellv6/client.go new file mode 100644 index 00000000000..bc306b562d9 --- /dev/null +++ b/proxy/snell/sing-snell/snellv6/client.go @@ -0,0 +1,359 @@ +package snellv6 + +import ( + "net" + "sync" + + snell "github.com/sagernet/sing-snell" + "github.com/sagernet/sing-snell/internal/reuse" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type Client struct { + psk []byte + userKey []byte + mode Mode + reuse bool + profile *Profile + dialer N.Dialer + server M.Socksaddr + + pool reuse.Pool[*reuseSession] +} + +type ClientOptions struct { + PSK []byte + UserKey []byte + Mode Mode + Reuse bool + Dialer N.Dialer + Server M.Socksaddr +} + +func NewClient(options ClientOptions) (*Client, error) { + if len(options.PSK) == 0 { + return nil, snell.ErrMissingPSK + } + if len(options.UserKey) > 255 { + return nil, E.New("snell: user key too long") + } + if options.Mode != ModeDefault && options.Mode != ModeUnshaped && options.Mode != ModeUnsafeRaw { + return nil, E.New("snell: unknown v6 mode: ", int(options.Mode)) + } + client := &Client{ + psk: options.PSK, + userKey: options.UserKey, + mode: options.Mode, + reuse: options.Reuse, + dialer: options.Dialer, + server: options.Server, + } + if options.Mode == ModeDefault { + client.profile = NewProfile(options.PSK) + } + if options.Reuse { + client.pool.Init() + } + return client, nil +} + +func (c *Client) DialConn(conn net.Conn, destination M.Socksaddr) (net.Conn, error) { + clientConn := &clientConn{client: c, Conn: conn, destination: destination} + return clientConn, clientConn.writeRequest(nil) +} + +func (c *Client) DialEarlyConn(conn net.Conn, destination M.Socksaddr) net.Conn { + return &clientConn{client: c, Conn: conn, destination: destination} +} + +func (c *Client) DialPacketConn(conn net.Conn) (N.NetPacketConn, error) { + return bufio.NewNetPacketConn(&clientPacketConn{Conn: conn, client: c}), nil +} + +var _ snell.Method = (*Client)(nil) + +type clientConn struct { + net.Conn + client *Client + destination M.Socksaddr + + access sync.Mutex + reader reuse.RecordReader + writer reuse.RecordWriter + readWaitOptions N.ReadWaitOptions + closeWriteOnce sync.Once + closeWriteErr error +} + +func (c *clientConn) requestPayload(payload []byte) (*buf.Buffer, error) { + // Surge 6.7.0 (11520): SNConnectorV4::targetHandshakeData: writes command 5 + // for v6 TCP handshakes even when connector reuse is disabled. + requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: c.client.userKey, Destination: c.destination} + request := buf.NewSize(requestPayload.Len() + len(payload)) + err := requestPayload.Write(request) + if err != nil { + request.Release() + return nil, err + } + if len(payload) > 0 { + common.Must1(request.Write(payload)) + } + return request, nil +} + +func (c *clientConn) writeRequest(payload []byte) error { + request, err := c.requestPayload(payload) + if err != nil { + return err + } + defer request.Release() + data := request.Bytes() + first := data + if len(first) > maxPayload { + first = data[:maxPayload] + } + writer, err := writeFirstRecord(c.Conn, c.client.mode, c.client.psk, c.client.profile, first) + if err != nil { + return E.Cause(err, "write request") + } + c.writer = writer + if len(data) > len(first) { + _, err = writer.Write(data[len(first):]) + if err != nil { + return E.Cause(err, "write request") + } + } + return nil +} + +func (c *clientConn) writeRequestBuffer(buffer *buf.Buffer) error { + requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: c.client.userKey, Destination: c.destination} + request := buf.With(buffer.ExtendHeader(requestPayload.Len())) + err := requestPayload.Write(request) + if err != nil { + buffer.Release() + return err + } + writer, err := writeFirstRecordBuffer(c.Conn, c.client.mode, c.client.psk, c.client.profile, buffer) + if err != nil { + return E.Cause(err, "write request") + } + c.writer = writer + return nil +} + +func (c *clientConn) readResponse() error { + if c.reader != nil { + return nil + } + reader, record, err := readFirstRecord(c.Conn, c.client.mode, c.client.psk, c.client.profile, c.readWaitOptions) + if err != nil { + return E.Cause(err, "read reply") + } + cached, err := reuse.ParseReply(record) + if err != nil { + return err + } + reader.SetCache(cached) + c.reader = reader + return nil +} + +func (c *clientConn) Read(p []byte) (int, error) { + err := c.readResponse() + if err != nil { + return 0, err + } + return c.reader.Read(p) +} + +func (c *clientConn) ReadBuffer(buffer *buf.Buffer) error { + err := c.readResponse() + if err != nil { + return err + } + return c.reader.ReadBuffer(buffer) +} + +func (c *clientConn) Write(p []byte) (int, error) { + if c.writer != nil { + return c.writer.Write(p) + } + c.access.Lock() + if c.writer != nil { + c.access.Unlock() + return c.writer.Write(p) + } + defer c.access.Unlock() + err := c.writeRequest(p) + if err != nil { + return 0, err + } + return len(p), nil +} + +func (c *clientConn) WriteBuffer(buffer *buf.Buffer) error { + if c.writer != nil { + return c.writer.WriteBuffer(buffer) + } + c.access.Lock() + if c.writer != nil { + c.access.Unlock() + return c.writer.WriteBuffer(buffer) + } + defer c.access.Unlock() + return c.writeRequestBuffer(buffer) +} + +func (c *clientConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + upstreamWriter, created := bufio.CreateVectorisedWriter(c.Conn) + if !created { + return nil, false + } + return &clientVectorisedWriter{conn: c, upstream: upstreamWriter}, true +} + +type clientVectorisedWriter struct { + conn *clientConn + upstream N.VectorisedWriter +} + +func (w *clientVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { + conn := w.conn + if conn.writer != nil { + return conn.writer.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) + } + conn.access.Lock() + if conn.writer != nil { + recordWriter := conn.writer + conn.access.Unlock() + return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) + } + for index, buffer := range buffers { + if buffer.IsEmpty() { + buffer.Release() + continue + } + err := conn.writeRequestBuffer(buffer) + if err != nil { + conn.access.Unlock() + buf.ReleaseMulti(buffers[index+1:]) + return err + } + if index+1 < len(buffers) { + recordWriter := conn.writer + conn.access.Unlock() + return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers[index+1:]) + } + conn.access.Unlock() + return nil + } + conn.access.Unlock() + return nil +} + +func (c *clientConn) CloseWrite() error { + c.closeWriteOnce.Do(func() { + c.access.Lock() + defer c.access.Unlock() + if c.writer == nil { + c.closeWriteErr = c.writeRequest(nil) + if c.closeWriteErr != nil { + return + } + } + c.closeWriteErr = c.writer.WriteZeroChunk() + }) + return c.closeWriteErr +} + +func (c *clientConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + c.readWaitOptions = options + if c.reader != nil { + c.reader.InitializeReadWaiter(options) + } + return false +} + +func (c *clientConn) WaitReadBuffer() (*buf.Buffer, error) { + err := c.readResponse() + if err != nil { + return nil, err + } + return c.reader.WaitReadBuffer() +} + +func (c *clientConn) CreateReadWaiter() (N.ReadWaiter, bool) { + return c, true +} + +func (c *clientConn) FrontHeadroom() int { + if c.writer != nil { + return c.writer.FrontHeadroom() + } + requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: c.client.userKey, Destination: c.destination} + switch c.client.mode { + case ModeUnsafeRaw: + return requestPayload.Len() + snell.HeaderPlainLen + case ModeUnshaped: + return requestPayload.Len() + snell.SaltLen + snell.HeaderCipherLen + default: + return requestPayload.Len() + c.client.profile.saltBlockLen + c.client.profile.recordPrefixMax + snell.HeaderCipherLen + c.client.profile.padMaxHeadroom + } +} + +func (c *clientConn) RearHeadroom() int { + if c.writer != nil { + return c.writer.RearHeadroom() + } + if c.client.mode == ModeUnsafeRaw { + return 0 + } + return snell.AEADTagLen +} + +func (c *clientConn) WriterMTU() int { + if c.writer != nil { + return c.writer.WriterMTU() + } + if c.client.mode == ModeDefault { + return c.client.profile.chunkMax + } + return maxPayload +} + +func (c *clientConn) NeedHandshakeForRead() bool { + return c.reader == nil +} + +func (c *clientConn) NeedHandshakeForWrite() bool { + return c.writer == nil +} + +func (c *clientConn) NeedAdditionalReadDeadline() bool { + return c.reader == nil +} + +func (c *clientConn) Upstream() any { + return c.Conn +} + +func (c *clientConn) RemoteAddr() net.Addr { + return c.destination.TCPAddr() +} + +var ( + _ N.ExtendedConn = (*clientConn)(nil) + _ N.ReadWaiter = (*clientConn)(nil) + _ N.ReadWaitCreator = (*clientConn)(nil) + _ N.VectorisedWriteCreator = (*clientConn)(nil) + _ N.VectorisedWriter = (*clientVectorisedWriter)(nil) + _ N.EarlyReader = (*clientConn)(nil) + _ N.EarlyWriter = (*clientConn)(nil) + _ N.WriteCloser = (*clientConn)(nil) +) diff --git a/proxy/snell/sing-snell/snellv6/handshake.go b/proxy/snell/sing-snell/snellv6/handshake.go new file mode 100644 index 00000000000..75fa1810f6c --- /dev/null +++ b/proxy/snell/sing-snell/snellv6/handshake.go @@ -0,0 +1,216 @@ +package snellv6 + +import ( + "crypto/rand" + "io" + "time" + + snell "github.com/sagernet/sing-snell" + "github.com/sagernet/sing-snell/internal/reuse" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + N "github.com/sagernet/sing/common/network" +) + +func writeFirstRecord(conn io.Writer, mode Mode, psk []byte, profile *Profile, payload []byte) (reuse.RecordWriter, error) { + switch mode { + case ModeUnsafeRaw: + writer := newRawWriter(conn) + // Surge 6.7.0 (11520): FUN_1000154e0: chunks unsafe-raw payloads larger than the + // 16-bit record payload field instead of truncating the length. + if len(payload) > maxPayload { + _, err := writer.Write(payload) + if err != nil { + return nil, err + } + return writer, nil + } + out := buf.NewSize(snell.HeaderPlainLen + len(payload)) + putHeader(out.Extend(snell.HeaderPlainLen), 0, len(payload)) + common.Must1(out.Write(payload)) + _, err := conn.Write(out.Bytes()) + out.Release() + if err != nil { + return nil, err + } + return writer, nil + case ModeUnshaped: + salt := make([]byte, snell.SaltLen) + _, err := io.ReadFull(rand.Reader, salt) + if err != nil { + return nil, err + } + key := snell.DeriveKey(psk, salt) + aead, err := snell.NewAEAD(key) + if err != nil { + return nil, err + } + nonce := make([]byte, snell.NonceLen) + recordCount := 1 + if len(payload) > 0 { + recordCount = (len(payload) + maxPayload - 1) / maxPayload + } + payloadCipherLen := len(payload) + if len(payload) > 0 { + payloadCipherLen += recordCount * snell.AEADTagLen + } + out := buf.NewSize(snell.SaltLen + recordCount*snell.HeaderCipherLen + payloadCipherLen) + common.Must1(out.Write(salt)) + w := newUnshapedWriter(conn, aead, nonce) + // Surge 6.7.0 (11520): FUN_10001596c: emits the salt once, then splits the first + // unshaped payload into 0xffff-byte records. + for remaining := payload; ; { + recordLen := min(len(remaining), maxPayload) + w.sealHeader(out.Extend(snell.HeaderCipherLen), recordLen) + if recordLen > 0 { + aead.Seal(out.Extend(recordLen + snell.AEADTagLen)[:0], nonce, remaining[:recordLen], nil) + snell.IncreaseNonce(nonce) + remaining = remaining[recordLen:] + if len(remaining) > 0 { + continue + } + } + break + } + _, err = conn.Write(out.Bytes()) + out.Release() + if err != nil { + return nil, err + } + return w, nil + case ModeDefault: + salt := make([]byte, snell.SaltLen) + _, err := io.ReadFull(rand.Reader, salt) + if err != nil { + return nil, err + } + aead, err := snell.NewAEAD(snell.DeriveKey(psk, salt)) + if err != nil { + return nil, err + } + writer := newShapedWriter(conn, profile, salt, aead, make([]byte, snell.NonceLen)) + if len(payload) == 0 { + writer.payloadLimitFor(time.Now()) + record := writer.makeSliceRecord(nil) + _, err = conn.Write(record.Bytes()) + record.Release() + } else { + _, err = writer.Write(payload) + } + if err != nil { + return nil, err + } + return writer, nil + default: + panic("snell: invalid v6 mode") + } +} + +func writeFirstRecordBuffer(conn io.Writer, mode Mode, psk []byte, profile *Profile, buffer *buf.Buffer) (reuse.RecordWriter, error) { + switch mode { + case ModeUnsafeRaw: + writer := newRawWriter(conn) + err := writer.WriteBuffer(buffer) + if err != nil { + return nil, err + } + return writer, nil + case ModeUnshaped: + if buffer.Len() > maxPayload { + writer, err := writeFirstRecord(conn, mode, psk, profile, buffer.Bytes()) + buffer.Release() + if err != nil { + return nil, err + } + return writer, nil + } + salt := make([]byte, snell.SaltLen) + _, err := io.ReadFull(rand.Reader, salt) + if err != nil { + buffer.Release() + return nil, err + } + key := snell.DeriveKey(psk, salt) + aead, err := snell.NewAEAD(key) + if err != nil { + buffer.Release() + return nil, err + } + nonce := make([]byte, snell.NonceLen) + record := buffer.ExtendHeader(snell.SaltLen + snell.HeaderCipherLen) + copy(record[:snell.SaltLen], salt) + writer := newUnshapedWriter(conn, aead, nonce) + writer.sealHeader(record[snell.SaltLen:], buffer.Len()-(snell.SaltLen+snell.HeaderCipherLen)) + payloadCipher := buffer.From(snell.SaltLen + snell.HeaderCipherLen) + buffer.Extend(snell.AEADTagLen) + aead.Seal(payloadCipher[:0], nonce, payloadCipher, nil) + snell.IncreaseNonce(nonce) + _, err = conn.Write(buffer.Bytes()) + buffer.Release() + if err != nil { + return nil, err + } + return writer, nil + case ModeDefault: + salt := make([]byte, snell.SaltLen) + _, err := io.ReadFull(rand.Reader, salt) + if err != nil { + buffer.Release() + return nil, err + } + aead, err := snell.NewAEAD(snell.DeriveKey(psk, salt)) + if err != nil { + buffer.Release() + return nil, err + } + writer := newShapedWriter(conn, profile, salt, aead, make([]byte, snell.NonceLen)) + err = writer.WriteBuffer(buffer) + if err != nil { + return nil, err + } + return writer, nil + default: + panic("snell: invalid v6 mode") + } +} + +func readFirstRecord(conn io.Reader, mode Mode, psk []byte, profile *Profile, readWaitOptions N.ReadWaitOptions) (reuse.RecordReader, *buf.Buffer, error) { + switch mode { + case ModeUnsafeRaw: + r := newRawReader(conn) + r.InitializeReadWaiter(readWaitOptions) + record, err := r.ReadRecord() + if err != nil { + return nil, nil, err + } + return r, record, nil + case ModeUnshaped: + salt := make([]byte, snell.SaltLen) + _, err := io.ReadFull(conn, salt) + if err != nil { + return nil, nil, err + } + key := snell.DeriveKey(psk, salt) + aead, err := snell.NewAEAD(key) + if err != nil { + return nil, nil, err + } + r := newUnshapedReader(conn, aead, make([]byte, snell.NonceLen)) + r.InitializeReadWaiter(readWaitOptions) + record, err := r.ReadRecord() + if err != nil { + return nil, nil, err + } + return r, record, nil + case ModeDefault: + reader := newShapedReader(conn, psk, profile) + reader.InitializeReadWaiter(readWaitOptions) + record, err := reader.ReadRecord() + if err != nil { + return nil, nil, err + } + return reader, record, nil + default: + panic("snell: invalid v6 mode") + } +} diff --git a/proxy/snell/sing-snell/snellv6/mixing.go b/proxy/snell/sing-snell/snellv6/mixing.go new file mode 100644 index 00000000000..ae478e83e6f --- /dev/null +++ b/proxy/snell/sing-snell/snellv6/mixing.go @@ -0,0 +1,54 @@ +package snellv6 + +func (p *Profile) mixPaddingPayload(seq uint32, padding []byte, payloadCipher []byte) { + n := min(len(padding), len(payloadCipher)) + if n == 0 { + return + } + for round := uint32(0); round < p.mixRounds; round++ { + switch p.mixMode { + case 0: + p.mixFixedStride(round, padding, payloadCipher, n) + case 1: + p.mixAlternatingBlock(round, padding, payloadCipher, n) + case 2: + p.mixPRFStride(seq, round, padding, payloadCipher, n) + } + } +} + +func (p *Profile) mixFixedStride(round uint32, padding []byte, payloadCipher []byte, n int) { + stride := max(p.mixStride+int(round%3), 1) + if stride == 1 { + for i := range n { + padding[i], payloadCipher[i] = payloadCipher[i], padding[i] + } + return + } + for off := p.mixOffsetBase % stride; off < n; off += stride { + padding[off], payloadCipher[off] = payloadCipher[off], padding[off] + } +} + +func (p *Profile) mixAlternatingBlock(round uint32, padding []byte, payloadCipher []byte, n int) { + block := p.mixBlock + for off := int(round&1) * block; off+block <= n; off += block * 2 { + for i := off; i < off+block; i++ { + padding[i], payloadCipher[i] = payloadCipher[i], padding[i] + } + } +} + +func (p *Profile) mixPRFStride(seq uint32, round uint32, padding []byte, payloadCipher []byte, n int) { + stride := max(p.mixStride+int(round%3), 1) + off := int((uint64(p.namespaces.prf32(labelMixOffset, seq, round)) + uint64(p.mixOffsetBase)) % uint64(stride)) + if stride == 1 { + for i := range n { + padding[i], payloadCipher[i] = payloadCipher[i], padding[i] + } + return + } + for ; off < n; off += stride { + padding[off], payloadCipher[off] = payloadCipher[off], padding[off] + } +} diff --git a/proxy/snell/sing-snell/snellv6/mode.go b/proxy/snell/sing-snell/snellv6/mode.go new file mode 100644 index 00000000000..d89b470f1eb --- /dev/null +++ b/proxy/snell/sing-snell/snellv6/mode.go @@ -0,0 +1,41 @@ +package snellv6 + +import ( + E "github.com/sagernet/sing/common/exceptions" +) + +type Mode int + +const ( + ModeDefault Mode = iota + ModeUnshaped + ModeUnsafeRaw +) + +func ParseMode(name string) (Mode, error) { + switch name { + case "", "default": + return ModeDefault, nil + case "unshaped": + return ModeUnshaped, nil + case "unsafe-raw": + return ModeUnsafeRaw, nil + default: + return 0, E.New("snell: unknown v6 mode: ", name) + } +} + +func (m Mode) String() string { + switch m { + case ModeDefault: + return "default" + case ModeUnshaped: + return "unshaped" + case ModeUnsafeRaw: + return "unsafe-raw" + default: + panic("snell: invalid v6 mode") + } +} + +const maxPayload = 0xffff diff --git a/proxy/snell/sing-snell/snellv6/packet.go b/proxy/snell/sing-snell/snellv6/packet.go new file mode 100644 index 00000000000..179241d4286 --- /dev/null +++ b/proxy/snell/sing-snell/snellv6/packet.go @@ -0,0 +1,479 @@ +package snellv6 + +import ( + "io" + "net" + "os" + "sync" + + snell "github.com/sagernet/sing-snell" + "github.com/sagernet/sing-snell/internal/reuse" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +const ( + maxUDPRequestHeaderLen = 1 + 1 + 255 + 2 + maxUDPResponseHeaderLen = 1 + 16 + 2 +) + +func (c *clientPacketConn) udpRequestAddrLen(destination M.Socksaddr) int { + if destination.IsIP() { + if destination.Unwrap().Addr.Is4() { + return 1 + 1 + 4 + 2 + } + return 1 + 1 + 16 + 2 + } + return 1 + len(destination.Fqdn) + 2 +} + +type clientPacketConn struct { + net.Conn + client *Client + + writeAccess sync.Mutex + writer reuse.RecordWriter + + readAccess sync.Mutex + reader reuse.RecordReader + + readWaitOptions N.ReadWaitOptions +} + +func (c *clientPacketConn) writeRequest() error { + c.writeAccess.Lock() + defer c.writeAccess.Unlock() + if c.writer != nil { + return nil + } + requestPayload := snell.Request{Command: snell.CommandUDP, ClientID: c.client.userKey} + request := buf.NewSize(requestPayload.Len()) + err := requestPayload.Write(request) + if err != nil { + request.Release() + return err + } + writer, err := writeFirstRecord(c.Conn, c.client.mode, c.client.psk, c.client.profile, request.Bytes()) + request.Release() + if err != nil { + return E.Cause(err, "write udp request") + } + c.writer = writer + return nil +} + +func (c *clientPacketConn) readReply() (reuse.RecordReader, error) { + c.readAccess.Lock() + defer c.readAccess.Unlock() + if c.reader != nil { + return c.reader, nil + } + reader, record, err := readFirstRecord(c.Conn, c.client.mode, c.client.psk, c.client.profile, c.readWaitOptions) + if err != nil { + return nil, E.Cause(err, "read udp reply") + } + cached, err := reuse.ParseReply(record) + if err != nil { + return nil, err + } + reader.SetCache(cached) + c.reader = reader + return reader, nil +} + +func (c *clientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { + err := c.writeRequest() + if err != nil { + buffer.Release() + return err + } + _, err = c.readReply() + if err != nil { + buffer.Release() + return err + } + header := buf.With(buffer.ExtendHeader(1 + c.udpRequestAddrLen(destination))) + common.Must(header.WriteByte(snell.UDPCommandForward)) + err = snell.WriteUDPRequestAddress(header, destination) + if err != nil { + buffer.Release() + return err + } + if buffer.Len() > maxPayload { + buffer.Release() + return snell.ErrPayloadTooLarge + } + return c.writer.WritePacketBuffer(buffer) +} + +func (c *clientPacketConn) CreatePacketBatchWriter() (N.PacketBatchWriter, bool) { + upstreamWriter, created := bufio.CreateVectorisedWriter(c.Conn) + if !created { + return nil, false + } + return &clientPacketBatchWriter{conn: c, upstream: upstreamWriter}, true +} + +type clientPacketBatchWriter struct { + conn *clientPacketConn + upstream N.VectorisedWriter + + access sync.Mutex + writer N.VectorisedWriter +} + +func (w *clientPacketBatchWriter) WritePacketBatch(buffers []*buf.Buffer, destinations []M.Socksaddr) error { + if len(buffers) == 0 || len(buffers) != len(destinations) { + buf.ReleaseMulti(buffers) + return os.ErrInvalid + } + w.access.Lock() + if w.writer == nil { + err := w.conn.writeRequest() + if err != nil { + w.access.Unlock() + buf.ReleaseMulti(buffers) + return err + } + _, err = w.conn.readReply() + if err != nil { + w.access.Unlock() + buf.ReleaseMulti(buffers) + return err + } + w.writer = w.conn.writer.CreatePacketVectorisedWriterFor(w.upstream) + } + recordWriter := w.writer + w.access.Unlock() + for index, buffer := range buffers { + header := buf.With(buffer.ExtendHeader(1 + w.conn.udpRequestAddrLen(destinations[index]))) + common.Must(header.WriteByte(snell.UDPCommandForward)) + err := snell.WriteUDPRequestAddress(header, destinations[index]) + if err != nil { + buf.ReleaseMulti(buffers) + return err + } + } + return recordWriter.WriteVectorised(buffers) +} + +func (c *clientPacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) { + reader, err := c.readReply() + if err != nil { + return M.Socksaddr{}, err + } + record, err := reader.NextRecord() + if err != nil { + return M.Socksaddr{}, err + } + destination, err := snell.ReadUDPResponseAddress(record) + if err != nil { + record.Release() + return M.Socksaddr{}, err + } + if record.Len() > buffer.FreeLen() { + record.Release() + return M.Socksaddr{}, io.ErrShortBuffer + } + _, err = buffer.Write(record.Bytes()) + record.Release() + if err != nil { + return M.Socksaddr{}, err + } + return destination, nil +} + +func (c *clientPacketConn) FrontHeadroom() int { + switch c.client.mode { + case ModeUnsafeRaw: + return snell.HeaderPlainLen + maxUDPRequestHeaderLen + case ModeUnshaped: + return snell.HeaderCipherLen + maxUDPRequestHeaderLen + default: + return c.client.profile.recordPrefixMax + snell.HeaderCipherLen + c.client.profile.padMaxHeadroom + maxUDPRequestHeaderLen + } +} + +func (c *clientPacketConn) RearHeadroom() int { + if c.client.mode == ModeUnsafeRaw { + return 0 + } + return snell.AEADTagLen +} + +func (c *clientPacketConn) WriterMTU() int { + switch c.client.mode { + case ModeUnsafeRaw, ModeUnshaped: + return maxPayload - maxUDPRequestHeaderLen + default: + payloadLimit := c.client.profile.chunkInitial + switch c.client.profile.chunkPolicy { + case 1: + payloadLimit = c.client.profile.chunkBuckets[0] + for _, chunkBucket := range c.client.profile.chunkBuckets[1:] { + payloadLimit = min(payloadLimit, chunkBucket) + } + case 2: + payloadLimit -= c.client.profile.chunkJitter + } + payloadLimit = max(0x40, min(payloadLimit, c.client.profile.chunkMax)) + return max(1, payloadLimit-maxUDPRequestHeaderLen) + } +} + +func (c *clientPacketConn) Upstream() any { + return c.Conn +} + +func (c *clientPacketConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + c.readAccess.Lock() + defer c.readAccess.Unlock() + c.readWaitOptions = options + if c.reader != nil { + c.reader.InitializeReadWaiter(options) + } + return false +} + +func (c *clientPacketConn) WaitReadPacket() (*buf.Buffer, M.Socksaddr, error) { + reader, err := c.readReply() + if err != nil { + return nil, M.Socksaddr{}, err + } + record, err := reader.WaitReadBuffer() + if err != nil { + return nil, M.Socksaddr{}, err + } + destination, err := snell.ReadUDPResponseAddress(record) + if err != nil { + record.Release() + return nil, M.Socksaddr{}, err + } + return record, destination, nil +} + +type serverPacketConn struct { + net.Conn + service *Service + reader reuse.RecordReader + readWaitOptions N.ReadWaitOptions + + writeAccess sync.Mutex + writer reuse.RecordWriter +} + +func (c *serverPacketConn) responseAddrLen(source M.Socksaddr) int { + if source.Unwrap().Addr.Is4() { + return 1 + 4 + 2 + } + return 1 + 16 + 2 +} + +func (c *serverPacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) { + record, err := c.reader.NextRecord() + if err != nil { + return M.Socksaddr{}, err + } + command, err := record.ReadByte() + if err != nil { + record.Release() + return M.Socksaddr{}, err + } + if command != snell.UDPCommandForward { + record.Release() + return M.Socksaddr{}, E.Extend(snell.ErrUnsupportedCommand, "udp ", command) + } + destination, err := snell.ReadUDPRequestAddress(record) + if err != nil { + record.Release() + return M.Socksaddr{}, err + } + if record.Len() > buffer.FreeLen() { + record.Release() + return M.Socksaddr{}, io.ErrShortBuffer + } + _, err = buffer.Write(record.Bytes()) + record.Release() + if err != nil { + return M.Socksaddr{}, err + } + return destination, nil +} + +func (c *serverPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { + c.writeAccess.Lock() + if c.writer == nil { + err := c.writeTunnelReply() + if err != nil { + c.writeAccess.Unlock() + buffer.Release() + return err + } + } + writer := c.writer + c.writeAccess.Unlock() + header := buf.With(buffer.ExtendHeader(c.responseAddrLen(destination))) + err := snell.WriteUDPResponseAddress(header, destination) + if err != nil { + buffer.Release() + return err + } + if buffer.Len() > maxPayload { + buffer.Release() + return snell.ErrPayloadTooLarge + } + return writer.WritePacketBuffer(buffer) +} + +func (c *serverPacketConn) CreatePacketBatchWriter() (N.PacketBatchWriter, bool) { + upstreamWriter, created := bufio.CreateVectorisedWriter(c.Conn) + if !created { + return nil, false + } + return &serverPacketBatchWriter{conn: c, upstream: upstreamWriter}, true +} + +type serverPacketBatchWriter struct { + conn *serverPacketConn + upstream N.VectorisedWriter + + access sync.Mutex + writer N.VectorisedWriter +} + +func (w *serverPacketBatchWriter) WritePacketBatch(buffers []*buf.Buffer, destinations []M.Socksaddr) error { + if len(buffers) == 0 || len(buffers) != len(destinations) { + buf.ReleaseMulti(buffers) + return os.ErrInvalid + } + w.access.Lock() + if w.writer == nil { + w.conn.writeAccess.Lock() + if w.conn.writer == nil { + err := w.conn.writeTunnelReply() + if err != nil { + w.conn.writeAccess.Unlock() + w.access.Unlock() + buf.ReleaseMulti(buffers) + return err + } + } + w.writer = w.conn.writer.CreatePacketVectorisedWriterFor(w.upstream) + w.conn.writeAccess.Unlock() + } + recordWriter := w.writer + w.access.Unlock() + for index, buffer := range buffers { + header := buf.With(buffer.ExtendHeader(w.conn.responseAddrLen(destinations[index]))) + err := snell.WriteUDPResponseAddress(header, destinations[index]) + if err != nil { + buf.ReleaseMulti(buffers) + return err + } + } + return recordWriter.WriteVectorised(buffers) +} + +func (c *serverPacketConn) writeTunnelReply() error { + reply := [1]byte{snell.ReplyTunnel} + if c.writer != nil { + _, err := c.writer.Write(reply[:]) + if err != nil { + return E.Cause(err, "write udp reply") + } + return nil + } + writer, err := writeFirstRecord(c.Conn, c.service.mode, c.service.psk, c.service.profile, reply[:]) + if err != nil { + return E.Cause(err, "write udp reply") + } + c.writer = writer + return nil +} + +func (c *serverPacketConn) FrontHeadroom() int { + switch c.service.mode { + case ModeUnsafeRaw: + return snell.HeaderPlainLen + maxUDPResponseHeaderLen + case ModeUnshaped: + return snell.HeaderCipherLen + maxUDPResponseHeaderLen + default: + return c.service.profile.recordPrefixMax + snell.HeaderCipherLen + c.service.profile.padMaxHeadroom + maxUDPResponseHeaderLen + } +} + +func (c *serverPacketConn) RearHeadroom() int { + if c.service.mode == ModeUnsafeRaw { + return 0 + } + return snell.AEADTagLen +} + +func (c *serverPacketConn) WriterMTU() int { + switch c.service.mode { + case ModeUnsafeRaw, ModeUnshaped: + return maxPayload - maxUDPResponseHeaderLen + default: + payloadLimit := c.service.profile.chunkInitial + switch c.service.profile.chunkPolicy { + case 1: + payloadLimit = c.service.profile.chunkBuckets[0] + for _, chunkBucket := range c.service.profile.chunkBuckets[1:] { + payloadLimit = min(payloadLimit, chunkBucket) + } + case 2: + payloadLimit -= c.service.profile.chunkJitter + } + payloadLimit = max(0x40, min(payloadLimit, c.service.profile.chunkMax)) + return max(1, payloadLimit-maxUDPResponseHeaderLen) + } +} + +func (c *serverPacketConn) Upstream() any { + return c.Conn +} + +func (c *serverPacketConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + c.readWaitOptions = options + c.reader.InitializeReadWaiter(options) + return false +} + +func (c *serverPacketConn) WaitReadPacket() (*buf.Buffer, M.Socksaddr, error) { + record, err := c.reader.WaitReadBuffer() + if err != nil { + return nil, M.Socksaddr{}, err + } + command, err := record.ReadByte() + if err != nil { + record.Release() + return nil, M.Socksaddr{}, err + } + if command != snell.UDPCommandForward { + record.Release() + return nil, M.Socksaddr{}, E.Extend(snell.ErrUnsupportedCommand, "udp ", command) + } + destination, err := snell.ReadUDPRequestAddress(record) + if err != nil { + record.Release() + return nil, M.Socksaddr{}, err + } + return record, destination, nil +} + +var ( + _ N.PacketConn = (*clientPacketConn)(nil) + _ N.PacketReadWaiter = (*clientPacketConn)(nil) + _ N.PacketBatchWriteCreator = (*clientPacketConn)(nil) + _ N.WriterWithMTU = (*clientPacketConn)(nil) + _ N.PacketBatchWriter = (*clientPacketBatchWriter)(nil) + _ N.PacketConn = (*serverPacketConn)(nil) + _ N.PacketReadWaiter = (*serverPacketConn)(nil) + _ N.PacketBatchWriteCreator = (*serverPacketConn)(nil) + _ N.WriterWithMTU = (*serverPacketConn)(nil) + _ N.PacketBatchWriter = (*serverPacketBatchWriter)(nil) +) diff --git a/proxy/snell/sing-snell/snellv6/profile.go b/proxy/snell/sing-snell/snellv6/profile.go new file mode 100644 index 00000000000..752b33f133e --- /dev/null +++ b/proxy/snell/sing-snell/snellv6/profile.go @@ -0,0 +1,522 @@ +package snellv6 + +import ( + "encoding/binary" + "math/bits" + + snell "github.com/sagernet/sing-snell" + + "golang.org/x/crypto/blake2b" +) + +// Surge 6.7.0 (11520): FUN_1000130ac: derives v6 profile values with this SplitMix64 gamma. +const goldenGamma = 0x9e3779b97f4a7c15 + +const saltLen = 16 + +const ( + // Surge 6.7.0 (11520): FUN_1000130ac: uses these PRF labels for v6 profile derivation. + labelPadding = 0 + labelBitPercent = 1 + labelMotif = 2 + labelMixOffset = 3 + labelSalt = 3 + labelProfileID = 5 + labelGenerator = 6 + labelPadMin = 7 + labelPadMax = 8 + labelPadCount = 9 + labelPadInterval = 10 + labelSmallLimit = 11 + labelBitMin = 12 + labelBitMax = 13 + labelPrefixMin = 14 + labelPrefixMax = 15 + labelMixMode = 16 + labelMixRounds = 17 + labelMixStride = 18 + labelMixOffsetBase = 19 + labelMixBlock = 20 + labelChunkPolicy = 21 + labelChunkInitial = 22 + labelChunkFirstCap = 22 + labelChunkMax = 23 + labelChunkStep = 24 + labelChunkJitter = 25 + labelChunkBucket = 26 + labelIdleReset = 27 + labelWritePolicy = 28 + labelWriteFirst = 29 + labelWriteBucket = 30 + labelWriteSeq = 31 + labelWriteJitter = 32 + labelRecordPrefix = 33 + labelPayloadPad = 34 + labelWriteTarget = 35 + labelWriteJitterV = 36 + labelWriteNext = 37 + labelChunkSize = 38 + labelChunkJitterV = 39 +) + +const ( + // Surge 6.7.0 (11520): FUN_1000130ac: uses these domain separators for handshake and chunk profile values. + handshakeDomain = 0x7053 + mixHandshakeDomain = 0x51a7 + chunkInitialDomain = 0xf17c +) + +const ( + // Surge 6.7.0 (11520): FUN_1000130ac: uses these namespace seeds for v6 profile derivation. + nsSeedProfile = 0xb46c2e7d9a1538f1 + nsSeedPrefix = 0x5d9217c083e64ab9 + nsSeedMotif = 0xa71f0c54d8396e2b + nsSeedSalt = 0x3e8a91b52740f6cd + nsSeedMix = 0xc9f4260b7d1e835a + nsSeedChunk = 0x62d0b5e19c4a783f + nsSeedWrite = 0x917b3c48e6a205d4 +) + +const ( + // Surge 6.7.0 (11520): FUN_100014910/FUN_1000149c4: uses these arithmetic constants for v6 profile PRFs. + domainMul = 0xd6e8feb86659fd93 + namespaceAdd = 0xa0761d6478bd642f + coefB = 0x589965cc75374cc3 + addB = 0x33a213ec50ffe2e9 + coefA = 0xe7037ed1a0b428db + addA = 0x8f3907f7b2b80c35 + maxExtraPadding = 0x02da +) + +// Surge 6.7.0 (11520): FUN_1000130ac: profileSeed is the BLAKE2b prefix mixed with the PSK to derive the profile secret. +var profileSeed = [...]byte{ + 0x8d, 0x41, 0xa7, 0x13, 0x5c, 0xe2, 0x09, 0xbb, 0x70, 0x2f, 0xd6, 0x94, + 0x33, 0x18, 0xc0, 0x6e, 0x4a, 0x91, 0x25, 0xfd, 0xb8, 0x03, 0x77, 0xac, +} + +// Surge 6.7.0 (11520): FUN_100014f98: generator 0 maps low nibbles through this bit-rotation table. +var bitRotateTable = [...]byte{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, + 0x03, 0x05, 0x09, 0x11, 0x21, 0x41, 0x81, 0x06, 0x0a, 0x12, 0x22, 0x42, 0x82, 0x0c, 0x18, 0x24, + 0x07, 0x0b, 0x13, 0x23, 0x43, 0x83, 0x0d, 0x19, 0x31, 0x61, 0xc1, 0x0e, 0x1c, 0x38, 0x70, 0xe0, + 0x0f, 0x17, 0x27, 0x47, 0x87, 0x1b, 0x33, 0x63, 0xc3, 0x1d, 0x39, 0x71, 0xe1, 0x3c, 0x78, 0xf0, + 0xf8, 0xf4, 0xec, 0xdc, 0xbc, 0x7c, 0xf2, 0xe6, 0xce, 0x9e, 0x3e, 0xf1, 0xe3, 0xc7, 0x8f, 0x1f, + 0xfc, 0xfa, 0xf6, 0xee, 0xde, 0xbe, 0x7e, 0xf9, 0xf5, 0xed, 0xdd, 0xbd, 0x7d, 0xf3, 0xe7, 0xdb, + 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, +} + +func splitmix64(x uint64) uint64 { + x ^= x >> 30 + x *= 0xbf58476d1ce4e5b9 + x ^= x >> 27 + x *= 0x94d049bb133111eb + x ^= x >> 31 + return x +} + +func prf32Fold(namespace uint64, label uint32, a uint64, b uint64) uint32 { + x := namespace ^ (b*coefB + addB) ^ (uint64(label) * goldenGamma) ^ (a*coefA + addA) + y := splitmix64(x) + return uint32(y ^ (y >> 32)) +} + +func pick(raw uint32, lo int, hi int) int { + if hi < lo { + panic("snell: invalid profile range") + } + return lo + int(uint64(raw)%uint64(hi-lo+1)) +} + +func pickU32(raw uint32, lo uint32, hi uint32) uint32 { + if hi < lo { + panic("snell: invalid profile range") + } + return lo + raw%(hi-lo+1) +} + +type namespaces struct { + profile uint64 + prefix uint64 + motif uint64 + salt uint64 + mix uint64 + chunk uint64 + write uint64 +} + +func deriveNamespace(secret []byte, label uint32, seedConst uint64) uint64 { + s0 := binary.LittleEndian.Uint64(secret[0:]) + s1 := binary.LittleEndian.Uint64(secret[8:]) + s2 := binary.LittleEndian.Uint64(secret[16:]) + s3 := binary.LittleEndian.Uint64(secret[24:]) + mixed := uint64(label)*domainMul ^ + (seedConst + namespaceAdd) ^ + s0 ^ + (s1 + goldenGamma) ^ + bits.RotateLeft64(s2, 17) ^ + bits.RotateLeft64(s3, -11) + return splitmix64(mixed) +} + +func newNamespaces(secret []byte) namespaces { + return namespaces{ + profile: deriveNamespace(secret, labelProfileID, nsSeedProfile), + prefix: deriveNamespace(secret, labelPadding, nsSeedPrefix), + motif: deriveNamespace(secret, labelMotif, nsSeedMotif), + salt: deriveNamespace(secret, labelSalt, nsSeedSalt), + mix: deriveNamespace(secret, labelMixMode, nsSeedMix), + chunk: deriveNamespace(secret, labelChunkPolicy, nsSeedChunk), + write: deriveNamespace(secret, labelWritePolicy, nsSeedWrite), + } +} + +func (n namespaces) forLabel(label uint32) uint64 { + switch label { + case labelPadding, labelBitPercent, labelPrefixMin, labelPrefixMax, labelRecordPrefix, labelPayloadPad: + return n.prefix + case labelMotif: + return n.motif + case labelMixOffset, labelMixMode, labelMixRounds, labelMixStride, labelMixOffsetBase, labelMixBlock: + return n.mix + case labelChunkPolicy, labelChunkInitial, labelChunkMax, labelChunkStep, labelChunkJitter, labelChunkBucket, labelChunkSize, labelChunkJitterV: + return n.chunk + case labelWritePolicy, labelWriteFirst, labelWriteBucket, labelWriteSeq, labelWriteJitter, labelWriteTarget, labelWriteJitterV, labelWriteNext: + return n.write + default: + return n.profile + } +} + +func (n namespaces) prf32(label uint32, a uint32, b uint32) uint32 { + return prf32Fold(n.forLabel(label), label, uint64(a), uint64(b)) +} + +func (n namespaces) prfBytes(label uint32, a uint32, output []byte) { + seed := n.forLabel(label) ^ + (uint64(a)*domainMul + 0xb57de1f3f82cb33f) ^ + (uint64(label) * 0xa24baed4963ee407) ^ + (uint64(len(output))*0x165667b19e3779f9 + 0x0d4cd3e7b14a36d7) + offset := 0 + for offset < len(output) { + seed += goldenGamma + block := splitmix64(seed) + for shift := 0; shift < 64 && offset < len(output); shift += 8 { + output[offset] = byte(block >> shift) + offset++ + } + } +} + +func (n namespaces) prfStatic(label uint32, domain uint32) uint32 { + return prf32Fold(n.forLabel(label), label, 0, uint64(domain)) +} + +type Profile struct { + namespaces namespaces + + generator uint32 + padMin int + padMax int + padCount uint32 + padInterval uint32 + smallLimit int + bitMin uint32 + bitMax uint32 + prefixMinRecord int + prefixMaxRecord int + mixMode uint32 + mixRounds uint32 + mixStride int + mixOffsetBase int + mixBlock int + chunkPolicy uint32 + chunkInitial int + firstRecordCap int + chunkMax int + chunkStep int + chunkJitter int + idleResetSec int + writePolicy uint32 + writeFirst uint32 + chunkBuckets [8]int + writeBuckets [8]int + writeSeq [8]int + writeJitter int + writeJitterPct int + g1 int + g2 int + g3 int + g4 int + g5 int + g6 int + + saltBlockLen int + mixStrideHandshake int + mixRoundsHandshake uint32 + + recordPrefixMax int + padMaxHeadroom int +} + +func profileSecret(psk []byte) [32]byte { + input := make([]byte, len(profileSeed)+len(psk)) + copy(input, profileSeed[:]) + copy(input[len(profileSeed):], psk) + return blake2b.Sum256(input) +} + +func NewProfile(psk []byte) *Profile { + secret := profileSecret(psk) + ns := newNamespaces(secret[:]) + + padMin := pick(ns.prfStatic(labelPadMin, 0), 0x18, 0xa0) + padMax := min(padMin+pick(ns.prfStatic(labelPadMax, 0), 0xa0, 0x3c0), maxExtraPadding) + + prefixMinHS := pick(ns.prfStatic(labelPrefixMin, handshakeDomain), 0x10, 0x60) + prefixMaxHS := min(prefixMinHS+pick(ns.prfStatic(labelPrefixMax, handshakeDomain), 0x10, 0xa0), 0x80) + prefixMinHS = min(prefixMinHS, prefixMaxHS) + saltPrefixLen := pick(ns.prfStatic(labelRecordPrefix, handshakeDomain), prefixMinHS, prefixMaxHS) + saltBlockLen := saltLen + saltPrefixLen + + mixRoundsHS := pickU32(ns.prfStatic(labelMixRounds, mixHandshakeDomain), 1, 4) + mixStrideHS := pick(ns.prfStatic(labelMixStride, mixHandshakeDomain), 0x11, 0xfb) + + prefixMinRecord := pick(ns.prfStatic(labelPrefixMin, 0), 0x08, 0x50) + prefixMaxRecord := min(prefixMinRecord+pick(ns.prfStatic(labelPrefixMax, 0), 0x10, 0xa0), 0x80) + prefixMinRecord = min(prefixMinRecord, prefixMaxRecord) + + chunkInitial := max(0x60, min(pick(ns.prfStatic(labelChunkInitial, 0), 0x200, 0x05b4), 0x05b4)) + firstRecordCap := max(0x100, min(pick(ns.prfStatic(labelChunkFirstCap, chunkInitialDomain), 0x100, 0x300), min(chunkInitial, 0x300))) + chunkMax := max(pick(ns.prfStatic(labelChunkMax, 0), 0x2000, 0x3fff), chunkInitial) + + profile := &Profile{ + namespaces: ns, + generator: ns.prfStatic(labelGenerator, 0) & 3, + padMin: padMin, + padMax: padMax, + padCount: pickU32(ns.prfStatic(labelPadCount, 0), 2, 8), + padInterval: pickU32(ns.prfStatic(labelPadInterval, 0), 2, 0x0b), + smallLimit: pick(ns.prfStatic(labelSmallLimit, 0), 0x60, 0x300), + bitMin: pickU32(ns.prfStatic(labelBitMin, 0), 0x18, 0x29), + bitMax: pickU32(ns.prfStatic(labelBitMax, 0), 0x3a, 0x4c), + prefixMinRecord: prefixMinRecord, + prefixMaxRecord: prefixMaxRecord, + mixMode: ns.prfStatic(labelMixMode, 0) % 3, + mixRounds: pickU32(ns.prfStatic(labelMixRounds, 0), 1, 3), + mixStride: pick(ns.prfStatic(labelMixStride, 0), 2, 13), + mixOffsetBase: pick(ns.prfStatic(labelMixOffsetBase, 0), 0, 15), + mixBlock: pick(ns.prfStatic(labelMixBlock, 0), 8, 0x40), + chunkPolicy: ns.prfStatic(labelChunkPolicy, 0) % 3, + chunkInitial: chunkInitial, + firstRecordCap: firstRecordCap, + chunkMax: chunkMax, + chunkStep: min(pick(ns.prfStatic(labelChunkStep, 0), 0x400, 0x1000), 0x0b68), + chunkJitter: min(pick(ns.prfStatic(labelChunkJitter, 0), 0x10, 0xc0), 0x0b6), + idleResetSec: pick(ns.prfStatic(labelIdleReset, 0), 0x0c, 0x5a), + writePolicy: ns.prfStatic(labelWritePolicy, 0) % 3, + writeFirst: pickU32(ns.prfStatic(labelWriteFirst, 0), 4, 8), + writeJitter: pick(ns.prfStatic(labelWriteJitter, 0), 0x08, 0x60), + writeJitterPct: pick(ns.prfStatic(labelWritePolicy, 0x504c), 8, 0x30), + g1: pick(ns.prfStatic(labelGenerator, 1), 0x18, 0x80), + g2: pick(ns.prfStatic(labelGenerator, 2), 0x10, 0x60), + g3: pick(ns.prfStatic(labelGenerator, 3), 0x10, 0x60), + g4: pick(ns.prfStatic(labelGenerator, 4), 0x00, 0x09), + g5: pick(ns.prfStatic(labelGenerator, 5), 0x01, 0x08), + g6: pick(ns.prfStatic(labelGenerator, 6), 0x07, 0x17), + saltBlockLen: saltBlockLen, + mixStrideHandshake: mixStrideHS, + mixRoundsHandshake: mixRoundsHS, + recordPrefixMax: prefixMaxRecord, + padMaxHeadroom: padMax + maxExtraPadding, + } + + for index := range 8 { + profile.chunkBuckets[index] = pick(ns.prfStatic(labelChunkBucket, uint32(index)), 0x1000, chunkMax) + profile.writeBuckets[index] = pick(ns.prfStatic(labelWriteBucket, uint32(index)), 0x140, 0x05b4) + profile.writeSeq[index] = pick(ns.prfStatic(labelWriteSeq, uint32(index)), 0x168, 0x05b4) + } + + return profile +} + +func (p *Profile) recordPrefixLen(seq uint32) int { + return pick(p.namespaces.prf32(labelRecordPrefix, seq, 0), p.prefixMinRecord, p.prefixMaxRecord) +} + +func (p *Profile) chunkPayloadLimit(seq uint32, chunkSize int) int { + if chunkSize == 0 { + chunkSize = p.chunkInitial + } + switch p.chunkPolicy { + case 1: + chunkSize = p.chunkBuckets[p.namespaces.prf32(labelChunkSize, seq, uint32(chunkSize))%uint32(len(p.chunkBuckets))] + case 2: + rawJitter := p.namespaces.prf32(labelChunkJitterV, seq, uint32(chunkSize)) + chunkSize += int(rawJitter%uint32(p.chunkJitter*2+1)) - p.chunkJitter + } + return max(0x40, min(chunkSize, p.chunkMax)) +} + +func (p *Profile) nextChunkSize(chunkSize int) int { + if chunkSize == 0 { + return p.chunkInitial + } + return min(chunkSize+p.chunkStep, p.chunkMax) +} + +func (p *Profile) paddingLen(seq uint32, payloadLen int, prefixLen int, saltPrefixLen int, totalLen int) int { + paddingLen := 0 + if seq < p.padCount || (payloadLen > 0 && payloadLen <= p.smallLimit) || (p.padInterval > 0 && seq%p.padInterval == 0) { + paddingLen = pick(p.namespaces.prf32(labelPayloadPad, seq, uint32(payloadLen)), p.padMin, p.padMax) + } + payloadTagLen := 0 + if payloadLen > 0 { + payloadTagLen = snell.AEADTagLen + } + frameLen := totalLen + prefixLen + snell.HeaderCipherLen + paddingLen + payloadLen + payloadTagLen + targetLen := p.writeTarget(seq, frameLen) + if targetLen > frameLen { + paddingLen += min(targetLen-frameLen, maxExtraPadding) + } + if totalLen > 0 { + paddingLen = p.firstRecordPaddingLen(paddingLen, prefixLen, saltPrefixLen, payloadLen) + } + return min(paddingLen, 0xffff) +} + +func (p *Profile) writeTarget(seq uint32, frameLen int) int { + if frameLen > 0x05b3 { + if frameLen <= 0xffff { + return frameLen + } + return 0xffff + } + var targetLen int + if seq < p.writeFirst { + targetLen = p.writeSeq[seq] + } else { + targetLen = p.writeBuckets[p.namespaces.prf32(labelWriteTarget, seq, uint32(frameLen))%uint32(len(p.writeBuckets))] + } + if p.writePolicy == 2 { + rawJitter := p.namespaces.prf32(labelWriteJitterV, seq, 0) + jitter := int(rawJitter%uint32(p.writeJitter*2+1)) - p.writeJitter + targetLen = max(targetLen+jitter, 1) + } + spread := min(maxExtraPadding, int(uint64(frameLen)*uint64(p.writeJitterPct)/100)) + if p.namespaces.prf32(labelWriteTarget, seq, uint32(spread))&1 == 0 { + if targetLen+spread <= 0xffff { + targetLen += spread + } else { + targetLen = 0xffff + } + } else { + halfSpread := spread >> 1 + if targetLen > halfSpread { + targetLen -= halfSpread + } + } + for targetLen >= 0 && frameLen > targetLen { + nextLen := p.writeBuckets[p.namespaces.prf32(labelWriteNext, seq, uint32(targetLen))%uint32(len(p.writeBuckets))] + if nextLen <= targetLen { + nextLen = targetLen + p.padMax + if nextLen > 0xffff { + return 0xffff + } + } + targetLen = nextLen + } + return targetLen +} + +func (p *Profile) firstRecordPaddingLen(paddingLen int, prefixLen int, saltPrefixLen int, payloadLen int) int { + overheadLen := saltPrefixLen + prefixLen + paddingLen + thresholdInput := payloadLen + 0x37 + if payloadLen == 0 { + thresholdInput = payloadLen + 0x27 + } + threshold := max((thresholdInput*25+0x4a)/75, 0xc0) + if overheadLen >= threshold { + return paddingLen + } + targetPaddingLen := threshold - saltPrefixLen - prefixLen + maxPaddingLen := p.padMax + maxExtraPadding + if maxPaddingLen <= targetPaddingLen { + targetPaddingLen = maxPaddingLen + } + if targetPaddingLen > 0xfffe { + return paddingLen + } + return targetPaddingLen +} + +func (p *Profile) fillPadding(seq uint32, output []byte) { + if len(output) == 0 { + return + } + p.namespaces.prfBytes(labelPadding, seq, output) + switch p.generator { + case 0: + bitCount := pickU32(p.namespaces.prf32(labelBitPercent, seq, 0), p.bitMin, p.bitMax) + rotate := 1 + scaled := int(bitCount) * 8 + if scaled > 0x31 { + rotate = 7 + if scaled <= 0x02ed { + rotate = (scaled + 0x32) / 100 + } + } + for index, value := range output { + raw := byte(int(value) + index) + tableRow := max(rotate, 1) + tableValue := bitRotateTable[tableRow*16+int((raw^value)&0x0f)] + output[index] = bits.RotateLeft8(tableValue, int((raw^(value>>4))&7)) + } + case 1: + for index, value := range output { + bucket := int(value) % (p.g1 + p.g2 + p.g3) + switch { + case bucket < p.g1: + // Surge 6.7.0 (11520): FUN_100014f98: truncates the byte/index mix before + // reducing it into the printable range. + output[index] = byte(pick(uint32(byte(int(value)+index)), 0x20, 0x7e)) + case bucket < p.g1+p.g2: + output[index] = byte(pick(uint32(byte(int(value)^index)), 0x80, 0xbf)) + default: + output[index] = byte(pick(uint32(byte(int(value)+index*7)), 0xc0, 0xff)) + } + } + case 2: + for index, value := range output { + low := (int(value&0x0f) + p.g4 + (index & 1)) % 10 + high := (int(value) + ((index & 3) << 4) + 0x30) & 0xf0 + output[index] = byte(high | low) + } + case 3: + motif := make([]byte, 32) + p.namespaces.prfBytes(labelMotif, seq, motif) + motifLen := p.g5 * 4 + if motifLen == 0 { + motifLen = 4 + } + period := max(p.g6, 5) + blockOffset := 0 + motifOffset := 0 + for index, value := range output { + switch { + case blockOffset < period-3: + output[index] = byte((p.g5+3)*index) ^ motif[motifOffset%len(motif)] + case blockOffset < period-1: + output[index] = byte(0x30 | (int(value) % 10)) + } + blockOffset++ + if blockOffset == period { + blockOffset = 0 + } + motifOffset++ + if motifOffset == motifLen { + motifOffset = 0 + } + } + } +} diff --git a/proxy/snell/sing-snell/snellv6/record.go b/proxy/snell/sing-snell/snellv6/record.go new file mode 100644 index 00000000000..0b444811849 --- /dev/null +++ b/proxy/snell/sing-snell/snellv6/record.go @@ -0,0 +1,642 @@ +package snellv6 + +import ( + "crypto/cipher" + "encoding/binary" + "io" + "sync" + + snell "github.com/sagernet/sing-snell" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" +) + +// Surge 6.7.0 (11520): FUN_100015c54/FUN_1000156e0: non-default records require zero padding length. +var errRecordPadding = E.New("snell: unexpected padding in non-default record") + +func parseHeader(header []byte) (paddingLen int, payloadLen int, err error) { + if header[0] != snell.HeaderVersion { + return 0, 0, E.Extend(snell.ErrBadVersion, header[0]) + } + if header[1] != 0 || header[2] != 0 { + return 0, 0, snell.ErrReservedNonZero + } + return int(binary.BigEndian.Uint16(header[3:5])), int(binary.BigEndian.Uint16(header[5:7])), nil +} + +func putHeader(header []byte, paddingLen int, payloadLen int) { + header[0] = snell.HeaderVersion + header[1] = 0 + header[2] = 0 + binary.BigEndian.PutUint16(header[3:5], uint16(paddingLen)) + binary.BigEndian.PutUint16(header[5:7], uint16(payloadLen)) +} + +type baseReader struct { + upstream io.Reader + readFunc func() (*buf.Buffer, error) + cache *buf.Buffer + readWaitOptions N.ReadWaitOptions +} + +func (r *baseReader) ReadRecord() (*buf.Buffer, error) { + return r.readFunc() +} + +func (r *baseReader) NextRecord() (*buf.Buffer, error) { + record := r.cache + if record != nil { + r.cache = nil + if record.IsEmpty() { + record.Release() + } else { + return record, nil + } + } + return r.ReadRecord() +} + +func (r *baseReader) SetCache(cache *buf.Buffer) { + r.cache = cache +} + +func (r *baseReader) ReleaseCache() { + if r.cache != nil { + r.cache.Release() + r.cache = nil + } +} + +func (r *baseReader) Read(p []byte) (n int, err error) { + for { + if r.cache != nil { + if r.cache.IsEmpty() { + r.cache.Release() + r.cache = nil + } else { + n = copy(p, r.cache.Bytes()) + r.cache.Advance(n) + return + } + } + r.cache, err = r.ReadRecord() + if err != nil { + return + } + } +} + +func (r *baseReader) ReadBuffer(buffer *buf.Buffer) error { + for { + if r.cache != nil { + if r.cache.IsEmpty() { + r.cache.Release() + r.cache = nil + } else { + n, _ := buffer.Write(r.cache.Bytes()) + r.cache.Advance(n) + return nil + } + } + var err error + r.cache, err = r.ReadRecord() + if err != nil { + return err + } + } +} + +func (r *baseReader) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + r.readWaitOptions = options + return false +} + +func (r *baseReader) WaitReadBuffer() (buffer *buf.Buffer, err error) { + for { + if r.cache == nil { + return r.ReadRecord() + } + buffer = r.cache + r.cache = nil + if buffer.IsEmpty() { + buffer.Release() + continue + } + if buffer.Start() >= r.readWaitOptions.FrontHeadroom && buffer.FreeLen() >= r.readWaitOptions.RearHeadroom { + return + } + buffer = r.readWaitOptions.Copy(buffer) + return buffer, nil + } +} + +func (r *baseReader) Upstream() any { + return r.upstream +} + +type unshapedReader struct { + baseReader + cipher cipher.AEAD + nonce []byte +} + +func newUnshapedReader(upstream io.Reader, aead cipher.AEAD, nonce []byte) *unshapedReader { + r := &unshapedReader{cipher: aead, nonce: nonce} + r.upstream = upstream + r.readFunc = r.read + return r +} + +func (r *unshapedReader) read() (*buf.Buffer, error) { + headerCipher := buf.NewSize(snell.HeaderCipherLen) + _, err := headerCipher.ReadFullFrom(r.upstream, snell.HeaderCipherLen) + if err != nil { + headerCipher.Release() + return nil, err + } + _, err = r.cipher.Open(headerCipher.Index(0), r.nonce, headerCipher.Bytes(), nil) + if err != nil { + headerCipher.Release() + return nil, E.Cause(err, "open record header") + } + snell.IncreaseNonce(r.nonce) + paddingLen, payloadLen, err := parseHeader(headerCipher.To(snell.HeaderPlainLen)) + headerCipher.Release() + if err != nil { + return nil, err + } + if paddingLen != 0 { + return nil, errRecordPadding + } + if payloadLen == 0 { + return nil, io.EOF + } + body := r.readWaitOptions.NewBufferSize(payloadLen + snell.AEADTagLen) + _, err = body.ReadFullFrom(r.upstream, payloadLen+snell.AEADTagLen) + if err != nil { + body.Release() + return nil, err + } + _, err = r.cipher.Open(body.Index(0), r.nonce, body.Bytes(), nil) + if err != nil { + body.Release() + return nil, E.Cause(err, "open record payload") + } + snell.IncreaseNonce(r.nonce) + body.Truncate(payloadLen) + r.readWaitOptions.PostReturn(body) + return body, nil +} + +type unshapedWriter struct { + upstream io.Writer + cipher cipher.AEAD + nonce []byte + access sync.Mutex +} + +func newUnshapedWriter(upstream io.Writer, aead cipher.AEAD, nonce []byte) *unshapedWriter { + return &unshapedWriter{upstream: upstream, cipher: aead, nonce: nonce} +} + +func (w *unshapedWriter) sealHeader(header []byte, payloadLen int) { + putHeader(header, 0, payloadLen) + w.cipher.Seal(header[:0], w.nonce, header[:snell.HeaderPlainLen], nil) + snell.IncreaseNonce(w.nonce) +} + +func (w *unshapedWriter) Write(p []byte) (n int, err error) { + if len(p) == 0 { + return 0, nil + } + w.access.Lock() + defer w.access.Unlock() + // snell-server v6.0.0b4: FUN_0013d580 splits one encrypt call at 0xffff and + // reallocs every record into a single output buffer written to the socket once. + recordCount := (len(p) + maxPayload - 1) / maxPayload + output := buf.NewSize(recordCount*(snell.HeaderCipherLen+snell.AEADTagLen) + len(p)) + defer output.Release() + for data := p; len(data) > 0; { + recordLen := min(len(data), maxPayload) + w.sealHeader(output.Extend(snell.HeaderCipherLen), recordLen) + w.cipher.Seal(output.Extend(recordLen+snell.AEADTagLen)[:0], w.nonce, data[:recordLen], nil) + snell.IncreaseNonce(w.nonce) + data = data[recordLen:] + } + _, err = w.upstream.Write(output.Bytes()) + if err != nil { + return 0, err + } + return len(p), nil +} + +func (w *unshapedWriter) makeSliceRecordLocked(payload []byte) *buf.Buffer { + record := buf.NewSize(snell.HeaderCipherLen + len(payload) + snell.AEADTagLen) + w.sealHeader(record.Extend(snell.HeaderCipherLen), len(payload)) + common.Must1(record.Write(payload)) + w.cipher.Seal(record.From(snell.HeaderCipherLen)[:0], w.nonce, record.From(snell.HeaderCipherLen), nil) + record.Extend(snell.AEADTagLen) + snell.IncreaseNonce(w.nonce) + return record +} + +func (w *unshapedWriter) makeBufferRecordLocked(buffer *buf.Buffer) *buf.Buffer { + dataLen := buffer.Len() + header := buffer.ExtendHeader(snell.HeaderCipherLen) + w.sealHeader(header, dataLen) + payloadCipher := buffer.From(snell.HeaderCipherLen) + buffer.Extend(snell.AEADTagLen) + w.cipher.Seal(payloadCipher[:0], w.nonce, payloadCipher[:dataLen], nil) + snell.IncreaseNonce(w.nonce) + return buffer +} + +func (w *unshapedWriter) WriteBuffer(buffer *buf.Buffer) error { + defer buffer.Release() + dataLen := buffer.Len() + if dataLen == 0 { + return nil + } + if dataLen > maxPayload { + return common.Error(w.Write(buffer.Bytes())) + } + w.access.Lock() + defer w.access.Unlock() + w.makeBufferRecordLocked(buffer) + return common.Error(w.upstream.Write(buffer.Bytes())) +} + +func (w *unshapedWriter) WritePacketBuffer(buffer *buf.Buffer) error { + if buffer.Len() > maxPayload { + buffer.Release() + return snell.ErrPayloadTooLarge + } + return w.WriteBuffer(buffer) +} + +func (w *unshapedWriter) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + upstreamWriter, created := bufio.CreateVectorisedWriter(w.upstream) + if !created { + return nil, false + } + return w.CreateVectorisedWriterFor(upstreamWriter), true +} + +func (w *unshapedWriter) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { + return &vectorisedUnshapedWriter{writer: w, upstream: upstream} +} + +type vectorisedUnshapedWriter struct { + writer *unshapedWriter + upstream N.VectorisedWriter +} + +func (w *vectorisedUnshapedWriter) WriteVectorised(buffers []*buf.Buffer) error { + var records []*buf.Buffer + defer func() { + buf.ReleaseMulti(records) + }() + recordWriter := w.writer + recordWriter.access.Lock() + defer recordWriter.access.Unlock() + for _, buffer := range buffers { + dataLen := buffer.Len() + if dataLen == 0 { + buffer.Release() + continue + } + for data := buffer.Bytes(); len(data) > 0; { + if len(data) == dataLen && dataLen <= maxPayload { + record := recordWriter.makeBufferRecordLocked(buffer) + buffer = nil + records = append(records, record) + break + } + recordLen := min(len(data), maxPayload) + records = append(records, recordWriter.makeSliceRecordLocked(data[:recordLen])) + data = data[recordLen:] + } + if buffer != nil { + buffer.Release() + } + } + if len(records) == 0 { + return nil + } + flushRecords := records + records = nil + return w.upstream.WriteVectorised(flushRecords) +} + +func (w *unshapedWriter) CreatePacketVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { + return &packetVectorisedUnshapedWriter{writer: w, upstream: upstream} +} + +type packetVectorisedUnshapedWriter struct { + writer *unshapedWriter + upstream N.VectorisedWriter +} + +func (w *packetVectorisedUnshapedWriter) WriteVectorised(buffers []*buf.Buffer) error { + var records []*buf.Buffer + defer func() { + buf.ReleaseMulti(records) + }() + recordWriter := w.writer + recordWriter.access.Lock() + defer recordWriter.access.Unlock() + for index, buffer := range buffers { + if buffer.IsEmpty() { + buffer.Release() + continue + } + if buffer.Len() > maxPayload { + buffer.Release() + buf.ReleaseMulti(buffers[index+1:]) + return snell.ErrPayloadTooLarge + } + record := recordWriter.makeBufferRecordLocked(buffer) + records = append(records, record) + } + if len(records) == 0 { + return nil + } + flushRecords := records + records = nil + return w.upstream.WriteVectorised(flushRecords) +} + +func (w *unshapedWriter) WriteZeroChunk() error { + buffer := buf.NewSize(snell.HeaderCipherLen) + w.access.Lock() + defer w.access.Unlock() + w.sealHeader(buffer.Extend(snell.HeaderCipherLen), 0) + err := common.Error(w.upstream.Write(buffer.Bytes())) + buffer.Release() + return err +} + +func (w *unshapedWriter) FrontHeadroom() int { + return snell.HeaderCipherLen +} + +func (w *unshapedWriter) RearHeadroom() int { + return snell.AEADTagLen +} + +func (w *unshapedWriter) WriterMTU() int { + return maxPayload +} + +func (w *unshapedWriter) Upstream() any { + return w.upstream +} + +type rawReader struct { + baseReader +} + +func newRawReader(upstream io.Reader) *rawReader { + r := &rawReader{} + r.upstream = upstream + r.readFunc = r.read + return r +} + +func (r *rawReader) read() (*buf.Buffer, error) { + header := buf.NewSize(snell.HeaderPlainLen) + _, err := header.ReadFullFrom(r.upstream, snell.HeaderPlainLen) + if err != nil { + header.Release() + return nil, err + } + paddingLen, payloadLen, err := parseHeader(header.Bytes()) + header.Release() + if err != nil { + return nil, err + } + if paddingLen != 0 { + return nil, errRecordPadding + } + if payloadLen == 0 { + return nil, io.EOF + } + body := r.readWaitOptions.NewBufferSize(payloadLen) + _, err = body.ReadFullFrom(r.upstream, payloadLen) + if err != nil { + body.Release() + return nil, err + } + r.readWaitOptions.PostReturn(body) + return body, nil +} + +type rawWriter struct { + upstream io.Writer + access sync.Mutex +} + +func newRawWriter(upstream io.Writer) *rawWriter { + return &rawWriter{upstream: upstream} +} + +func (w *rawWriter) Write(p []byte) (n int, err error) { + if len(p) == 0 { + return 0, nil + } + // snell-server v6.0.0b4: FUN_0013d010 splits one encrypt call at 0xffff and + // reallocs every record into a single output buffer written to the socket once. + recordCount := (len(p) + maxPayload - 1) / maxPayload + output := buf.NewSize(recordCount*snell.HeaderPlainLen + len(p)) + defer output.Release() + for data := p; len(data) > 0; { + recordLen := min(len(data), maxPayload) + putHeader(output.Extend(snell.HeaderPlainLen), 0, recordLen) + common.Must1(output.Write(data[:recordLen])) + data = data[recordLen:] + } + w.access.Lock() + defer w.access.Unlock() + _, err = w.upstream.Write(output.Bytes()) + if err != nil { + return 0, err + } + return len(p), nil +} + +func (w *rawWriter) makeSliceRecordLocked(payload []byte) *buf.Buffer { + record := buf.NewSize(snell.HeaderPlainLen + len(payload)) + putHeader(record.Extend(snell.HeaderPlainLen), 0, len(payload)) + common.Must1(record.Write(payload)) + return record +} + +func (w *rawWriter) makeBufferRecordLocked(buffer *buf.Buffer) *buf.Buffer { + dataLen := buffer.Len() + putHeader(buffer.ExtendHeader(snell.HeaderPlainLen), 0, dataLen) + return buffer +} + +func (w *rawWriter) WriteBuffer(buffer *buf.Buffer) error { + defer buffer.Release() + dataLen := buffer.Len() + if dataLen == 0 { + return nil + } + if dataLen > maxPayload { + return common.Error(w.Write(buffer.Bytes())) + } + w.access.Lock() + defer w.access.Unlock() + w.makeBufferRecordLocked(buffer) + return common.Error(w.upstream.Write(buffer.Bytes())) +} + +func (w *rawWriter) WritePacketBuffer(buffer *buf.Buffer) error { + if buffer.Len() > maxPayload { + buffer.Release() + return snell.ErrPayloadTooLarge + } + return w.WriteBuffer(buffer) +} + +func (w *rawWriter) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + upstreamWriter, created := bufio.CreateVectorisedWriter(w.upstream) + if !created { + return nil, false + } + return w.CreateVectorisedWriterFor(upstreamWriter), true +} + +func (w *rawWriter) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { + return &vectorisedRawWriter{writer: w, upstream: upstream} +} + +type vectorisedRawWriter struct { + writer *rawWriter + upstream N.VectorisedWriter +} + +func (w *vectorisedRawWriter) WriteVectorised(buffers []*buf.Buffer) error { + var records []*buf.Buffer + defer func() { + buf.ReleaseMulti(records) + }() + recordWriter := w.writer + recordWriter.access.Lock() + defer recordWriter.access.Unlock() + for _, buffer := range buffers { + dataLen := buffer.Len() + if dataLen == 0 { + buffer.Release() + continue + } + for data := buffer.Bytes(); len(data) > 0; { + if len(data) == dataLen && dataLen <= maxPayload { + record := recordWriter.makeBufferRecordLocked(buffer) + buffer = nil + records = append(records, record) + break + } + recordLen := min(len(data), maxPayload) + records = append(records, recordWriter.makeSliceRecordLocked(data[:recordLen])) + data = data[recordLen:] + } + if buffer != nil { + buffer.Release() + } + } + if len(records) == 0 { + return nil + } + flushRecords := records + records = nil + return w.upstream.WriteVectorised(flushRecords) +} + +func (w *rawWriter) CreatePacketVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { + return &packetVectorisedRawWriter{writer: w, upstream: upstream} +} + +type packetVectorisedRawWriter struct { + writer *rawWriter + upstream N.VectorisedWriter +} + +func (w *packetVectorisedRawWriter) WriteVectorised(buffers []*buf.Buffer) error { + var records []*buf.Buffer + defer func() { + buf.ReleaseMulti(records) + }() + recordWriter := w.writer + recordWriter.access.Lock() + defer recordWriter.access.Unlock() + for index, buffer := range buffers { + if buffer.IsEmpty() { + buffer.Release() + continue + } + if buffer.Len() > maxPayload { + buffer.Release() + buf.ReleaseMulti(buffers[index+1:]) + return snell.ErrPayloadTooLarge + } + record := recordWriter.makeBufferRecordLocked(buffer) + records = append(records, record) + } + if len(records) == 0 { + return nil + } + flushRecords := records + records = nil + return w.upstream.WriteVectorised(flushRecords) +} + +func (w *rawWriter) WriteZeroChunk() error { + buffer := buf.NewSize(snell.HeaderPlainLen) + putHeader(buffer.Extend(snell.HeaderPlainLen), 0, 0) + w.access.Lock() + defer w.access.Unlock() + err := common.Error(w.upstream.Write(buffer.Bytes())) + buffer.Release() + return err +} + +func (w *rawWriter) FrontHeadroom() int { + return snell.HeaderPlainLen +} + +func (w *rawWriter) RearHeadroom() int { + return 0 +} + +func (w *rawWriter) WriterMTU() int { + return maxPayload +} + +func (w *rawWriter) Upstream() any { + return w.upstream +} + +var ( + _ N.ExtendedReader = (*unshapedReader)(nil) + _ N.ReadWaiter = (*unshapedReader)(nil) + _ N.ExtendedWriter = (*unshapedWriter)(nil) + _ N.VectorisedWriteCreator = (*unshapedWriter)(nil) + _ N.VectorisedWriter = (*vectorisedUnshapedWriter)(nil) + _ N.VectorisedWriter = (*packetVectorisedUnshapedWriter)(nil) + _ N.FrontHeadroom = (*unshapedWriter)(nil) + _ N.ExtendedReader = (*rawReader)(nil) + _ N.ReadWaiter = (*rawReader)(nil) + _ N.ExtendedWriter = (*rawWriter)(nil) + _ N.VectorisedWriteCreator = (*rawWriter)(nil) + _ N.VectorisedWriter = (*vectorisedRawWriter)(nil) + _ N.VectorisedWriter = (*packetVectorisedRawWriter)(nil) + _ N.VectorisedWriteCreator = (*shapedWriter)(nil) + _ N.WriterWithMTU = (*unshapedWriter)(nil) + _ N.WriterWithMTU = (*rawWriter)(nil) + _ N.WriterWithMTU = (*shapedWriter)(nil) +) diff --git a/proxy/snell/sing-snell/snellv6/reuse.go b/proxy/snell/sing-snell/snellv6/reuse.go new file mode 100644 index 00000000000..1bd32949006 --- /dev/null +++ b/proxy/snell/sing-snell/snellv6/reuse.go @@ -0,0 +1,853 @@ +package snellv6 + +import ( + "context" + "errors" + "io" + "net" + "sync" + "sync/atomic" + "time" + + snell "github.com/sagernet/sing-snell" + "github.com/sagernet/sing-snell/internal/reuse" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +const ( + remoteEOFCode = reuse.RemoteEOFCode + remoteEOFMessage = reuse.RemoteEOFMessage +) + +func (c *Client) DialContext(ctx context.Context, destination M.Socksaddr) (net.Conn, error) { + if c.reuse { + session, err := c.reuseSession(ctx) + if err != nil { + return nil, err + } + conn, err := session.DialConn(destination) + if err != nil { + session.Close() + return nil, err + } + return conn, nil + } + if c.pool.IsClosed() { + return nil, net.ErrClosed + } + if c.dialer == nil { + return nil, E.New("snell: missing dialer") + } + conn, err := c.dialer.DialContext(ctx, N.NetworkTCP, c.server) + if err != nil { + return nil, err + } + proxyConn, err := c.DialConn(conn, destination) + if err != nil { + conn.Close() + return nil, err + } + return proxyConn, nil +} + +func (c *Client) reuseSession(ctx context.Context) (*reuseSession, error) { + session, found, closed := c.pool.Take() + if closed { + return nil, net.ErrClosed + } + if c.dialer == nil { + return nil, E.New("snell: missing dialer") + } + if found { + return session, nil + } + conn, err := c.dialer.DialContext(ctx, N.NetworkTCP, c.server) + if err != nil { + return nil, err + } + if c.pool.IsClosed() { + conn.Close() + return nil, net.ErrClosed + } + session = c.newReuseSession(conn) + session.state.Store(uint32(reuse.StateActive)) + return session, nil +} + +func (c *Client) Close() error { + return c.pool.Close() +} + +type reuseSession struct { + net.Conn + client *Client + + state atomic.Uint32 + reader reuse.RecordReader + writer reuse.RecordWriter +} + +func (c *Client) newReuseSession(conn net.Conn) *reuseSession { + return &reuseSession{Conn: conn, client: c} +} + +func (s *reuseSession) ReuseState() *atomic.Uint32 { + return &s.state +} + +func (s *reuseSession) DialConn(destination M.Socksaddr) (net.Conn, error) { + state := reuse.State(s.state.Load()) + if state == reuse.StateClosed { + return nil, net.ErrClosed + } + if state != reuse.StateActive { + return nil, E.New("snell: reuse session is busy") + } + + requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: s.client.userKey, Destination: destination} + request := buf.NewSize(requestPayload.Len()) + err := requestPayload.Write(request) + if err != nil { + request.Release() + s.Release(false) + return nil, err + } + if s.writer == nil { + s.writer, err = writeFirstRecord(s.Conn, s.client.mode, s.client.psk, s.client.profile, request.Bytes()) + } else { + _, err = s.writer.Write(request.Bytes()) + } + request.Release() + if err != nil { + s.Release(false) + return nil, E.Cause(err, "write request") + } + return &reuseConn{Conn: s.Conn, session: s, destination: destination}, nil +} + +func (s *reuseSession) Release(reusable bool) { + if !reusable { + s.Close() + return + } + if !s.state.CompareAndSwap(uint32(reuse.StateActive), uint32(reuse.StateReady)) { + if reuse.State(s.state.Load()) != reuse.StateClosed { + s.Close() + } + return + } + s.client.pool.MoveToPool(s, reuse.StateReady, false) +} + +func (s *reuseSession) Close() error { + if reuse.State(s.state.Swap(uint32(reuse.StateClosed))) == reuse.StateClosed { + return nil + } + return s.Conn.Close() +} + +func (s *reuseSession) startDrain() { + if !s.state.CompareAndSwap(uint32(reuse.StateActive), uint32(reuse.StateWaiting)) { + if reuse.State(s.state.Load()) != reuse.StateClosed { + s.Close() + } + return + } + if !s.client.pool.MoveToPool(s, reuse.StateWaiting, true) { + return + } + go s.drain() +} + +func (s *reuseSession) drain() { + defer s.client.pool.DrainDone() + var discarded int + for { + record, err := s.reader.NextRecord() + if errors.Is(err, io.EOF) { + s.state.CompareAndSwap(uint32(reuse.StateWaiting), uint32(reuse.StateReady)) + return + } + if err != nil { + s.Close() + return + } + // Surge 6.7.0 (11520): SNConnectorV4::socket:didReadData: discards data + // received while waiting for server EOF and closes after 0x80001 bytes. + discarded += record.Len() + record.Release() + if discarded >= reuse.WaitingDiscardLimit { + s.Close() + return + } + } +} + +type reuseConn struct { + net.Conn + session *reuseSession + destination M.Socksaddr + + closeWriteOnce sync.Once + closeWriteErr error + closeOnce sync.Once + closeErr error + replyRead atomic.Bool + readWaitOptions N.ReadWaitOptions + closed atomic.Bool + readActionCount atomic.Int32 + readClosed atomic.Bool +} + +func (c *reuseConn) readResponse() error { + if c.replyRead.Load() { + return nil + } + var record *buf.Buffer + var err error + if c.session.reader == nil { + c.session.reader, record, err = readFirstRecord(c.session.Conn, c.session.client.mode, c.session.client.psk, c.session.client.profile, c.readWaitOptions) + } else { + record, err = c.session.reader.ReadRecord() + } + if err != nil { + return E.Cause(err, "read reply") + } + cached, err := reuse.ParseReply(record) + if err != nil { + return err + } + c.session.reader.SetCache(cached) + c.replyRead.Store(true) + return nil +} + +func (c *reuseConn) Read(p []byte) (int, error) { + if c.closed.Load() { + return 0, net.ErrClosed + } + c.readActionCount.Add(1) + defer c.readActionCount.Add(-1) + if c.closed.Load() { + return 0, net.ErrClosed + } + err := c.readResponse() + if err != nil { + return 0, err + } + var discarded int + for { + n, err := c.session.reader.Read(p) + if c.closed.Load() { + discarded += n + if errors.Is(err, io.EOF) { + c.readClosed.Store(true) + c.session.state.CompareAndSwap(uint32(reuse.StateWaiting), uint32(reuse.StateReady)) + return 0, err + } + if err != nil { + c.session.Close() + return 0, err + } + if discarded >= reuse.WaitingDiscardLimit { + c.session.Close() + return 0, E.New("snell: read too much data after client EOF") + } + continue + } + if errors.Is(err, io.EOF) { + c.readClosed.Store(true) + } + return n, err + } +} + +func (c *reuseConn) ReadBuffer(buffer *buf.Buffer) error { + if c.closed.Load() { + return net.ErrClosed + } + c.readActionCount.Add(1) + defer c.readActionCount.Add(-1) + if c.closed.Load() { + return net.ErrClosed + } + err := c.readResponse() + if err != nil { + return err + } + var discarded int + for { + bufferLen := buffer.Len() + err = c.session.reader.ReadBuffer(buffer) + if c.closed.Load() { + discarded += buffer.Len() - bufferLen + buffer.Truncate(bufferLen) + if errors.Is(err, io.EOF) { + c.readClosed.Store(true) + c.session.state.CompareAndSwap(uint32(reuse.StateWaiting), uint32(reuse.StateReady)) + return err + } + if err != nil { + c.session.Close() + return err + } + if discarded >= reuse.WaitingDiscardLimit { + c.session.Close() + return E.New("snell: read too much data after client EOF") + } + continue + } + if errors.Is(err, io.EOF) { + c.readClosed.Store(true) + } + return err + } +} + +func (c *reuseConn) Write(p []byte) (int, error) { + return c.session.writer.Write(p) +} + +func (c *reuseConn) WriteBuffer(buffer *buf.Buffer) error { + return c.session.writer.WriteBuffer(buffer) +} + +func (c *reuseConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + return c.session.writer.CreateVectorisedWriter() +} + +func (c *reuseConn) CloseWrite() error { + c.closeWriteOnce.Do(func() { + c.closeWriteErr = c.session.writer.WriteZeroChunk() + }) + return c.closeWriteErr +} + +func (c *reuseConn) Close() error { + c.closeOnce.Do(func() { + c.closed.Store(true) + if !c.replyRead.Load() { + c.session.Release(false) + return + } + c.closeErr = c.CloseWrite() + if c.closeErr != nil { + c.session.Release(false) + return + } + c.session.Conn.SetReadDeadline(time.Time{}) + c.session.Conn.SetWriteDeadline(time.Time{}) + if c.readClosed.Load() { + c.session.Release(true) + return + } + // Surge 6.7.0 (11520): SNConnectorV4::readServerEOFIfNotInReadState: starts waiting-state EOF + // reads when _socketReadActionCounter is 0. + if c.readActionCount.Load() > 0 { + if !c.session.state.CompareAndSwap(uint32(reuse.StateActive), uint32(reuse.StateWaiting)) { + if reuse.State(c.session.state.Load()) != reuse.StateClosed { + c.session.Close() + } + return + } + poolState := reuse.StateWaiting + if c.readClosed.Load() { + c.session.state.CompareAndSwap(uint32(reuse.StateWaiting), uint32(reuse.StateReady)) + poolState = reuse.StateReady + } + if !c.session.client.pool.MoveToPool(c.session, poolState, false) { + return + } + return + } + c.session.startDrain() + }) + return c.closeErr +} + +func (c *reuseConn) CreateReadWaiter() (N.ReadWaiter, bool) { + return &reuseReadWaiter{conn: c}, true +} + +type reuseReadWaiter struct { + conn *reuseConn +} + +func (w *reuseReadWaiter) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + w.conn.readWaitOptions = options + if w.conn.session.reader != nil { + w.conn.session.reader.InitializeReadWaiter(options) + } + return false +} + +func (w *reuseReadWaiter) WaitReadBuffer() (*buf.Buffer, error) { + if w.conn.closed.Load() { + return nil, net.ErrClosed + } + w.conn.readActionCount.Add(1) + defer w.conn.readActionCount.Add(-1) + if w.conn.closed.Load() { + return nil, net.ErrClosed + } + err := w.conn.readResponse() + if err != nil { + return nil, err + } + var discarded int + for { + buffer, err := w.conn.session.reader.WaitReadBuffer() + if w.conn.closed.Load() { + if buffer != nil { + discarded += buffer.Len() + buffer.Release() + } + if errors.Is(err, io.EOF) { + w.conn.readClosed.Store(true) + w.conn.session.state.CompareAndSwap(uint32(reuse.StateWaiting), uint32(reuse.StateReady)) + return nil, err + } + if err != nil { + w.conn.session.Close() + return nil, err + } + if discarded >= reuse.WaitingDiscardLimit { + w.conn.session.Close() + return nil, E.New("snell: read too much data after client EOF") + } + continue + } + if errors.Is(err, io.EOF) { + w.conn.readClosed.Store(true) + } + return buffer, err + } +} + +func (c *reuseConn) FrontHeadroom() int { + return c.session.writer.FrontHeadroom() +} + +func (c *reuseConn) RearHeadroom() int { + return c.session.writer.RearHeadroom() +} + +func (c *reuseConn) WriterMTU() int { + return c.session.writer.WriterMTU() +} + +func (c *reuseConn) NeedHandshakeForRead() bool { + return !c.replyRead.Load() +} + +func (c *reuseConn) NeedAdditionalReadDeadline() bool { + return !c.replyRead.Load() +} + +func (c *reuseConn) Upstream() any { + return c.session.Conn +} + +func (c *reuseConn) RemoteAddr() net.Addr { + return c.destination.TCPAddr() +} + +type serverReuseSession[U comparable] struct { + net.Conn + service *Service + multiService *MultiService[U] + reader reuse.RecordReader + writer reuse.RecordWriter +} + +func (s *serverReuseSession[U]) Serve(ctx context.Context, source M.Socksaddr, onClose N.CloseHandlerFunc, record *buf.Buffer, request snell.Request) (err error) { + callOnClose := true + if onClose != nil { + defer func() { + if callOnClose { + onClose(err) + } + }() + } + for { + requestCtx := ctx + if s.multiService != nil && request.Command != snell.CommandPing { + requestCtx, err = s.multiService.authenticate(ctx, request) + if err != nil { + record.Release() + s.Conn.Close() + return err + } + } + switch request.Command { + case snell.CommandConnect, snell.CommandConnectV2: + case snell.CommandUDP: + // snell-server v6.0.0b4: FUN_00141bc0 rejects UDP tunnel requests when the + // first UDP datagram starts in the same decrypted frame as the tunnel request. + if !record.IsEmpty() { + record.Release() + s.Conn.Close() + return errInvalidUDPTunnelRequest + } + record.Release() + packetConn := &serverPacketConn{Conn: s.Conn, service: s.service, reader: s.reader, writer: s.writer} + err = packetConn.writeTunnelReply() + if err != nil { + s.Conn.Close() + return err + } + s.writer = packetConn.writer + callOnClose = false + s.service.handler.NewPacketConnectionEx(requestCtx, packetConn, source, M.Socksaddr{}, onClose) + return nil + case snell.CommandPing: + record.Release() + pong := [1]byte{snell.ReplyPong} + if s.writer == nil { + s.writer, err = writeFirstRecord(s.Conn, s.service.mode, s.service.psk, s.service.profile, pong[:]) + } else { + _, err = s.writer.Write(pong[:]) + } + if err != nil { + s.Conn.Close() + return err + } + s.Conn.Close() + return nil + default: + record.Release() + s.Conn.Close() + return E.Extend(snell.ErrUnsupportedCommand, request.Command) + } + serverConn := &serverReuseConn[U]{Conn: s.Conn, session: s, completion: make(chan struct{})} + if record.IsEmpty() { + record.Release() + } else { + s.reader.SetCache(record) + } + s.service.handler.NewConnectionEx(requestCtx, serverConn, source, request.Destination, serverConn.completeLogicalConnection) + select { + case <-serverConn.completion: + case <-requestCtx.Done(): + s.Conn.Close() + return requestCtx.Err() + } + err = serverConn.CloseWrite() + if err != nil { + s.Conn.Close() + return err + } + if serverConn.aborted.Load() { + return nil + } + if !serverConn.readClosed.Load() { + // snell-server v6.0.0b4: after writing the EOF packet, FUN_00141bc0 keeps + // consuming client records (forwarding them into the still writable + // outgoing socket) until the zero chunk reaches FUN_001419e0 -> + // FUN_001402c0 -> FUN_001401b0, which resets the tunnel stage so the + // next record is parsed as a request. The handler has already released + // the upstream, so discard instead, capped like the client waiting state. + var discarded int + for { + var drainRecord *buf.Buffer + drainRecord, err = s.reader.NextRecord() + if err != nil { + break + } + discarded += drainRecord.Len() + drainRecord.Release() + if discarded >= reuse.WaitingDiscardLimit { + s.Conn.Close() + return E.New("snell: read too much data after request end") + } + } + if !errors.Is(err, io.EOF) { + s.Conn.Close() + return E.Cause(err, "drain request") + } + } + record, err = s.reader.ReadRecord() + if err != nil { + if errors.Is(err, io.EOF) { + return nil + } + s.Conn.Close() + return E.Cause(err, "read request") + } + request, err = s.service.readRequest(record) + if err != nil { + record.Release() + s.Conn.Close() + return E.Cause(err, "decode request") + } + } +} + +type serverReuseConn[U comparable] struct { + net.Conn + session *serverReuseSession[U] + + access sync.Mutex + closeWriteOnce sync.Once + closeWriteErr error + closeOnce sync.Once + closeErr error + readClosed atomic.Bool + aborted atomic.Bool + replyWritten bool + completeOnce sync.Once + completion chan struct{} +} + +func (c *serverReuseConn[U]) writeResponse(payload []byte) error { + first := payload + if len(first) > maxPayload-1 { + first = payload[:maxPayload-1] + } + reply := make([]byte, 1+len(first)) + reply[0] = snell.ReplyTunnel + copy(reply[1:], first) + var err error + if c.session.writer == nil { + c.session.writer, err = writeFirstRecord(c.session.Conn, c.session.service.mode, c.session.service.psk, c.session.service.profile, reply) + } else { + _, err = c.session.writer.Write(reply) + } + if err != nil { + return E.Cause(err, "write reply") + } + c.replyWritten = true + if len(payload) > len(first) { + _, err = c.session.writer.Write(payload[len(first):]) + if err != nil { + return err + } + } + return nil +} + +func (c *serverReuseConn[U]) writeResponseBuffer(buffer *buf.Buffer) error { + if c.session.writer != nil { + buffer.ExtendHeader(1)[0] = snell.ReplyTunnel + err := c.session.writer.WriteBuffer(buffer) + if err != nil { + return E.Cause(err, "write reply") + } + c.replyWritten = true + return nil + } + buffer.ExtendHeader(1)[0] = snell.ReplyTunnel + writer, err := writeFirstRecordBuffer(c.session.Conn, c.session.service.mode, c.session.service.psk, c.session.service.profile, buffer) + if err != nil { + return E.Cause(err, "write reply") + } + c.session.writer = writer + c.replyWritten = true + return nil +} + +func (c *serverReuseConn[U]) writeErrorResponse() error { + message := []byte(remoteEOFMessage) + reply := make([]byte, 3+len(message)) + reply[0] = snell.ReplyError + reply[1] = remoteEOFCode + reply[2] = byte(len(message)) + copy(reply[3:], message) + var err error + if c.session.writer == nil { + c.session.writer, err = writeFirstRecord(c.session.Conn, c.session.service.mode, c.session.service.psk, c.session.service.profile, reply) + } else { + _, err = c.session.writer.Write(reply) + } + if err != nil { + return E.Cause(err, "write error reply") + } + return nil +} + +func (c *serverReuseConn[U]) Read(p []byte) (int, error) { + n, err := c.session.reader.Read(p) + if errors.Is(err, io.EOF) { + c.readClosed.Store(true) + } + return n, err +} + +func (c *serverReuseConn[U]) ReadBuffer(buffer *buf.Buffer) error { + err := c.session.reader.ReadBuffer(buffer) + if errors.Is(err, io.EOF) { + c.readClosed.Store(true) + } + return err +} + +func (c *serverReuseConn[U]) Write(p []byte) (int, error) { + c.access.Lock() + defer c.access.Unlock() + if c.replyWritten { + return c.session.writer.Write(p) + } + err := c.writeResponse(p) + if err != nil { + return 0, err + } + return len(p), nil +} + +func (c *serverReuseConn[U]) WriteBuffer(buffer *buf.Buffer) error { + c.access.Lock() + defer c.access.Unlock() + if c.replyWritten { + return c.session.writer.WriteBuffer(buffer) + } + return c.writeResponseBuffer(buffer) +} + +func (c *serverReuseConn[U]) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + upstreamWriter, created := bufio.CreateVectorisedWriter(c.session.Conn) + if !created { + return nil, false + } + return &serverReuseVectorisedWriter[U]{conn: c, upstream: upstreamWriter}, true +} + +type serverReuseVectorisedWriter[U comparable] struct { + conn *serverReuseConn[U] + upstream N.VectorisedWriter +} + +func (w *serverReuseVectorisedWriter[U]) WriteVectorised(buffers []*buf.Buffer) error { + conn := w.conn + conn.access.Lock() + if conn.replyWritten { + recordWriter := conn.session.writer + conn.access.Unlock() + return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) + } + for index, buffer := range buffers { + if buffer.IsEmpty() { + buffer.Release() + continue + } + err := conn.writeResponseBuffer(buffer) + if err != nil { + conn.access.Unlock() + buf.ReleaseMulti(buffers[index+1:]) + return err + } + if index+1 < len(buffers) { + recordWriter := conn.session.writer + conn.access.Unlock() + return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers[index+1:]) + } + conn.access.Unlock() + return nil + } + conn.access.Unlock() + return nil +} + +func (c *serverReuseConn[U]) CloseWrite() error { + c.closeWriteOnce.Do(func() { + c.access.Lock() + defer c.access.Unlock() + // snell-server v6.0.0b4: FUN_001414e0 -> FUN_00140390 reports upstream EOF + // before any payload as ReplyError 0x65 "Remote EOF", marks the context + // aborted, and FUN_00141390 closes the tunnel after the error data is written. + if !c.replyWritten { + c.closeWriteErr = c.writeErrorResponse() + if c.closeWriteErr == nil { + c.aborted.Store(true) + c.closeWriteErr = c.session.Conn.Close() + } + return + } + c.closeWriteErr = c.session.writer.WriteZeroChunk() + }) + return c.closeWriteErr +} + +func (c *serverReuseConn[U]) Close() error { + c.closeOnce.Do(func() { + c.closeErr = c.CloseWrite() + c.completeLogicalConnection(c.closeErr) + }) + return c.closeErr +} + +func (c *serverReuseConn[U]) completeLogicalConnection(error) { + c.completeOnce.Do(func() { + close(c.completion) + }) +} + +func (c *serverReuseConn[U]) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + return c.session.reader.InitializeReadWaiter(options) +} + +func (c *serverReuseConn[U]) WaitReadBuffer() (*buf.Buffer, error) { + buffer, err := c.session.reader.WaitReadBuffer() + if errors.Is(err, io.EOF) { + c.readClosed.Store(true) + } + return buffer, err +} + +func (c *serverReuseConn[U]) FrontHeadroom() int { + if c.replyWritten { + return c.session.writer.FrontHeadroom() + } + if c.session.writer != nil { + return 1 + c.session.writer.FrontHeadroom() + } + switch c.session.service.mode { + case ModeUnsafeRaw: + return 1 + snell.HeaderPlainLen + case ModeUnshaped: + return 1 + snell.SaltLen + snell.HeaderCipherLen + default: + return 1 + c.session.service.profile.saltBlockLen + c.session.service.profile.recordPrefixMax + snell.HeaderCipherLen + c.session.service.profile.padMaxHeadroom + } +} + +func (c *serverReuseConn[U]) RearHeadroom() int { + if c.session.writer != nil { + return c.session.writer.RearHeadroom() + } + if c.session.service.mode == ModeUnsafeRaw { + return 0 + } + return snell.AEADTagLen +} + +func (c *serverReuseConn[U]) WriterMTU() int { + if c.session.writer != nil { + return c.session.writer.WriterMTU() + } + if c.session.service.mode == ModeDefault { + return c.session.service.profile.chunkMax + } + return maxPayload +} + +func (c *serverReuseConn[U]) Upstream() any { + return c.session.Conn +} + +var ( + _ N.ExtendedConn = (*reuseConn)(nil) + _ N.ReadWaitCreator = (*reuseConn)(nil) + _ N.VectorisedWriteCreator = (*reuseConn)(nil) + _ N.EarlyReader = (*reuseConn)(nil) + _ N.WriteCloser = (*reuseConn)(nil) + _ N.ReadWaiter = (*reuseReadWaiter)(nil) + _ N.ExtendedConn = (*serverReuseConn[struct{}])(nil) + _ N.ReadWaiter = (*serverReuseConn[struct{}])(nil) + _ N.VectorisedWriteCreator = (*serverReuseConn[struct{}])(nil) + _ N.VectorisedWriter = (*serverReuseVectorisedWriter[struct{}])(nil) + _ N.WriteCloser = (*serverReuseConn[struct{}])(nil) +) diff --git a/proxy/snell/sing-snell/snellv6/salt.go b/proxy/snell/sing-snell/snellv6/salt.go new file mode 100644 index 00000000000..106852bb408 --- /dev/null +++ b/proxy/snell/sing-snell/snellv6/salt.go @@ -0,0 +1,56 @@ +package snellv6 + +// Surge 6.7.0 (11520): FUN_100015274/FUN_1000140bc: shuffles and masks v6 default-mode salts with this namespace xor. +const saltNSXor = 0xdaa66d2c7ddf743f + +func saltShufflePRF(nsSalt uint64, domain uint32, index uint32) uint32 { + rdx := uint64(index)*coefB + addB + rdi := nsSalt ^ saltNSXor + rsi := uint64(domain)*coefA + addA + y := splitmix64((rdx ^ rdi) ^ rsi) + return uint32(y ^ (y >> 32)) +} + +func shufflePerm(nsSalt uint64, rounds uint8, length int) []byte { + out := make([]byte, length) + for index := range out { + out[index] = byte(index) + } + if length == 0 { + return out + } + if rounds == 0 { + rounds = 1 + } + for round := uint32(0); round < uint32(rounds); round++ { + domain := uint32(mixHandshakeDomain) + round + for i := range length { + span := uint64(length - i) + raw := uint64(saltShufflePRF(nsSalt, domain, uint32(i))) + j := i + int(raw%span) + out[i], out[j] = out[j], out[i] + } + } + return out +} + +func saltMask(nsSalt uint64, mixStride uint8, index uint32) byte { + prf := prf32Fold(nsSalt, labelMotif, uint64(mixHandshakeDomain), uint64(index)) + return byte(index)*mixStride ^ byte(prf) +} + +func (p *Profile) extractSalt(block []byte) [saltLen]byte { + perm := shufflePerm(p.namespaces.salt, byte(p.mixRoundsHandshake), len(block)) + var out [saltLen]byte + for i := range saltLen { + out[i] = saltMask(p.namespaces.salt, byte(p.mixStrideHandshake), uint32(i)) ^ block[perm[i]] + } + return out +} + +func (p *Profile) writeSaltBlock(salt []byte, block []byte) { + perm := shufflePerm(p.namespaces.salt, byte(p.mixRoundsHandshake), len(block)) + for i := range saltLen { + block[perm[i]] = saltMask(p.namespaces.salt, byte(p.mixStrideHandshake), uint32(i)) ^ salt[i] + } +} diff --git a/proxy/snell/sing-snell/snellv6/server.go b/proxy/snell/sing-snell/snellv6/server.go new file mode 100644 index 00000000000..4fedc1eee2d --- /dev/null +++ b/proxy/snell/sing-snell/snellv6/server.go @@ -0,0 +1,460 @@ +package snellv6 + +import ( + "context" + "io" + "net" + "sync" + + snell "github.com/sagernet/sing-snell" + "github.com/sagernet/sing-snell/internal/reuse" + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type Service struct { + psk []byte + mode Mode + profile *Profile + handler snell.Handler +} + +type ServerOptions struct { + PSK []byte + Mode Mode + Handler snell.Handler +} + +func NewService(options ServerOptions) (*Service, error) { + if len(options.PSK) == 0 { + return nil, snell.ErrMissingPSK + } + // snell-server v6.0.0b4: FUN_00138120: rejects PSKs outside the 12..255-byte range. + if len(options.PSK) < 12 || len(options.PSK) > 255 { + return nil, E.New("snell: psk length must be between 12 and 255 bytes") + } + if options.Mode != ModeDefault && options.Mode != ModeUnshaped && options.Mode != ModeUnsafeRaw { + return nil, E.New("snell: unknown v6 mode: ", int(options.Mode)) + } + service := &Service{psk: options.PSK, mode: options.Mode, handler: options.Handler} + if options.Mode == ModeDefault { + service.profile = NewProfile(options.PSK) + } + return service, nil +} + +func (s *Service) NewConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error { + err := s.newConnection(ctx, conn, source, onClose) + if err != nil { + return &snell.ServerError{Conn: conn, Source: source, Cause: err} + } + return nil +} + +type MultiService[U comparable] struct { + *Service + users map[string]U +} + +var errInvalidUDPTunnelRequest = E.New("snell: invalid udp tunnel request") + +func NewMultiService[U comparable](options ServerOptions) (*MultiService[U], error) { + service, err := NewService(options) + if err != nil { + return nil, err + } + return &MultiService[U]{Service: service, users: make(map[string]U)}, nil +} + +func (s *MultiService[U]) UpdateUsers(users []U, userKeys [][]byte) error { + if len(users) != len(userKeys) { + return E.New("snell: user/key count mismatch") + } + if len(users) == 0 { + return snell.ErrNoUsers + } + userMap := make(map[string]U, len(users)) + for index, user := range users { + key := userKeys[index] + if len(key) == 0 { + return snell.ErrBadUserKey + } + if len(key) > 255 { + return E.New("snell: user key too long") + } + keyString := string(key) + if _, loaded := userMap[keyString]; loaded { + return snell.ErrDuplicateUserKey + } + userMap[keyString] = user + } + s.users = userMap + return nil +} + +func (s *MultiService[U]) NewConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error { + err := s.newConnection(ctx, conn, source, onClose) + if err != nil { + return &snell.ServerError{Conn: conn, Source: source, Cause: err} + } + return nil +} + +func (s *MultiService[U]) authenticate(ctx context.Context, request snell.Request) (context.Context, error) { + user, loaded := s.users[string(request.ClientID)] + if !loaded { + return nil, snell.ErrBadUserKey + } + return auth.ContextWithUser(ctx, user), nil +} + +func (s *Service) readRequest(record *buf.Buffer) (snell.Request, error) { + prefix := make([]byte, 3) + _, err := io.ReadFull(record, prefix) + if err != nil { + return snell.Request{}, err + } + if prefix[0] != snell.RequestVersion { + return snell.Request{}, E.Extend(snell.ErrBadVersion, "request version ", prefix[0]) + } + request := snell.Request{Command: prefix[1]} + // snell-server v6.0.0b4: FUN_00141bc0 handles SN_COMMAND_PING immediately after + // the version/command bytes and aborts with PONG data, without consuming client-id bytes. + if request.Command == snell.CommandPing { + return request, nil + } + clientIDLen := int(prefix[2]) + if clientIDLen > 0 { + request.ClientID = make([]byte, clientIDLen) + _, err = io.ReadFull(record, request.ClientID) + if err != nil { + return snell.Request{}, err + } + } + switch request.Command { + case snell.CommandConnect, snell.CommandConnectV2: + request.Destination, err = snell.ReadConnectAddress(record) + if err != nil { + return snell.Request{}, err + } + case snell.CommandUDP: + default: + return snell.Request{}, E.Extend(snell.ErrUnsupportedCommand, request.Command) + } + return request, nil +} + +func (s *Service) newConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error { + reader, record, err := readFirstRecord(conn, s.mode, s.psk, s.profile, N.ReadWaitOptions{}) + if err != nil { + return E.Cause(err, "read request") + } + request, err := s.readRequest(record) + if err != nil { + record.Release() + return E.Cause(err, "decode request") + } + + switch request.Command { + case snell.CommandConnect: + serverConn := &serverConn{Conn: conn, service: s, reader: reader} + if record.IsEmpty() { + record.Release() + } else { + reader.SetCache(record) + } + s.handler.NewConnectionEx(ctx, serverConn, source, request.Destination, onClose) + return nil + case snell.CommandConnectV2: + reuseSession := &serverReuseSession[struct{}]{Conn: conn, service: s, reader: reader} + return reuseSession.Serve(ctx, source, onClose, record, request) + case snell.CommandUDP: + // snell-server v6.0.0b4: FUN_00141bc0 rejects UDP tunnel requests when the + // first UDP datagram starts in the same decrypted frame as the tunnel request. + if !record.IsEmpty() { + record.Release() + conn.Close() + return errInvalidUDPTunnelRequest + } + record.Release() + packetConn := &serverPacketConn{Conn: conn, service: s, reader: reader} + err = packetConn.writeTunnelReply() + if err != nil { + return err + } + s.handler.NewPacketConnectionEx(ctx, packetConn, source, M.Socksaddr{}, onClose) + return nil + case snell.CommandPing: + record.Release() + pong := [1]byte{snell.ReplyPong} + _, err = writeFirstRecord(conn, s.mode, s.psk, s.profile, pong[:]) + closeErr := conn.Close() + if err != nil { + return err + } + return closeErr + default: + record.Release() + return E.Extend(snell.ErrUnsupportedCommand, request.Command) + } +} + +func (s *MultiService[U]) newConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error { + reader, record, err := readFirstRecord(conn, s.mode, s.psk, s.profile, N.ReadWaitOptions{}) + if err != nil { + return E.Cause(err, "read request") + } + request, err := s.readRequest(record) + if err != nil { + record.Release() + return E.Cause(err, "decode request") + } + + switch request.Command { + case snell.CommandConnect: + requestCtx, err := s.authenticate(ctx, request) + if err != nil { + record.Release() + return err + } + serverConn := &serverConn{Conn: conn, service: s.Service, reader: reader} + if record.IsEmpty() { + record.Release() + } else { + reader.SetCache(record) + } + s.handler.NewConnectionEx(requestCtx, serverConn, source, request.Destination, onClose) + return nil + case snell.CommandConnectV2: + reuseSession := &serverReuseSession[U]{Conn: conn, service: s.Service, multiService: s, reader: reader} + return reuseSession.Serve(ctx, source, onClose, record, request) + case snell.CommandUDP: + requestCtx, err := s.authenticate(ctx, request) + if err != nil { + record.Release() + return err + } + // snell-server v6.0.0b4: FUN_00141bc0 rejects UDP tunnel requests when the + // first UDP datagram starts in the same decrypted frame as the tunnel request. + if !record.IsEmpty() { + record.Release() + conn.Close() + return errInvalidUDPTunnelRequest + } + record.Release() + packetConn := &serverPacketConn{Conn: conn, service: s.Service, reader: reader} + err = packetConn.writeTunnelReply() + if err != nil { + return err + } + s.handler.NewPacketConnectionEx(requestCtx, packetConn, source, M.Socksaddr{}, onClose) + return nil + case snell.CommandPing: + record.Release() + pong := [1]byte{snell.ReplyPong} + _, err = writeFirstRecord(conn, s.mode, s.psk, s.profile, pong[:]) + closeErr := conn.Close() + if err != nil { + return err + } + return closeErr + default: + record.Release() + return E.Extend(snell.ErrUnsupportedCommand, request.Command) + } +} + +var ( + _ snell.Service = (*Service)(nil) + _ snell.Service = (*MultiService[int])(nil) +) + +type serverConn struct { + net.Conn + service *Service + reader reuse.RecordReader + + access sync.Mutex + writer reuse.RecordWriter + + closeWriteOnce sync.Once + closeWriteErr error +} + +func (c *serverConn) writeResponse(payload []byte) error { + first := payload + if len(first) > maxPayload-1 { + first = payload[:maxPayload-1] + } + reply := make([]byte, 1+len(first)) + reply[0] = snell.ReplyTunnel + copy(reply[1:], first) + writer, err := writeFirstRecord(c.Conn, c.service.mode, c.service.psk, c.service.profile, reply) + if err != nil { + return E.Cause(err, "write reply") + } + c.writer = writer + if len(payload) > len(first) { + _, err = writer.Write(payload[len(first):]) + if err != nil { + return err + } + } + return nil +} + +func (c *serverConn) writeResponseBuffer(buffer *buf.Buffer) error { + buffer.ExtendHeader(1)[0] = snell.ReplyTunnel + writer, err := writeFirstRecordBuffer(c.Conn, c.service.mode, c.service.psk, c.service.profile, buffer) + if err != nil { + return E.Cause(err, "write reply") + } + c.writer = writer + return nil +} + +func (c *serverConn) Read(p []byte) (int, error) { + return c.reader.Read(p) +} + +func (c *serverConn) ReadBuffer(buffer *buf.Buffer) error { + return c.reader.ReadBuffer(buffer) +} + +func (c *serverConn) Write(p []byte) (int, error) { + c.access.Lock() + if c.writer != nil { + writer := c.writer + c.access.Unlock() + return writer.Write(p) + } + defer c.access.Unlock() + err := c.writeResponse(p) + if err != nil { + return 0, err + } + return len(p), nil +} + +func (c *serverConn) WriteBuffer(buffer *buf.Buffer) error { + c.access.Lock() + if c.writer != nil { + writer := c.writer + c.access.Unlock() + return writer.WriteBuffer(buffer) + } + defer c.access.Unlock() + return c.writeResponseBuffer(buffer) +} + +func (c *serverConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + upstreamWriter, created := bufio.CreateVectorisedWriter(c.Conn) + if !created { + return nil, false + } + return &serverVectorisedWriter{conn: c, upstream: upstreamWriter}, true +} + +type serverVectorisedWriter struct { + conn *serverConn + upstream N.VectorisedWriter +} + +func (w *serverVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { + conn := w.conn + conn.access.Lock() + if conn.writer != nil { + recordWriter := conn.writer + conn.access.Unlock() + return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) + } + for index, buffer := range buffers { + if buffer.IsEmpty() { + buffer.Release() + continue + } + err := conn.writeResponseBuffer(buffer) + if err != nil { + conn.access.Unlock() + buf.ReleaseMulti(buffers[index+1:]) + return err + } + if index+1 < len(buffers) { + recordWriter := conn.writer + conn.access.Unlock() + return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers[index+1:]) + } + conn.access.Unlock() + return nil + } + conn.access.Unlock() + return nil +} + +func (c *serverConn) CloseWrite() error { + c.closeWriteOnce.Do(func() { + c.access.Lock() + defer c.access.Unlock() + // snell-server v6.0.0b4 closes non-reusable command-1 tunnels on upstream EOF instead of writing a Snell zero chunk. + c.closeWriteErr = c.Conn.Close() + }) + return c.closeWriteErr +} + +func (c *serverConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + return c.reader.InitializeReadWaiter(options) +} + +func (c *serverConn) WaitReadBuffer() (*buf.Buffer, error) { + return c.reader.WaitReadBuffer() +} + +func (c *serverConn) FrontHeadroom() int { + if c.writer != nil { + return c.writer.FrontHeadroom() + } + switch c.service.mode { + case ModeUnsafeRaw: + return 1 + snell.HeaderPlainLen + case ModeUnshaped: + return 1 + snell.SaltLen + snell.HeaderCipherLen + default: + return 1 + c.service.profile.saltBlockLen + c.service.profile.recordPrefixMax + snell.HeaderCipherLen + c.service.profile.padMaxHeadroom + } +} + +func (c *serverConn) RearHeadroom() int { + if c.writer != nil { + return c.writer.RearHeadroom() + } + if c.service.mode == ModeUnsafeRaw { + return 0 + } + return snell.AEADTagLen +} + +func (c *serverConn) WriterMTU() int { + if c.writer != nil { + return c.writer.WriterMTU() + } + if c.service.mode == ModeDefault { + return c.service.profile.chunkMax + } + return maxPayload +} + +func (c *serverConn) Upstream() any { + return c.Conn +} + +var ( + _ N.ExtendedConn = (*serverConn)(nil) + _ N.ReadWaiter = (*serverConn)(nil) + _ N.VectorisedWriteCreator = (*serverConn)(nil) + _ N.VectorisedWriter = (*serverVectorisedWriter)(nil) + _ N.WriteCloser = (*serverConn)(nil) +) diff --git a/proxy/snell/sing-snell/snellv6/shaped.go b/proxy/snell/sing-snell/snellv6/shaped.go new file mode 100644 index 00000000000..57c9eb007fe --- /dev/null +++ b/proxy/snell/sing-snell/snellv6/shaped.go @@ -0,0 +1,489 @@ +package snellv6 + +import ( + "crypto/cipher" + "encoding/binary" + "io" + "sync" + "time" + + snell "github.com/sagernet/sing-snell" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" +) + +type shapedWriter struct { + upstream io.Writer + profile *Profile + cipher cipher.AEAD + salt []byte + nonce []byte + seq uint32 + saltSent bool + chunkSize int + lastWriteUnix int64 + access sync.Mutex +} + +func newShapedWriter(upstream io.Writer, profile *Profile, salt []byte, aead cipher.AEAD, nonce []byte) *shapedWriter { + return &shapedWriter{upstream: upstream, profile: profile, cipher: aead, salt: salt, nonce: nonce} +} + +func (w *shapedWriter) makeSliceRecord(payload []byte) *buf.Buffer { + prefixLen := w.profile.recordPrefixLen(w.seq) + saltBlockLen := 0 + saltPrefixLen := 0 + if !w.saltSent { + saltBlockLen = w.profile.saltBlockLen + saltPrefixLen = saltBlockLen - saltLen + } + paddingLen := w.profile.paddingLen(w.seq, len(payload), prefixLen, saltPrefixLen, saltBlockLen) + payloadCipherLen := 0 + if len(payload) > 0 { + payloadCipherLen = len(payload) + snell.AEADTagLen + } + out := buf.NewSize(saltBlockLen + prefixLen + snell.HeaderCipherLen + paddingLen + payloadCipherLen) + + if saltBlockLen > 0 { + block := out.Extend(saltBlockLen) + // Surge 6.7.0 (11520): FUN_100014610: fills the salt block with shaped padding + // at sentinel sequence 0xffffffff before overwriting the salt positions. + w.profile.fillPadding(0xffffffff, block) + w.profile.writeSaltBlock(w.salt, block) + w.saltSent = true + } + prefix := out.Extend(prefixLen) + w.profile.fillPadding(w.seq, prefix) + + header := out.Extend(snell.HeaderCipherLen) + putHeader(header, paddingLen, len(payload)) + w.cipher.Seal(header[:0], w.nonce, header[:snell.HeaderPlainLen], prefix) + snell.IncreaseNonce(w.nonce) + + padding := out.Extend(paddingLen) + w.profile.fillPadding(w.seq, padding) + if len(payload) > 0 { + region := out.Extend(payloadCipherLen) + copy(region, payload) + w.cipher.Seal(region[:0], w.nonce, region[:len(payload)], padding) + snell.IncreaseNonce(w.nonce) + w.profile.mixPaddingPayload(w.seq, padding, region) + } + w.seq++ + return out +} + +func (w *shapedWriter) makeBufferRecord(buffer *buf.Buffer) *buf.Buffer { + dataLen := buffer.Len() + prefixLen := w.profile.recordPrefixLen(w.seq) + saltBlockLen := 0 + saltPrefixLen := 0 + if !w.saltSent { + saltBlockLen = w.profile.saltBlockLen + saltPrefixLen = saltBlockLen - saltLen + } + paddingLen := w.profile.paddingLen(w.seq, dataLen, prefixLen, saltPrefixLen, saltBlockLen) + frontLen := saltBlockLen + prefixLen + snell.HeaderCipherLen + paddingLen + front := buffer.ExtendHeader(frontLen) + if saltBlockLen > 0 { + block := front[:saltBlockLen] + // Surge 6.7.0 (11520): FUN_100014610: fills the salt block with shaped padding + // at sentinel sequence 0xffffffff before overwriting the salt positions. + w.profile.fillPadding(0xffffffff, block) + w.profile.writeSaltBlock(w.salt, block) + w.saltSent = true + } + prefix := front[saltBlockLen : saltBlockLen+prefixLen] + w.profile.fillPadding(w.seq, prefix) + + header := front[saltBlockLen+prefixLen : saltBlockLen+prefixLen+snell.HeaderCipherLen] + putHeader(header, paddingLen, dataLen) + w.cipher.Seal(header[:0], w.nonce, header[:snell.HeaderPlainLen], prefix) + snell.IncreaseNonce(w.nonce) + + padding := front[saltBlockLen+prefixLen+snell.HeaderCipherLen:] + w.profile.fillPadding(w.seq, padding) + if dataLen > 0 { + region := buffer.From(frontLen) + buffer.Extend(snell.AEADTagLen) + w.cipher.Seal(region[:0], w.nonce, region[:dataLen], padding) + snell.IncreaseNonce(w.nonce) + region = buffer.From(frontLen) + w.profile.mixPaddingPayload(w.seq, padding, region) + } + w.seq++ + return buffer +} + +func (w *shapedWriter) writeRecords(records []*buf.Buffer) error { + if len(records) == 1 { + _, err := w.upstream.Write(records[0].Bytes()) + return err + } + out := buf.NewSize(buf.LenMulti(records)) + defer out.Release() + for _, record := range records { + copy(out.Extend(record.Len()), record.Bytes()) + } + _, err := w.upstream.Write(out.Bytes()) + return err +} + +func (w *shapedWriter) payloadLimitFor(now time.Time) int { + nowUnix := now.Unix() + // Surge 6.7.0 (11520): FUN_1000142d4/FUN_100014610: v6 writer idle reset is second-granular. + if w.lastWriteUnix == 0 || nowUnix-w.lastWriteUnix > int64(w.profile.idleResetSec) { + w.chunkSize = w.profile.chunkInitial + } + if w.chunkSize == 0 { + w.chunkSize = w.profile.chunkInitial + } + payloadLimit := w.profile.chunkPayloadLimit(w.seq, w.chunkSize) + if w.seq == 0 { + payloadLimit = min(payloadLimit, w.profile.firstRecordCap) + } + w.chunkSize = w.profile.nextChunkSize(w.chunkSize) + w.lastWriteUnix = nowUnix + return max(1, min(payloadLimit, maxPayload)) +} + +func (w *shapedWriter) Write(p []byte) (n int, err error) { + w.access.Lock() + defer w.access.Unlock() + now := time.Now() + originalLen := len(p) + var records []*buf.Buffer + defer func() { + buf.ReleaseMulti(records) + }() + for len(p) > 0 { + payloadLimit := w.payloadLimitFor(now) + recordLen := min(len(p), payloadLimit) + records = append(records, w.makeSliceRecord(p[:recordLen])) + p = p[recordLen:] + } + if len(records) == 0 { + return 0, nil + } + err = w.writeRecords(records) + if err != nil { + return 0, err + } + n = originalLen + return +} + +func (w *shapedWriter) WriteBuffer(buffer *buf.Buffer) error { + defer buffer.Release() + dataLen := buffer.Len() + if dataLen == 0 { + return nil + } + w.access.Lock() + defer w.access.Unlock() + now := time.Now() + payloadLimit := w.payloadLimitFor(now) + if dataLen <= payloadLimit { + w.makeBufferRecord(buffer) + _, err := w.upstream.Write(buffer.Bytes()) + return err + } + var records []*buf.Buffer + defer func() { + buf.ReleaseMulti(records) + }() + for data := buffer.Bytes(); len(data) > 0; { + recordLen := min(len(data), payloadLimit) + records = append(records, w.makeSliceRecord(data[:recordLen])) + data = data[recordLen:] + if len(data) > 0 { + payloadLimit = w.payloadLimitFor(now) + } + } + return w.writeRecords(records) +} + +func (w *shapedWriter) WritePacketBuffer(buffer *buf.Buffer) error { + dataLen := buffer.Len() + if dataLen == 0 { + buffer.Release() + return nil + } + w.access.Lock() + defer w.access.Unlock() + nowUnix := time.Now().Unix() + chunkSize := w.chunkSize + if w.lastWriteUnix == 0 || nowUnix-w.lastWriteUnix > int64(w.profile.idleResetSec) { + chunkSize = w.profile.chunkInitial + } + if chunkSize == 0 { + chunkSize = w.profile.chunkInitial + } + payloadLimit := w.profile.chunkPayloadLimit(w.seq, chunkSize) + if w.seq == 0 { + payloadLimit = min(payloadLimit, w.profile.firstRecordCap) + } + payloadLimit = max(1, min(payloadLimit, maxPayload)) + if dataLen > payloadLimit { + buffer.Release() + return snell.ErrPayloadTooLarge + } + w.chunkSize = w.profile.nextChunkSize(chunkSize) + w.lastWriteUnix = nowUnix + record := w.makeBufferRecord(buffer) + _, err := w.upstream.Write(record.Bytes()) + record.Release() + return err +} + +func (w *shapedWriter) WriteZeroChunk() error { + w.access.Lock() + defer w.access.Unlock() + w.payloadLimitFor(time.Now()) + record := w.makeSliceRecord(nil) + defer record.Release() + _, err := w.upstream.Write(record.Bytes()) + return err +} + +func (w *shapedWriter) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + upstreamWriter, created := bufio.CreateVectorisedWriter(w.upstream) + if !created { + return nil, false + } + return w.CreateVectorisedWriterFor(upstreamWriter), true +} + +func (w *shapedWriter) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { + return &vectorisedShapedWriter{writer: w, upstream: upstream} +} + +type vectorisedShapedWriter struct { + writer *shapedWriter + upstream N.VectorisedWriter +} + +func (w *vectorisedShapedWriter) WriteVectorised(buffers []*buf.Buffer) error { + var records []*buf.Buffer + defer func() { + buf.ReleaseMulti(records) + }() + recordWriter := w.writer + recordWriter.access.Lock() + defer recordWriter.access.Unlock() + for _, buffer := range buffers { + dataLen := buffer.Len() + if dataLen == 0 { + buffer.Release() + continue + } + for data := buffer.Bytes(); len(data) > 0; { + payloadLimit := recordWriter.payloadLimitFor(time.Now()) + recordLen := min(len(data), payloadLimit) + if len(data) == dataLen && dataLen <= payloadLimit { + record := recordWriter.makeBufferRecord(buffer) + buffer = nil + records = append(records, record) + break + } + records = append(records, recordWriter.makeSliceRecord(data[:recordLen])) + data = data[recordLen:] + } + if buffer != nil { + buffer.Release() + } + } + if len(records) == 0 { + return nil + } + flushRecords := records + records = nil + return w.upstream.WriteVectorised(flushRecords) +} + +var _ N.VectorisedWriter = (*vectorisedShapedWriter)(nil) + +func (w *shapedWriter) CreatePacketVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { + return &packetVectorisedShapedWriter{writer: w, upstream: upstream} +} + +type packetVectorisedShapedWriter struct { + writer *shapedWriter + upstream N.VectorisedWriter +} + +func (w *packetVectorisedShapedWriter) WriteVectorised(buffers []*buf.Buffer) error { + var records []*buf.Buffer + defer func() { + buf.ReleaseMulti(records) + }() + recordWriter := w.writer + recordWriter.access.Lock() + defer recordWriter.access.Unlock() + for index, buffer := range buffers { + if buffer.IsEmpty() { + buffer.Release() + continue + } + dataLen := buffer.Len() + nowUnix := time.Now().Unix() + chunkSize := recordWriter.chunkSize + if recordWriter.lastWriteUnix == 0 || nowUnix-recordWriter.lastWriteUnix > int64(recordWriter.profile.idleResetSec) { + chunkSize = recordWriter.profile.chunkInitial + } + if chunkSize == 0 { + chunkSize = recordWriter.profile.chunkInitial + } + payloadLimit := recordWriter.profile.chunkPayloadLimit(recordWriter.seq, chunkSize) + if recordWriter.seq == 0 { + payloadLimit = min(payloadLimit, recordWriter.profile.firstRecordCap) + } + payloadLimit = max(1, min(payloadLimit, maxPayload)) + if dataLen > payloadLimit { + buffer.Release() + buf.ReleaseMulti(buffers[index+1:]) + return snell.ErrPayloadTooLarge + } + recordWriter.chunkSize = recordWriter.profile.nextChunkSize(chunkSize) + recordWriter.lastWriteUnix = nowUnix + record := recordWriter.makeBufferRecord(buffer) + records = append(records, record) + } + if len(records) == 0 { + return nil + } + flushRecords := records + records = nil + return w.upstream.WriteVectorised(flushRecords) +} + +var _ N.VectorisedWriter = (*packetVectorisedShapedWriter)(nil) + +func (w *shapedWriter) FrontHeadroom() int { + headroom := w.profile.recordPrefixMax + snell.HeaderCipherLen + w.profile.padMaxHeadroom + if !w.saltSent { + headroom += w.profile.saltBlockLen + } + return headroom +} + +func (w *shapedWriter) RearHeadroom() int { + return snell.AEADTagLen +} + +func (w *shapedWriter) WriterMTU() int { + return w.profile.chunkMax +} + +func (w *shapedWriter) Upstream() any { + return w.upstream +} + +type shapedReader struct { + baseReader + psk []byte + profile *Profile + cipher cipher.AEAD + nonce []byte + seq uint32 +} + +func newShapedReader(upstream io.Reader, psk []byte, profile *Profile) *shapedReader { + r := &shapedReader{psk: psk, profile: profile, nonce: make([]byte, snell.NonceLen)} + r.upstream = upstream + r.readFunc = r.read + return r +} + +func (r *shapedReader) read() (*buf.Buffer, error) { + if r.cipher == nil { + block := make([]byte, r.profile.saltBlockLen) + _, err := io.ReadFull(r.upstream, block) + if err != nil { + return nil, err + } + salt := r.profile.extractSalt(block) + aead, err := snell.NewAEAD(snell.DeriveKey(r.psk, salt[:])) + if err != nil { + return nil, err + } + r.cipher = aead + } + + prefixLen := r.profile.recordPrefixLen(r.seq) + head := buf.NewSize(prefixLen + snell.HeaderCipherLen) + _, err := head.ReadFullFrom(r.upstream, prefixLen+snell.HeaderCipherLen) + if err != nil { + head.Release() + return nil, err + } + prefix := head.To(prefixLen) + headerCipher := head.Range(prefixLen, prefixLen+snell.HeaderCipherLen) + _, err = r.cipher.Open(headerCipher[:0], r.nonce, headerCipher, prefix) + if err != nil { + head.Release() + return nil, E.Cause(err, "open shaped header") + } + snell.IncreaseNonce(r.nonce) + if headerCipher[0] != snell.HeaderVersion { + head.Release() + return nil, E.Extend(snell.ErrBadVersion, headerCipher[0]) + } + // Surge 6.7.0 (11520): FUN_100013abc: default-shaped reader ignores the two reserved header bytes. + paddingLen := int(binary.BigEndian.Uint16(headerCipher[3:5])) + payloadLen := int(binary.BigEndian.Uint16(headerCipher[5:7])) + head.Release() + seq := r.seq + r.seq++ + + if payloadLen == 0 { + if paddingLen > 0 { + discard := buf.NewSize(paddingLen) + _, err = discard.ReadFullFrom(r.upstream, paddingLen) + discard.Release() + if err != nil { + return nil, err + } + } + return nil, io.EOF + } + + var padding *buf.Buffer + if paddingLen > 0 { + padding = buf.NewSize(paddingLen) + _, err = padding.ReadFullFrom(r.upstream, paddingLen) + if err != nil { + padding.Release() + return nil, err + } + } + body := r.readWaitOptions.NewBufferSize(payloadLen + snell.AEADTagLen) + _, err = body.ReadFullFrom(r.upstream, payloadLen+snell.AEADTagLen) + if err != nil { + if padding != nil { + padding.Release() + } + body.Release() + return nil, err + } + var paddingBytes []byte + if padding != nil { + paddingBytes = padding.Bytes() + } + payloadCipher := body.Bytes() + r.profile.mixPaddingPayload(seq, paddingBytes, payloadCipher) + _, err = r.cipher.Open(payloadCipher[:0], r.nonce, payloadCipher, paddingBytes) + if padding != nil { + padding.Release() + } + if err != nil { + body.Release() + return nil, E.Cause(err, "open shaped payload") + } + snell.IncreaseNonce(r.nonce) + body.Truncate(payloadLen) + r.readWaitOptions.PostReturn(body) + return body, nil +} diff --git a/proxy/snell/sing-snell/test/docker_test.go b/proxy/snell/sing-snell/test/docker_test.go new file mode 100644 index 00000000000..60d74969f0f --- /dev/null +++ b/proxy/snell/sing-snell/test/docker_test.go @@ -0,0 +1,210 @@ +package test + +import ( + "archive/zip" + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "io" + "net" + "net/http" + "os" + "path/filepath" + "testing" + "time" + + "github.com/sagernet/sing/common/debug" + F "github.com/sagernet/sing/common/format" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/stdcopy" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/require" +) + +const baseImage = "debian:bookworm-slim" + +type binarySpec struct { + tag string + sha256 string +} + +var binaries = map[string]binarySpec{ + "v4": {tag: "v4.1.1", sha256: "8e02974916645aafc0521c49faf0ab9916a0a8a7922ae7261cfd74b96899d25a"}, + "v5": {tag: "v5.0.1", sha256: "5b2e221f2c6e29b1db8e47053e1221be29d5627da807cb932b089f514a3609f0"}, + "v6": {tag: "v6.0.0b4", sha256: "ef2feaaf69d40673c2f3c8dccff28be6eadb75e790cb9e0cabd18bc66675c46f"}, +} + +func cacheDir() string { + dir := os.Getenv("SNELL_CACHE_DIR") + if dir == "" { + dir = filepath.Join(os.TempDir(), "sing-snell-test-bin") + } + return dir +} + +func snellBinary(t *testing.T, version string) string { + spec, found := binaries[version] + require.True(t, found, "unknown snell version: %s", version) + + if override := os.Getenv("SNELL_BIN_DIR"); override != "" { + dir := filepath.Join(override, spec.tag+"-linux-amd64") + verifyBinary(t, filepath.Join(dir, "snell-server"), spec.sha256) + return dir + } + + dir := filepath.Join(cacheDir(), spec.tag) + binaryPath := filepath.Join(dir, "snell-server") + if checkBinary(binaryPath, spec.sha256) { + return dir + } + require.NoError(t, os.MkdirAll(dir, 0o755)) + + url := "https://dl.nssurge.com/snell/snell-server-" + spec.tag + "-linux-amd64.zip" + t.Logf("downloading %s", url) + archive := downloadFile(t, url) + reader, err := zip.NewReader(bytes.NewReader(archive), int64(len(archive))) + require.NoError(t, err) + var extracted bool + for _, file := range reader.File { + if filepath.Base(file.Name) != "snell-server" { + continue + } + source, openErr := file.Open() + require.NoError(t, openErr) + content, readErr := io.ReadAll(source) + source.Close() + require.NoError(t, readErr) + require.NoError(t, os.WriteFile(binaryPath, content, 0o755)) + extracted = true + break + } + require.True(t, extracted, "snell-server not found in archive") + verifyBinary(t, binaryPath, spec.sha256) + return dir +} + +func checkBinary(path string, want string) bool { + content, err := os.ReadFile(path) + if err != nil { + return false + } + digest := sha256.Sum256(content) + return hex.EncodeToString(digest[:]) == want +} + +func verifyBinary(t *testing.T, path string, want string) { + content, err := os.ReadFile(path) + require.NoError(t, err) + digest := sha256.Sum256(content) + require.Equal(t, want, hex.EncodeToString(digest[:]), "binary sha256 mismatch: %s", path) + require.NoError(t, os.Chmod(path, 0o755)) +} + +func downloadFile(t *testing.T, url string) []byte { + response, err := http.Get(url) + require.NoError(t, err) + defer response.Body.Close() + require.Equal(t, http.StatusOK, response.StatusCode, "download %s", url) + content, err := io.ReadAll(response.Body) + require.NoError(t, err) + return content +} + +func startSnellServer(t *testing.T, version string, configContent string, port uint16) { + binDir := snellBinary(t, version) + + confDir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(confDir, "snell.conf"), []byte(configContent), 0o644)) + + dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + require.NoError(t, err) + t.Cleanup(func() { dockerClient.Close() }) + + ctx := context.Background() + ensureImage(t, dockerClient, baseImage) + + containerConfig := &container.Config{ + Image: baseImage, + Entrypoint: []string{"/snell/snell-server"}, + Cmd: []string{"-l", logLevel(), "-c", "/conf/snell.conf"}, + } + hostConfig := &container.HostConfig{ + NetworkMode: "host", + Binds: []string{ + binDir + ":/snell:ro", + confDir + ":/conf:ro", + }, + } + platform := &ocispec.Platform{OS: "linux", Architecture: "amd64"} + + created, err := dockerClient.ContainerCreate(ctx, containerConfig, hostConfig, nil, platform, "") + require.NoError(t, err) + t.Cleanup(func() { + dockerClient.ContainerRemove(context.Background(), created.ID, container.RemoveOptions{Force: true}) + }) + require.NoError(t, dockerClient.ContainerStart(ctx, created.ID, container.StartOptions{})) + + if debug.Enabled { + attach, attachErr := dockerClient.ContainerAttach(ctx, created.ID, container.AttachOptions{ + Stdout: true, Stderr: true, Logs: true, Stream: true, + }) + require.NoError(t, attachErr) + go stdcopy.StdCopy(os.Stderr, os.Stderr, attach.Reader) + } + + waitForListen(t, dockerClient, created.ID, port) +} + +func waitForListen(t *testing.T, dockerClient *client.Client, containerID string, port uint16) { + deadline := time.Now().Add(30 * time.Second) + address := "127.0.0.1:" + F.ToString(port) + for time.Now().Before(deadline) { + conn, err := net.DialTimeout("tcp", address, time.Second) + if err == nil { + conn.Close() + return + } + info, inspectErr := dockerClient.ContainerInspect(context.Background(), containerID) + if inspectErr == nil && !info.State.Running { + logs := containerLogs(dockerClient, containerID) + t.Fatalf("snell server exited early (code %d):\n%s", info.State.ExitCode, logs) + } + time.Sleep(200 * time.Millisecond) + } + t.Fatalf("snell server did not listen on %s:\n%s", address, containerLogs(dockerClient, containerID)) +} + +func containerLogs(dockerClient *client.Client, containerID string) string { + reader, err := dockerClient.ContainerLogs(context.Background(), containerID, container.LogsOptions{ + ShowStdout: true, ShowStderr: true, + }) + if err != nil { + return "" + } + defer reader.Close() + var out bytes.Buffer + stdcopy.StdCopy(&out, &out, reader) + return out.String() +} + +func ensureImage(t *testing.T, dockerClient *client.Client, ref string) { + _, err := dockerClient.ImageInspect(context.Background(), ref) + if err == nil { + return + } + reader, pullErr := dockerClient.ImagePull(context.Background(), ref, image.PullOptions{Platform: "linux/amd64"}) + require.NoError(t, pullErr) + defer reader.Close() + io.Copy(io.Discard, reader) +} + +func logLevel() string { + if debug.Enabled { + return "trace" + } + return "error" +} diff --git a/proxy/snell/sing-snell/test/go.mod b/proxy/snell/sing-snell/test/go.mod new file mode 100644 index 00000000000..be39d4124a6 --- /dev/null +++ b/proxy/snell/sing-snell/test/go.mod @@ -0,0 +1,46 @@ +module github.com/sagernet/sing-snell/test + +go 1.25.0 + +require ( + github.com/docker/docker v28.5.2+incompatible + github.com/opencontainers/image-spec v1.1.1 + github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d + github.com/sagernet/sing-snell v0.0.0 + github.com/stretchr/testify v1.11.1 +) + +replace github.com/sagernet/sing-snell => ../ + +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.7.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/sys/atomicwriter v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/morikuni/aec v1.1.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect + golang.org/x/crypto v0.42.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/time v0.15.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/v3 v3.5.2 // indirect +) diff --git a/proxy/snell/sing-snell/test/go.sum b/proxy/snell/sing-snell/test/go.sum new file mode 100644 index 00000000000..807aae81856 --- /dev/null +++ b/proxy/snell/sing-snell/test/go.sum @@ -0,0 +1,112 @@ +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= +github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d h1:BhsQU0Iug1tU4xR52cjm8Sc+LBo+KwdyLTRn3ie9moo= +github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/proxy/snell/sing-snell/test/helper_test.go b/proxy/snell/sing-snell/test/helper_test.go new file mode 100644 index 00000000000..d1fa70b3c95 --- /dev/null +++ b/proxy/snell/sing-snell/test/helper_test.go @@ -0,0 +1,53 @@ +package test + +import ( + "io" + "net" + "testing" + + "github.com/stretchr/testify/require" +) + +func freePort(t *testing.T) uint16 { + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + port := uint16(listener.Addr().(*net.TCPAddr).Port) + listener.Close() + return port +} + +func startTCPEcho(t *testing.T) uint16 { + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { listener.Close() }) + go func() { + for { + conn, acceptErr := listener.Accept() + if acceptErr != nil { + return + } + go func() { + io.Copy(conn, conn) + conn.Close() + }() + } + }() + return uint16(listener.Addr().(*net.TCPAddr).Port) +} + +func startUDPEcho(t *testing.T) uint16 { + packetConn, err := net.ListenPacket("udp", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { packetConn.Close() }) + go func() { + buffer := make([]byte, 64*1024) + for { + n, addr, readErr := packetConn.ReadFrom(buffer) + if readErr != nil { + return + } + packetConn.WriteTo(buffer[:n], addr) + } + }() + return uint16(packetConn.LocalAddr().(*net.UDPAddr).Port) +} diff --git a/proxy/snell/sing-snell/test/loopback_test.go b/proxy/snell/sing-snell/test/loopback_test.go new file mode 100644 index 00000000000..8a37d39f622 --- /dev/null +++ b/proxy/snell/sing-snell/test/loopback_test.go @@ -0,0 +1,44 @@ +package test + +import ( + "bytes" + "crypto/rand" + "io" + "net" + "testing" + "time" + + snell "github.com/sagernet/sing-snell" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "github.com/stretchr/testify/require" +) + +type normalScenario struct { + address string + client snell.Method +} + +func (s normalScenario) HalfCloseEcho(t *testing.T, label string) { + echoPort := startTCPHalfCloseEcho(t) + serverConn, err := net.Dial("tcp", s.address) + require.NoError(t, err) + defer serverConn.Close() + proxyConn, err := s.client.DialConn(serverConn, M.ParseSocksaddrHostPort("127.0.0.1", echoPort)) + require.NoError(t, err) + defer proxyConn.Close() + + payload := make([]byte, 4*1024) + _, err = rand.Read(payload) + require.NoError(t, err) + copy(payload, []byte(label)) + _, err = proxyConn.Write(payload) + require.NoError(t, err) + require.NoError(t, N.CloseWrite(proxyConn)) + require.NoError(t, proxyConn.SetReadDeadline(time.Now().Add(10*time.Second))) + received, err := io.ReadAll(proxyConn) + require.NoError(t, err) + require.Equal(t, len(payload), len(received), "%s response length", label) + require.True(t, bytes.Equal(payload, received), "%s payload mismatch", label) +} diff --git a/proxy/snell/sing-snell/test/reuse_helpers_test.go b/proxy/snell/sing-snell/test/reuse_helpers_test.go new file mode 100644 index 00000000000..6d406000d9e --- /dev/null +++ b/proxy/snell/sing-snell/test/reuse_helpers_test.go @@ -0,0 +1,297 @@ +package test + +import ( + "bytes" + "context" + "crypto/rand" + "io" + "net" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/sagernet/sing/common/bufio" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "github.com/stretchr/testify/require" +) + +type reuseTCPClient interface { + DialContext(ctx context.Context, destination M.Socksaddr) (net.Conn, error) + Close() error +} + +type countingTCPProxy struct { + address string + count atomic.Int32 +} + +func (p *countingTCPProxy) Start(t *testing.T, target string) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + p.address = listener.Addr().String() + t.Cleanup(func() { listener.Close() }) + go func() { + for { + conn, acceptErr := listener.Accept() + if acceptErr != nil { + return + } + p.count.Add(1) + go p.Proxy(conn, target) + } + }() +} + +func (p *countingTCPProxy) Proxy(conn net.Conn, target string) { + upstream, err := net.Dial("tcp", target) + if err != nil { + conn.Close() + return + } + var wait sync.WaitGroup + wait.Add(2) + go func() { + defer wait.Done() + io.Copy(upstream, conn) + N.CloseWrite(upstream) + }() + go func() { + defer wait.Done() + io.Copy(conn, upstream) + N.CloseWrite(conn) + }() + wait.Wait() + conn.Close() + upstream.Close() +} + +type reuseScenario struct { + client reuseTCPClient + destination M.Socksaddr +} + +func startTCPHalfCloseEcho(t *testing.T, closeWriteDelay ...time.Duration) uint16 { + delay := time.Duration(0) + if len(closeWriteDelay) > 0 { + delay = closeWriteDelay[0] + } + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { listener.Close() }) + go func() { + for { + conn, acceptErr := listener.Accept() + if acceptErr != nil { + return + } + go func() { + defer conn.Close() + payload, readErr := io.ReadAll(conn) + if readErr != nil { + return + } + _, writeErr := io.Copy(conn, bytes.NewReader(payload)) + if writeErr != nil { + return + } + time.Sleep(delay) + N.CloseWrite(conn) + time.Sleep(200 * time.Millisecond) + }() + } + }() + return uint16(listener.Addr().(*net.TCPAddr).Port) +} + +func startTCPHalfCloseTailEcho(t *testing.T, tailLen int, closeWriteDelay time.Duration) uint16 { + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { listener.Close() }) + go func() { + for { + conn, acceptErr := listener.Accept() + if acceptErr != nil { + return + } + go func() { + defer conn.Close() + payload, readErr := io.ReadAll(conn) + if readErr != nil { + return + } + _, writeErr := io.Copy(conn, bytes.NewReader(payload)) + if writeErr != nil { + return + } + if tailLen > 0 { + tail := bytes.Repeat([]byte{0x5A}, tailLen) + _, writeErr = conn.Write(tail) + if writeErr != nil { + return + } + } + time.Sleep(closeWriteDelay) + N.CloseWrite(conn) + time.Sleep(200 * time.Millisecond) + }() + } + }() + return uint16(listener.Addr().(*net.TCPAddr).Port) +} + +func (s reuseScenario) RoundTrip(t *testing.T, label string) { + payload := make([]byte, 4*1024) + _, err := rand.Read(payload) + require.NoError(t, err) + copy(payload, []byte(label)) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + conn, err := s.client.DialContext(ctx, s.destination) + require.NoError(t, err) + _, err = conn.Write(payload) + require.NoError(t, err) + time.Sleep(20 * time.Millisecond) + require.NoError(t, N.CloseWrite(conn)) + require.NoError(t, conn.SetReadDeadline(time.Now().Add(10*time.Second))) + received, err := io.ReadAll(conn) + require.NoError(t, err) + require.Equal(t, len(payload), len(received), "%s response length", label) + require.True(t, bytes.Equal(payload, received), "%s payload mismatch", label) + require.NoError(t, conn.Close()) +} + +func (s reuseScenario) PartialEchoRoundTrip(t *testing.T, label string, payloadLen int, echoLen int) { + payload := make([]byte, payloadLen) + _, err := rand.Read(payload) + require.NoError(t, err) + copy(payload, []byte(label)) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + conn, err := s.client.DialContext(ctx, s.destination) + require.NoError(t, err) + _, err = conn.Write(payload) + require.NoError(t, err) + require.NoError(t, N.CloseWrite(conn)) + require.NoError(t, conn.SetReadDeadline(time.Now().Add(10*time.Second))) + received, err := io.ReadAll(conn) + require.NoError(t, err) + require.True(t, bytes.Equal(payload[:echoLen], received), "%s payload mismatch", label) + require.NoError(t, conn.Close()) +} + +func (s reuseScenario) CloseBeforeServerEOF(t *testing.T, label string, waitAfterClose time.Duration) { + payload := make([]byte, 4*1024) + _, err := rand.Read(payload) + require.NoError(t, err) + copy(payload, []byte(label)) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + conn, err := s.client.DialContext(ctx, s.destination) + require.NoError(t, err) + _, err = conn.Write(payload) + require.NoError(t, err) + require.NoError(t, N.CloseWrite(conn)) + require.NoError(t, conn.SetReadDeadline(time.Now().Add(10*time.Second))) + received := make([]byte, len(payload)) + _, err = io.ReadFull(conn, received) + require.NoError(t, err) + require.True(t, bytes.Equal(payload, received), "%s payload mismatch", label) + closeStart := time.Now() + require.NoError(t, conn.Close()) + if time.Since(closeStart) > 250*time.Millisecond { + t.Fatalf("%s close waited for server EOF instead of leaving the session draining", label) + } + time.Sleep(waitAfterClose) +} + +func (s reuseScenario) CloseWhileReadBlocked(t *testing.T, label string) { + payload := make([]byte, 4*1024) + _, err := rand.Read(payload) + require.NoError(t, err) + copy(payload, []byte(label)) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + conn, err := s.client.DialContext(ctx, s.destination) + require.NoError(t, err) + _, err = conn.Write(payload) + require.NoError(t, err) + require.NoError(t, N.CloseWrite(conn)) + require.NoError(t, conn.SetReadDeadline(time.Now().Add(10*time.Second))) + received := make([]byte, len(payload)) + _, err = io.ReadFull(conn, received) + require.NoError(t, err) + require.True(t, bytes.Equal(payload, received), "%s payload mismatch", label) + + readStarted := make(chan struct{}) + readResult := make(chan struct { + n int + err error + }, 1) + go func() { + close(readStarted) + var one [1]byte + n, readErr := conn.Read(one[:]) + readResult <- struct { + n int + err error + }{n: n, err: readErr} + }() + <-readStarted + select { + case result := <-readResult: + t.Fatalf("%s read returned before close: n=%d err=%v", label, result.n, result.err) + case <-time.After(50 * time.Millisecond): + } + + closeStart := time.Now() + require.NoError(t, conn.Close()) + if time.Since(closeStart) > 250*time.Millisecond { + t.Fatalf("%s close waited for server EOF instead of leaving the active read to finish", label) + } + select { + case result := <-readResult: + t.Fatalf("%s read returned before server EOF after close: n=%d err=%v", label, result.n, result.err) + case <-time.After(100 * time.Millisecond): + } + select { + case result := <-readResult: + require.Zero(t, result.n, "%s read bytes", label) + require.ErrorIs(t, result.err, io.EOF, "%s read error", label) + case <-time.After(3 * time.Second): + t.Fatalf("%s active read did not finish after server EOF", label) + } +} + +func (s reuseScenario) ReadWaiter(t *testing.T, label string) { + payload := make([]byte, 4*1024) + _, err := rand.Read(payload) + require.NoError(t, err) + copy(payload, []byte(label)) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + conn, err := s.client.DialContext(ctx, s.destination) + require.NoError(t, err) + readWaiter, created := bufio.CreateReadWaiter(conn) + require.True(t, created) + needCopy := readWaiter.InitializeReadWaiter(N.ReadWaitOptions{FrontHeadroom: 2048, RearHeadroom: 256, MTU: 1200}) + require.False(t, needCopy) + _, err = conn.Write(payload) + require.NoError(t, err) + require.NoError(t, N.CloseWrite(conn)) + require.NoError(t, conn.SetReadDeadline(time.Now().Add(10*time.Second))) + received := make([]byte, 0, len(payload)) + for len(received) < len(payload) { + buffer, readErr := readWaiter.WaitReadBuffer() + require.NoError(t, readErr) + require.GreaterOrEqual(t, buffer.Start(), 2048) + require.GreaterOrEqual(t, buffer.FreeLen(), 256) + received = append(received, buffer.Bytes()...) + buffer.Release() + } + require.Equal(t, len(payload), len(received), "%s response length", label) + require.True(t, bytes.Equal(payload, received), "%s payload mismatch", label) + require.NoError(t, conn.Close()) +} diff --git a/proxy/snell/sing-snell/test/tls_obfs_loopback_test.go b/proxy/snell/sing-snell/test/tls_obfs_loopback_test.go new file mode 100644 index 00000000000..31fe6ac6580 --- /dev/null +++ b/proxy/snell/sing-snell/test/tls_obfs_loopback_test.go @@ -0,0 +1,240 @@ +package test + +import ( + "bytes" + "context" + "crypto/rand" + "io" + "net" + "testing" + "time" + + snell "github.com/sagernet/sing-snell" + "github.com/sagernet/sing-snell/snellv4" + "github.com/sagernet/sing-snell/snellv5" + "github.com/sagernet/sing/common/buf" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "github.com/stretchr/testify/require" +) + +func TestV5ObfsTLSLoopback(t *testing.T) { + service, err := snellv5.NewService(snellv5.ServiceOptions{ + PSK: []byte(testPSK), + ObfsMode: snell.ObfsModeTLS, + Handler: localEchoHandler{}, + }) + require.NoError(t, err) + serviceAddress := startLocalSnellService(t, service) + client, err := snellv4.NewClient(snellv4.ClientOptions{ + PSK: []byte(testPSK), + ObfsMode: snell.ObfsModeTLS, + ObfsHost: "obfs.example", + }) + require.NoError(t, err) + normalScenario{address: serviceAddress, client: client}.HalfCloseEcho(t, "v5-tls-tcp") + + var proxy countingTCPProxy + proxy.Start(t, serviceAddress) + reuseClient, err := snellv4.NewClient(snellv4.ClientOptions{ + PSK: []byte(testPSK), + Reuse: true, + ObfsMode: snell.ObfsModeTLS, + ObfsHost: "obfs.example", + Dialer: N.SystemDialer, + Server: M.ParseSocksaddr(proxy.address), + }) + require.NoError(t, err) + defer reuseClient.Close() + reuseScenario{client: reuseClient, destination: M.ParseSocksaddrHostPort("127.0.0.1", 443)}.RoundTrip(t, "v5-tls-reuse-1") + reuseScenario{client: reuseClient, destination: M.ParseSocksaddrHostPort("127.0.0.1", 443)}.RoundTrip(t, "v5-tls-reuse-2") + require.Equal(t, int32(1), proxy.count.Load()) + + serverConn, err := net.Dial("tcp", serviceAddress) + require.NoError(t, err) + defer serverConn.Close() + packetConn, err := client.DialPacketConn(serverConn) + require.NoError(t, err) + roundTripPacket(t, packetConn, serverConn, "v5-tls-udp-v4-wire") +} + +func TestV5ServerAcceptsV4ClientLoopback(t *testing.T) { + service, err := snellv5.NewService(snellv5.ServiceOptions{ + PSK: []byte(testPSK), + Handler: localEchoHandler{}, + }) + require.NoError(t, err) + serviceAddress := startLocalSnellService(t, service) + client, err := snellv4.NewClient(snellv4.ClientOptions{PSK: []byte(testPSK)}) + require.NoError(t, err) + normalScenario{address: serviceAddress, client: client}.HalfCloseEcho(t, "v5-service-v4-client-tcp") + + var proxy countingTCPProxy + proxy.Start(t, serviceAddress) + reuseClient, err := snellv4.NewClient(snellv4.ClientOptions{ + PSK: []byte(testPSK), + Reuse: true, + Dialer: N.SystemDialer, + Server: M.ParseSocksaddr(proxy.address), + }) + require.NoError(t, err) + defer reuseClient.Close() + reuseScenario{client: reuseClient, destination: M.ParseSocksaddrHostPort("127.0.0.1", 443)}.RoundTrip(t, "v5-service-v4-client-reuse-1") + reuseScenario{client: reuseClient, destination: M.ParseSocksaddrHostPort("127.0.0.1", 443)}.RoundTrip(t, "v5-service-v4-client-reuse-2") + require.Equal(t, int32(1), proxy.count.Load()) + + serverConn, err := net.Dial("tcp", serviceAddress) + require.NoError(t, err) + defer serverConn.Close() + packetConn, err := client.DialPacketConn(serverConn) + require.NoError(t, err) + roundTripPacket(t, packetConn, serverConn, "v5-service-v4-client-udp") +} + +func startLocalSnellService(t *testing.T, service snell.Service) string { + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { listener.Close() }) + go func() { + for { + conn, acceptErr := listener.Accept() + if acceptErr != nil { + return + } + go func() { + serveErr := service.NewConnection(context.Background(), conn, M.SocksaddrFromNet(conn.RemoteAddr()), nil) + if serveErr != nil { + conn.Close() + } + }() + } + }() + return listener.Addr().String() +} + +func roundTripPacket(t *testing.T, packetConn N.NetPacketConn, serverConn net.Conn, label string) { + message := make([]byte, 1200) + _, err := rand.Read(message) + require.NoError(t, err) + target := M.ParseSocksaddrHostPort("203.0.113.1", 443).UDPAddr() + _, err = packetConn.WriteTo(message, target) + require.NoError(t, err) + received := make([]byte, 2048) + require.NoError(t, serverConn.SetReadDeadline(time.Now().Add(10*time.Second))) + n, addr, err := packetConn.ReadFrom(received) + require.NoError(t, err) + require.Equal(t, target.String(), addr.String(), label) + require.True(t, bytes.Equal(message, received[:n]), label) +} + +func TestV5ServerReuseHandlerCloseBeforeClientEOF(t *testing.T) { + service, err := snellv5.NewService(snellv5.ServiceOptions{ + PSK: []byte(testPSK), + Handler: partialEchoHandler{echoLen: 4 * 1024}, + }) + require.NoError(t, err) + serviceAddress := startLocalSnellService(t, service) + var proxy countingTCPProxy + proxy.Start(t, serviceAddress) + reuseClient, err := snellv4.NewClient(snellv4.ClientOptions{ + PSK: []byte(testPSK), + Reuse: true, + Dialer: N.SystemDialer, + Server: M.ParseSocksaddr(proxy.address), + }) + require.NoError(t, err) + defer reuseClient.Close() + scenario := reuseScenario{client: reuseClient, destination: M.ParseSocksaddrHostPort("127.0.0.1", 443)} + scenario.PartialEchoRoundTrip(t, "v5-server-drain-1", 8*1024, 4*1024) + scenario.PartialEchoRoundTrip(t, "v5-server-drain-2", 8*1024, 4*1024) + require.Equal(t, int32(1), proxy.count.Load()) +} + +type localEchoHandler struct{} + +func (localEchoHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + go func() { + var closeErr error + defer func() { + writeCloseErr := N.CloseWrite(conn) + if closeErr == nil { + closeErr = writeCloseErr + } + connCloseErr := conn.Close() + if closeErr == nil { + closeErr = connCloseErr + } + if onClose != nil { + onClose(closeErr) + } + }() + payload, err := io.ReadAll(conn) + if err != nil { + closeErr = err + return + } + _, err = io.Copy(conn, bytes.NewReader(payload)) + if err != nil { + closeErr = err + } + }() +} + +type partialEchoHandler struct { + localEchoHandler + echoLen int +} + +func (h partialEchoHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + go func() { + payload := make([]byte, h.echoLen) + _, err := io.ReadFull(conn, payload) + if err == nil { + _, err = conn.Write(payload) + } + closeErr := conn.Close() + if err == nil { + err = closeErr + } + if onClose != nil { + onClose(err) + } + }() +} + +func (localEchoHandler) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + go func() { + var closeErr error + defer func() { + connCloseErr := conn.Close() + if closeErr == nil { + closeErr = connCloseErr + } + if onClose != nil { + onClose(closeErr) + } + }() + for { + select { + case <-ctx.Done(): + closeErr = ctx.Err() + return + default: + } + buffer := buf.NewSize(64 * 1024) + buffer.Resize(512, 0) + packetDestination, err := conn.ReadPacket(buffer) + if err != nil { + buffer.Release() + closeErr = err + return + } + err = conn.WritePacket(buffer, packetDestination) + if err != nil { + closeErr = err + return + } + } + }() +} diff --git a/proxy/snell/sing-snell/test/v4_test.go b/proxy/snell/sing-snell/test/v4_test.go new file mode 100644 index 00000000000..3b6c0286f89 --- /dev/null +++ b/proxy/snell/sing-snell/test/v4_test.go @@ -0,0 +1,369 @@ +package test + +import ( + "bytes" + "crypto/rand" + "fmt" + "io" + "net" + "testing" + "time" + + snell "github.com/sagernet/sing-snell" + "github.com/sagernet/sing-snell/snellv4" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "github.com/stretchr/testify/require" +) + +func TestV4TCP(t *testing.T) { + port := freePort(t) + config := fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nipv6 = false\n", port, testPSK) + startSnellServer(t, "v4", config, port) + echoPort := startTCPEcho(t) + + client, err := snellv4.NewClient(snellv4.ClientOptions{PSK: []byte(testPSK)}) + require.NoError(t, err) + serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + require.NoError(t, err) + defer serverConn.Close() + + proxyConn, err := client.DialConn(serverConn, M.ParseSocksaddrHostPort("127.0.0.1", echoPort)) + require.NoError(t, err) + + payload := make([]byte, 1024*1024) + rand.Read(payload) + go func() { + proxyConn.Write(payload) + }() + received := make([]byte, len(payload)) + proxyConn.SetReadDeadline(time.Now().Add(20 * time.Second)) + _, err = io.ReadFull(proxyConn, received) + require.NoError(t, err) + require.True(t, bytes.Equal(payload, received), "payload mismatch") + normalScenario{address: fmt.Sprintf("127.0.0.1:%d", port), client: client}.HalfCloseEcho(t, "v4-official") +} + +func TestV4TCPReuse(t *testing.T) { + port := freePort(t) + config := fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nipv6 = false\n", port, testPSK) + startSnellServer(t, "v4", config, port) + var proxy countingTCPProxy + proxy.Start(t, fmt.Sprintf("127.0.0.1:%d", port)) + echoPort := startTCPHalfCloseEcho(t) + delayedEchoPort := startTCPHalfCloseEcho(t, 750*time.Millisecond) + tailEchoPort := startTCPHalfCloseTailEcho(t, 32*1024, 250*time.Millisecond) + oversizedTailEchoPort := startTCPHalfCloseTailEcho(t, 0x80001, 250*time.Millisecond) + + client, err := snellv4.NewClient(snellv4.ClientOptions{ + PSK: []byte(testPSK), + Reuse: true, + Dialer: N.SystemDialer, + Server: M.ParseSocksaddr(proxy.address), + }) + require.NoError(t, err) + defer client.Close() + + scenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", echoPort)} + scenario.RoundTrip(t, "v4-reuse-1") + scenario.RoundTrip(t, "v4-reuse-2") + delayedScenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", delayedEchoPort)} + delayedScenario.CloseBeforeServerEOF(t, "v4-reuse-drain", time.Second) + scenario.RoundTrip(t, "v4-reuse-after-drain") + tailScenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", tailEchoPort)} + tailScenario.CloseBeforeServerEOF(t, "v4-reuse-tail", 500*time.Millisecond) + scenario.RoundTrip(t, "v4-reuse-after-tail") + require.Equal(t, int32(1), proxy.count.Load()) + oversizedTailScenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", oversizedTailEchoPort)} + oversizedTailScenario.CloseBeforeServerEOF(t, "v4-reuse-oversized-tail", 500*time.Millisecond) + scenario.RoundTrip(t, "v4-reuse-after-oversized-tail") + require.Equal(t, int32(2), proxy.count.Load()) +} + +func TestV4TCPReuseClientCloseCancelsDrain(t *testing.T) { + port := freePort(t) + config := fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nipv6 = false\n", port, testPSK) + startSnellServer(t, "v4", config, port) + var proxy countingTCPProxy + proxy.Start(t, fmt.Sprintf("127.0.0.1:%d", port)) + delayedEchoPort := startTCPHalfCloseEcho(t, 750*time.Millisecond) + + client, err := snellv4.NewClient(snellv4.ClientOptions{ + PSK: []byte(testPSK), + Reuse: true, + Dialer: N.SystemDialer, + Server: M.ParseSocksaddr(proxy.address), + }) + require.NoError(t, err) + + scenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", delayedEchoPort)} + scenario.CloseBeforeServerEOF(t, "v4-reuse-close-cancel", 0) + closeStart := time.Now() + require.NoError(t, client.Close()) + if time.Since(closeStart) > 250*time.Millisecond { + t.Fatal("client close waited for draining EOF instead of canceling the session") + } +} + +func TestV4TCPReuseClientCloseLetsActiveReadFinish(t *testing.T) { + port := freePort(t) + config := fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nipv6 = false\n", port, testPSK) + startSnellServer(t, "v4", config, port) + var proxy countingTCPProxy + proxy.Start(t, fmt.Sprintf("127.0.0.1:%d", port)) + delayedEchoPort := startTCPHalfCloseEcho(t, 750*time.Millisecond) + echoPort := startTCPHalfCloseEcho(t) + + client, err := snellv4.NewClient(snellv4.ClientOptions{ + PSK: []byte(testPSK), + Reuse: true, + Dialer: N.SystemDialer, + Server: M.ParseSocksaddr(proxy.address), + }) + require.NoError(t, err) + defer client.Close() + + reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", delayedEchoPort)}.CloseWhileReadBlocked(t, "v4-reuse-active-read-close") + reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", echoPort)}.RoundTrip(t, "v4-reuse-after-active-read-close") + require.Equal(t, int32(1), proxy.count.Load()) +} + +func TestV4UDP(t *testing.T) { + port := freePort(t) + config := fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nipv6 = false\n", port, testPSK) + startSnellServer(t, "v4", config, port) + echoPort := startUDPEcho(t) + + client, err := snellv4.NewClient(snellv4.ClientOptions{PSK: []byte(testPSK)}) + require.NoError(t, err) + serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + require.NoError(t, err) + defer serverConn.Close() + + packetConn, err := client.DialPacketConn(serverConn) + require.NoError(t, err) + target := M.ParseSocksaddrHostPort("127.0.0.1", echoPort).UDPAddr() + + for round := range 10 { + message := make([]byte, 1200) + rand.Read(message) + _, err = packetConn.WriteTo(message, target) + require.NoError(t, err) + + received := make([]byte, 2048) + serverConn.SetReadDeadline(time.Now().Add(10 * time.Second)) + n, _, readErr := packetConn.ReadFrom(received) + require.NoError(t, readErr) + require.True(t, bytes.Equal(message, received[:n]), "round %d", round) + } +} + +func TestV4TCPVectorisedWriter(t *testing.T) { + port := freePort(t) + config := fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nipv6 = false\n", port, testPSK) + startSnellServer(t, "v4", config, port) + echoPort := startTCPEcho(t) + + client, err := snellv4.NewClient(snellv4.ClientOptions{PSK: []byte(testPSK)}) + require.NoError(t, err) + serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + require.NoError(t, err) + defer serverConn.Close() + + proxyConn := client.DialEarlyConn(serverConn, M.ParseSocksaddrHostPort("127.0.0.1", echoPort)) + vectorisedWriter, created := bufio.CreateVectorisedWriter(proxyConn) + require.True(t, created) + + partA := make([]byte, 700) + partB := make([]byte, 900) + _, err = rand.Read(partA) + require.NoError(t, err) + _, err = rand.Read(partB) + require.NoError(t, err) + expected := append(append([]byte(nil), partA...), partB...) + + frontHeadroom := N.CalculateFrontHeadroom(proxyConn) + rearHeadroom := N.CalculateRearHeadroom(proxyConn) + buffers := make([]*buf.Buffer, 2) + for index, payload := range [][]byte{partA, partB} { + buffer := buf.NewSize(frontHeadroom + len(payload) + rearHeadroom) + buffer.Resize(frontHeadroom, 0) + _, err = buffer.Write(payload) + require.NoError(t, err) + buffers[index] = buffer + } + err = vectorisedWriter.WriteVectorised(buffers) + require.NoError(t, err) + + received := make([]byte, len(expected)) + require.NoError(t, proxyConn.SetReadDeadline(time.Now().Add(10*time.Second))) + _, err = io.ReadFull(proxyConn, received) + require.NoError(t, err) + require.Equal(t, expected, received) +} + +func TestV4UDPPacketBatchWriter(t *testing.T) { + for _, testCase := range []struct { + name string + version string + }{ + {name: "official-v4", version: "v4"}, + {name: "official-v5", version: "v5"}, + } { + t.Run(testCase.name, func(t *testing.T) { + port := freePort(t) + switch testCase.version { + case "v4": + config := fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nipv6 = false\n", port, testPSK) + startSnellServer(t, "v4", config, port) + case "v5": + startSnellServer(t, "v5", v5Config(testPSK, port), port) + default: + t.Fatal("unknown server version") + } + echoPort := startUDPEcho(t) + + client, err := snellv4.NewClient(snellv4.ClientOptions{PSK: []byte(testPSK)}) + require.NoError(t, err) + serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + require.NoError(t, err) + defer serverConn.Close() + + packetConn, err := client.DialPacketConn(serverConn) + require.NoError(t, err) + batchWriter, created := bufio.CreatePacketBatchWriter(packetConn) + require.True(t, created) + + target := M.ParseSocksaddrHostPort("127.0.0.1", echoPort) + frontHeadroom := N.CalculateFrontHeadroom(packetConn) + rearHeadroom := N.CalculateRearHeadroom(packetConn) + payloads := make([][]byte, 3) + buffers := make([]*buf.Buffer, len(payloads)) + destinations := make([]M.Socksaddr, len(payloads)) + for index := range payloads { + payload := make([]byte, 1200) + _, err = rand.Read(payload) + require.NoError(t, err) + payload[0] = byte(index) + payloads[index] = payload + buffer := buf.NewSize(frontHeadroom + len(payload) + rearHeadroom) + buffer.Resize(frontHeadroom, 0) + _, err = buffer.Write(payload) + require.NoError(t, err) + buffers[index] = buffer + destinations[index] = target + } + err = batchWriter.WritePacketBatch(buffers, destinations) + require.NoError(t, err) + + received := make(map[string]int, len(payloads)) + for range len(payloads) { + reply := make([]byte, 2048) + err = packetConn.SetReadDeadline(time.Now().Add(10 * time.Second)) + require.NoError(t, err) + n, _, readErr := packetConn.ReadFrom(reply) + require.NoError(t, readErr) + received[string(reply[:n])]++ + } + for _, payload := range payloads { + require.Equal(t, 1, received[string(payload)]) + } + }) + } +} + +func TestV4ObfsHTTP(t *testing.T) { + t.Run("tcp", func(t *testing.T) { + port := freePort(t) + config := fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nobfs = http\nipv6 = false\n", port, testPSK) + startSnellServer(t, "v4", config, port) + echoPort := startTCPEcho(t) + + client, err := snellv4.NewClient(snellv4.ClientOptions{ + PSK: []byte(testPSK), + ObfsMode: snell.ObfsModeHTTP, + ObfsHost: "obfs.example", + }) + require.NoError(t, err) + serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + require.NoError(t, err) + defer serverConn.Close() + + proxyConn, err := client.DialConn(serverConn, M.ParseSocksaddrHostPort("127.0.0.1", echoPort)) + require.NoError(t, err) + + payload := make([]byte, 256*1024) + rand.Read(payload) + go func() { + proxyConn.Write(payload) + }() + received := make([]byte, len(payload)) + proxyConn.SetReadDeadline(time.Now().Add(20 * time.Second)) + _, err = io.ReadFull(proxyConn, received) + require.NoError(t, err) + require.True(t, bytes.Equal(payload, received), "payload mismatch") + normalScenario{address: fmt.Sprintf("127.0.0.1:%d", port), client: client}.HalfCloseEcho(t, "v4-official-obfs") + }) + + t.Run("reuse", func(t *testing.T) { + port := freePort(t) + config := fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nobfs = http\nipv6 = false\n", port, testPSK) + startSnellServer(t, "v4", config, port) + var proxy countingTCPProxy + proxy.Start(t, fmt.Sprintf("127.0.0.1:%d", port)) + echoPort := startTCPHalfCloseEcho(t) + + client, err := snellv4.NewClient(snellv4.ClientOptions{ + PSK: []byte(testPSK), + Reuse: true, + ObfsMode: snell.ObfsModeHTTP, + ObfsHost: "obfs.example", + Dialer: N.SystemDialer, + Server: M.ParseSocksaddr(proxy.address), + }) + require.NoError(t, err) + defer client.Close() + + scenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", echoPort)} + scenario.RoundTrip(t, "v4-obfs-reuse-1") + scenario.RoundTrip(t, "v4-obfs-reuse-2") + require.Equal(t, int32(1), proxy.count.Load()) + }) + + t.Run("udp", func(t *testing.T) { + port := freePort(t) + config := fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nobfs = http\nipv6 = false\n", port, testPSK) + startSnellServer(t, "v4", config, port) + echoPort := startUDPEcho(t) + + client, err := snellv4.NewClient(snellv4.ClientOptions{ + PSK: []byte(testPSK), + ObfsMode: snell.ObfsModeHTTP, + ObfsHost: "obfs.example", + }) + require.NoError(t, err) + serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + require.NoError(t, err) + defer serverConn.Close() + + packetConn, err := client.DialPacketConn(serverConn) + require.NoError(t, err) + target := M.ParseSocksaddrHostPort("127.0.0.1", echoPort).UDPAddr() + + for round := range 5 { + message := make([]byte, 1200) + rand.Read(message) + _, err = packetConn.WriteTo(message, target) + require.NoError(t, err) + + received := make([]byte, 2048) + serverConn.SetReadDeadline(time.Now().Add(10 * time.Second)) + n, _, readErr := packetConn.ReadFrom(received) + require.NoError(t, readErr) + require.True(t, bytes.Equal(message, received[:n]), "round %d", round) + } + }) +} diff --git a/proxy/snell/sing-snell/test/v5_test.go b/proxy/snell/sing-snell/test/v5_test.go new file mode 100644 index 00000000000..33c2e63b01d --- /dev/null +++ b/proxy/snell/sing-snell/test/v5_test.go @@ -0,0 +1,610 @@ +package test + +import ( + "bytes" + "crypto/cipher" + "crypto/rand" + "encoding/binary" + "errors" + "fmt" + "io" + "net" + "testing" + "time" + + snell "github.com/sagernet/sing-snell" + "github.com/sagernet/sing-snell/snellv4" + "github.com/sagernet/sing-snell/snellv5" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "github.com/stretchr/testify/require" +) + +const testPSK = "snell-interop-test-psk-0123456789" + +var errV5RawZeroChunk = errors.New("snell v5 raw zero chunk") + +func v5Config(psk string, port uint16) string { + return fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nipv6 = false\n", port, psk) +} + +func TestV5FirstResponseRecordShape(t *testing.T) { + for _, testCase := range []struct { + name string + official bool + }{ + {name: "official-v5.0.1", official: true}, + {name: "local-v5"}, + } { + t.Run(testCase.name, func(subTest *testing.T) { + var serverAddress string + var targetPort uint16 + if testCase.official { + port := freePort(subTest) + startSnellServer(subTest, "v5", v5Config(testPSK, port), port) + serverAddress = fmt.Sprintf("127.0.0.1:%d", port) + targetPort = startTCPEcho(subTest) + } else { + service, err := snellv5.NewService(snellv5.ServiceOptions{ + PSK: []byte(testPSK), + Handler: localEchoHandler{}, + }) + require.NoError(subTest, err) + serverAddress = startLocalSnellService(subTest, service) + targetPort = 443 + } + + serverConn, err := net.Dial("tcp", serverAddress) + require.NoError(subTest, err) + defer serverConn.Close() + + earlyPayload := []byte("v5 first response record shape") + host := "127.0.0.1" + requestPlain := []byte{snell.RequestVersion, snell.CommandConnectV2, 0, byte(len(host))} + requestPlain = append(requestPlain, host...) + requestPlain = binary.BigEndian.AppendUint16(requestPlain, targetPort) + requestPlain = append(requestPlain, earlyPayload...) + + clientSalt := make([]byte, snell.SaltLen) + _, err = rand.Read(clientSalt) + require.NoError(subTest, err) + clientAEAD, err := snell.NewAEAD(snell.DeriveKey([]byte(testPSK), clientSalt)) + require.NoError(subTest, err) + clientNonce := make([]byte, snell.NonceLen) + requestHeader := make([]byte, snell.HeaderPlainLen) + requestHeader[0] = snell.HeaderVersion + binary.BigEndian.PutUint16(requestHeader[5:7], uint16(len(requestPlain))) + clientWire := make([]byte, 0, snell.SaltLen+snell.HeaderCipherLen+len(requestPlain)+snell.AEADTagLen+snell.HeaderCipherLen) + clientWire = append(clientWire, clientSalt...) + clientWire = append(clientWire, clientAEAD.Seal(nil, clientNonce, requestHeader, nil)...) + snell.IncreaseNonce(clientNonce) + clientWire = append(clientWire, clientAEAD.Seal(nil, clientNonce, requestPlain, nil)...) + snell.IncreaseNonce(clientNonce) + clear(requestHeader) + requestHeader[0] = snell.HeaderVersion + clientWire = append(clientWire, clientAEAD.Seal(nil, clientNonce, requestHeader, nil)...) + + _, err = serverConn.Write(clientWire) + require.NoError(subTest, err) + err = serverConn.SetReadDeadline(time.Now().Add(10 * time.Second)) + require.NoError(subTest, err) + + responseSalt := make([]byte, snell.SaltLen) + _, err = io.ReadFull(serverConn, responseSalt) + require.NoError(subTest, err) + responseAEAD, err := snell.NewAEAD(snell.DeriveKey([]byte(testPSK), responseSalt)) + require.NoError(subTest, err) + responseNonce := make([]byte, snell.NonceLen) + responseHeaderCipher := make([]byte, snell.HeaderCipherLen) + _, err = io.ReadFull(serverConn, responseHeaderCipher) + require.NoError(subTest, err) + responseHeader, err := responseAEAD.Open(nil, responseNonce, responseHeaderCipher, nil) + require.NoError(subTest, err) + snell.IncreaseNonce(responseNonce) + require.Len(subTest, responseHeader, snell.HeaderPlainLen) + require.Equal(subTest, byte(snell.HeaderVersion), responseHeader[0]) + require.Equal(subTest, byte(0), responseHeader[1]) + require.Equal(subTest, byte(0), responseHeader[2]) + // snell-server v5.0.1: FUN_00139670 subtracts first-record padding + // from the first payload limit, and FUN_00138af0 serializes and mixes it. + responsePaddingLen := int(binary.BigEndian.Uint16(responseHeader[3:5])) + require.GreaterOrEqual(subTest, responsePaddingLen, 0x100) + require.LessOrEqual(subTest, responsePaddingLen, 0x1ff) + responsePayloadLen := int(binary.BigEndian.Uint16(responseHeader[5:7])) + require.Equal(subTest, 1+len(earlyPayload), responsePayloadLen) + responsePadding := make([]byte, responsePaddingLen) + _, err = io.ReadFull(serverConn, responsePadding) + require.NoError(subTest, err) + responsePayloadCipher := make([]byte, responsePayloadLen+snell.AEADTagLen) + _, err = io.ReadFull(serverConn, responsePayloadCipher) + require.NoError(subTest, err) + mixLimit := min(len(responsePadding), len(responsePayloadCipher)) + for index := 0; index < mixLimit; index += 2 { + responsePadding[index], responsePayloadCipher[index] = responsePayloadCipher[index], responsePadding[index] + } + responsePayload, err := responseAEAD.Open(nil, responseNonce, responsePayloadCipher, nil) + require.NoError(subTest, err) + expectedPayload := append([]byte{snell.ReplyTunnel}, earlyPayload...) + require.Equal(subTest, expectedPayload, responsePayload) + }) + } +} + +func TestV5TCP(t *testing.T) { + port := freePort(t) + startSnellServer(t, "v5", v5Config(testPSK, port), port) + echoPort := startTCPEcho(t) + + client, err := snellv4.NewClient(snellv4.ClientOptions{PSK: []byte(testPSK)}) + require.NoError(t, err) + serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + require.NoError(t, err) + defer serverConn.Close() + + proxyConn, err := client.DialConn(serverConn, M.ParseSocksaddrHostPort("127.0.0.1", echoPort)) + require.NoError(t, err) + + payload := make([]byte, 1024*1024) + rand.Read(payload) + go func() { + proxyConn.Write(payload) + }() + received := make([]byte, len(payload)) + proxyConn.SetReadDeadline(time.Now().Add(20 * time.Second)) + _, err = io.ReadFull(proxyConn, received) + require.NoError(t, err) + require.True(t, bytes.Equal(payload, received), "payload mismatch") + normalScenario{address: fmt.Sprintf("127.0.0.1:%d", port), client: client}.HalfCloseEcho(t, "v5-official") +} + +func TestV5TCPVectorisedWriter(t *testing.T) { + port := freePort(t) + startSnellServer(t, "v5", v5Config(testPSK, port), port) + echoPort := startTCPEcho(t) + + client, err := snellv4.NewClient(snellv4.ClientOptions{PSK: []byte(testPSK)}) + require.NoError(t, err) + serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + require.NoError(t, err) + defer serverConn.Close() + + proxyConn := client.DialEarlyConn(serverConn, M.ParseSocksaddrHostPort("127.0.0.1", echoPort)) + vectorisedWriter, created := bufio.CreateVectorisedWriter(proxyConn) + require.True(t, created) + + partA := make([]byte, 700) + partB := make([]byte, 900) + _, err = rand.Read(partA) + require.NoError(t, err) + _, err = rand.Read(partB) + require.NoError(t, err) + expected := append(append([]byte(nil), partA...), partB...) + + frontHeadroom := N.CalculateFrontHeadroom(proxyConn) + rearHeadroom := N.CalculateRearHeadroom(proxyConn) + buffers := make([]*buf.Buffer, 2) + for index, payload := range [][]byte{partA, partB} { + buffer := buf.NewSize(frontHeadroom + len(payload) + rearHeadroom) + buffer.Resize(frontHeadroom, 0) + _, err = buffer.Write(payload) + require.NoError(t, err) + buffers[index] = buffer + } + err = vectorisedWriter.WriteVectorised(buffers) + require.NoError(t, err) + + received := make([]byte, len(expected)) + require.NoError(t, proxyConn.SetReadDeadline(time.Now().Add(10*time.Second))) + _, err = io.ReadFull(proxyConn, received) + require.NoError(t, err) + require.Equal(t, expected, received) +} + +func TestV5TCPReuse(t *testing.T) { + port := freePort(t) + startSnellServer(t, "v5", v5Config(testPSK, port), port) + var proxy countingTCPProxy + proxy.Start(t, fmt.Sprintf("127.0.0.1:%d", port)) + echoPort := startTCPHalfCloseEcho(t) + delayedEchoPort := startTCPHalfCloseEcho(t, 750*time.Millisecond) + tailEchoPort := startTCPHalfCloseTailEcho(t, 32*1024, 250*time.Millisecond) + oversizedTailEchoPort := startTCPHalfCloseTailEcho(t, 0x80001, 250*time.Millisecond) + + client, err := snellv4.NewClient(snellv4.ClientOptions{ + PSK: []byte(testPSK), + Reuse: true, + Dialer: N.SystemDialer, + Server: M.ParseSocksaddr(proxy.address), + }) + require.NoError(t, err) + defer client.Close() + + scenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", echoPort)} + scenario.RoundTrip(t, "v5-reuse-1") + scenario.RoundTrip(t, "v5-reuse-2") + delayedScenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", delayedEchoPort)} + delayedScenario.CloseBeforeServerEOF(t, "v5-reuse-drain", time.Second) + scenario.RoundTrip(t, "v5-reuse-after-drain") + tailScenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", tailEchoPort)} + tailScenario.CloseBeforeServerEOF(t, "v5-reuse-tail", 500*time.Millisecond) + scenario.RoundTrip(t, "v5-reuse-after-tail") + require.Equal(t, int32(1), proxy.count.Load()) + oversizedTailScenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", oversizedTailEchoPort)} + oversizedTailScenario.CloseBeforeServerEOF(t, "v5-reuse-oversized-tail", 500*time.Millisecond) + scenario.RoundTrip(t, "v5-reuse-after-oversized-tail") + require.Equal(t, int32(2), proxy.count.Load()) +} + +func TestV5TCPReuseClientCloseLetsActiveReadFinish(t *testing.T) { + port := freePort(t) + startSnellServer(t, "v5", v5Config(testPSK, port), port) + var proxy countingTCPProxy + proxy.Start(t, fmt.Sprintf("127.0.0.1:%d", port)) + delayedEchoPort := startTCPHalfCloseEcho(t, 750*time.Millisecond) + echoPort := startTCPHalfCloseEcho(t) + + client, err := snellv4.NewClient(snellv4.ClientOptions{ + PSK: []byte(testPSK), + Reuse: true, + Dialer: N.SystemDialer, + Server: M.ParseSocksaddr(proxy.address), + }) + require.NoError(t, err) + defer client.Close() + + reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", delayedEchoPort)}.CloseWhileReadBlocked(t, "v5-reuse-active-read-close") + reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", echoPort)}.RoundTrip(t, "v5-reuse-after-active-read-close") + require.Equal(t, int32(1), proxy.count.Load()) +} + +func TestV5ObfsHTTP(t *testing.T) { + t.Run("tcp", func(t *testing.T) { + port := freePort(t) + startSnellServer(t, "v5", v5Config(testPSK, port)+"obfs = http\n", port) + echoPort := startTCPEcho(t) + + client, err := snellv4.NewClient(snellv4.ClientOptions{ + PSK: []byte(testPSK), + ObfsMode: snell.ObfsModeHTTP, + ObfsHost: "obfs.example", + }) + require.NoError(t, err) + serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + require.NoError(t, err) + defer serverConn.Close() + + proxyConn, err := client.DialConn(serverConn, M.ParseSocksaddrHostPort("127.0.0.1", echoPort)) + require.NoError(t, err) + + payload := make([]byte, 256*1024) + rand.Read(payload) + go func() { + proxyConn.Write(payload) + }() + received := make([]byte, len(payload)) + proxyConn.SetReadDeadline(time.Now().Add(20 * time.Second)) + _, err = io.ReadFull(proxyConn, received) + require.NoError(t, err) + require.True(t, bytes.Equal(payload, received), "payload mismatch") + normalScenario{address: fmt.Sprintf("127.0.0.1:%d", port), client: client}.HalfCloseEcho(t, "v5-official-obfs") + }) + + t.Run("reuse", func(t *testing.T) { + port := freePort(t) + startSnellServer(t, "v5", v5Config(testPSK, port)+"obfs = http\n", port) + var proxy countingTCPProxy + proxy.Start(t, fmt.Sprintf("127.0.0.1:%d", port)) + echoPort := startTCPHalfCloseEcho(t) + + client, err := snellv4.NewClient(snellv4.ClientOptions{ + PSK: []byte(testPSK), + Reuse: true, + ObfsMode: snell.ObfsModeHTTP, + ObfsHost: "obfs.example", + Dialer: N.SystemDialer, + Server: M.ParseSocksaddr(proxy.address), + }) + require.NoError(t, err) + defer client.Close() + + scenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", echoPort)} + scenario.RoundTrip(t, "v5-obfs-reuse-1") + scenario.RoundTrip(t, "v5-obfs-reuse-2") + require.Equal(t, int32(1), proxy.count.Load()) + }) +} + +func TestV5UDP(t *testing.T) { + port := freePort(t) + startSnellServer(t, "v5", v5Config(testPSK, port), port) + echoPort := startUDPEcho(t) + + client, err := snellv4.NewClient(snellv4.ClientOptions{PSK: []byte(testPSK)}) + require.NoError(t, err) + target := M.ParseSocksaddrHostPort("127.0.0.1", echoPort).UDPAddr() + + message := make([]byte, 1200) + rand.Read(message) + serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + require.NoError(t, err) + defer serverConn.Close() + packetConn, err := client.DialPacketConn(serverConn) + require.NoError(t, err) + _, err = packetConn.WriteTo(message, target) + require.NoError(t, err) + + received := make([]byte, 2048) + serverConn.SetReadDeadline(time.Now().Add(2 * time.Second)) + n, _, err := packetConn.ReadFrom(received) + require.NoError(t, err) + require.True(t, bytes.Equal(message, received[:n])) +} + +func TestV5PingReplyClosesConnection(t *testing.T) { + for _, testCase := range []struct { + name string + run func(*testing.T) string + }{ + { + name: "official-v5.0.1", + run: func(t *testing.T) string { + port := freePort(t) + startSnellServer(t, "v5", v5Config(testPSK, port), port) + return fmt.Sprintf("127.0.0.1:%d", port) + }, + }, + { + name: "local-v5", + run: func(t *testing.T) string { + service, err := snellv5.NewService(snellv5.ServiceOptions{ + PSK: []byte(testPSK), + Handler: localEchoHandler{}, + }) + require.NoError(t, err) + return startLocalSnellService(t, service) + }, + }, + { + name: "local-v5-multi-without-user", + run: func(t *testing.T) string { + service, err := snellv5.NewMultiService[int](snellv5.ServiceOptions{ + PSK: []byte(testPSK), + Handler: localEchoHandler{}, + }) + require.NoError(t, err) + return startLocalSnellService(t, service) + }, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + serverConn, err := net.Dial("tcp", testCase.run(t)) + require.NoError(t, err) + defer serverConn.Close() + + rawConn := &v5RawConn{Conn: serverConn, psk: []byte(testPSK)} + salt := make([]byte, snell.SaltLen) + _, err = rand.Read(salt) + require.NoError(t, err) + rawConn.WriteFirstRecord(t, salt, []byte{snell.RequestVersion, snell.CommandPing, 1, 'x'}) + require.NoError(t, serverConn.SetReadDeadline(time.Now().Add(10*time.Second))) + reply, err := rawConn.ReadFirstRecord(t) + require.NoError(t, err) + require.Equal(t, []byte{snell.ReplyPong}, reply) + + _, err = rawConn.ReadRecord(t) + require.Error(t, err) + require.False(t, errors.Is(err, errV5RawZeroChunk), "ping closes the connection instead of writing a Snell zero chunk") + }) + } +} + +func TestV5LegacyCommandConnectClosesWithoutZeroChunkAfterReply(t *testing.T) { + for _, testCase := range []struct { + name string + run func(*testing.T) (string, uint16) + }{ + { + name: "official-v5.0.1", + run: func(t *testing.T) (string, uint16) { + port := freePort(t) + startSnellServer(t, "v5", v5Config(testPSK, port), port) + return fmt.Sprintf("127.0.0.1:%d", port), startTCPHalfCloseEcho(t) + }, + }, + { + name: "local-v5", + run: func(t *testing.T) (string, uint16) { + service, err := snellv5.NewService(snellv5.ServiceOptions{ + PSK: []byte(testPSK), + Handler: localEchoHandler{}, + }) + require.NoError(t, err) + return startLocalSnellService(t, service), 443 + }, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + serverAddress, targetPort := testCase.run(t) + serverConn, err := net.Dial("tcp", serverAddress) + require.NoError(t, err) + defer serverConn.Close() + + payload := []byte("v5 legacy command connect") + host := "127.0.0.1" + request := []byte{snell.RequestVersion, snell.CommandConnect, 0, byte(len(host))} + request = append(request, host...) + request = binary.BigEndian.AppendUint16(request, targetPort) + request = append(request, payload...) + + rawConn := &v5RawConn{Conn: serverConn, psk: []byte(testPSK)} + salt := make([]byte, snell.SaltLen) + _, err = rand.Read(salt) + require.NoError(t, err) + rawConn.WriteFirstRecord(t, salt, request) + rawConn.WriteRecord(t, nil) + require.NoError(t, serverConn.SetReadDeadline(time.Now().Add(10*time.Second))) + reply, err := rawConn.ReadFirstRecord(t) + require.NoError(t, err) + require.Equal(t, append([]byte{snell.ReplyTunnel}, payload...), reply) + + _, err = rawConn.ReadRecord(t) + require.Error(t, err) + require.False(t, errors.Is(err, errV5RawZeroChunk), "legacy command-1 closes the TCP connection instead of writing a Snell zero chunk") + }) + } +} + +func TestV5SaltReplayRejectsDuplicate(t *testing.T) { + for _, testCase := range []struct { + name string + run func(*testing.T) (string, uint16) + }{ + { + name: "official-v5.0.1", + run: func(t *testing.T) (string, uint16) { + port := freePort(t) + startSnellServer(t, "v5", v5Config(testPSK, port), port) + return fmt.Sprintf("127.0.0.1:%d", port), startTCPHalfCloseEcho(t) + }, + }, + { + name: "local-v5", + run: func(t *testing.T) (string, uint16) { + service, err := snellv5.NewService(snellv5.ServiceOptions{ + PSK: []byte(testPSK), + Handler: localEchoHandler{}, + }) + require.NoError(t, err) + return startLocalSnellService(t, service), 443 + }, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + serverAddress, targetPort := testCase.run(t) + payload := []byte("v5 duplicated salt") + host := "127.0.0.1" + request := []byte{snell.RequestVersion, snell.CommandConnectV2, 0, byte(len(host))} + request = append(request, host...) + request = binary.BigEndian.AppendUint16(request, targetPort) + request = append(request, payload...) + salt := make([]byte, snell.SaltLen) + _, err := rand.Read(salt) + require.NoError(t, err) + + firstServerConn, err := net.Dial("tcp", serverAddress) + require.NoError(t, err) + firstRawConn := &v5RawConn{Conn: firstServerConn, psk: []byte(testPSK)} + firstRawConn.WriteFirstRecord(t, salt, request) + firstRawConn.WriteRecord(t, nil) + require.NoError(t, firstServerConn.SetReadDeadline(time.Now().Add(10*time.Second))) + reply, err := firstRawConn.ReadFirstRecord(t) + require.NoError(t, err) + require.Equal(t, append([]byte{snell.ReplyTunnel}, payload...), reply) + firstServerConn.Close() + + secondServerConn, err := net.Dial("tcp", serverAddress) + require.NoError(t, err) + defer secondServerConn.Close() + secondRawConn := &v5RawConn{Conn: secondServerConn, psk: []byte(testPSK)} + secondRawConn.WriteFirstRecord(t, salt, request) + secondRawConn.WriteRecord(t, nil) + require.NoError(t, secondServerConn.SetReadDeadline(time.Now().Add(10*time.Second))) + _, err = secondRawConn.ReadFirstRecord(t) + require.Error(t, err) + }) + } +} + +type v5RawConn struct { + net.Conn + psk []byte + requestAEAD cipher.AEAD + requestNonce []byte + responseAEAD cipher.AEAD + responseNonce []byte +} + +func (c *v5RawConn) WriteFirstRecord(t *testing.T, salt []byte, payload []byte) { + requestAEAD, err := snell.NewAEAD(snell.DeriveKey(c.psk, salt)) + require.NoError(t, err) + c.requestAEAD = requestAEAD + c.requestNonce = make([]byte, snell.NonceLen) + record := c.SealRecord(t, payload) + wire := make([]byte, 0, len(salt)+len(record)) + wire = append(wire, salt...) + wire = append(wire, record...) + _, err = c.Conn.Write(wire) + require.NoError(t, err) +} + +func (c *v5RawConn) WriteRecord(t *testing.T, payload []byte) { + _, err := c.Conn.Write(c.SealRecord(t, payload)) + require.NoError(t, err) +} + +func (c *v5RawConn) SealRecord(t *testing.T, payload []byte) []byte { + header := make([]byte, snell.HeaderPlainLen) + header[0] = snell.HeaderVersion + binary.BigEndian.PutUint16(header[5:7], uint16(len(payload))) + record := c.requestAEAD.Seal(nil, c.requestNonce, header, nil) + snell.IncreaseNonce(c.requestNonce) + if len(payload) > 0 { + record = append(record, c.requestAEAD.Seal(nil, c.requestNonce, payload, nil)...) + snell.IncreaseNonce(c.requestNonce) + } + return record +} + +func (c *v5RawConn) ReadFirstRecord(t *testing.T) ([]byte, error) { + salt := make([]byte, snell.SaltLen) + _, err := io.ReadFull(c.Conn, salt) + if err != nil { + return nil, err + } + responseAEAD, err := snell.NewAEAD(snell.DeriveKey(c.psk, salt)) + require.NoError(t, err) + c.responseAEAD = responseAEAD + c.responseNonce = make([]byte, snell.NonceLen) + return c.ReadRecord(t) +} + +func (c *v5RawConn) ReadRecord(t *testing.T) ([]byte, error) { + headerCipher := make([]byte, snell.HeaderCipherLen) + _, err := io.ReadFull(c.Conn, headerCipher) + if err != nil { + return nil, err + } + header, err := c.responseAEAD.Open(nil, c.responseNonce, headerCipher, nil) + require.NoError(t, err) + snell.IncreaseNonce(c.responseNonce) + require.Len(t, header, snell.HeaderPlainLen) + require.Equal(t, byte(snell.HeaderVersion), header[0]) + paddingLen := int(binary.BigEndian.Uint16(header[3:5])) + payloadLen := int(binary.BigEndian.Uint16(header[5:7])) + if payloadLen == 0 { + return nil, errV5RawZeroChunk + } + padding := make([]byte, paddingLen) + if paddingLen > 0 { + _, err = io.ReadFull(c.Conn, padding) + if err != nil { + return nil, err + } + } + payloadCipher := make([]byte, payloadLen+snell.AEADTagLen) + _, err = io.ReadFull(c.Conn, payloadCipher) + if err != nil { + return nil, err + } + mixLimit := min(len(padding), len(payloadCipher)) + for index := 0; index < mixLimit; index += 2 { + padding[index], payloadCipher[index] = payloadCipher[index], padding[index] + } + payload, err := c.responseAEAD.Open(nil, c.responseNonce, payloadCipher, nil) + require.NoError(t, err) + snell.IncreaseNonce(c.responseNonce) + return payload, nil +} diff --git a/proxy/snell/sing-snell/test/v6_test.go b/proxy/snell/sing-snell/test/v6_test.go new file mode 100644 index 00000000000..7d6ee0d9a4a --- /dev/null +++ b/proxy/snell/sing-snell/test/v6_test.go @@ -0,0 +1,415 @@ +package test + +import ( + "bytes" + "crypto/rand" + "fmt" + "io" + "net" + "testing" + "time" + + snell "github.com/sagernet/sing-snell" + "github.com/sagernet/sing-snell/snellv6" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "github.com/stretchr/testify/require" +) + +func v6Config(psk string, port uint16, mode string) string { + return fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nmode = %s\n", port, psk, mode) +} + +var v6Modes = map[string]snellv6.Mode{ + "unshaped": snellv6.ModeUnshaped, + "unsafe-raw": snellv6.ModeUnsafeRaw, + "default": snellv6.ModeDefault, +} + +func TestV6TCP(t *testing.T) { + for name, mode := range v6Modes { + t.Run(name, func(t *testing.T) { + port := freePort(t) + startSnellServer(t, "v6", v6Config(testPSK, port, name), port) + echoPort := startTCPEcho(t) + + client, err := snellv6.NewClient(snellv6.ClientOptions{ + PSK: []byte(testPSK), + Mode: mode, + }) + require.NoError(t, err) + serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + require.NoError(t, err) + defer serverConn.Close() + + proxyConn, err := client.DialConn(serverConn, M.ParseSocksaddrHostPort("127.0.0.1", echoPort)) + require.NoError(t, err) + + payload := make([]byte, 512*1024) + rand.Read(payload) + go func() { + proxyConn.Write(payload) + }() + received := make([]byte, len(payload)) + proxyConn.SetReadDeadline(time.Now().Add(20 * time.Second)) + _, err = io.ReadFull(proxyConn, received) + require.NoError(t, err) + require.True(t, bytes.Equal(payload, received), "payload mismatch") + normalScenario{address: fmt.Sprintf("127.0.0.1:%d", port), client: client}.HalfCloseEcho(t, "v6-official-"+name) + }) + } +} + +func TestV6TCPVectorisedWriter(t *testing.T) { + for name, mode := range v6Modes { + t.Run(name, func(t *testing.T) { + port := freePort(t) + startSnellServer(t, "v6", v6Config(testPSK, port, name), port) + echoPort := startTCPEcho(t) + + client, err := snellv6.NewClient(snellv6.ClientOptions{ + PSK: []byte(testPSK), + Mode: mode, + }) + require.NoError(t, err) + serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + require.NoError(t, err) + defer serverConn.Close() + + proxyConn := client.DialEarlyConn(serverConn, M.ParseSocksaddrHostPort("127.0.0.1", echoPort)) + vectorisedWriter, created := bufio.CreateVectorisedWriter(proxyConn) + require.True(t, created) + + partA := make([]byte, 700) + partB := make([]byte, 900) + _, err = rand.Read(partA) + require.NoError(t, err) + _, err = rand.Read(partB) + require.NoError(t, err) + expected := append(append([]byte(nil), partA...), partB...) + + frontHeadroom := N.CalculateFrontHeadroom(proxyConn) + rearHeadroom := N.CalculateRearHeadroom(proxyConn) + buffers := make([]*buf.Buffer, 2) + for index, payload := range [][]byte{partA, partB} { + buffer := buf.NewSize(frontHeadroom + len(payload) + rearHeadroom) + buffer.Resize(frontHeadroom, 0) + _, err = buffer.Write(payload) + require.NoError(t, err) + buffers[index] = buffer + } + err = vectorisedWriter.WriteVectorised(buffers) + require.NoError(t, err) + + received := make([]byte, len(expected)) + require.NoError(t, proxyConn.SetReadDeadline(time.Now().Add(10*time.Second))) + _, err = io.ReadFull(proxyConn, received) + require.NoError(t, err) + require.Equal(t, expected, received) + }) + } +} + +func TestV6TCPUnshapedLargeWriteBufferFirstRecord(t *testing.T) { + port := freePort(t) + startSnellServer(t, "v6", v6Config(testPSK, port, "unshaped"), port) + echoPort := startTCPEcho(t) + + client, err := snellv6.NewClient(snellv6.ClientOptions{ + PSK: []byte(testPSK), + Mode: snellv6.ModeUnshaped, + }) + require.NoError(t, err) + serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + require.NoError(t, err) + defer serverConn.Close() + + destination := M.ParseSocksaddrHostPort("127.0.0.1", echoPort) + proxyConn := client.DialEarlyConn(serverConn, destination) + defer proxyConn.Close() + + payload := make([]byte, 80*1024) + _, err = rand.Read(payload) + require.NoError(t, err) + request := snell.Request{Command: snell.CommandConnect, Destination: destination} + buffer := buf.NewSize(request.Len() + len(payload)) + buffer.Resize(request.Len(), 0) + n, err := buffer.Write(payload) + require.NoError(t, err) + require.Equal(t, len(payload), n) + + extendedWriter, ok := proxyConn.(N.ExtendedWriter) + require.True(t, ok) + err = extendedWriter.WriteBuffer(buffer) + require.NoError(t, err) + + received := make([]byte, len(payload)) + require.NoError(t, proxyConn.SetReadDeadline(time.Now().Add(20*time.Second))) + _, err = io.ReadFull(proxyConn, received) + require.NoError(t, err) + require.True(t, bytes.Equal(payload, received), "payload mismatch") +} + +func TestV6TCPReuse(t *testing.T) { + for name, mode := range v6Modes { + t.Run(name, func(t *testing.T) { + port := freePort(t) + startSnellServer(t, "v6", v6Config(testPSK, port, name), port) + var proxy countingTCPProxy + proxy.Start(t, fmt.Sprintf("127.0.0.1:%d", port)) + echoPort := startTCPHalfCloseEcho(t) + delayedEchoPort := startTCPHalfCloseEcho(t, 750*time.Millisecond) + tailEchoPort := startTCPHalfCloseTailEcho(t, 32*1024, 250*time.Millisecond) + oversizedTailEchoPort := startTCPHalfCloseTailEcho(t, 0x80001, 250*time.Millisecond) + + client, err := snellv6.NewClient(snellv6.ClientOptions{ + PSK: []byte(testPSK), + Mode: mode, + Reuse: true, + Dialer: N.SystemDialer, + Server: M.ParseSocksaddr(proxy.address), + }) + require.NoError(t, err) + defer client.Close() + + scenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", echoPort)} + scenario.RoundTrip(t, "v6-reuse-1-"+name) + scenario.RoundTrip(t, "v6-reuse-2-"+name) + delayedScenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", delayedEchoPort)} + delayedScenario.CloseBeforeServerEOF(t, "v6-reuse-drain-"+name, time.Second) + scenario.RoundTrip(t, "v6-reuse-after-drain-"+name) + tailScenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", tailEchoPort)} + tailScenario.CloseBeforeServerEOF(t, "v6-reuse-tail-"+name, 500*time.Millisecond) + scenario.RoundTrip(t, "v6-reuse-after-tail-"+name) + require.Equal(t, int32(1), proxy.count.Load()) + oversizedTailScenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", oversizedTailEchoPort)} + oversizedTailScenario.CloseBeforeServerEOF(t, "v6-reuse-oversized-tail-"+name, 500*time.Millisecond) + scenario.RoundTrip(t, "v6-reuse-after-oversized-tail-"+name) + require.Equal(t, int32(2), proxy.count.Load()) + }) + } +} + +func TestV6TCPReuseClientCloseCancelsDrain(t *testing.T) { + for name, mode := range v6Modes { + t.Run(name, func(t *testing.T) { + port := freePort(t) + startSnellServer(t, "v6", v6Config(testPSK, port, name), port) + var proxy countingTCPProxy + proxy.Start(t, fmt.Sprintf("127.0.0.1:%d", port)) + delayedEchoPort := startTCPHalfCloseEcho(t, 750*time.Millisecond) + + client, err := snellv6.NewClient(snellv6.ClientOptions{ + PSK: []byte(testPSK), + Mode: mode, + Reuse: true, + Dialer: N.SystemDialer, + Server: M.ParseSocksaddr(proxy.address), + }) + require.NoError(t, err) + + scenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", delayedEchoPort)} + scenario.CloseBeforeServerEOF(t, "v6-reuse-close-cancel-"+name, 0) + closeStart := time.Now() + require.NoError(t, client.Close()) + if time.Since(closeStart) > 250*time.Millisecond { + t.Fatal("client close waited for draining EOF instead of canceling the session") + } + }) + } +} + +func TestV6TCPReuseClientCloseLetsActiveReadFinish(t *testing.T) { + for name, mode := range v6Modes { + t.Run(name, func(t *testing.T) { + port := freePort(t) + startSnellServer(t, "v6", v6Config(testPSK, port, name), port) + var proxy countingTCPProxy + proxy.Start(t, fmt.Sprintf("127.0.0.1:%d", port)) + delayedEchoPort := startTCPHalfCloseEcho(t, 750*time.Millisecond) + echoPort := startTCPHalfCloseEcho(t) + + client, err := snellv6.NewClient(snellv6.ClientOptions{ + PSK: []byte(testPSK), + Mode: mode, + Reuse: true, + Dialer: N.SystemDialer, + Server: M.ParseSocksaddr(proxy.address), + }) + require.NoError(t, err) + defer client.Close() + + reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", delayedEchoPort)}.CloseWhileReadBlocked(t, "v6-reuse-active-read-close-"+name) + reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", echoPort)}.RoundTrip(t, "v6-reuse-after-active-read-close-"+name) + require.Equal(t, int32(1), proxy.count.Load()) + }) + } +} + +func TestV6ServerAcceptsV6ClientLoopback(t *testing.T) { + for name, mode := range v6Modes { + t.Run(name, func(t *testing.T) { + service, err := snellv6.NewService(snellv6.ServerOptions{ + PSK: []byte(testPSK), + Mode: mode, + Handler: localEchoHandler{}, + }) + require.NoError(t, err) + serviceAddress := startLocalSnellService(t, service) + var proxy countingTCPProxy + proxy.Start(t, serviceAddress) + + client, err := snellv6.NewClient(snellv6.ClientOptions{ + PSK: []byte(testPSK), + Mode: mode, + Reuse: true, + Dialer: N.SystemDialer, + Server: M.ParseSocksaddr(proxy.address), + }) + require.NoError(t, err) + defer client.Close() + + scenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", 443)} + scenario.RoundTrip(t, "v6-service-v6-client-reuse-1-"+name) + scenario.RoundTrip(t, "v6-service-v6-client-reuse-2-"+name) + require.Equal(t, int32(1), proxy.count.Load()) + }) + } +} + +func TestV6ServerReuseHandlerCloseBeforeClientEOF(t *testing.T) { + for name, mode := range v6Modes { + t.Run(name, func(t *testing.T) { + service, err := snellv6.NewService(snellv6.ServerOptions{ + PSK: []byte(testPSK), + Mode: mode, + Handler: partialEchoHandler{echoLen: 4 * 1024}, + }) + require.NoError(t, err) + serviceAddress := startLocalSnellService(t, service) + var proxy countingTCPProxy + proxy.Start(t, serviceAddress) + + client, err := snellv6.NewClient(snellv6.ClientOptions{ + PSK: []byte(testPSK), + Mode: mode, + Reuse: true, + Dialer: N.SystemDialer, + Server: M.ParseSocksaddr(proxy.address), + }) + require.NoError(t, err) + defer client.Close() + + scenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", 443)} + scenario.PartialEchoRoundTrip(t, "v6-server-drain-1-"+name, 8*1024, 4*1024) + scenario.PartialEchoRoundTrip(t, "v6-server-drain-2-"+name, 8*1024, 4*1024) + require.Equal(t, int32(1), proxy.count.Load()) + }) + } +} + +func TestV6UDP(t *testing.T) { + for name, mode := range v6Modes { + t.Run(name, func(t *testing.T) { + port := freePort(t) + startSnellServer(t, "v6", v6Config(testPSK, port, name), port) + echoPort := startUDPEcho(t) + + client, err := snellv6.NewClient(snellv6.ClientOptions{ + PSK: []byte(testPSK), + Mode: mode, + }) + require.NoError(t, err) + serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + require.NoError(t, err) + defer serverConn.Close() + + packetConn, err := client.DialPacketConn(serverConn) + require.NoError(t, err) + target := M.ParseSocksaddrHostPort("127.0.0.1", echoPort).UDPAddr() + + for round := range 10 { + messageLen := 1200 + if mode == snellv6.ModeDefault { + messageLen = 32 + } + message := make([]byte, messageLen) + rand.Read(message) + _, err = packetConn.WriteTo(message, target) + require.NoError(t, err) + + received := make([]byte, 2048) + serverConn.SetReadDeadline(time.Now().Add(10 * time.Second)) + n, _, readErr := packetConn.ReadFrom(received) + require.NoError(t, readErr) + require.True(t, bytes.Equal(message, received[:n]), "round %d", round) + } + }) + } +} + +func TestV6UDPPacketBatchWriter(t *testing.T) { + for name, mode := range v6Modes { + t.Run(name, func(t *testing.T) { + port := freePort(t) + startSnellServer(t, "v6", v6Config(testPSK, port, name), port) + echoPort := startUDPEcho(t) + + client, err := snellv6.NewClient(snellv6.ClientOptions{ + PSK: []byte(testPSK), + Mode: mode, + }) + require.NoError(t, err) + serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + require.NoError(t, err) + defer serverConn.Close() + + packetConn, err := client.DialPacketConn(serverConn) + require.NoError(t, err) + batchWriter, created := bufio.CreatePacketBatchWriter(packetConn) + require.True(t, created) + + target := M.ParseSocksaddrHostPort("127.0.0.1", echoPort) + frontHeadroom := N.CalculateFrontHeadroom(packetConn) + rearHeadroom := N.CalculateRearHeadroom(packetConn) + payloadSize := 1200 + if mode == snellv6.ModeDefault { + payloadSize = 32 + } + payloads := make([][]byte, 3) + buffers := make([]*buf.Buffer, len(payloads)) + destinations := make([]M.Socksaddr, len(payloads)) + for index := range payloads { + payload := make([]byte, payloadSize) + _, err = rand.Read(payload) + require.NoError(t, err) + payload[0] = byte(index) + payloads[index] = payload + buffer := buf.NewSize(frontHeadroom + len(payload) + rearHeadroom) + buffer.Resize(frontHeadroom, 0) + _, err = buffer.Write(payload) + require.NoError(t, err) + buffers[index] = buffer + destinations[index] = target + } + err = batchWriter.WritePacketBatch(buffers, destinations) + require.NoError(t, err) + + received := make(map[string]int, len(payloads)) + for range len(payloads) { + reply := make([]byte, 2048) + err = packetConn.SetReadDeadline(time.Now().Add(10 * time.Second)) + require.NoError(t, err) + n, _, readErr := packetConn.ReadFrom(reply) + require.NoError(t, readErr) + received[string(reply[:n])]++ + } + for _, payload := range payloads { + require.Equal(t, 1, received[string(payload)]) + } + }) + } +} From 5a7ef3dd8489eb2f25826125734958f1539e4f20 Mon Sep 17 00:00:00 2001 From: Minis <285644962+Minis233@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:18:35 +0800 Subject: [PATCH 05/15] chore(snell): drop sing v0.8.12 pins from vendored go.sum --- proxy/snell/sing-snell/go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/proxy/snell/sing-snell/go.sum b/proxy/snell/sing-snell/go.sum index fcd58aedf99..fdfa1de822c 100644 --- a/proxy/snell/sing-snell/go.sum +++ b/proxy/snell/sing-snell/go.sum @@ -2,8 +2,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d h1:BhsQU0Iug1tU4xR52cjm8Sc+LBo+KwdyLTRn3ie9moo= -github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= From a70e7b1d99f810c9c581d49af1084ecf61e86551 Mon Sep 17 00:00:00 2001 From: Minis <285644962+Minis233@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:23:16 +0800 Subject: [PATCH 06/15] fix(snell): inline sing-snell and align with library APIs Import SagerNet/sing-snell sources into proxy/snell/internal/singsnell (same module, no nested go.mod) and rewrite the outbound to match the current ClientOptions / DialPacketConn APIs used by sing-box testing. Supports Snell v4 (and 3/5 via v4 client) and official Snell v6. --- go.mod | 3 - proxy/snell/NOTICE | 5 +- .../singsnell}/address.go | 0 .../singsnell}/crypto.go | 0 .../singsnell}/internal/reuse/reuse.go | 2 +- .../singsnell}/obfs.go | 0 .../singsnell}/request.go | 0 .../singsnell}/snell.go | 0 .../singsnell}/snellv4/client.go | 4 +- .../singsnell}/snellv4/packet.go | 4 +- .../singsnell}/snellv4/record.go | 2 +- .../singsnell}/snellv4/reuse.go | 4 +- .../singsnell}/snellv5/packet.go | 4 +- .../singsnell}/snellv5/record.go | 2 +- .../singsnell}/snellv5/reuse.go | 4 +- .../singsnell}/snellv5/server.go | 2 +- .../singsnell}/snellv6/client.go | 4 +- .../singsnell}/snellv6/handshake.go | 4 +- .../singsnell}/snellv6/mixing.go | 0 .../singsnell}/snellv6/mode.go | 0 .../singsnell}/snellv6/packet.go | 4 +- .../singsnell}/snellv6/profile.go | 2 +- .../singsnell}/snellv6/record.go | 2 +- .../singsnell}/snellv6/reuse.go | 4 +- .../singsnell}/snellv6/salt.go | 0 .../singsnell}/snellv6/server.go | 4 +- .../singsnell}/snellv6/shaped.go | 2 +- proxy/snell/outbound.go | 73 ++- .../sing-snell/.github/update_dependencies.sh | 5 - .../sing-snell/.github/workflows/lint.yml | 30 - .../sing-snell/.github/workflows/test.yml | 34 - proxy/snell/sing-snell/.gitignore | 6 - proxy/snell/sing-snell/LICENSE | 14 - proxy/snell/sing-snell/Makefile | 18 - proxy/snell/sing-snell/README.md | 17 - proxy/snell/sing-snell/go.mod | 10 - proxy/snell/sing-snell/go.sum | 12 - proxy/snell/sing-snell/test/docker_test.go | 210 ------ proxy/snell/sing-snell/test/go.mod | 46 -- proxy/snell/sing-snell/test/go.sum | 112 ---- proxy/snell/sing-snell/test/helper_test.go | 53 -- proxy/snell/sing-snell/test/loopback_test.go | 44 -- .../sing-snell/test/reuse_helpers_test.go | 297 --------- .../sing-snell/test/tls_obfs_loopback_test.go | 240 ------- proxy/snell/sing-snell/test/v4_test.go | 369 ----------- proxy/snell/sing-snell/test/v5_test.go | 610 ------------------ proxy/snell/sing-snell/test/v6_test.go | 415 ------------ 47 files changed, 68 insertions(+), 2609 deletions(-) rename proxy/snell/{sing-snell => internal/singsnell}/address.go (100%) rename proxy/snell/{sing-snell => internal/singsnell}/crypto.go (100%) rename proxy/snell/{sing-snell => internal/singsnell}/internal/reuse/reuse.go (98%) rename proxy/snell/{sing-snell => internal/singsnell}/obfs.go (100%) rename proxy/snell/{sing-snell => internal/singsnell}/request.go (100%) rename proxy/snell/{sing-snell => internal/singsnell}/snell.go (100%) rename proxy/snell/{sing-snell => internal/singsnell}/snellv4/client.go (97%) rename proxy/snell/{sing-snell => internal/singsnell}/snellv4/packet.go (97%) rename proxy/snell/{sing-snell => internal/singsnell}/snellv4/record.go (99%) rename proxy/snell/{sing-snell => internal/singsnell}/snellv4/reuse.go (98%) rename proxy/snell/{sing-snell => internal/singsnell}/snellv5/packet.go (97%) rename proxy/snell/{sing-snell => internal/singsnell}/snellv5/record.go (99%) rename proxy/snell/{sing-snell => internal/singsnell}/snellv5/reuse.go (98%) rename proxy/snell/{sing-snell => internal/singsnell}/snellv5/server.go (99%) rename proxy/snell/{sing-snell => internal/singsnell}/snellv6/client.go (98%) rename proxy/snell/{sing-snell => internal/singsnell}/snellv6/handshake.go (97%) rename proxy/snell/{sing-snell => internal/singsnell}/snellv6/mixing.go (100%) rename proxy/snell/{sing-snell => internal/singsnell}/snellv6/mode.go (100%) rename proxy/snell/{sing-snell => internal/singsnell}/snellv6/packet.go (98%) rename proxy/snell/{sing-snell => internal/singsnell}/snellv6/profile.go (99%) rename proxy/snell/{sing-snell => internal/singsnell}/snellv6/record.go (99%) rename proxy/snell/{sing-snell => internal/singsnell}/snellv6/reuse.go (99%) rename proxy/snell/{sing-snell => internal/singsnell}/snellv6/salt.go (100%) rename proxy/snell/{sing-snell => internal/singsnell}/snellv6/server.go (98%) rename proxy/snell/{sing-snell => internal/singsnell}/snellv6/shaped.go (99%) delete mode 100644 proxy/snell/sing-snell/.github/update_dependencies.sh delete mode 100644 proxy/snell/sing-snell/.github/workflows/lint.yml delete mode 100644 proxy/snell/sing-snell/.github/workflows/test.yml delete mode 100644 proxy/snell/sing-snell/.gitignore delete mode 100644 proxy/snell/sing-snell/LICENSE delete mode 100644 proxy/snell/sing-snell/Makefile delete mode 100644 proxy/snell/sing-snell/README.md delete mode 100644 proxy/snell/sing-snell/go.mod delete mode 100644 proxy/snell/sing-snell/go.sum delete mode 100644 proxy/snell/sing-snell/test/docker_test.go delete mode 100644 proxy/snell/sing-snell/test/go.mod delete mode 100644 proxy/snell/sing-snell/test/go.sum delete mode 100644 proxy/snell/sing-snell/test/helper_test.go delete mode 100644 proxy/snell/sing-snell/test/loopback_test.go delete mode 100644 proxy/snell/sing-snell/test/reuse_helpers_test.go delete mode 100644 proxy/snell/sing-snell/test/tls_obfs_loopback_test.go delete mode 100644 proxy/snell/sing-snell/test/v4_test.go delete mode 100644 proxy/snell/sing-snell/test/v5_test.go delete mode 100644 proxy/snell/sing-snell/test/v6_test.go diff --git a/go.mod b/go.mod index e65b3060abe..21adde4fe29 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,6 @@ require ( github.com/sagernet/sing-quic v0.6.3 github.com/sagernet/sing-shadowsocks v0.2.9 github.com/sagernet/sing-shadowsocks2 v0.2.2 - github.com/sagernet/sing-snell v0.0.0 github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 github.com/stretchr/testify v1.11.1 github.com/v2fly/BrowserBridge v0.0.0-20210430233438-0570fc1d7d08 @@ -72,5 +71,3 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -replace github.com/sagernet/sing-snell => ./proxy/snell/sing-snell diff --git a/proxy/snell/NOTICE b/proxy/snell/NOTICE index 6d68a8b697e..263c604515d 100644 --- a/proxy/snell/NOTICE +++ b/proxy/snell/NOTICE @@ -1,3 +1,2 @@ -Snell outbound vendors https://github.com/SagerNet/sing-snell (GPL-3.0) -with sing dependency pinned to v0.8.11 for exclave-core compatibility. -Supports Snell v3–v6. +Snell protocol implementation inlined from https://github.com/SagerNet/sing-snell (GPL-3.0) +Supports Snell v4 (via snellv4) and v6 (via snellv6), matching sing-box testing branch. diff --git a/proxy/snell/sing-snell/address.go b/proxy/snell/internal/singsnell/address.go similarity index 100% rename from proxy/snell/sing-snell/address.go rename to proxy/snell/internal/singsnell/address.go diff --git a/proxy/snell/sing-snell/crypto.go b/proxy/snell/internal/singsnell/crypto.go similarity index 100% rename from proxy/snell/sing-snell/crypto.go rename to proxy/snell/internal/singsnell/crypto.go diff --git a/proxy/snell/sing-snell/internal/reuse/reuse.go b/proxy/snell/internal/singsnell/internal/reuse/reuse.go similarity index 98% rename from proxy/snell/sing-snell/internal/reuse/reuse.go rename to proxy/snell/internal/singsnell/internal/reuse/reuse.go index 28c783cf3fd..c5e209138f9 100644 --- a/proxy/snell/sing-snell/internal/reuse/reuse.go +++ b/proxy/snell/internal/singsnell/internal/reuse/reuse.go @@ -6,7 +6,7 @@ import ( "sync/atomic" "time" - snell "github.com/sagernet/sing-snell" + snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" diff --git a/proxy/snell/sing-snell/obfs.go b/proxy/snell/internal/singsnell/obfs.go similarity index 100% rename from proxy/snell/sing-snell/obfs.go rename to proxy/snell/internal/singsnell/obfs.go diff --git a/proxy/snell/sing-snell/request.go b/proxy/snell/internal/singsnell/request.go similarity index 100% rename from proxy/snell/sing-snell/request.go rename to proxy/snell/internal/singsnell/request.go diff --git a/proxy/snell/sing-snell/snell.go b/proxy/snell/internal/singsnell/snell.go similarity index 100% rename from proxy/snell/sing-snell/snell.go rename to proxy/snell/internal/singsnell/snell.go diff --git a/proxy/snell/sing-snell/snellv4/client.go b/proxy/snell/internal/singsnell/snellv4/client.go similarity index 97% rename from proxy/snell/sing-snell/snellv4/client.go rename to proxy/snell/internal/singsnell/snellv4/client.go index 961a4d59da0..e384affdf6a 100644 --- a/proxy/snell/sing-snell/snellv4/client.go +++ b/proxy/snell/internal/singsnell/snellv4/client.go @@ -4,8 +4,8 @@ import ( "net" "sync" - snell "github.com/sagernet/sing-snell" - "github.com/sagernet/sing-snell/internal/reuse" + snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" + "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" diff --git a/proxy/snell/sing-snell/snellv4/packet.go b/proxy/snell/internal/singsnell/snellv4/packet.go similarity index 97% rename from proxy/snell/sing-snell/snellv4/packet.go rename to proxy/snell/internal/singsnell/snellv4/packet.go index 28c717e7c66..41d9821945b 100644 --- a/proxy/snell/sing-snell/snellv4/packet.go +++ b/proxy/snell/internal/singsnell/snellv4/packet.go @@ -6,8 +6,8 @@ import ( "os" "sync" - snell "github.com/sagernet/sing-snell" - "github.com/sagernet/sing-snell/internal/reuse" + snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" + "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" diff --git a/proxy/snell/sing-snell/snellv4/record.go b/proxy/snell/internal/singsnell/snellv4/record.go similarity index 99% rename from proxy/snell/sing-snell/snellv4/record.go rename to proxy/snell/internal/singsnell/snellv4/record.go index 018003656d6..4717cee050a 100644 --- a/proxy/snell/sing-snell/snellv4/record.go +++ b/proxy/snell/internal/singsnell/snellv4/record.go @@ -11,7 +11,7 @@ import ( "sync" "time" - snell "github.com/sagernet/sing-snell" + snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" diff --git a/proxy/snell/sing-snell/snellv4/reuse.go b/proxy/snell/internal/singsnell/snellv4/reuse.go similarity index 98% rename from proxy/snell/sing-snell/snellv4/reuse.go rename to proxy/snell/internal/singsnell/snellv4/reuse.go index 8d780a70842..c166b254014 100644 --- a/proxy/snell/sing-snell/snellv4/reuse.go +++ b/proxy/snell/internal/singsnell/snellv4/reuse.go @@ -9,8 +9,8 @@ import ( "sync/atomic" "time" - snell "github.com/sagernet/sing-snell" - "github.com/sagernet/sing-snell/internal/reuse" + snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" + "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" diff --git a/proxy/snell/sing-snell/snellv5/packet.go b/proxy/snell/internal/singsnell/snellv5/packet.go similarity index 97% rename from proxy/snell/sing-snell/snellv5/packet.go rename to proxy/snell/internal/singsnell/snellv5/packet.go index 4bcc1d18563..10828cbde5b 100644 --- a/proxy/snell/sing-snell/snellv5/packet.go +++ b/proxy/snell/internal/singsnell/snellv5/packet.go @@ -9,8 +9,8 @@ import ( "os" "sync" - snell "github.com/sagernet/sing-snell" - "github.com/sagernet/sing-snell/internal/reuse" + snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" + "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" diff --git a/proxy/snell/sing-snell/snellv5/record.go b/proxy/snell/internal/singsnell/snellv5/record.go similarity index 99% rename from proxy/snell/sing-snell/snellv5/record.go rename to proxy/snell/internal/singsnell/snellv5/record.go index 251c2e5f13e..3a106ba5476 100644 --- a/proxy/snell/sing-snell/snellv5/record.go +++ b/proxy/snell/internal/singsnell/snellv5/record.go @@ -10,7 +10,7 @@ import ( "sync" "time" - snell "github.com/sagernet/sing-snell" + snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" diff --git a/proxy/snell/sing-snell/snellv5/reuse.go b/proxy/snell/internal/singsnell/snellv5/reuse.go similarity index 98% rename from proxy/snell/sing-snell/snellv5/reuse.go rename to proxy/snell/internal/singsnell/snellv5/reuse.go index 21709588bee..97ab8df324a 100644 --- a/proxy/snell/sing-snell/snellv5/reuse.go +++ b/proxy/snell/internal/singsnell/snellv5/reuse.go @@ -9,8 +9,8 @@ import ( "sync" "sync/atomic" - snell "github.com/sagernet/sing-snell" - "github.com/sagernet/sing-snell/internal/reuse" + snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" + "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" diff --git a/proxy/snell/sing-snell/snellv5/server.go b/proxy/snell/internal/singsnell/snellv5/server.go similarity index 99% rename from proxy/snell/sing-snell/snellv5/server.go rename to proxy/snell/internal/singsnell/snellv5/server.go index 3455262d05c..81a3605ce60 100644 --- a/proxy/snell/sing-snell/snellv5/server.go +++ b/proxy/snell/internal/singsnell/snellv5/server.go @@ -9,7 +9,7 @@ import ( "net" "sync" - snell "github.com/sagernet/sing-snell" + snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" "github.com/sagernet/sing/common/buf" diff --git a/proxy/snell/sing-snell/snellv6/client.go b/proxy/snell/internal/singsnell/snellv6/client.go similarity index 98% rename from proxy/snell/sing-snell/snellv6/client.go rename to proxy/snell/internal/singsnell/snellv6/client.go index bc306b562d9..589fa077f14 100644 --- a/proxy/snell/sing-snell/snellv6/client.go +++ b/proxy/snell/internal/singsnell/snellv6/client.go @@ -4,8 +4,8 @@ import ( "net" "sync" - snell "github.com/sagernet/sing-snell" - "github.com/sagernet/sing-snell/internal/reuse" + snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" + "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" diff --git a/proxy/snell/sing-snell/snellv6/handshake.go b/proxy/snell/internal/singsnell/snellv6/handshake.go similarity index 97% rename from proxy/snell/sing-snell/snellv6/handshake.go rename to proxy/snell/internal/singsnell/snellv6/handshake.go index 75fa1810f6c..cf24b31b45c 100644 --- a/proxy/snell/sing-snell/snellv6/handshake.go +++ b/proxy/snell/internal/singsnell/snellv6/handshake.go @@ -5,8 +5,8 @@ import ( "io" "time" - snell "github.com/sagernet/sing-snell" - "github.com/sagernet/sing-snell/internal/reuse" + snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" + "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" N "github.com/sagernet/sing/common/network" diff --git a/proxy/snell/sing-snell/snellv6/mixing.go b/proxy/snell/internal/singsnell/snellv6/mixing.go similarity index 100% rename from proxy/snell/sing-snell/snellv6/mixing.go rename to proxy/snell/internal/singsnell/snellv6/mixing.go diff --git a/proxy/snell/sing-snell/snellv6/mode.go b/proxy/snell/internal/singsnell/snellv6/mode.go similarity index 100% rename from proxy/snell/sing-snell/snellv6/mode.go rename to proxy/snell/internal/singsnell/snellv6/mode.go diff --git a/proxy/snell/sing-snell/snellv6/packet.go b/proxy/snell/internal/singsnell/snellv6/packet.go similarity index 98% rename from proxy/snell/sing-snell/snellv6/packet.go rename to proxy/snell/internal/singsnell/snellv6/packet.go index 179241d4286..28c0e422e6e 100644 --- a/proxy/snell/sing-snell/snellv6/packet.go +++ b/proxy/snell/internal/singsnell/snellv6/packet.go @@ -6,8 +6,8 @@ import ( "os" "sync" - snell "github.com/sagernet/sing-snell" - "github.com/sagernet/sing-snell/internal/reuse" + snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" + "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" diff --git a/proxy/snell/sing-snell/snellv6/profile.go b/proxy/snell/internal/singsnell/snellv6/profile.go similarity index 99% rename from proxy/snell/sing-snell/snellv6/profile.go rename to proxy/snell/internal/singsnell/snellv6/profile.go index 752b33f133e..32953422fc1 100644 --- a/proxy/snell/sing-snell/snellv6/profile.go +++ b/proxy/snell/internal/singsnell/snellv6/profile.go @@ -4,7 +4,7 @@ import ( "encoding/binary" "math/bits" - snell "github.com/sagernet/sing-snell" + snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" "golang.org/x/crypto/blake2b" ) diff --git a/proxy/snell/sing-snell/snellv6/record.go b/proxy/snell/internal/singsnell/snellv6/record.go similarity index 99% rename from proxy/snell/sing-snell/snellv6/record.go rename to proxy/snell/internal/singsnell/snellv6/record.go index 0b444811849..042420ce66a 100644 --- a/proxy/snell/sing-snell/snellv6/record.go +++ b/proxy/snell/internal/singsnell/snellv6/record.go @@ -6,7 +6,7 @@ import ( "io" "sync" - snell "github.com/sagernet/sing-snell" + snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" diff --git a/proxy/snell/sing-snell/snellv6/reuse.go b/proxy/snell/internal/singsnell/snellv6/reuse.go similarity index 99% rename from proxy/snell/sing-snell/snellv6/reuse.go rename to proxy/snell/internal/singsnell/snellv6/reuse.go index 1bd32949006..9cce732fcef 100644 --- a/proxy/snell/sing-snell/snellv6/reuse.go +++ b/proxy/snell/internal/singsnell/snellv6/reuse.go @@ -9,8 +9,8 @@ import ( "sync/atomic" "time" - snell "github.com/sagernet/sing-snell" - "github.com/sagernet/sing-snell/internal/reuse" + snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" + "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" diff --git a/proxy/snell/sing-snell/snellv6/salt.go b/proxy/snell/internal/singsnell/snellv6/salt.go similarity index 100% rename from proxy/snell/sing-snell/snellv6/salt.go rename to proxy/snell/internal/singsnell/snellv6/salt.go diff --git a/proxy/snell/sing-snell/snellv6/server.go b/proxy/snell/internal/singsnell/snellv6/server.go similarity index 98% rename from proxy/snell/sing-snell/snellv6/server.go rename to proxy/snell/internal/singsnell/snellv6/server.go index 4fedc1eee2d..e2990787e6d 100644 --- a/proxy/snell/sing-snell/snellv6/server.go +++ b/proxy/snell/internal/singsnell/snellv6/server.go @@ -6,8 +6,8 @@ import ( "net" "sync" - snell "github.com/sagernet/sing-snell" - "github.com/sagernet/sing-snell/internal/reuse" + snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" + "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" "github.com/sagernet/sing/common/auth" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" diff --git a/proxy/snell/sing-snell/snellv6/shaped.go b/proxy/snell/internal/singsnell/snellv6/shaped.go similarity index 99% rename from proxy/snell/sing-snell/snellv6/shaped.go rename to proxy/snell/internal/singsnell/snellv6/shaped.go index 57c9eb007fe..10b73a6a721 100644 --- a/proxy/snell/sing-snell/snellv6/shaped.go +++ b/proxy/snell/internal/singsnell/snellv6/shaped.go @@ -7,7 +7,7 @@ import ( "sync" "time" - snell "github.com/sagernet/sing-snell" + snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" diff --git a/proxy/snell/outbound.go b/proxy/snell/outbound.go index 310af2d4187..f3f3fa4e830 100644 --- a/proxy/snell/outbound.go +++ b/proxy/snell/outbound.go @@ -6,9 +6,9 @@ import ( "strings" "sync" - "github.com/sagernet/sing-snell" - "github.com/sagernet/sing-snell/snellv4" - "github.com/sagernet/sing-snell/snellv6" + snellproto "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" + "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/snellv4" + "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/snellv6" "github.com/sagernet/sing/common/bufio" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" @@ -32,17 +32,19 @@ func init() { })) } -// snellClient is the subset used by the outbound for v4 and v6 clients. +// snellClient matches SagerNet sing-snell client surface used by sing-box. type snellClient interface { DialContext(ctx context.Context, destination M.Socksaddr) (net.Conn, error) - ListenPacket(ctx context.Context) (N.NetPacketConn, error) + DialPacketConn(conn net.Conn) (N.NetPacketConn, error) Close() error } -// Outbound implements Snell v3–v6 client outbound (via SagerNet sing-snell). +// Outbound implements Snell v4/v6 client outbound (SagerNet sing-snell). +// Versions 3/5 map onto the v4 client path where supported by the library. type Outbound struct { serverAddr v2net.Destination psk string + userKey string obfs string obfsHost string version int @@ -63,6 +65,8 @@ func NewClient(ctx context.Context, config *ClientConfig) (*Outbound, error) { if version == 0 { version = 4 } + // sing-snell currently exposes dedicated clients for v4 and v6. + // Accept 3/4/5 → v4 client, 6 → v6 client. if version < 3 || version > 6 { return nil, newError("unsupported snell version ", version, " (want 3-6)") } @@ -109,49 +113,44 @@ func (o *Outbound) getClient(dialer internet.Dialer) (snellClient, error) { return nil, newError("tls/security enabled on streamSettings; snell uses built-in crypto (use obfs=tls for fake TLS)") } - obfsMode, err := snell.ParseObfsMode(o.obfs) - if err != nil { - return nil, err - } - obfsHost := o.obfsHost - if obfsHost == "" { - switch obfsMode { - case snell.ObfsModeTLS: - obfsHost = snell.DefaultTLSObfsHost - default: - obfsHost = snell.DefaultObfsHost - } - } - obfsCfg := snell.ObfsConfig{Mode: obfsMode, Host: obfsHost} sdialer := singbridge.NewDialerWrapper(dialer) server := M.ParseSocksaddr(o.serverAddr.NetAddr()) psk := []byte(o.psk) + userKey := []byte(o.userKey) - var client snellClient + var ( + client snellClient + err error + ) if o.version == 6 { mode, err := snellv6.ParseMode(o.mode) if err != nil { return nil, err } client, err = snellv6.NewClient(snellv6.ClientOptions{ - Dialer: sdialer, - Server: server, - PSK: psk, - Obfs: obfsCfg, - Mode: mode, - Reuse: o.reuse, + PSK: psk, + UserKey: userKey, + Mode: mode, + Reuse: o.reuse, + Dialer: sdialer, + Server: server, }) if err != nil { return nil, err } } else { + obfsMode, err := snellproto.ParseObfsMode(o.obfs) + if err != nil { + return nil, err + } client, err = snellv4.NewClient(snellv4.ClientOptions{ - Dialer: sdialer, - Server: server, - PSK: psk, - Version: o.version, - Obfs: obfsCfg, - Reuse: o.reuse, + PSK: psk, + UserKey: userKey, + Reuse: o.reuse, + ObfsMode: obfsMode, + ObfsHost: o.obfsHost, + Dialer: sdialer, + Server: server, }) if err != nil { return nil, err @@ -207,8 +206,14 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int return singbridge.ReturnError(bufio.CopyConn(detachedCtx, singbridge.NewPipeConnWrapper(link), serverConn)) } - pc, err := client.ListenPacket(detachedCtx) + // UDP-over-TCP: dial raw TCP to snell server, then DialPacketConn (sing-box pattern). + raw, err := dialer.Dial(detachedCtx, o.serverAddr) + if err != nil { + return err + } + pc, err := client.DialPacketConn(raw) if err != nil { + _ = raw.Close() return err } return singbridge.ReturnError(bufio.CopyPacketConn( diff --git a/proxy/snell/sing-snell/.github/update_dependencies.sh b/proxy/snell/sing-snell/.github/update_dependencies.sh deleted file mode 100644 index 4702ddfe018..00000000000 --- a/proxy/snell/sing-snell/.github/update_dependencies.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -PROJECTS=$(dirname "$0")/../.. -go get -x github.com/sagernet/$1@$(git -C $PROJECTS/$1 rev-parse HEAD) -go mod tidy diff --git a/proxy/snell/sing-snell/.github/workflows/lint.yml b/proxy/snell/sing-snell/.github/workflows/lint.yml deleted file mode 100644 index 87b6f104cf7..00000000000 --- a/proxy/snell/sing-snell/.github/workflows/lint.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: lint - -on: - pull_request: - push: - workflow_dispatch: - -permissions: - contents: read - -jobs: - lint: - name: lint - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Checkout - uses: actions/checkout@v5 - - - name: Setup Go - uses: actions/setup-go@v6 - with: - go-version: '1.26' - cache: true - - - name: Install golangci-lint - run: make lint_install - - - name: Run golangci-lint - run: make lint diff --git a/proxy/snell/sing-snell/.github/workflows/test.yml b/proxy/snell/sing-snell/.github/workflows/test.yml deleted file mode 100644 index 4396d2593d2..00000000000 --- a/proxy/snell/sing-snell/.github/workflows/test.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: test - -on: - pull_request: - push: - workflow_dispatch: - -permissions: - contents: read - -jobs: - test: - name: test - runs-on: ubuntu-latest - timeout-minutes: 10 - strategy: - fail-fast: false - matrix: - go-version: - - '~1.26' - - '~1.25' - - steps: - - name: Checkout - uses: actions/checkout@v5 - - - name: Setup Go - uses: actions/setup-go@v6 - with: - go-version: ${{ matrix.go-version }} - cache: true - - - name: Run unit tests - run: make test \ No newline at end of file diff --git a/proxy/snell/sing-snell/.gitignore b/proxy/snell/sing-snell/.gitignore deleted file mode 100644 index 89444706f85..00000000000 --- a/proxy/snell/sing-snell/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -/.idea/ -/vendor/ -.DS_Store -/CLAUDE.md -/.claude/ -/testenv/ diff --git a/proxy/snell/sing-snell/LICENSE b/proxy/snell/sing-snell/LICENSE deleted file mode 100644 index 3e3e29e359d..00000000000 --- a/proxy/snell/sing-snell/LICENSE +++ /dev/null @@ -1,14 +0,0 @@ -Copyright (C) 2022 by nekohasekai - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . \ No newline at end of file diff --git a/proxy/snell/sing-snell/Makefile b/proxy/snell/sing-snell/Makefile deleted file mode 100644 index fe99adeab1b..00000000000 --- a/proxy/snell/sing-snell/Makefile +++ /dev/null @@ -1,18 +0,0 @@ -.PHONY: fmt lint lint_install test - -fmt: - @golangci-lint fmt - -lint: - GOOS=linux golangci-lint run ./... - GOOS=android golangci-lint run ./... - GOOS=windows golangci-lint run ./... - GOOS=darwin golangci-lint run ./... -# GOOS=freebsd golangci-lint run ./... - -lint_install: - go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest - -test: - go test ./... - go -C test test -v -count=1 -timeout 15m ./... diff --git a/proxy/snell/sing-snell/README.md b/proxy/snell/sing-snell/README.md deleted file mode 100644 index 76f3efcdff7..00000000000 --- a/proxy/snell/sing-snell/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# sing-snell - -[Surge snell](https://kb.nssurge.com/surge-knowledge-base/release-notes/snell) implemented in Go. - -## Features - -* All complete features except the v5 QUIC proxy. -* Behavior as consistent with the official implementation as possible. -* Performance at least on par with the official implementation. - -## Why - -Surge believes that being closed-source and not proliferated can keep this protocol -covert, but this is already impossible in 2026. Considering that snell still has -advantages that other random-traffic protocols do not possess, such as multiplexing -support with complete TCP semantics and traffic-characteristic diversity, we made this -instead of reinventing the wheel. diff --git a/proxy/snell/sing-snell/go.mod b/proxy/snell/sing-snell/go.mod deleted file mode 100644 index e7415788e7c..00000000000 --- a/proxy/snell/sing-snell/go.mod +++ /dev/null @@ -1,10 +0,0 @@ -module github.com/sagernet/sing-snell - -go 1.24.0 - -require ( - github.com/sagernet/sing v0.8.11 - golang.org/x/crypto v0.42.0 -) - -require golang.org/x/sys v0.41.0 // indirect diff --git a/proxy/snell/sing-snell/go.sum b/proxy/snell/sing-snell/go.sum deleted file mode 100644 index fdfa1de822c..00000000000 --- a/proxy/snell/sing-snell/go.sum +++ /dev/null @@ -1,12 +0,0 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/proxy/snell/sing-snell/test/docker_test.go b/proxy/snell/sing-snell/test/docker_test.go deleted file mode 100644 index 60d74969f0f..00000000000 --- a/proxy/snell/sing-snell/test/docker_test.go +++ /dev/null @@ -1,210 +0,0 @@ -package test - -import ( - "archive/zip" - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "io" - "net" - "net/http" - "os" - "path/filepath" - "testing" - "time" - - "github.com/sagernet/sing/common/debug" - F "github.com/sagernet/sing/common/format" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/client" - "github.com/docker/docker/pkg/stdcopy" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/stretchr/testify/require" -) - -const baseImage = "debian:bookworm-slim" - -type binarySpec struct { - tag string - sha256 string -} - -var binaries = map[string]binarySpec{ - "v4": {tag: "v4.1.1", sha256: "8e02974916645aafc0521c49faf0ab9916a0a8a7922ae7261cfd74b96899d25a"}, - "v5": {tag: "v5.0.1", sha256: "5b2e221f2c6e29b1db8e47053e1221be29d5627da807cb932b089f514a3609f0"}, - "v6": {tag: "v6.0.0b4", sha256: "ef2feaaf69d40673c2f3c8dccff28be6eadb75e790cb9e0cabd18bc66675c46f"}, -} - -func cacheDir() string { - dir := os.Getenv("SNELL_CACHE_DIR") - if dir == "" { - dir = filepath.Join(os.TempDir(), "sing-snell-test-bin") - } - return dir -} - -func snellBinary(t *testing.T, version string) string { - spec, found := binaries[version] - require.True(t, found, "unknown snell version: %s", version) - - if override := os.Getenv("SNELL_BIN_DIR"); override != "" { - dir := filepath.Join(override, spec.tag+"-linux-amd64") - verifyBinary(t, filepath.Join(dir, "snell-server"), spec.sha256) - return dir - } - - dir := filepath.Join(cacheDir(), spec.tag) - binaryPath := filepath.Join(dir, "snell-server") - if checkBinary(binaryPath, spec.sha256) { - return dir - } - require.NoError(t, os.MkdirAll(dir, 0o755)) - - url := "https://dl.nssurge.com/snell/snell-server-" + spec.tag + "-linux-amd64.zip" - t.Logf("downloading %s", url) - archive := downloadFile(t, url) - reader, err := zip.NewReader(bytes.NewReader(archive), int64(len(archive))) - require.NoError(t, err) - var extracted bool - for _, file := range reader.File { - if filepath.Base(file.Name) != "snell-server" { - continue - } - source, openErr := file.Open() - require.NoError(t, openErr) - content, readErr := io.ReadAll(source) - source.Close() - require.NoError(t, readErr) - require.NoError(t, os.WriteFile(binaryPath, content, 0o755)) - extracted = true - break - } - require.True(t, extracted, "snell-server not found in archive") - verifyBinary(t, binaryPath, spec.sha256) - return dir -} - -func checkBinary(path string, want string) bool { - content, err := os.ReadFile(path) - if err != nil { - return false - } - digest := sha256.Sum256(content) - return hex.EncodeToString(digest[:]) == want -} - -func verifyBinary(t *testing.T, path string, want string) { - content, err := os.ReadFile(path) - require.NoError(t, err) - digest := sha256.Sum256(content) - require.Equal(t, want, hex.EncodeToString(digest[:]), "binary sha256 mismatch: %s", path) - require.NoError(t, os.Chmod(path, 0o755)) -} - -func downloadFile(t *testing.T, url string) []byte { - response, err := http.Get(url) - require.NoError(t, err) - defer response.Body.Close() - require.Equal(t, http.StatusOK, response.StatusCode, "download %s", url) - content, err := io.ReadAll(response.Body) - require.NoError(t, err) - return content -} - -func startSnellServer(t *testing.T, version string, configContent string, port uint16) { - binDir := snellBinary(t, version) - - confDir := t.TempDir() - require.NoError(t, os.WriteFile(filepath.Join(confDir, "snell.conf"), []byte(configContent), 0o644)) - - dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - require.NoError(t, err) - t.Cleanup(func() { dockerClient.Close() }) - - ctx := context.Background() - ensureImage(t, dockerClient, baseImage) - - containerConfig := &container.Config{ - Image: baseImage, - Entrypoint: []string{"/snell/snell-server"}, - Cmd: []string{"-l", logLevel(), "-c", "/conf/snell.conf"}, - } - hostConfig := &container.HostConfig{ - NetworkMode: "host", - Binds: []string{ - binDir + ":/snell:ro", - confDir + ":/conf:ro", - }, - } - platform := &ocispec.Platform{OS: "linux", Architecture: "amd64"} - - created, err := dockerClient.ContainerCreate(ctx, containerConfig, hostConfig, nil, platform, "") - require.NoError(t, err) - t.Cleanup(func() { - dockerClient.ContainerRemove(context.Background(), created.ID, container.RemoveOptions{Force: true}) - }) - require.NoError(t, dockerClient.ContainerStart(ctx, created.ID, container.StartOptions{})) - - if debug.Enabled { - attach, attachErr := dockerClient.ContainerAttach(ctx, created.ID, container.AttachOptions{ - Stdout: true, Stderr: true, Logs: true, Stream: true, - }) - require.NoError(t, attachErr) - go stdcopy.StdCopy(os.Stderr, os.Stderr, attach.Reader) - } - - waitForListen(t, dockerClient, created.ID, port) -} - -func waitForListen(t *testing.T, dockerClient *client.Client, containerID string, port uint16) { - deadline := time.Now().Add(30 * time.Second) - address := "127.0.0.1:" + F.ToString(port) - for time.Now().Before(deadline) { - conn, err := net.DialTimeout("tcp", address, time.Second) - if err == nil { - conn.Close() - return - } - info, inspectErr := dockerClient.ContainerInspect(context.Background(), containerID) - if inspectErr == nil && !info.State.Running { - logs := containerLogs(dockerClient, containerID) - t.Fatalf("snell server exited early (code %d):\n%s", info.State.ExitCode, logs) - } - time.Sleep(200 * time.Millisecond) - } - t.Fatalf("snell server did not listen on %s:\n%s", address, containerLogs(dockerClient, containerID)) -} - -func containerLogs(dockerClient *client.Client, containerID string) string { - reader, err := dockerClient.ContainerLogs(context.Background(), containerID, container.LogsOptions{ - ShowStdout: true, ShowStderr: true, - }) - if err != nil { - return "" - } - defer reader.Close() - var out bytes.Buffer - stdcopy.StdCopy(&out, &out, reader) - return out.String() -} - -func ensureImage(t *testing.T, dockerClient *client.Client, ref string) { - _, err := dockerClient.ImageInspect(context.Background(), ref) - if err == nil { - return - } - reader, pullErr := dockerClient.ImagePull(context.Background(), ref, image.PullOptions{Platform: "linux/amd64"}) - require.NoError(t, pullErr) - defer reader.Close() - io.Copy(io.Discard, reader) -} - -func logLevel() string { - if debug.Enabled { - return "trace" - } - return "error" -} diff --git a/proxy/snell/sing-snell/test/go.mod b/proxy/snell/sing-snell/test/go.mod deleted file mode 100644 index be39d4124a6..00000000000 --- a/proxy/snell/sing-snell/test/go.mod +++ /dev/null @@ -1,46 +0,0 @@ -module github.com/sagernet/sing-snell/test - -go 1.25.0 - -require ( - github.com/docker/docker v28.5.2+incompatible - github.com/opencontainers/image-spec v1.1.1 - github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d - github.com/sagernet/sing-snell v0.0.0 - github.com/stretchr/testify v1.11.1 -) - -replace github.com/sagernet/sing-snell => ../ - -require ( - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/containerd/errdefs v1.0.0 // indirect - github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/containerd/log v0.1.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/distribution/reference v0.6.0 // indirect - github.com/docker/go-connections v0.7.0 // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/sys/atomicwriter v0.1.0 // indirect - github.com/moby/term v0.5.2 // indirect - github.com/morikuni/aec v1.1.0 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect - go.opentelemetry.io/otel v1.44.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 // indirect - go.opentelemetry.io/otel/metric v1.44.0 // indirect - go.opentelemetry.io/otel/trace v1.44.0 // indirect - golang.org/x/crypto v0.42.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/time v0.15.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - gotest.tools/v3 v3.5.2 // indirect -) diff --git a/proxy/snell/sing-snell/test/go.sum b/proxy/snell/sing-snell/test/go.sum deleted file mode 100644 index 807aae81856..00000000000 --- a/proxy/snell/sing-snell/test/go.sum +++ /dev/null @@ -1,112 +0,0 @@ -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= -github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= -github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= -github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= -github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= -github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= -github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= -github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= -github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= -github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= -github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= -github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= -github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= -github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= -github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d h1:BhsQU0Iug1tU4xR52cjm8Sc+LBo+KwdyLTRn3ie9moo= -github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= -go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= -go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s= -go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= -go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= -go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= -go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= -go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= -go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= -go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= -go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= -go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= -go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= -golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= -golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= -gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/proxy/snell/sing-snell/test/helper_test.go b/proxy/snell/sing-snell/test/helper_test.go deleted file mode 100644 index d1fa70b3c95..00000000000 --- a/proxy/snell/sing-snell/test/helper_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package test - -import ( - "io" - "net" - "testing" - - "github.com/stretchr/testify/require" -) - -func freePort(t *testing.T) uint16 { - listener, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - port := uint16(listener.Addr().(*net.TCPAddr).Port) - listener.Close() - return port -} - -func startTCPEcho(t *testing.T) uint16 { - listener, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - t.Cleanup(func() { listener.Close() }) - go func() { - for { - conn, acceptErr := listener.Accept() - if acceptErr != nil { - return - } - go func() { - io.Copy(conn, conn) - conn.Close() - }() - } - }() - return uint16(listener.Addr().(*net.TCPAddr).Port) -} - -func startUDPEcho(t *testing.T) uint16 { - packetConn, err := net.ListenPacket("udp", "127.0.0.1:0") - require.NoError(t, err) - t.Cleanup(func() { packetConn.Close() }) - go func() { - buffer := make([]byte, 64*1024) - for { - n, addr, readErr := packetConn.ReadFrom(buffer) - if readErr != nil { - return - } - packetConn.WriteTo(buffer[:n], addr) - } - }() - return uint16(packetConn.LocalAddr().(*net.UDPAddr).Port) -} diff --git a/proxy/snell/sing-snell/test/loopback_test.go b/proxy/snell/sing-snell/test/loopback_test.go deleted file mode 100644 index 8a37d39f622..00000000000 --- a/proxy/snell/sing-snell/test/loopback_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package test - -import ( - "bytes" - "crypto/rand" - "io" - "net" - "testing" - "time" - - snell "github.com/sagernet/sing-snell" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" - - "github.com/stretchr/testify/require" -) - -type normalScenario struct { - address string - client snell.Method -} - -func (s normalScenario) HalfCloseEcho(t *testing.T, label string) { - echoPort := startTCPHalfCloseEcho(t) - serverConn, err := net.Dial("tcp", s.address) - require.NoError(t, err) - defer serverConn.Close() - proxyConn, err := s.client.DialConn(serverConn, M.ParseSocksaddrHostPort("127.0.0.1", echoPort)) - require.NoError(t, err) - defer proxyConn.Close() - - payload := make([]byte, 4*1024) - _, err = rand.Read(payload) - require.NoError(t, err) - copy(payload, []byte(label)) - _, err = proxyConn.Write(payload) - require.NoError(t, err) - require.NoError(t, N.CloseWrite(proxyConn)) - require.NoError(t, proxyConn.SetReadDeadline(time.Now().Add(10*time.Second))) - received, err := io.ReadAll(proxyConn) - require.NoError(t, err) - require.Equal(t, len(payload), len(received), "%s response length", label) - require.True(t, bytes.Equal(payload, received), "%s payload mismatch", label) -} diff --git a/proxy/snell/sing-snell/test/reuse_helpers_test.go b/proxy/snell/sing-snell/test/reuse_helpers_test.go deleted file mode 100644 index 6d406000d9e..00000000000 --- a/proxy/snell/sing-snell/test/reuse_helpers_test.go +++ /dev/null @@ -1,297 +0,0 @@ -package test - -import ( - "bytes" - "context" - "crypto/rand" - "io" - "net" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/sagernet/sing/common/bufio" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" - - "github.com/stretchr/testify/require" -) - -type reuseTCPClient interface { - DialContext(ctx context.Context, destination M.Socksaddr) (net.Conn, error) - Close() error -} - -type countingTCPProxy struct { - address string - count atomic.Int32 -} - -func (p *countingTCPProxy) Start(t *testing.T, target string) { - listener, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - p.address = listener.Addr().String() - t.Cleanup(func() { listener.Close() }) - go func() { - for { - conn, acceptErr := listener.Accept() - if acceptErr != nil { - return - } - p.count.Add(1) - go p.Proxy(conn, target) - } - }() -} - -func (p *countingTCPProxy) Proxy(conn net.Conn, target string) { - upstream, err := net.Dial("tcp", target) - if err != nil { - conn.Close() - return - } - var wait sync.WaitGroup - wait.Add(2) - go func() { - defer wait.Done() - io.Copy(upstream, conn) - N.CloseWrite(upstream) - }() - go func() { - defer wait.Done() - io.Copy(conn, upstream) - N.CloseWrite(conn) - }() - wait.Wait() - conn.Close() - upstream.Close() -} - -type reuseScenario struct { - client reuseTCPClient - destination M.Socksaddr -} - -func startTCPHalfCloseEcho(t *testing.T, closeWriteDelay ...time.Duration) uint16 { - delay := time.Duration(0) - if len(closeWriteDelay) > 0 { - delay = closeWriteDelay[0] - } - listener, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - t.Cleanup(func() { listener.Close() }) - go func() { - for { - conn, acceptErr := listener.Accept() - if acceptErr != nil { - return - } - go func() { - defer conn.Close() - payload, readErr := io.ReadAll(conn) - if readErr != nil { - return - } - _, writeErr := io.Copy(conn, bytes.NewReader(payload)) - if writeErr != nil { - return - } - time.Sleep(delay) - N.CloseWrite(conn) - time.Sleep(200 * time.Millisecond) - }() - } - }() - return uint16(listener.Addr().(*net.TCPAddr).Port) -} - -func startTCPHalfCloseTailEcho(t *testing.T, tailLen int, closeWriteDelay time.Duration) uint16 { - listener, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - t.Cleanup(func() { listener.Close() }) - go func() { - for { - conn, acceptErr := listener.Accept() - if acceptErr != nil { - return - } - go func() { - defer conn.Close() - payload, readErr := io.ReadAll(conn) - if readErr != nil { - return - } - _, writeErr := io.Copy(conn, bytes.NewReader(payload)) - if writeErr != nil { - return - } - if tailLen > 0 { - tail := bytes.Repeat([]byte{0x5A}, tailLen) - _, writeErr = conn.Write(tail) - if writeErr != nil { - return - } - } - time.Sleep(closeWriteDelay) - N.CloseWrite(conn) - time.Sleep(200 * time.Millisecond) - }() - } - }() - return uint16(listener.Addr().(*net.TCPAddr).Port) -} - -func (s reuseScenario) RoundTrip(t *testing.T, label string) { - payload := make([]byte, 4*1024) - _, err := rand.Read(payload) - require.NoError(t, err) - copy(payload, []byte(label)) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - conn, err := s.client.DialContext(ctx, s.destination) - require.NoError(t, err) - _, err = conn.Write(payload) - require.NoError(t, err) - time.Sleep(20 * time.Millisecond) - require.NoError(t, N.CloseWrite(conn)) - require.NoError(t, conn.SetReadDeadline(time.Now().Add(10*time.Second))) - received, err := io.ReadAll(conn) - require.NoError(t, err) - require.Equal(t, len(payload), len(received), "%s response length", label) - require.True(t, bytes.Equal(payload, received), "%s payload mismatch", label) - require.NoError(t, conn.Close()) -} - -func (s reuseScenario) PartialEchoRoundTrip(t *testing.T, label string, payloadLen int, echoLen int) { - payload := make([]byte, payloadLen) - _, err := rand.Read(payload) - require.NoError(t, err) - copy(payload, []byte(label)) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - conn, err := s.client.DialContext(ctx, s.destination) - require.NoError(t, err) - _, err = conn.Write(payload) - require.NoError(t, err) - require.NoError(t, N.CloseWrite(conn)) - require.NoError(t, conn.SetReadDeadline(time.Now().Add(10*time.Second))) - received, err := io.ReadAll(conn) - require.NoError(t, err) - require.True(t, bytes.Equal(payload[:echoLen], received), "%s payload mismatch", label) - require.NoError(t, conn.Close()) -} - -func (s reuseScenario) CloseBeforeServerEOF(t *testing.T, label string, waitAfterClose time.Duration) { - payload := make([]byte, 4*1024) - _, err := rand.Read(payload) - require.NoError(t, err) - copy(payload, []byte(label)) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - conn, err := s.client.DialContext(ctx, s.destination) - require.NoError(t, err) - _, err = conn.Write(payload) - require.NoError(t, err) - require.NoError(t, N.CloseWrite(conn)) - require.NoError(t, conn.SetReadDeadline(time.Now().Add(10*time.Second))) - received := make([]byte, len(payload)) - _, err = io.ReadFull(conn, received) - require.NoError(t, err) - require.True(t, bytes.Equal(payload, received), "%s payload mismatch", label) - closeStart := time.Now() - require.NoError(t, conn.Close()) - if time.Since(closeStart) > 250*time.Millisecond { - t.Fatalf("%s close waited for server EOF instead of leaving the session draining", label) - } - time.Sleep(waitAfterClose) -} - -func (s reuseScenario) CloseWhileReadBlocked(t *testing.T, label string) { - payload := make([]byte, 4*1024) - _, err := rand.Read(payload) - require.NoError(t, err) - copy(payload, []byte(label)) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - conn, err := s.client.DialContext(ctx, s.destination) - require.NoError(t, err) - _, err = conn.Write(payload) - require.NoError(t, err) - require.NoError(t, N.CloseWrite(conn)) - require.NoError(t, conn.SetReadDeadline(time.Now().Add(10*time.Second))) - received := make([]byte, len(payload)) - _, err = io.ReadFull(conn, received) - require.NoError(t, err) - require.True(t, bytes.Equal(payload, received), "%s payload mismatch", label) - - readStarted := make(chan struct{}) - readResult := make(chan struct { - n int - err error - }, 1) - go func() { - close(readStarted) - var one [1]byte - n, readErr := conn.Read(one[:]) - readResult <- struct { - n int - err error - }{n: n, err: readErr} - }() - <-readStarted - select { - case result := <-readResult: - t.Fatalf("%s read returned before close: n=%d err=%v", label, result.n, result.err) - case <-time.After(50 * time.Millisecond): - } - - closeStart := time.Now() - require.NoError(t, conn.Close()) - if time.Since(closeStart) > 250*time.Millisecond { - t.Fatalf("%s close waited for server EOF instead of leaving the active read to finish", label) - } - select { - case result := <-readResult: - t.Fatalf("%s read returned before server EOF after close: n=%d err=%v", label, result.n, result.err) - case <-time.After(100 * time.Millisecond): - } - select { - case result := <-readResult: - require.Zero(t, result.n, "%s read bytes", label) - require.ErrorIs(t, result.err, io.EOF, "%s read error", label) - case <-time.After(3 * time.Second): - t.Fatalf("%s active read did not finish after server EOF", label) - } -} - -func (s reuseScenario) ReadWaiter(t *testing.T, label string) { - payload := make([]byte, 4*1024) - _, err := rand.Read(payload) - require.NoError(t, err) - copy(payload, []byte(label)) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - conn, err := s.client.DialContext(ctx, s.destination) - require.NoError(t, err) - readWaiter, created := bufio.CreateReadWaiter(conn) - require.True(t, created) - needCopy := readWaiter.InitializeReadWaiter(N.ReadWaitOptions{FrontHeadroom: 2048, RearHeadroom: 256, MTU: 1200}) - require.False(t, needCopy) - _, err = conn.Write(payload) - require.NoError(t, err) - require.NoError(t, N.CloseWrite(conn)) - require.NoError(t, conn.SetReadDeadline(time.Now().Add(10*time.Second))) - received := make([]byte, 0, len(payload)) - for len(received) < len(payload) { - buffer, readErr := readWaiter.WaitReadBuffer() - require.NoError(t, readErr) - require.GreaterOrEqual(t, buffer.Start(), 2048) - require.GreaterOrEqual(t, buffer.FreeLen(), 256) - received = append(received, buffer.Bytes()...) - buffer.Release() - } - require.Equal(t, len(payload), len(received), "%s response length", label) - require.True(t, bytes.Equal(payload, received), "%s payload mismatch", label) - require.NoError(t, conn.Close()) -} diff --git a/proxy/snell/sing-snell/test/tls_obfs_loopback_test.go b/proxy/snell/sing-snell/test/tls_obfs_loopback_test.go deleted file mode 100644 index 31fe6ac6580..00000000000 --- a/proxy/snell/sing-snell/test/tls_obfs_loopback_test.go +++ /dev/null @@ -1,240 +0,0 @@ -package test - -import ( - "bytes" - "context" - "crypto/rand" - "io" - "net" - "testing" - "time" - - snell "github.com/sagernet/sing-snell" - "github.com/sagernet/sing-snell/snellv4" - "github.com/sagernet/sing-snell/snellv5" - "github.com/sagernet/sing/common/buf" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" - - "github.com/stretchr/testify/require" -) - -func TestV5ObfsTLSLoopback(t *testing.T) { - service, err := snellv5.NewService(snellv5.ServiceOptions{ - PSK: []byte(testPSK), - ObfsMode: snell.ObfsModeTLS, - Handler: localEchoHandler{}, - }) - require.NoError(t, err) - serviceAddress := startLocalSnellService(t, service) - client, err := snellv4.NewClient(snellv4.ClientOptions{ - PSK: []byte(testPSK), - ObfsMode: snell.ObfsModeTLS, - ObfsHost: "obfs.example", - }) - require.NoError(t, err) - normalScenario{address: serviceAddress, client: client}.HalfCloseEcho(t, "v5-tls-tcp") - - var proxy countingTCPProxy - proxy.Start(t, serviceAddress) - reuseClient, err := snellv4.NewClient(snellv4.ClientOptions{ - PSK: []byte(testPSK), - Reuse: true, - ObfsMode: snell.ObfsModeTLS, - ObfsHost: "obfs.example", - Dialer: N.SystemDialer, - Server: M.ParseSocksaddr(proxy.address), - }) - require.NoError(t, err) - defer reuseClient.Close() - reuseScenario{client: reuseClient, destination: M.ParseSocksaddrHostPort("127.0.0.1", 443)}.RoundTrip(t, "v5-tls-reuse-1") - reuseScenario{client: reuseClient, destination: M.ParseSocksaddrHostPort("127.0.0.1", 443)}.RoundTrip(t, "v5-tls-reuse-2") - require.Equal(t, int32(1), proxy.count.Load()) - - serverConn, err := net.Dial("tcp", serviceAddress) - require.NoError(t, err) - defer serverConn.Close() - packetConn, err := client.DialPacketConn(serverConn) - require.NoError(t, err) - roundTripPacket(t, packetConn, serverConn, "v5-tls-udp-v4-wire") -} - -func TestV5ServerAcceptsV4ClientLoopback(t *testing.T) { - service, err := snellv5.NewService(snellv5.ServiceOptions{ - PSK: []byte(testPSK), - Handler: localEchoHandler{}, - }) - require.NoError(t, err) - serviceAddress := startLocalSnellService(t, service) - client, err := snellv4.NewClient(snellv4.ClientOptions{PSK: []byte(testPSK)}) - require.NoError(t, err) - normalScenario{address: serviceAddress, client: client}.HalfCloseEcho(t, "v5-service-v4-client-tcp") - - var proxy countingTCPProxy - proxy.Start(t, serviceAddress) - reuseClient, err := snellv4.NewClient(snellv4.ClientOptions{ - PSK: []byte(testPSK), - Reuse: true, - Dialer: N.SystemDialer, - Server: M.ParseSocksaddr(proxy.address), - }) - require.NoError(t, err) - defer reuseClient.Close() - reuseScenario{client: reuseClient, destination: M.ParseSocksaddrHostPort("127.0.0.1", 443)}.RoundTrip(t, "v5-service-v4-client-reuse-1") - reuseScenario{client: reuseClient, destination: M.ParseSocksaddrHostPort("127.0.0.1", 443)}.RoundTrip(t, "v5-service-v4-client-reuse-2") - require.Equal(t, int32(1), proxy.count.Load()) - - serverConn, err := net.Dial("tcp", serviceAddress) - require.NoError(t, err) - defer serverConn.Close() - packetConn, err := client.DialPacketConn(serverConn) - require.NoError(t, err) - roundTripPacket(t, packetConn, serverConn, "v5-service-v4-client-udp") -} - -func startLocalSnellService(t *testing.T, service snell.Service) string { - listener, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - t.Cleanup(func() { listener.Close() }) - go func() { - for { - conn, acceptErr := listener.Accept() - if acceptErr != nil { - return - } - go func() { - serveErr := service.NewConnection(context.Background(), conn, M.SocksaddrFromNet(conn.RemoteAddr()), nil) - if serveErr != nil { - conn.Close() - } - }() - } - }() - return listener.Addr().String() -} - -func roundTripPacket(t *testing.T, packetConn N.NetPacketConn, serverConn net.Conn, label string) { - message := make([]byte, 1200) - _, err := rand.Read(message) - require.NoError(t, err) - target := M.ParseSocksaddrHostPort("203.0.113.1", 443).UDPAddr() - _, err = packetConn.WriteTo(message, target) - require.NoError(t, err) - received := make([]byte, 2048) - require.NoError(t, serverConn.SetReadDeadline(time.Now().Add(10*time.Second))) - n, addr, err := packetConn.ReadFrom(received) - require.NoError(t, err) - require.Equal(t, target.String(), addr.String(), label) - require.True(t, bytes.Equal(message, received[:n]), label) -} - -func TestV5ServerReuseHandlerCloseBeforeClientEOF(t *testing.T) { - service, err := snellv5.NewService(snellv5.ServiceOptions{ - PSK: []byte(testPSK), - Handler: partialEchoHandler{echoLen: 4 * 1024}, - }) - require.NoError(t, err) - serviceAddress := startLocalSnellService(t, service) - var proxy countingTCPProxy - proxy.Start(t, serviceAddress) - reuseClient, err := snellv4.NewClient(snellv4.ClientOptions{ - PSK: []byte(testPSK), - Reuse: true, - Dialer: N.SystemDialer, - Server: M.ParseSocksaddr(proxy.address), - }) - require.NoError(t, err) - defer reuseClient.Close() - scenario := reuseScenario{client: reuseClient, destination: M.ParseSocksaddrHostPort("127.0.0.1", 443)} - scenario.PartialEchoRoundTrip(t, "v5-server-drain-1", 8*1024, 4*1024) - scenario.PartialEchoRoundTrip(t, "v5-server-drain-2", 8*1024, 4*1024) - require.Equal(t, int32(1), proxy.count.Load()) -} - -type localEchoHandler struct{} - -func (localEchoHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { - go func() { - var closeErr error - defer func() { - writeCloseErr := N.CloseWrite(conn) - if closeErr == nil { - closeErr = writeCloseErr - } - connCloseErr := conn.Close() - if closeErr == nil { - closeErr = connCloseErr - } - if onClose != nil { - onClose(closeErr) - } - }() - payload, err := io.ReadAll(conn) - if err != nil { - closeErr = err - return - } - _, err = io.Copy(conn, bytes.NewReader(payload)) - if err != nil { - closeErr = err - } - }() -} - -type partialEchoHandler struct { - localEchoHandler - echoLen int -} - -func (h partialEchoHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { - go func() { - payload := make([]byte, h.echoLen) - _, err := io.ReadFull(conn, payload) - if err == nil { - _, err = conn.Write(payload) - } - closeErr := conn.Close() - if err == nil { - err = closeErr - } - if onClose != nil { - onClose(err) - } - }() -} - -func (localEchoHandler) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { - go func() { - var closeErr error - defer func() { - connCloseErr := conn.Close() - if closeErr == nil { - closeErr = connCloseErr - } - if onClose != nil { - onClose(closeErr) - } - }() - for { - select { - case <-ctx.Done(): - closeErr = ctx.Err() - return - default: - } - buffer := buf.NewSize(64 * 1024) - buffer.Resize(512, 0) - packetDestination, err := conn.ReadPacket(buffer) - if err != nil { - buffer.Release() - closeErr = err - return - } - err = conn.WritePacket(buffer, packetDestination) - if err != nil { - closeErr = err - return - } - } - }() -} diff --git a/proxy/snell/sing-snell/test/v4_test.go b/proxy/snell/sing-snell/test/v4_test.go deleted file mode 100644 index 3b6c0286f89..00000000000 --- a/proxy/snell/sing-snell/test/v4_test.go +++ /dev/null @@ -1,369 +0,0 @@ -package test - -import ( - "bytes" - "crypto/rand" - "fmt" - "io" - "net" - "testing" - "time" - - snell "github.com/sagernet/sing-snell" - "github.com/sagernet/sing-snell/snellv4" - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" - - "github.com/stretchr/testify/require" -) - -func TestV4TCP(t *testing.T) { - port := freePort(t) - config := fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nipv6 = false\n", port, testPSK) - startSnellServer(t, "v4", config, port) - echoPort := startTCPEcho(t) - - client, err := snellv4.NewClient(snellv4.ClientOptions{PSK: []byte(testPSK)}) - require.NoError(t, err) - serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) - require.NoError(t, err) - defer serverConn.Close() - - proxyConn, err := client.DialConn(serverConn, M.ParseSocksaddrHostPort("127.0.0.1", echoPort)) - require.NoError(t, err) - - payload := make([]byte, 1024*1024) - rand.Read(payload) - go func() { - proxyConn.Write(payload) - }() - received := make([]byte, len(payload)) - proxyConn.SetReadDeadline(time.Now().Add(20 * time.Second)) - _, err = io.ReadFull(proxyConn, received) - require.NoError(t, err) - require.True(t, bytes.Equal(payload, received), "payload mismatch") - normalScenario{address: fmt.Sprintf("127.0.0.1:%d", port), client: client}.HalfCloseEcho(t, "v4-official") -} - -func TestV4TCPReuse(t *testing.T) { - port := freePort(t) - config := fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nipv6 = false\n", port, testPSK) - startSnellServer(t, "v4", config, port) - var proxy countingTCPProxy - proxy.Start(t, fmt.Sprintf("127.0.0.1:%d", port)) - echoPort := startTCPHalfCloseEcho(t) - delayedEchoPort := startTCPHalfCloseEcho(t, 750*time.Millisecond) - tailEchoPort := startTCPHalfCloseTailEcho(t, 32*1024, 250*time.Millisecond) - oversizedTailEchoPort := startTCPHalfCloseTailEcho(t, 0x80001, 250*time.Millisecond) - - client, err := snellv4.NewClient(snellv4.ClientOptions{ - PSK: []byte(testPSK), - Reuse: true, - Dialer: N.SystemDialer, - Server: M.ParseSocksaddr(proxy.address), - }) - require.NoError(t, err) - defer client.Close() - - scenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", echoPort)} - scenario.RoundTrip(t, "v4-reuse-1") - scenario.RoundTrip(t, "v4-reuse-2") - delayedScenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", delayedEchoPort)} - delayedScenario.CloseBeforeServerEOF(t, "v4-reuse-drain", time.Second) - scenario.RoundTrip(t, "v4-reuse-after-drain") - tailScenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", tailEchoPort)} - tailScenario.CloseBeforeServerEOF(t, "v4-reuse-tail", 500*time.Millisecond) - scenario.RoundTrip(t, "v4-reuse-after-tail") - require.Equal(t, int32(1), proxy.count.Load()) - oversizedTailScenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", oversizedTailEchoPort)} - oversizedTailScenario.CloseBeforeServerEOF(t, "v4-reuse-oversized-tail", 500*time.Millisecond) - scenario.RoundTrip(t, "v4-reuse-after-oversized-tail") - require.Equal(t, int32(2), proxy.count.Load()) -} - -func TestV4TCPReuseClientCloseCancelsDrain(t *testing.T) { - port := freePort(t) - config := fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nipv6 = false\n", port, testPSK) - startSnellServer(t, "v4", config, port) - var proxy countingTCPProxy - proxy.Start(t, fmt.Sprintf("127.0.0.1:%d", port)) - delayedEchoPort := startTCPHalfCloseEcho(t, 750*time.Millisecond) - - client, err := snellv4.NewClient(snellv4.ClientOptions{ - PSK: []byte(testPSK), - Reuse: true, - Dialer: N.SystemDialer, - Server: M.ParseSocksaddr(proxy.address), - }) - require.NoError(t, err) - - scenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", delayedEchoPort)} - scenario.CloseBeforeServerEOF(t, "v4-reuse-close-cancel", 0) - closeStart := time.Now() - require.NoError(t, client.Close()) - if time.Since(closeStart) > 250*time.Millisecond { - t.Fatal("client close waited for draining EOF instead of canceling the session") - } -} - -func TestV4TCPReuseClientCloseLetsActiveReadFinish(t *testing.T) { - port := freePort(t) - config := fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nipv6 = false\n", port, testPSK) - startSnellServer(t, "v4", config, port) - var proxy countingTCPProxy - proxy.Start(t, fmt.Sprintf("127.0.0.1:%d", port)) - delayedEchoPort := startTCPHalfCloseEcho(t, 750*time.Millisecond) - echoPort := startTCPHalfCloseEcho(t) - - client, err := snellv4.NewClient(snellv4.ClientOptions{ - PSK: []byte(testPSK), - Reuse: true, - Dialer: N.SystemDialer, - Server: M.ParseSocksaddr(proxy.address), - }) - require.NoError(t, err) - defer client.Close() - - reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", delayedEchoPort)}.CloseWhileReadBlocked(t, "v4-reuse-active-read-close") - reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", echoPort)}.RoundTrip(t, "v4-reuse-after-active-read-close") - require.Equal(t, int32(1), proxy.count.Load()) -} - -func TestV4UDP(t *testing.T) { - port := freePort(t) - config := fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nipv6 = false\n", port, testPSK) - startSnellServer(t, "v4", config, port) - echoPort := startUDPEcho(t) - - client, err := snellv4.NewClient(snellv4.ClientOptions{PSK: []byte(testPSK)}) - require.NoError(t, err) - serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) - require.NoError(t, err) - defer serverConn.Close() - - packetConn, err := client.DialPacketConn(serverConn) - require.NoError(t, err) - target := M.ParseSocksaddrHostPort("127.0.0.1", echoPort).UDPAddr() - - for round := range 10 { - message := make([]byte, 1200) - rand.Read(message) - _, err = packetConn.WriteTo(message, target) - require.NoError(t, err) - - received := make([]byte, 2048) - serverConn.SetReadDeadline(time.Now().Add(10 * time.Second)) - n, _, readErr := packetConn.ReadFrom(received) - require.NoError(t, readErr) - require.True(t, bytes.Equal(message, received[:n]), "round %d", round) - } -} - -func TestV4TCPVectorisedWriter(t *testing.T) { - port := freePort(t) - config := fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nipv6 = false\n", port, testPSK) - startSnellServer(t, "v4", config, port) - echoPort := startTCPEcho(t) - - client, err := snellv4.NewClient(snellv4.ClientOptions{PSK: []byte(testPSK)}) - require.NoError(t, err) - serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) - require.NoError(t, err) - defer serverConn.Close() - - proxyConn := client.DialEarlyConn(serverConn, M.ParseSocksaddrHostPort("127.0.0.1", echoPort)) - vectorisedWriter, created := bufio.CreateVectorisedWriter(proxyConn) - require.True(t, created) - - partA := make([]byte, 700) - partB := make([]byte, 900) - _, err = rand.Read(partA) - require.NoError(t, err) - _, err = rand.Read(partB) - require.NoError(t, err) - expected := append(append([]byte(nil), partA...), partB...) - - frontHeadroom := N.CalculateFrontHeadroom(proxyConn) - rearHeadroom := N.CalculateRearHeadroom(proxyConn) - buffers := make([]*buf.Buffer, 2) - for index, payload := range [][]byte{partA, partB} { - buffer := buf.NewSize(frontHeadroom + len(payload) + rearHeadroom) - buffer.Resize(frontHeadroom, 0) - _, err = buffer.Write(payload) - require.NoError(t, err) - buffers[index] = buffer - } - err = vectorisedWriter.WriteVectorised(buffers) - require.NoError(t, err) - - received := make([]byte, len(expected)) - require.NoError(t, proxyConn.SetReadDeadline(time.Now().Add(10*time.Second))) - _, err = io.ReadFull(proxyConn, received) - require.NoError(t, err) - require.Equal(t, expected, received) -} - -func TestV4UDPPacketBatchWriter(t *testing.T) { - for _, testCase := range []struct { - name string - version string - }{ - {name: "official-v4", version: "v4"}, - {name: "official-v5", version: "v5"}, - } { - t.Run(testCase.name, func(t *testing.T) { - port := freePort(t) - switch testCase.version { - case "v4": - config := fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nipv6 = false\n", port, testPSK) - startSnellServer(t, "v4", config, port) - case "v5": - startSnellServer(t, "v5", v5Config(testPSK, port), port) - default: - t.Fatal("unknown server version") - } - echoPort := startUDPEcho(t) - - client, err := snellv4.NewClient(snellv4.ClientOptions{PSK: []byte(testPSK)}) - require.NoError(t, err) - serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) - require.NoError(t, err) - defer serverConn.Close() - - packetConn, err := client.DialPacketConn(serverConn) - require.NoError(t, err) - batchWriter, created := bufio.CreatePacketBatchWriter(packetConn) - require.True(t, created) - - target := M.ParseSocksaddrHostPort("127.0.0.1", echoPort) - frontHeadroom := N.CalculateFrontHeadroom(packetConn) - rearHeadroom := N.CalculateRearHeadroom(packetConn) - payloads := make([][]byte, 3) - buffers := make([]*buf.Buffer, len(payloads)) - destinations := make([]M.Socksaddr, len(payloads)) - for index := range payloads { - payload := make([]byte, 1200) - _, err = rand.Read(payload) - require.NoError(t, err) - payload[0] = byte(index) - payloads[index] = payload - buffer := buf.NewSize(frontHeadroom + len(payload) + rearHeadroom) - buffer.Resize(frontHeadroom, 0) - _, err = buffer.Write(payload) - require.NoError(t, err) - buffers[index] = buffer - destinations[index] = target - } - err = batchWriter.WritePacketBatch(buffers, destinations) - require.NoError(t, err) - - received := make(map[string]int, len(payloads)) - for range len(payloads) { - reply := make([]byte, 2048) - err = packetConn.SetReadDeadline(time.Now().Add(10 * time.Second)) - require.NoError(t, err) - n, _, readErr := packetConn.ReadFrom(reply) - require.NoError(t, readErr) - received[string(reply[:n])]++ - } - for _, payload := range payloads { - require.Equal(t, 1, received[string(payload)]) - } - }) - } -} - -func TestV4ObfsHTTP(t *testing.T) { - t.Run("tcp", func(t *testing.T) { - port := freePort(t) - config := fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nobfs = http\nipv6 = false\n", port, testPSK) - startSnellServer(t, "v4", config, port) - echoPort := startTCPEcho(t) - - client, err := snellv4.NewClient(snellv4.ClientOptions{ - PSK: []byte(testPSK), - ObfsMode: snell.ObfsModeHTTP, - ObfsHost: "obfs.example", - }) - require.NoError(t, err) - serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) - require.NoError(t, err) - defer serverConn.Close() - - proxyConn, err := client.DialConn(serverConn, M.ParseSocksaddrHostPort("127.0.0.1", echoPort)) - require.NoError(t, err) - - payload := make([]byte, 256*1024) - rand.Read(payload) - go func() { - proxyConn.Write(payload) - }() - received := make([]byte, len(payload)) - proxyConn.SetReadDeadline(time.Now().Add(20 * time.Second)) - _, err = io.ReadFull(proxyConn, received) - require.NoError(t, err) - require.True(t, bytes.Equal(payload, received), "payload mismatch") - normalScenario{address: fmt.Sprintf("127.0.0.1:%d", port), client: client}.HalfCloseEcho(t, "v4-official-obfs") - }) - - t.Run("reuse", func(t *testing.T) { - port := freePort(t) - config := fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nobfs = http\nipv6 = false\n", port, testPSK) - startSnellServer(t, "v4", config, port) - var proxy countingTCPProxy - proxy.Start(t, fmt.Sprintf("127.0.0.1:%d", port)) - echoPort := startTCPHalfCloseEcho(t) - - client, err := snellv4.NewClient(snellv4.ClientOptions{ - PSK: []byte(testPSK), - Reuse: true, - ObfsMode: snell.ObfsModeHTTP, - ObfsHost: "obfs.example", - Dialer: N.SystemDialer, - Server: M.ParseSocksaddr(proxy.address), - }) - require.NoError(t, err) - defer client.Close() - - scenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", echoPort)} - scenario.RoundTrip(t, "v4-obfs-reuse-1") - scenario.RoundTrip(t, "v4-obfs-reuse-2") - require.Equal(t, int32(1), proxy.count.Load()) - }) - - t.Run("udp", func(t *testing.T) { - port := freePort(t) - config := fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nobfs = http\nipv6 = false\n", port, testPSK) - startSnellServer(t, "v4", config, port) - echoPort := startUDPEcho(t) - - client, err := snellv4.NewClient(snellv4.ClientOptions{ - PSK: []byte(testPSK), - ObfsMode: snell.ObfsModeHTTP, - ObfsHost: "obfs.example", - }) - require.NoError(t, err) - serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) - require.NoError(t, err) - defer serverConn.Close() - - packetConn, err := client.DialPacketConn(serverConn) - require.NoError(t, err) - target := M.ParseSocksaddrHostPort("127.0.0.1", echoPort).UDPAddr() - - for round := range 5 { - message := make([]byte, 1200) - rand.Read(message) - _, err = packetConn.WriteTo(message, target) - require.NoError(t, err) - - received := make([]byte, 2048) - serverConn.SetReadDeadline(time.Now().Add(10 * time.Second)) - n, _, readErr := packetConn.ReadFrom(received) - require.NoError(t, readErr) - require.True(t, bytes.Equal(message, received[:n]), "round %d", round) - } - }) -} diff --git a/proxy/snell/sing-snell/test/v5_test.go b/proxy/snell/sing-snell/test/v5_test.go deleted file mode 100644 index 33c2e63b01d..00000000000 --- a/proxy/snell/sing-snell/test/v5_test.go +++ /dev/null @@ -1,610 +0,0 @@ -package test - -import ( - "bytes" - "crypto/cipher" - "crypto/rand" - "encoding/binary" - "errors" - "fmt" - "io" - "net" - "testing" - "time" - - snell "github.com/sagernet/sing-snell" - "github.com/sagernet/sing-snell/snellv4" - "github.com/sagernet/sing-snell/snellv5" - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" - - "github.com/stretchr/testify/require" -) - -const testPSK = "snell-interop-test-psk-0123456789" - -var errV5RawZeroChunk = errors.New("snell v5 raw zero chunk") - -func v5Config(psk string, port uint16) string { - return fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nipv6 = false\n", port, psk) -} - -func TestV5FirstResponseRecordShape(t *testing.T) { - for _, testCase := range []struct { - name string - official bool - }{ - {name: "official-v5.0.1", official: true}, - {name: "local-v5"}, - } { - t.Run(testCase.name, func(subTest *testing.T) { - var serverAddress string - var targetPort uint16 - if testCase.official { - port := freePort(subTest) - startSnellServer(subTest, "v5", v5Config(testPSK, port), port) - serverAddress = fmt.Sprintf("127.0.0.1:%d", port) - targetPort = startTCPEcho(subTest) - } else { - service, err := snellv5.NewService(snellv5.ServiceOptions{ - PSK: []byte(testPSK), - Handler: localEchoHandler{}, - }) - require.NoError(subTest, err) - serverAddress = startLocalSnellService(subTest, service) - targetPort = 443 - } - - serverConn, err := net.Dial("tcp", serverAddress) - require.NoError(subTest, err) - defer serverConn.Close() - - earlyPayload := []byte("v5 first response record shape") - host := "127.0.0.1" - requestPlain := []byte{snell.RequestVersion, snell.CommandConnectV2, 0, byte(len(host))} - requestPlain = append(requestPlain, host...) - requestPlain = binary.BigEndian.AppendUint16(requestPlain, targetPort) - requestPlain = append(requestPlain, earlyPayload...) - - clientSalt := make([]byte, snell.SaltLen) - _, err = rand.Read(clientSalt) - require.NoError(subTest, err) - clientAEAD, err := snell.NewAEAD(snell.DeriveKey([]byte(testPSK), clientSalt)) - require.NoError(subTest, err) - clientNonce := make([]byte, snell.NonceLen) - requestHeader := make([]byte, snell.HeaderPlainLen) - requestHeader[0] = snell.HeaderVersion - binary.BigEndian.PutUint16(requestHeader[5:7], uint16(len(requestPlain))) - clientWire := make([]byte, 0, snell.SaltLen+snell.HeaderCipherLen+len(requestPlain)+snell.AEADTagLen+snell.HeaderCipherLen) - clientWire = append(clientWire, clientSalt...) - clientWire = append(clientWire, clientAEAD.Seal(nil, clientNonce, requestHeader, nil)...) - snell.IncreaseNonce(clientNonce) - clientWire = append(clientWire, clientAEAD.Seal(nil, clientNonce, requestPlain, nil)...) - snell.IncreaseNonce(clientNonce) - clear(requestHeader) - requestHeader[0] = snell.HeaderVersion - clientWire = append(clientWire, clientAEAD.Seal(nil, clientNonce, requestHeader, nil)...) - - _, err = serverConn.Write(clientWire) - require.NoError(subTest, err) - err = serverConn.SetReadDeadline(time.Now().Add(10 * time.Second)) - require.NoError(subTest, err) - - responseSalt := make([]byte, snell.SaltLen) - _, err = io.ReadFull(serverConn, responseSalt) - require.NoError(subTest, err) - responseAEAD, err := snell.NewAEAD(snell.DeriveKey([]byte(testPSK), responseSalt)) - require.NoError(subTest, err) - responseNonce := make([]byte, snell.NonceLen) - responseHeaderCipher := make([]byte, snell.HeaderCipherLen) - _, err = io.ReadFull(serverConn, responseHeaderCipher) - require.NoError(subTest, err) - responseHeader, err := responseAEAD.Open(nil, responseNonce, responseHeaderCipher, nil) - require.NoError(subTest, err) - snell.IncreaseNonce(responseNonce) - require.Len(subTest, responseHeader, snell.HeaderPlainLen) - require.Equal(subTest, byte(snell.HeaderVersion), responseHeader[0]) - require.Equal(subTest, byte(0), responseHeader[1]) - require.Equal(subTest, byte(0), responseHeader[2]) - // snell-server v5.0.1: FUN_00139670 subtracts first-record padding - // from the first payload limit, and FUN_00138af0 serializes and mixes it. - responsePaddingLen := int(binary.BigEndian.Uint16(responseHeader[3:5])) - require.GreaterOrEqual(subTest, responsePaddingLen, 0x100) - require.LessOrEqual(subTest, responsePaddingLen, 0x1ff) - responsePayloadLen := int(binary.BigEndian.Uint16(responseHeader[5:7])) - require.Equal(subTest, 1+len(earlyPayload), responsePayloadLen) - responsePadding := make([]byte, responsePaddingLen) - _, err = io.ReadFull(serverConn, responsePadding) - require.NoError(subTest, err) - responsePayloadCipher := make([]byte, responsePayloadLen+snell.AEADTagLen) - _, err = io.ReadFull(serverConn, responsePayloadCipher) - require.NoError(subTest, err) - mixLimit := min(len(responsePadding), len(responsePayloadCipher)) - for index := 0; index < mixLimit; index += 2 { - responsePadding[index], responsePayloadCipher[index] = responsePayloadCipher[index], responsePadding[index] - } - responsePayload, err := responseAEAD.Open(nil, responseNonce, responsePayloadCipher, nil) - require.NoError(subTest, err) - expectedPayload := append([]byte{snell.ReplyTunnel}, earlyPayload...) - require.Equal(subTest, expectedPayload, responsePayload) - }) - } -} - -func TestV5TCP(t *testing.T) { - port := freePort(t) - startSnellServer(t, "v5", v5Config(testPSK, port), port) - echoPort := startTCPEcho(t) - - client, err := snellv4.NewClient(snellv4.ClientOptions{PSK: []byte(testPSK)}) - require.NoError(t, err) - serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) - require.NoError(t, err) - defer serverConn.Close() - - proxyConn, err := client.DialConn(serverConn, M.ParseSocksaddrHostPort("127.0.0.1", echoPort)) - require.NoError(t, err) - - payload := make([]byte, 1024*1024) - rand.Read(payload) - go func() { - proxyConn.Write(payload) - }() - received := make([]byte, len(payload)) - proxyConn.SetReadDeadline(time.Now().Add(20 * time.Second)) - _, err = io.ReadFull(proxyConn, received) - require.NoError(t, err) - require.True(t, bytes.Equal(payload, received), "payload mismatch") - normalScenario{address: fmt.Sprintf("127.0.0.1:%d", port), client: client}.HalfCloseEcho(t, "v5-official") -} - -func TestV5TCPVectorisedWriter(t *testing.T) { - port := freePort(t) - startSnellServer(t, "v5", v5Config(testPSK, port), port) - echoPort := startTCPEcho(t) - - client, err := snellv4.NewClient(snellv4.ClientOptions{PSK: []byte(testPSK)}) - require.NoError(t, err) - serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) - require.NoError(t, err) - defer serverConn.Close() - - proxyConn := client.DialEarlyConn(serverConn, M.ParseSocksaddrHostPort("127.0.0.1", echoPort)) - vectorisedWriter, created := bufio.CreateVectorisedWriter(proxyConn) - require.True(t, created) - - partA := make([]byte, 700) - partB := make([]byte, 900) - _, err = rand.Read(partA) - require.NoError(t, err) - _, err = rand.Read(partB) - require.NoError(t, err) - expected := append(append([]byte(nil), partA...), partB...) - - frontHeadroom := N.CalculateFrontHeadroom(proxyConn) - rearHeadroom := N.CalculateRearHeadroom(proxyConn) - buffers := make([]*buf.Buffer, 2) - for index, payload := range [][]byte{partA, partB} { - buffer := buf.NewSize(frontHeadroom + len(payload) + rearHeadroom) - buffer.Resize(frontHeadroom, 0) - _, err = buffer.Write(payload) - require.NoError(t, err) - buffers[index] = buffer - } - err = vectorisedWriter.WriteVectorised(buffers) - require.NoError(t, err) - - received := make([]byte, len(expected)) - require.NoError(t, proxyConn.SetReadDeadline(time.Now().Add(10*time.Second))) - _, err = io.ReadFull(proxyConn, received) - require.NoError(t, err) - require.Equal(t, expected, received) -} - -func TestV5TCPReuse(t *testing.T) { - port := freePort(t) - startSnellServer(t, "v5", v5Config(testPSK, port), port) - var proxy countingTCPProxy - proxy.Start(t, fmt.Sprintf("127.0.0.1:%d", port)) - echoPort := startTCPHalfCloseEcho(t) - delayedEchoPort := startTCPHalfCloseEcho(t, 750*time.Millisecond) - tailEchoPort := startTCPHalfCloseTailEcho(t, 32*1024, 250*time.Millisecond) - oversizedTailEchoPort := startTCPHalfCloseTailEcho(t, 0x80001, 250*time.Millisecond) - - client, err := snellv4.NewClient(snellv4.ClientOptions{ - PSK: []byte(testPSK), - Reuse: true, - Dialer: N.SystemDialer, - Server: M.ParseSocksaddr(proxy.address), - }) - require.NoError(t, err) - defer client.Close() - - scenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", echoPort)} - scenario.RoundTrip(t, "v5-reuse-1") - scenario.RoundTrip(t, "v5-reuse-2") - delayedScenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", delayedEchoPort)} - delayedScenario.CloseBeforeServerEOF(t, "v5-reuse-drain", time.Second) - scenario.RoundTrip(t, "v5-reuse-after-drain") - tailScenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", tailEchoPort)} - tailScenario.CloseBeforeServerEOF(t, "v5-reuse-tail", 500*time.Millisecond) - scenario.RoundTrip(t, "v5-reuse-after-tail") - require.Equal(t, int32(1), proxy.count.Load()) - oversizedTailScenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", oversizedTailEchoPort)} - oversizedTailScenario.CloseBeforeServerEOF(t, "v5-reuse-oversized-tail", 500*time.Millisecond) - scenario.RoundTrip(t, "v5-reuse-after-oversized-tail") - require.Equal(t, int32(2), proxy.count.Load()) -} - -func TestV5TCPReuseClientCloseLetsActiveReadFinish(t *testing.T) { - port := freePort(t) - startSnellServer(t, "v5", v5Config(testPSK, port), port) - var proxy countingTCPProxy - proxy.Start(t, fmt.Sprintf("127.0.0.1:%d", port)) - delayedEchoPort := startTCPHalfCloseEcho(t, 750*time.Millisecond) - echoPort := startTCPHalfCloseEcho(t) - - client, err := snellv4.NewClient(snellv4.ClientOptions{ - PSK: []byte(testPSK), - Reuse: true, - Dialer: N.SystemDialer, - Server: M.ParseSocksaddr(proxy.address), - }) - require.NoError(t, err) - defer client.Close() - - reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", delayedEchoPort)}.CloseWhileReadBlocked(t, "v5-reuse-active-read-close") - reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", echoPort)}.RoundTrip(t, "v5-reuse-after-active-read-close") - require.Equal(t, int32(1), proxy.count.Load()) -} - -func TestV5ObfsHTTP(t *testing.T) { - t.Run("tcp", func(t *testing.T) { - port := freePort(t) - startSnellServer(t, "v5", v5Config(testPSK, port)+"obfs = http\n", port) - echoPort := startTCPEcho(t) - - client, err := snellv4.NewClient(snellv4.ClientOptions{ - PSK: []byte(testPSK), - ObfsMode: snell.ObfsModeHTTP, - ObfsHost: "obfs.example", - }) - require.NoError(t, err) - serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) - require.NoError(t, err) - defer serverConn.Close() - - proxyConn, err := client.DialConn(serverConn, M.ParseSocksaddrHostPort("127.0.0.1", echoPort)) - require.NoError(t, err) - - payload := make([]byte, 256*1024) - rand.Read(payload) - go func() { - proxyConn.Write(payload) - }() - received := make([]byte, len(payload)) - proxyConn.SetReadDeadline(time.Now().Add(20 * time.Second)) - _, err = io.ReadFull(proxyConn, received) - require.NoError(t, err) - require.True(t, bytes.Equal(payload, received), "payload mismatch") - normalScenario{address: fmt.Sprintf("127.0.0.1:%d", port), client: client}.HalfCloseEcho(t, "v5-official-obfs") - }) - - t.Run("reuse", func(t *testing.T) { - port := freePort(t) - startSnellServer(t, "v5", v5Config(testPSK, port)+"obfs = http\n", port) - var proxy countingTCPProxy - proxy.Start(t, fmt.Sprintf("127.0.0.1:%d", port)) - echoPort := startTCPHalfCloseEcho(t) - - client, err := snellv4.NewClient(snellv4.ClientOptions{ - PSK: []byte(testPSK), - Reuse: true, - ObfsMode: snell.ObfsModeHTTP, - ObfsHost: "obfs.example", - Dialer: N.SystemDialer, - Server: M.ParseSocksaddr(proxy.address), - }) - require.NoError(t, err) - defer client.Close() - - scenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", echoPort)} - scenario.RoundTrip(t, "v5-obfs-reuse-1") - scenario.RoundTrip(t, "v5-obfs-reuse-2") - require.Equal(t, int32(1), proxy.count.Load()) - }) -} - -func TestV5UDP(t *testing.T) { - port := freePort(t) - startSnellServer(t, "v5", v5Config(testPSK, port), port) - echoPort := startUDPEcho(t) - - client, err := snellv4.NewClient(snellv4.ClientOptions{PSK: []byte(testPSK)}) - require.NoError(t, err) - target := M.ParseSocksaddrHostPort("127.0.0.1", echoPort).UDPAddr() - - message := make([]byte, 1200) - rand.Read(message) - serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) - require.NoError(t, err) - defer serverConn.Close() - packetConn, err := client.DialPacketConn(serverConn) - require.NoError(t, err) - _, err = packetConn.WriteTo(message, target) - require.NoError(t, err) - - received := make([]byte, 2048) - serverConn.SetReadDeadline(time.Now().Add(2 * time.Second)) - n, _, err := packetConn.ReadFrom(received) - require.NoError(t, err) - require.True(t, bytes.Equal(message, received[:n])) -} - -func TestV5PingReplyClosesConnection(t *testing.T) { - for _, testCase := range []struct { - name string - run func(*testing.T) string - }{ - { - name: "official-v5.0.1", - run: func(t *testing.T) string { - port := freePort(t) - startSnellServer(t, "v5", v5Config(testPSK, port), port) - return fmt.Sprintf("127.0.0.1:%d", port) - }, - }, - { - name: "local-v5", - run: func(t *testing.T) string { - service, err := snellv5.NewService(snellv5.ServiceOptions{ - PSK: []byte(testPSK), - Handler: localEchoHandler{}, - }) - require.NoError(t, err) - return startLocalSnellService(t, service) - }, - }, - { - name: "local-v5-multi-without-user", - run: func(t *testing.T) string { - service, err := snellv5.NewMultiService[int](snellv5.ServiceOptions{ - PSK: []byte(testPSK), - Handler: localEchoHandler{}, - }) - require.NoError(t, err) - return startLocalSnellService(t, service) - }, - }, - } { - t.Run(testCase.name, func(t *testing.T) { - serverConn, err := net.Dial("tcp", testCase.run(t)) - require.NoError(t, err) - defer serverConn.Close() - - rawConn := &v5RawConn{Conn: serverConn, psk: []byte(testPSK)} - salt := make([]byte, snell.SaltLen) - _, err = rand.Read(salt) - require.NoError(t, err) - rawConn.WriteFirstRecord(t, salt, []byte{snell.RequestVersion, snell.CommandPing, 1, 'x'}) - require.NoError(t, serverConn.SetReadDeadline(time.Now().Add(10*time.Second))) - reply, err := rawConn.ReadFirstRecord(t) - require.NoError(t, err) - require.Equal(t, []byte{snell.ReplyPong}, reply) - - _, err = rawConn.ReadRecord(t) - require.Error(t, err) - require.False(t, errors.Is(err, errV5RawZeroChunk), "ping closes the connection instead of writing a Snell zero chunk") - }) - } -} - -func TestV5LegacyCommandConnectClosesWithoutZeroChunkAfterReply(t *testing.T) { - for _, testCase := range []struct { - name string - run func(*testing.T) (string, uint16) - }{ - { - name: "official-v5.0.1", - run: func(t *testing.T) (string, uint16) { - port := freePort(t) - startSnellServer(t, "v5", v5Config(testPSK, port), port) - return fmt.Sprintf("127.0.0.1:%d", port), startTCPHalfCloseEcho(t) - }, - }, - { - name: "local-v5", - run: func(t *testing.T) (string, uint16) { - service, err := snellv5.NewService(snellv5.ServiceOptions{ - PSK: []byte(testPSK), - Handler: localEchoHandler{}, - }) - require.NoError(t, err) - return startLocalSnellService(t, service), 443 - }, - }, - } { - t.Run(testCase.name, func(t *testing.T) { - serverAddress, targetPort := testCase.run(t) - serverConn, err := net.Dial("tcp", serverAddress) - require.NoError(t, err) - defer serverConn.Close() - - payload := []byte("v5 legacy command connect") - host := "127.0.0.1" - request := []byte{snell.RequestVersion, snell.CommandConnect, 0, byte(len(host))} - request = append(request, host...) - request = binary.BigEndian.AppendUint16(request, targetPort) - request = append(request, payload...) - - rawConn := &v5RawConn{Conn: serverConn, psk: []byte(testPSK)} - salt := make([]byte, snell.SaltLen) - _, err = rand.Read(salt) - require.NoError(t, err) - rawConn.WriteFirstRecord(t, salt, request) - rawConn.WriteRecord(t, nil) - require.NoError(t, serverConn.SetReadDeadline(time.Now().Add(10*time.Second))) - reply, err := rawConn.ReadFirstRecord(t) - require.NoError(t, err) - require.Equal(t, append([]byte{snell.ReplyTunnel}, payload...), reply) - - _, err = rawConn.ReadRecord(t) - require.Error(t, err) - require.False(t, errors.Is(err, errV5RawZeroChunk), "legacy command-1 closes the TCP connection instead of writing a Snell zero chunk") - }) - } -} - -func TestV5SaltReplayRejectsDuplicate(t *testing.T) { - for _, testCase := range []struct { - name string - run func(*testing.T) (string, uint16) - }{ - { - name: "official-v5.0.1", - run: func(t *testing.T) (string, uint16) { - port := freePort(t) - startSnellServer(t, "v5", v5Config(testPSK, port), port) - return fmt.Sprintf("127.0.0.1:%d", port), startTCPHalfCloseEcho(t) - }, - }, - { - name: "local-v5", - run: func(t *testing.T) (string, uint16) { - service, err := snellv5.NewService(snellv5.ServiceOptions{ - PSK: []byte(testPSK), - Handler: localEchoHandler{}, - }) - require.NoError(t, err) - return startLocalSnellService(t, service), 443 - }, - }, - } { - t.Run(testCase.name, func(t *testing.T) { - serverAddress, targetPort := testCase.run(t) - payload := []byte("v5 duplicated salt") - host := "127.0.0.1" - request := []byte{snell.RequestVersion, snell.CommandConnectV2, 0, byte(len(host))} - request = append(request, host...) - request = binary.BigEndian.AppendUint16(request, targetPort) - request = append(request, payload...) - salt := make([]byte, snell.SaltLen) - _, err := rand.Read(salt) - require.NoError(t, err) - - firstServerConn, err := net.Dial("tcp", serverAddress) - require.NoError(t, err) - firstRawConn := &v5RawConn{Conn: firstServerConn, psk: []byte(testPSK)} - firstRawConn.WriteFirstRecord(t, salt, request) - firstRawConn.WriteRecord(t, nil) - require.NoError(t, firstServerConn.SetReadDeadline(time.Now().Add(10*time.Second))) - reply, err := firstRawConn.ReadFirstRecord(t) - require.NoError(t, err) - require.Equal(t, append([]byte{snell.ReplyTunnel}, payload...), reply) - firstServerConn.Close() - - secondServerConn, err := net.Dial("tcp", serverAddress) - require.NoError(t, err) - defer secondServerConn.Close() - secondRawConn := &v5RawConn{Conn: secondServerConn, psk: []byte(testPSK)} - secondRawConn.WriteFirstRecord(t, salt, request) - secondRawConn.WriteRecord(t, nil) - require.NoError(t, secondServerConn.SetReadDeadline(time.Now().Add(10*time.Second))) - _, err = secondRawConn.ReadFirstRecord(t) - require.Error(t, err) - }) - } -} - -type v5RawConn struct { - net.Conn - psk []byte - requestAEAD cipher.AEAD - requestNonce []byte - responseAEAD cipher.AEAD - responseNonce []byte -} - -func (c *v5RawConn) WriteFirstRecord(t *testing.T, salt []byte, payload []byte) { - requestAEAD, err := snell.NewAEAD(snell.DeriveKey(c.psk, salt)) - require.NoError(t, err) - c.requestAEAD = requestAEAD - c.requestNonce = make([]byte, snell.NonceLen) - record := c.SealRecord(t, payload) - wire := make([]byte, 0, len(salt)+len(record)) - wire = append(wire, salt...) - wire = append(wire, record...) - _, err = c.Conn.Write(wire) - require.NoError(t, err) -} - -func (c *v5RawConn) WriteRecord(t *testing.T, payload []byte) { - _, err := c.Conn.Write(c.SealRecord(t, payload)) - require.NoError(t, err) -} - -func (c *v5RawConn) SealRecord(t *testing.T, payload []byte) []byte { - header := make([]byte, snell.HeaderPlainLen) - header[0] = snell.HeaderVersion - binary.BigEndian.PutUint16(header[5:7], uint16(len(payload))) - record := c.requestAEAD.Seal(nil, c.requestNonce, header, nil) - snell.IncreaseNonce(c.requestNonce) - if len(payload) > 0 { - record = append(record, c.requestAEAD.Seal(nil, c.requestNonce, payload, nil)...) - snell.IncreaseNonce(c.requestNonce) - } - return record -} - -func (c *v5RawConn) ReadFirstRecord(t *testing.T) ([]byte, error) { - salt := make([]byte, snell.SaltLen) - _, err := io.ReadFull(c.Conn, salt) - if err != nil { - return nil, err - } - responseAEAD, err := snell.NewAEAD(snell.DeriveKey(c.psk, salt)) - require.NoError(t, err) - c.responseAEAD = responseAEAD - c.responseNonce = make([]byte, snell.NonceLen) - return c.ReadRecord(t) -} - -func (c *v5RawConn) ReadRecord(t *testing.T) ([]byte, error) { - headerCipher := make([]byte, snell.HeaderCipherLen) - _, err := io.ReadFull(c.Conn, headerCipher) - if err != nil { - return nil, err - } - header, err := c.responseAEAD.Open(nil, c.responseNonce, headerCipher, nil) - require.NoError(t, err) - snell.IncreaseNonce(c.responseNonce) - require.Len(t, header, snell.HeaderPlainLen) - require.Equal(t, byte(snell.HeaderVersion), header[0]) - paddingLen := int(binary.BigEndian.Uint16(header[3:5])) - payloadLen := int(binary.BigEndian.Uint16(header[5:7])) - if payloadLen == 0 { - return nil, errV5RawZeroChunk - } - padding := make([]byte, paddingLen) - if paddingLen > 0 { - _, err = io.ReadFull(c.Conn, padding) - if err != nil { - return nil, err - } - } - payloadCipher := make([]byte, payloadLen+snell.AEADTagLen) - _, err = io.ReadFull(c.Conn, payloadCipher) - if err != nil { - return nil, err - } - mixLimit := min(len(padding), len(payloadCipher)) - for index := 0; index < mixLimit; index += 2 { - padding[index], payloadCipher[index] = payloadCipher[index], padding[index] - } - payload, err := c.responseAEAD.Open(nil, c.responseNonce, payloadCipher, nil) - require.NoError(t, err) - snell.IncreaseNonce(c.responseNonce) - return payload, nil -} diff --git a/proxy/snell/sing-snell/test/v6_test.go b/proxy/snell/sing-snell/test/v6_test.go deleted file mode 100644 index 7d6ee0d9a4a..00000000000 --- a/proxy/snell/sing-snell/test/v6_test.go +++ /dev/null @@ -1,415 +0,0 @@ -package test - -import ( - "bytes" - "crypto/rand" - "fmt" - "io" - "net" - "testing" - "time" - - snell "github.com/sagernet/sing-snell" - "github.com/sagernet/sing-snell/snellv6" - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" - - "github.com/stretchr/testify/require" -) - -func v6Config(psk string, port uint16, mode string) string { - return fmt.Sprintf("[snell-server]\nlisten = 0.0.0.0:%d\npsk = %s\nmode = %s\n", port, psk, mode) -} - -var v6Modes = map[string]snellv6.Mode{ - "unshaped": snellv6.ModeUnshaped, - "unsafe-raw": snellv6.ModeUnsafeRaw, - "default": snellv6.ModeDefault, -} - -func TestV6TCP(t *testing.T) { - for name, mode := range v6Modes { - t.Run(name, func(t *testing.T) { - port := freePort(t) - startSnellServer(t, "v6", v6Config(testPSK, port, name), port) - echoPort := startTCPEcho(t) - - client, err := snellv6.NewClient(snellv6.ClientOptions{ - PSK: []byte(testPSK), - Mode: mode, - }) - require.NoError(t, err) - serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) - require.NoError(t, err) - defer serverConn.Close() - - proxyConn, err := client.DialConn(serverConn, M.ParseSocksaddrHostPort("127.0.0.1", echoPort)) - require.NoError(t, err) - - payload := make([]byte, 512*1024) - rand.Read(payload) - go func() { - proxyConn.Write(payload) - }() - received := make([]byte, len(payload)) - proxyConn.SetReadDeadline(time.Now().Add(20 * time.Second)) - _, err = io.ReadFull(proxyConn, received) - require.NoError(t, err) - require.True(t, bytes.Equal(payload, received), "payload mismatch") - normalScenario{address: fmt.Sprintf("127.0.0.1:%d", port), client: client}.HalfCloseEcho(t, "v6-official-"+name) - }) - } -} - -func TestV6TCPVectorisedWriter(t *testing.T) { - for name, mode := range v6Modes { - t.Run(name, func(t *testing.T) { - port := freePort(t) - startSnellServer(t, "v6", v6Config(testPSK, port, name), port) - echoPort := startTCPEcho(t) - - client, err := snellv6.NewClient(snellv6.ClientOptions{ - PSK: []byte(testPSK), - Mode: mode, - }) - require.NoError(t, err) - serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) - require.NoError(t, err) - defer serverConn.Close() - - proxyConn := client.DialEarlyConn(serverConn, M.ParseSocksaddrHostPort("127.0.0.1", echoPort)) - vectorisedWriter, created := bufio.CreateVectorisedWriter(proxyConn) - require.True(t, created) - - partA := make([]byte, 700) - partB := make([]byte, 900) - _, err = rand.Read(partA) - require.NoError(t, err) - _, err = rand.Read(partB) - require.NoError(t, err) - expected := append(append([]byte(nil), partA...), partB...) - - frontHeadroom := N.CalculateFrontHeadroom(proxyConn) - rearHeadroom := N.CalculateRearHeadroom(proxyConn) - buffers := make([]*buf.Buffer, 2) - for index, payload := range [][]byte{partA, partB} { - buffer := buf.NewSize(frontHeadroom + len(payload) + rearHeadroom) - buffer.Resize(frontHeadroom, 0) - _, err = buffer.Write(payload) - require.NoError(t, err) - buffers[index] = buffer - } - err = vectorisedWriter.WriteVectorised(buffers) - require.NoError(t, err) - - received := make([]byte, len(expected)) - require.NoError(t, proxyConn.SetReadDeadline(time.Now().Add(10*time.Second))) - _, err = io.ReadFull(proxyConn, received) - require.NoError(t, err) - require.Equal(t, expected, received) - }) - } -} - -func TestV6TCPUnshapedLargeWriteBufferFirstRecord(t *testing.T) { - port := freePort(t) - startSnellServer(t, "v6", v6Config(testPSK, port, "unshaped"), port) - echoPort := startTCPEcho(t) - - client, err := snellv6.NewClient(snellv6.ClientOptions{ - PSK: []byte(testPSK), - Mode: snellv6.ModeUnshaped, - }) - require.NoError(t, err) - serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) - require.NoError(t, err) - defer serverConn.Close() - - destination := M.ParseSocksaddrHostPort("127.0.0.1", echoPort) - proxyConn := client.DialEarlyConn(serverConn, destination) - defer proxyConn.Close() - - payload := make([]byte, 80*1024) - _, err = rand.Read(payload) - require.NoError(t, err) - request := snell.Request{Command: snell.CommandConnect, Destination: destination} - buffer := buf.NewSize(request.Len() + len(payload)) - buffer.Resize(request.Len(), 0) - n, err := buffer.Write(payload) - require.NoError(t, err) - require.Equal(t, len(payload), n) - - extendedWriter, ok := proxyConn.(N.ExtendedWriter) - require.True(t, ok) - err = extendedWriter.WriteBuffer(buffer) - require.NoError(t, err) - - received := make([]byte, len(payload)) - require.NoError(t, proxyConn.SetReadDeadline(time.Now().Add(20*time.Second))) - _, err = io.ReadFull(proxyConn, received) - require.NoError(t, err) - require.True(t, bytes.Equal(payload, received), "payload mismatch") -} - -func TestV6TCPReuse(t *testing.T) { - for name, mode := range v6Modes { - t.Run(name, func(t *testing.T) { - port := freePort(t) - startSnellServer(t, "v6", v6Config(testPSK, port, name), port) - var proxy countingTCPProxy - proxy.Start(t, fmt.Sprintf("127.0.0.1:%d", port)) - echoPort := startTCPHalfCloseEcho(t) - delayedEchoPort := startTCPHalfCloseEcho(t, 750*time.Millisecond) - tailEchoPort := startTCPHalfCloseTailEcho(t, 32*1024, 250*time.Millisecond) - oversizedTailEchoPort := startTCPHalfCloseTailEcho(t, 0x80001, 250*time.Millisecond) - - client, err := snellv6.NewClient(snellv6.ClientOptions{ - PSK: []byte(testPSK), - Mode: mode, - Reuse: true, - Dialer: N.SystemDialer, - Server: M.ParseSocksaddr(proxy.address), - }) - require.NoError(t, err) - defer client.Close() - - scenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", echoPort)} - scenario.RoundTrip(t, "v6-reuse-1-"+name) - scenario.RoundTrip(t, "v6-reuse-2-"+name) - delayedScenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", delayedEchoPort)} - delayedScenario.CloseBeforeServerEOF(t, "v6-reuse-drain-"+name, time.Second) - scenario.RoundTrip(t, "v6-reuse-after-drain-"+name) - tailScenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", tailEchoPort)} - tailScenario.CloseBeforeServerEOF(t, "v6-reuse-tail-"+name, 500*time.Millisecond) - scenario.RoundTrip(t, "v6-reuse-after-tail-"+name) - require.Equal(t, int32(1), proxy.count.Load()) - oversizedTailScenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", oversizedTailEchoPort)} - oversizedTailScenario.CloseBeforeServerEOF(t, "v6-reuse-oversized-tail-"+name, 500*time.Millisecond) - scenario.RoundTrip(t, "v6-reuse-after-oversized-tail-"+name) - require.Equal(t, int32(2), proxy.count.Load()) - }) - } -} - -func TestV6TCPReuseClientCloseCancelsDrain(t *testing.T) { - for name, mode := range v6Modes { - t.Run(name, func(t *testing.T) { - port := freePort(t) - startSnellServer(t, "v6", v6Config(testPSK, port, name), port) - var proxy countingTCPProxy - proxy.Start(t, fmt.Sprintf("127.0.0.1:%d", port)) - delayedEchoPort := startTCPHalfCloseEcho(t, 750*time.Millisecond) - - client, err := snellv6.NewClient(snellv6.ClientOptions{ - PSK: []byte(testPSK), - Mode: mode, - Reuse: true, - Dialer: N.SystemDialer, - Server: M.ParseSocksaddr(proxy.address), - }) - require.NoError(t, err) - - scenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", delayedEchoPort)} - scenario.CloseBeforeServerEOF(t, "v6-reuse-close-cancel-"+name, 0) - closeStart := time.Now() - require.NoError(t, client.Close()) - if time.Since(closeStart) > 250*time.Millisecond { - t.Fatal("client close waited for draining EOF instead of canceling the session") - } - }) - } -} - -func TestV6TCPReuseClientCloseLetsActiveReadFinish(t *testing.T) { - for name, mode := range v6Modes { - t.Run(name, func(t *testing.T) { - port := freePort(t) - startSnellServer(t, "v6", v6Config(testPSK, port, name), port) - var proxy countingTCPProxy - proxy.Start(t, fmt.Sprintf("127.0.0.1:%d", port)) - delayedEchoPort := startTCPHalfCloseEcho(t, 750*time.Millisecond) - echoPort := startTCPHalfCloseEcho(t) - - client, err := snellv6.NewClient(snellv6.ClientOptions{ - PSK: []byte(testPSK), - Mode: mode, - Reuse: true, - Dialer: N.SystemDialer, - Server: M.ParseSocksaddr(proxy.address), - }) - require.NoError(t, err) - defer client.Close() - - reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", delayedEchoPort)}.CloseWhileReadBlocked(t, "v6-reuse-active-read-close-"+name) - reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", echoPort)}.RoundTrip(t, "v6-reuse-after-active-read-close-"+name) - require.Equal(t, int32(1), proxy.count.Load()) - }) - } -} - -func TestV6ServerAcceptsV6ClientLoopback(t *testing.T) { - for name, mode := range v6Modes { - t.Run(name, func(t *testing.T) { - service, err := snellv6.NewService(snellv6.ServerOptions{ - PSK: []byte(testPSK), - Mode: mode, - Handler: localEchoHandler{}, - }) - require.NoError(t, err) - serviceAddress := startLocalSnellService(t, service) - var proxy countingTCPProxy - proxy.Start(t, serviceAddress) - - client, err := snellv6.NewClient(snellv6.ClientOptions{ - PSK: []byte(testPSK), - Mode: mode, - Reuse: true, - Dialer: N.SystemDialer, - Server: M.ParseSocksaddr(proxy.address), - }) - require.NoError(t, err) - defer client.Close() - - scenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", 443)} - scenario.RoundTrip(t, "v6-service-v6-client-reuse-1-"+name) - scenario.RoundTrip(t, "v6-service-v6-client-reuse-2-"+name) - require.Equal(t, int32(1), proxy.count.Load()) - }) - } -} - -func TestV6ServerReuseHandlerCloseBeforeClientEOF(t *testing.T) { - for name, mode := range v6Modes { - t.Run(name, func(t *testing.T) { - service, err := snellv6.NewService(snellv6.ServerOptions{ - PSK: []byte(testPSK), - Mode: mode, - Handler: partialEchoHandler{echoLen: 4 * 1024}, - }) - require.NoError(t, err) - serviceAddress := startLocalSnellService(t, service) - var proxy countingTCPProxy - proxy.Start(t, serviceAddress) - - client, err := snellv6.NewClient(snellv6.ClientOptions{ - PSK: []byte(testPSK), - Mode: mode, - Reuse: true, - Dialer: N.SystemDialer, - Server: M.ParseSocksaddr(proxy.address), - }) - require.NoError(t, err) - defer client.Close() - - scenario := reuseScenario{client: client, destination: M.ParseSocksaddrHostPort("127.0.0.1", 443)} - scenario.PartialEchoRoundTrip(t, "v6-server-drain-1-"+name, 8*1024, 4*1024) - scenario.PartialEchoRoundTrip(t, "v6-server-drain-2-"+name, 8*1024, 4*1024) - require.Equal(t, int32(1), proxy.count.Load()) - }) - } -} - -func TestV6UDP(t *testing.T) { - for name, mode := range v6Modes { - t.Run(name, func(t *testing.T) { - port := freePort(t) - startSnellServer(t, "v6", v6Config(testPSK, port, name), port) - echoPort := startUDPEcho(t) - - client, err := snellv6.NewClient(snellv6.ClientOptions{ - PSK: []byte(testPSK), - Mode: mode, - }) - require.NoError(t, err) - serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) - require.NoError(t, err) - defer serverConn.Close() - - packetConn, err := client.DialPacketConn(serverConn) - require.NoError(t, err) - target := M.ParseSocksaddrHostPort("127.0.0.1", echoPort).UDPAddr() - - for round := range 10 { - messageLen := 1200 - if mode == snellv6.ModeDefault { - messageLen = 32 - } - message := make([]byte, messageLen) - rand.Read(message) - _, err = packetConn.WriteTo(message, target) - require.NoError(t, err) - - received := make([]byte, 2048) - serverConn.SetReadDeadline(time.Now().Add(10 * time.Second)) - n, _, readErr := packetConn.ReadFrom(received) - require.NoError(t, readErr) - require.True(t, bytes.Equal(message, received[:n]), "round %d", round) - } - }) - } -} - -func TestV6UDPPacketBatchWriter(t *testing.T) { - for name, mode := range v6Modes { - t.Run(name, func(t *testing.T) { - port := freePort(t) - startSnellServer(t, "v6", v6Config(testPSK, port, name), port) - echoPort := startUDPEcho(t) - - client, err := snellv6.NewClient(snellv6.ClientOptions{ - PSK: []byte(testPSK), - Mode: mode, - }) - require.NoError(t, err) - serverConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) - require.NoError(t, err) - defer serverConn.Close() - - packetConn, err := client.DialPacketConn(serverConn) - require.NoError(t, err) - batchWriter, created := bufio.CreatePacketBatchWriter(packetConn) - require.True(t, created) - - target := M.ParseSocksaddrHostPort("127.0.0.1", echoPort) - frontHeadroom := N.CalculateFrontHeadroom(packetConn) - rearHeadroom := N.CalculateRearHeadroom(packetConn) - payloadSize := 1200 - if mode == snellv6.ModeDefault { - payloadSize = 32 - } - payloads := make([][]byte, 3) - buffers := make([]*buf.Buffer, len(payloads)) - destinations := make([]M.Socksaddr, len(payloads)) - for index := range payloads { - payload := make([]byte, payloadSize) - _, err = rand.Read(payload) - require.NoError(t, err) - payload[0] = byte(index) - payloads[index] = payload - buffer := buf.NewSize(frontHeadroom + len(payload) + rearHeadroom) - buffer.Resize(frontHeadroom, 0) - _, err = buffer.Write(payload) - require.NoError(t, err) - buffers[index] = buffer - destinations[index] = target - } - err = batchWriter.WritePacketBatch(buffers, destinations) - require.NoError(t, err) - - received := make(map[string]int, len(payloads)) - for range len(payloads) { - reply := make([]byte, 2048) - err = packetConn.SetReadDeadline(time.Now().Add(10 * time.Second)) - require.NoError(t, err) - n, _, readErr := packetConn.ReadFrom(reply) - require.NoError(t, readErr) - received[string(reply[:n])]++ - } - for _, payload := range payloads { - require.Equal(t, 1, received[string(payload)]) - } - }) - } -} From 00569253d1f751f57cb59f8e3700cdf4e74e7491 Mon Sep 17 00:00:00 2001 From: Minis <285644962+Minis233@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:27:39 +0800 Subject: [PATCH 07/15] fix(snell): strip sing v0.8.12-only vectorised APIs exclave-core is on sing v0.8.11. Drop VectorisedWriteCreator / PacketBatch* paths and server-only reuse types from inlined sing-snell so the client builds cleanly while keeping Snell v4/v6 wire support. --- .../singsnell/internal/reuse/reuse.go | 2 - .../internal/singsnell/snellv4/client.go | 45 - .../internal/singsnell/snellv4/packet.go | 50 -- .../internal/singsnell/snellv4/record.go | 140 +--- .../snell/internal/singsnell/snellv4/reuse.go | 4 - .../internal/singsnell/snellv5/packet.go | 289 ------- .../internal/singsnell/snellv5/record.go | 790 ------------------ .../snell/internal/singsnell/snellv5/reuse.go | 482 ----------- .../internal/singsnell/snellv5/server.go | 695 --------------- .../internal/singsnell/snellv6/client.go | 45 - .../internal/singsnell/snellv6/packet.go | 98 --- .../internal/singsnell/snellv6/record.go | 147 +--- .../snell/internal/singsnell/snellv6/reuse.go | 369 -------- .../internal/singsnell/snellv6/server.go | 460 ---------- .../internal/singsnell/snellv6/shaped.go | 89 +- 15 files changed, 48 insertions(+), 3657 deletions(-) delete mode 100644 proxy/snell/internal/singsnell/snellv5/packet.go delete mode 100644 proxy/snell/internal/singsnell/snellv5/record.go delete mode 100644 proxy/snell/internal/singsnell/snellv5/reuse.go delete mode 100644 proxy/snell/internal/singsnell/snellv5/server.go delete mode 100644 proxy/snell/internal/singsnell/snellv6/server.go diff --git a/proxy/snell/internal/singsnell/internal/reuse/reuse.go b/proxy/snell/internal/singsnell/internal/reuse/reuse.go index c5e209138f9..64f72a0bf3a 100644 --- a/proxy/snell/internal/singsnell/internal/reuse/reuse.go +++ b/proxy/snell/internal/singsnell/internal/reuse/reuse.go @@ -240,8 +240,6 @@ type RecordReader interface { type RecordWriter interface { N.ExtendedWriter - N.VectorisedWriteCreator - CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter CreatePacketVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter WritePacketBuffer(buffer *buf.Buffer) error WriteZeroChunk() error diff --git a/proxy/snell/internal/singsnell/snellv4/client.go b/proxy/snell/internal/singsnell/snellv4/client.go index e384affdf6a..2b017f627f4 100644 --- a/proxy/snell/internal/singsnell/snellv4/client.go +++ b/proxy/snell/internal/singsnell/snellv4/client.go @@ -203,52 +203,8 @@ func (c *clientConn) WriteBuffer(buffer *buf.Buffer) error { return c.writeRequestBuffer(buffer) } -func (c *clientConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - upstreamWriter, created := bufio.CreateVectorisedWriter(c.Conn) - if !created { - return nil, false - } - return &clientVectorisedWriter{conn: c, upstream: upstreamWriter}, true -} -type clientVectorisedWriter struct { - conn *clientConn - upstream N.VectorisedWriter -} -func (w *clientVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { - conn := w.conn - if conn.writer != nil { - return conn.writer.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) - } - conn.access.Lock() - if conn.writer != nil { - recordWriter := conn.writer - conn.access.Unlock() - return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) - } - for index, buffer := range buffers { - if buffer.IsEmpty() { - buffer.Release() - continue - } - err := conn.writeRequestBuffer(buffer) - if err != nil { - conn.access.Unlock() - buf.ReleaseMulti(buffers[index+1:]) - return err - } - if index+1 < len(buffers) { - recordWriter := conn.writer - conn.access.Unlock() - return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers[index+1:]) - } - conn.access.Unlock() - return nil - } - conn.access.Unlock() - return nil -} func (c *clientConn) CloseWrite() error { c.closeWriteOnce.Do(func() { @@ -325,7 +281,6 @@ var ( _ N.ExtendedConn = (*clientConn)(nil) _ N.ReadWaiter = (*clientConn)(nil) _ N.ReadWaitCreator = (*clientConn)(nil) - _ N.VectorisedWriteCreator = (*clientConn)(nil) _ N.VectorisedWriter = (*clientVectorisedWriter)(nil) _ N.EarlyReader = (*clientConn)(nil) _ N.EarlyWriter = (*clientConn)(nil) diff --git a/proxy/snell/internal/singsnell/snellv4/packet.go b/proxy/snell/internal/singsnell/snellv4/packet.go index 41d9821945b..57c139c4b63 100644 --- a/proxy/snell/internal/singsnell/snellv4/packet.go +++ b/proxy/snell/internal/singsnell/snellv4/packet.go @@ -117,56 +117,8 @@ func (c *clientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksad return c.writer.WritePacketBuffer(buffer) } -func (c *clientPacketConn) CreatePacketBatchWriter() (N.PacketBatchWriter, bool) { - upstreamWriter, created := bufio.CreateVectorisedWriter(c.Conn) - if !created { - return nil, false - } - return &clientPacketBatchWriter{conn: c, upstream: upstreamWriter}, true -} -type clientPacketBatchWriter struct { - conn *clientPacketConn - upstream N.VectorisedWriter - - access sync.Mutex - writer N.VectorisedWriter -} -func (w *clientPacketBatchWriter) WritePacketBatch(buffers []*buf.Buffer, destinations []M.Socksaddr) error { - if len(buffers) == 0 || len(buffers) != len(destinations) { - buf.ReleaseMulti(buffers) - return os.ErrInvalid - } - w.access.Lock() - if w.writer == nil { - err := w.conn.writeRequest() - if err != nil { - w.access.Unlock() - buf.ReleaseMulti(buffers) - return err - } - _, err = w.conn.readReply() - if err != nil { - w.access.Unlock() - buf.ReleaseMulti(buffers) - return err - } - w.writer = w.conn.writer.CreatePacketVectorisedWriterFor(w.upstream) - } - recordWriter := w.writer - w.access.Unlock() - for index, buffer := range buffers { - header := buf.With(buffer.ExtendHeader(1 + w.conn.udpRequestAddrLen(destinations[index]))) - common.Must(header.WriteByte(snell.UDPCommandForward)) - err := snell.WriteUDPRequestAddress(header, destinations[index]) - if err != nil { - buf.ReleaseMulti(buffers) - return err - } - } - return recordWriter.WriteVectorised(buffers) -} func (c *clientPacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) { recordReader, err := c.readReply() @@ -266,7 +218,5 @@ func (c *clientPacketConn) WaitReadPacket() (*buf.Buffer, M.Socksaddr, error) { var ( _ N.PacketConn = (*clientPacketConn)(nil) _ N.PacketReadWaiter = (*clientPacketConn)(nil) - _ N.PacketBatchWriteCreator = (*clientPacketConn)(nil) _ N.WriterWithMTU = (*clientPacketConn)(nil) - _ N.PacketBatchWriter = (*clientPacketBatchWriter)(nil) ) diff --git a/proxy/snell/internal/singsnell/snellv4/record.go b/proxy/snell/internal/singsnell/snellv4/record.go index 4717cee050a..ea7ea2110f6 100644 --- a/proxy/snell/internal/singsnell/snellv4/record.go +++ b/proxy/snell/internal/singsnell/snellv4/record.go @@ -618,155 +618,40 @@ func (w *writer) WriteBuffer(buffer *buf.Buffer) error { return w.writeBufferRecordLocked(buffer, paddingLen) } -func (w *writer) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - upstreamWriter, created := bufio.CreateVectorisedWriter(w.upstream) - if !created { - return nil, false - } - return w.CreateVectorisedWriterFor(upstreamWriter), true -} -func (w *writer) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { - return &vectorisedWriter{writer: w, upstream: upstream} -} -type vectorisedWriter struct { - writer *writer - upstream N.VectorisedWriter -} func (w *vectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { - payloadLen := buf.LenMulti(buffers) - if payloadLen == 0 { - buf.ReleaseMulti(buffers) - return nil - } - var records []*buf.Buffer - defer func() { - buf.ReleaseMulti(records) - }() - recordWriter := w.writer - recordWriter.access.Lock() - defer recordWriter.access.Unlock() - err := recordWriter.initialize() - if err != nil { - buf.ReleaseMulti(buffers) - return err - } - nowUnix := time.Now().Unix() - payloadLimit := recordWriter.payloadLimitFor(nowUnix) - if payloadLimit <= 0 || payloadLimit > maxPayload { - panic("snell: invalid v4 payload limit") - } - // Surge 6.7.0 (11520): SNConnectorEngineAdapterV4::encryptData:outData: - // advances the growth window once per encrypt call and splits that call - // using the same payload limit. - recordWriter.advancePayloadLimit(payloadLimit, nowUnix) - index := 0 - for remainingPayload := payloadLen; remainingPayload > 0; { - for buffers[index].IsEmpty() { - buffers[index].Release() - index++ + // Compatibility fallback for sing v0.8.11 (no VectorisedWriteCreator chaining). + for _, buffer := range buffers { + if buffer.IsEmpty() { + continue } - recordLen := min(remainingPayload, payloadLimit) - paddingLen := recordWriter.framePaddingLen(recordLen) - buffer := buffers[index] - if buffer.Len() == recordLen { - record, err := recordWriter.makeBufferRecordLocked(buffer, paddingLen) - if err != nil { - buffer.Release() - buf.ReleaseMulti(buffers[index+1:]) - return err - } - records = append(records, record) - index++ - } else { - payload := make([]byte, recordLen) - for copiedLen := 0; copiedLen < recordLen; { - buffer = buffers[index] - if buffer.IsEmpty() { - buffer.Release() - index++ - continue - } - copyLen := min(recordLen-copiedLen, buffer.Len()) - copy(payload[copiedLen:], buffer.Bytes()[:copyLen]) - buffer.Advance(copyLen) - copiedLen += copyLen - if buffer.IsEmpty() { - buffer.Release() - index++ - } - } - record, err := recordWriter.makeSliceRecordLocked(payload, paddingLen) - if err != nil { - buf.ReleaseMulti(buffers[index:]) - return err - } - records = append(records, record) + _, err := w.Write(buffer.Bytes()) + if err != nil { + return err } - remainingPayload -= recordLen } - buf.ReleaseMulti(buffers[index:]) - flushRecords := records - records = nil - return w.upstream.WriteVectorised(flushRecords) + return nil } func (w *writer) CreatePacketVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { return &packetVectorisedWriter{writer: w, upstream: upstream} } -type packetVectorisedWriter struct { - writer *writer - upstream N.VectorisedWriter -} func (w *packetVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { - var records []*buf.Buffer - defer func() { - buf.ReleaseMulti(records) - }() - recordWriter := w.writer - recordWriter.access.Lock() - defer recordWriter.access.Unlock() - err := recordWriter.initialize() - if err != nil { - buf.ReleaseMulti(buffers) - return err - } - for index, buffer := range buffers { + // Compatibility fallback for sing v0.8.11 (no VectorisedWriteCreator chaining). + for _, buffer := range buffers { if buffer.IsEmpty() { - buffer.Release() continue } - nowUnix := time.Now().Unix() - payloadLimit := recordWriter.payloadLimitFor(nowUnix) - if payloadLimit <= 0 || payloadLimit > maxPayload { - panic("snell: invalid v4 payload limit") - } - dataLen := buffer.Len() - if dataLen > payloadLimit { - buffer.Release() - buf.ReleaseMulti(buffers[index+1:]) - return snell.ErrPayloadTooLarge - } - recordWriter.advancePayloadLimit(payloadLimit, nowUnix) - paddingLen := recordWriter.framePaddingLen(dataLen) - record, err := recordWriter.makeBufferRecordLocked(buffer, paddingLen) + _, err := w.Write(buffer.Bytes()) if err != nil { - buffer.Release() - buf.ReleaseMulti(buffers[index+1:]) return err } - records = append(records, record) } - if len(records) == 0 { - return nil - } - flushRecords := records - records = nil - return w.upstream.WriteVectorised(flushRecords) + return nil } func (w *writer) WritePacketBuffer(buffer *buf.Buffer) error { @@ -821,7 +706,6 @@ var ( _ N.ExtendedReader = (*reader)(nil) _ N.ReadWaiter = (*reader)(nil) _ N.ExtendedWriter = (*writer)(nil) - _ N.VectorisedWriteCreator = (*writer)(nil) _ N.VectorisedWriter = (*vectorisedWriter)(nil) _ N.VectorisedWriter = (*packetVectorisedWriter)(nil) _ N.FrontHeadroom = (*writer)(nil) diff --git a/proxy/snell/internal/singsnell/snellv4/reuse.go b/proxy/snell/internal/singsnell/snellv4/reuse.go index c166b254014..4d5517f2166 100644 --- a/proxy/snell/internal/singsnell/snellv4/reuse.go +++ b/proxy/snell/internal/singsnell/snellv4/reuse.go @@ -312,9 +312,6 @@ func (c *reuseConn) WriteBuffer(buffer *buf.Buffer) error { return c.session.writer.WriteBuffer(buffer) } -func (c *reuseConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - return c.session.writer.CreateVectorisedWriter() -} func (c *reuseConn) CloseWrite() error { c.closeWriteOnce.Do(func() { @@ -455,7 +452,6 @@ func (c *reuseConn) RemoteAddr() net.Addr { var ( _ N.ExtendedConn = (*reuseConn)(nil) _ N.ReadWaitCreator = (*reuseConn)(nil) - _ N.VectorisedWriteCreator = (*reuseConn)(nil) _ N.EarlyReader = (*reuseConn)(nil) _ N.WriteCloser = (*reuseConn)(nil) _ N.ReadWaiter = (*reuseReadWaiter)(nil) diff --git a/proxy/snell/internal/singsnell/snellv5/packet.go b/proxy/snell/internal/singsnell/snellv5/packet.go deleted file mode 100644 index 10828cbde5b..00000000000 --- a/proxy/snell/internal/singsnell/snellv5/packet.go +++ /dev/null @@ -1,289 +0,0 @@ -package snellv5 - -import ( - "crypto/rand" - "encoding/binary" - "io" - "net" - "net/netip" - "os" - "sync" - - snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" - "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" - E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -const ( - maxUDPResponseHeaderLen = 1 + 16 + 2 - minUDPPayloadLimit = framePayloadStep -) - -type serverPacketConn struct { - net.Conn - service *Service - reader reuse.RecordReader - readWaitOptions N.ReadWaitOptions - - writeAccess sync.Mutex - writer reuse.RecordWriter -} - -func (c *serverPacketConn) responseAddrLen(source M.Socksaddr) int { - if source.Unwrap().Addr.Is4() { - return 1 + 4 + 2 - } - return 1 + 16 + 2 -} - -func (c *serverPacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) { - record, err := c.reader.NextRecord() - if err != nil { - return M.Socksaddr{}, err - } - command, err := record.ReadByte() - if err != nil { - record.Release() - return M.Socksaddr{}, err - } - if command != snell.UDPCommandForward { - record.Release() - return M.Socksaddr{}, E.Extend(snell.ErrUnsupportedCommand, "udp ", command) - } - destination, err := c.readRequestPacketDestination(record) - if err != nil { - record.Release() - return M.Socksaddr{}, err - } - if record.Len() > buffer.FreeLen() { - record.Release() - return M.Socksaddr{}, io.ErrShortBuffer - } - _, err = buffer.Write(record.Bytes()) - record.Release() - if err != nil { - return M.Socksaddr{}, err - } - return destination, nil -} - -func (c *serverPacketConn) readRequestPacketDestination(record *buf.Buffer) (M.Socksaddr, error) { - first, err := record.ReadByte() - if err != nil { - return M.Socksaddr{}, err - } - if first != 0x00 { - var host []byte - host, err = record.ReadBytes(int(first)) - if err != nil { - return M.Socksaddr{}, err - } - var portBytes []byte - portBytes, err = record.ReadBytes(2) - if err != nil { - return M.Socksaddr{}, err - } - return M.ParseSocksaddrHostPort(string(host), binary.BigEndian.Uint16(portBytes)), nil - } - addressFamily, err := record.ReadByte() - if err != nil { - return M.Socksaddr{}, err - } - switch addressFamily { - case snell.AddressTypeIPv4: - var addressBytes []byte - addressBytes, err = record.ReadBytes(4) - if err != nil { - return M.Socksaddr{}, err - } - var portBytes []byte - portBytes, err = record.ReadBytes(2) - if err != nil { - return M.Socksaddr{}, err - } - var address [4]byte - copy(address[:], addressBytes) - return M.SocksaddrFrom(netip.AddrFrom4(address), binary.BigEndian.Uint16(portBytes)), nil - case snell.AddressTypeIPv6: - var addressBytes []byte - addressBytes, err = record.ReadBytes(16) - if err != nil { - return M.Socksaddr{}, err - } - var portBytes []byte - portBytes, err = record.ReadBytes(2) - if err != nil { - return M.Socksaddr{}, err - } - var address [16]byte - copy(address[:], addressBytes) - return M.SocksaddrFrom(netip.AddrFrom16(address), binary.BigEndian.Uint16(portBytes)).Unwrap(), nil - default: - // snell-server v5.0.1: FUN_00140a50 consumes the 0x00 marker and the - // unknown address-family byte, then forwards the rest as payload with an - // empty destination instead of aborting the UDP tunnel. - return M.Socksaddr{}, nil - } -} - -func (c *serverPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { - c.writeAccess.Lock() - if c.writer == nil { - err := c.writeTunnelReply() - if err != nil { - c.writeAccess.Unlock() - buffer.Release() - return err - } - } - recordWriter := c.writer - c.writeAccess.Unlock() - header := buf.With(buffer.ExtendHeader(c.responseAddrLen(destination))) - err := snell.WriteUDPResponseAddress(header, destination) - if err != nil { - buffer.Release() - return err - } - if buffer.Len() > maxPayload { - buffer.Release() - return snell.ErrPayloadTooLarge - } - return recordWriter.WritePacketBuffer(buffer) -} - -func (c *serverPacketConn) CreatePacketBatchWriter() (N.PacketBatchWriter, bool) { - upstreamWriter, created := bufio.CreateVectorisedWriter(c.Conn) - if !created { - return nil, false - } - return &serverPacketBatchWriter{conn: c, upstream: upstreamWriter}, true -} - -type serverPacketBatchWriter struct { - conn *serverPacketConn - upstream N.VectorisedWriter - - access sync.Mutex - writer N.VectorisedWriter -} - -func (w *serverPacketBatchWriter) WritePacketBatch(buffers []*buf.Buffer, destinations []M.Socksaddr) error { - if len(buffers) == 0 || len(buffers) != len(destinations) { - buf.ReleaseMulti(buffers) - return os.ErrInvalid - } - w.access.Lock() - if w.writer == nil { - w.conn.writeAccess.Lock() - if w.conn.writer == nil { - err := w.conn.writeTunnelReply() - if err != nil { - w.conn.writeAccess.Unlock() - w.access.Unlock() - buf.ReleaseMulti(buffers) - return err - } - } - w.writer = w.conn.writer.CreatePacketVectorisedWriterFor(w.upstream) - w.conn.writeAccess.Unlock() - } - recordWriter := w.writer - w.access.Unlock() - for index, buffer := range buffers { - header := buf.With(buffer.ExtendHeader(w.conn.responseAddrLen(destinations[index]))) - err := snell.WriteUDPResponseAddress(header, destinations[index]) - if err != nil { - buf.ReleaseMulti(buffers) - return err - } - } - return recordWriter.WriteVectorised(buffers) -} - -func (c *serverPacketConn) writeTunnelReply() error { - reply := [1]byte{snell.ReplyTunnel} - if c.writer != nil { - _, err := c.writer.Write(reply[:]) - if err != nil { - return E.Cause(err, "write udp reply") - } - return nil - } - salt := make([]byte, snell.SaltLen) - _, err := io.ReadFull(rand.Reader, salt) - if err != nil { - return err - } - key := snell.DeriveKey(c.service.psk, salt) - aead, err := snell.NewAEAD(key) - if err != nil { - return err - } - nonce := make([]byte, snell.NonceLen) - recordWriter, err := newWriter(c.Conn, aead, nonce) - if err != nil { - return err - } - err = recordWriter.WriteFirst(salt, reply[:]) - if err != nil { - return E.Cause(err, "write udp reply") - } - c.writer = recordWriter - return nil -} - -func (c *serverPacketConn) FrontHeadroom() int { - return snell.HeaderCipherLen + maxUDPResponseHeaderLen -} - -func (c *serverPacketConn) RearHeadroom() int { - return snell.AEADTagLen -} - -func (c *serverPacketConn) WriterMTU() int { - return minUDPPayloadLimit - maxUDPResponseHeaderLen -} - -func (c *serverPacketConn) Upstream() any { - return c.Conn -} - -func (c *serverPacketConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { - c.readWaitOptions = options - c.reader.InitializeReadWaiter(options) - return false -} - -func (c *serverPacketConn) WaitReadPacket() (*buf.Buffer, M.Socksaddr, error) { - record, err := c.reader.WaitReadBuffer() - if err != nil { - return nil, M.Socksaddr{}, err - } - command, err := record.ReadByte() - if err != nil { - record.Release() - return nil, M.Socksaddr{}, err - } - if command != snell.UDPCommandForward { - record.Release() - return nil, M.Socksaddr{}, E.Extend(snell.ErrUnsupportedCommand, "udp ", command) - } - destination, err := c.readRequestPacketDestination(record) - if err != nil { - record.Release() - return nil, M.Socksaddr{}, err - } - return record, destination, nil -} - -var ( - _ N.PacketConn = (*serverPacketConn)(nil) - _ N.PacketReadWaiter = (*serverPacketConn)(nil) - _ N.PacketBatchWriteCreator = (*serverPacketConn)(nil) - _ N.WriterWithMTU = (*serverPacketConn)(nil) - _ N.PacketBatchWriter = (*serverPacketBatchWriter)(nil) -) diff --git a/proxy/snell/internal/singsnell/snellv5/record.go b/proxy/snell/internal/singsnell/snellv5/record.go deleted file mode 100644 index 3a106ba5476..00000000000 --- a/proxy/snell/internal/singsnell/snellv5/record.go +++ /dev/null @@ -1,790 +0,0 @@ -package snellv5 - -import ( - "crypto/cipher" - "crypto/rand" - "encoding/binary" - "io" - "math/big" - "math/bits" - "sync" - "time" - - snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" - E "github.com/sagernet/sing/common/exceptions" - N "github.com/sagernet/sing/common/network" -) - -const ( - // snell-server v5.0.1: FUN_0013bbe0 calls getsockopt(TCP_MAXSEG) on the - // accepted client socket but, on the normal success path, passes 0x5b4 to - // FUN_00139080; FUN_00139670 then computes the first payload limit as - // frameSize - 0x37 - paddingLen, and uses frameSize - 0x27 for growth. - maxPayload = snell.MaxPayloadLen - frameSize = 0x5b4 - firstRecordOverhead = 0x37 - resetRecordOverhead = 0x27 - initialPaddingMin = 0x100 - initialPaddingSpan = 0x100 - maxInitialPaddingLen = initialPaddingMin + initialPaddingSpan - 1 - framePayloadStep = frameSize - resetRecordOverhead - framePayloadResetInterval = 31 -) - -type reader struct { - upstream io.Reader - cipher cipher.AEAD - nonce []byte - cache *buf.Buffer - readWaitOptions N.ReadWaitOptions -} - -func newReader(upstream io.Reader, aead cipher.AEAD, nonce []byte) *reader { - return &reader{upstream: upstream, cipher: aead, nonce: nonce} -} - -func (r *reader) ReadRecord() (*buf.Buffer, error) { - headerCipher := buf.NewSize(snell.HeaderCipherLen) - _, err := headerCipher.ReadFullFrom(r.upstream, snell.HeaderCipherLen) - if err != nil { - headerCipher.Release() - return nil, err - } - _, err = r.cipher.Open(headerCipher.Index(0), r.nonce, headerCipher.Bytes(), nil) - if err != nil { - headerCipher.Release() - return nil, E.Cause(err, "open record header") - } - snell.IncreaseNonce(r.nonce) - header := headerCipher.To(snell.HeaderPlainLen) - if header[0] != snell.HeaderVersion { - headerCipher.Release() - return nil, E.Extend(snell.ErrBadVersion, header[0]) - } - paddingLen := int(binary.BigEndian.Uint16(header[3:5])) - payloadLen := int(binary.BigEndian.Uint16(header[5:7])) - headerCipher.Release() - - if payloadLen == 0 { - return nil, io.EOF - } - - var padding *buf.Buffer - if paddingLen > 0 { - padding = buf.NewSize(paddingLen) - _, err = padding.ReadFullFrom(r.upstream, paddingLen) - if err != nil { - padding.Release() - return nil, err - } - } - body := r.readWaitOptions.NewBufferSize(payloadLen + snell.AEADTagLen) - _, err = body.ReadFullFrom(r.upstream, payloadLen+snell.AEADTagLen) - if err != nil { - if padding != nil { - padding.Release() - } - body.Release() - return nil, err - } - if padding != nil { - paddingBytes := padding.Bytes() - payloadCipher := body.Bytes() - limit := min(len(payloadCipher), len(paddingBytes)) - for index := 0; index < limit; index += 2 { - paddingBytes[index], payloadCipher[index] = payloadCipher[index], paddingBytes[index] - } - padding.Release() - } - payloadCipher := body.Bytes() - _, err = r.cipher.Open(payloadCipher[:0], r.nonce, payloadCipher, nil) - if err != nil { - body.Release() - return nil, E.Cause(err, "open record payload") - } - snell.IncreaseNonce(r.nonce) - body.Truncate(payloadLen) - r.readWaitOptions.PostReturn(body) - return body, nil -} - -func (r *reader) NextRecord() (*buf.Buffer, error) { - record := r.cache - if record != nil { - r.cache = nil - if record.IsEmpty() { - record.Release() - } else { - return record, nil - } - } - return r.ReadRecord() -} - -func (r *reader) SetCache(cache *buf.Buffer) { - r.cache = cache -} - -func (r *reader) ReleaseCache() { - if r.cache != nil { - r.cache.Release() - r.cache = nil - } -} - -func (r *reader) Read(p []byte) (n int, err error) { - for { - if r.cache != nil { - if r.cache.IsEmpty() { - r.cache.Release() - r.cache = nil - } else { - n = copy(p, r.cache.Bytes()) - r.cache.Advance(n) - return - } - } - r.cache, err = r.ReadRecord() - if err != nil { - return - } - } -} - -func (r *reader) ReadBuffer(buffer *buf.Buffer) error { - for { - if r.cache != nil { - if r.cache.IsEmpty() { - r.cache.Release() - r.cache = nil - } else { - n, _ := buffer.Write(r.cache.Bytes()) - r.cache.Advance(n) - return nil - } - } - var err error - r.cache, err = r.ReadRecord() - if err != nil { - return err - } - } -} - -func (r *reader) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { - r.readWaitOptions = options - return false -} - -func (r *reader) WaitReadBuffer() (buffer *buf.Buffer, err error) { - for { - if r.cache == nil { - return r.ReadRecord() - } - buffer = r.cache - r.cache = nil - if buffer.IsEmpty() { - buffer.Release() - continue - } - if buffer.Start() >= r.readWaitOptions.FrontHeadroom && buffer.FreeLen() >= r.readWaitOptions.RearHeadroom { - return - } - buffer = r.readWaitOptions.Copy(buffer) - return buffer, nil - } -} - -func (r *reader) Upstream() any { - return r.upstream -} - -type writer struct { - upstream io.Writer - cipher cipher.AEAD - nonce []byte - access sync.Mutex - lastFrameUnix int64 - framePayloadLen int - initialPaddingLen int -} - -func newWriter(upstream io.Writer, aead cipher.AEAD, nonce []byte) (*writer, error) { - paddingDelta, err := rand.Int(rand.Reader, big.NewInt(initialPaddingSpan)) - if err != nil { - return nil, err - } - return &writer{ - upstream: upstream, - cipher: aead, - nonce: nonce, - initialPaddingLen: initialPaddingMin + int(paddingDelta.Int64()), - }, nil -} - -func (w *writer) sealHeader(header []byte, paddingLen int, payloadLen int) { - header[0] = snell.HeaderVersion - header[1] = 0 - header[2] = 0 - binary.BigEndian.PutUint16(header[3:5], uint16(paddingLen)) - binary.BigEndian.PutUint16(header[5:7], uint16(payloadLen)) - w.cipher.Seal(header[:0], w.nonce, header[:snell.HeaderPlainLen], nil) - snell.IncreaseNonce(w.nonce) -} - -func (w *writer) nextPayloadLimit() int { - now := time.Now().Unix() - var payloadLimit int - if w.lastFrameUnix == 0 { - payloadLimit = frameSize - firstRecordOverhead - w.initialPaddingLen - } else if now-w.lastFrameUnix < framePayloadResetInterval { - payloadLimit = w.framePayloadLen - if payloadLimit == 0 { - payloadLimit = framePayloadStep - } - } else { - payloadLimit = framePayloadStep - } - w.lastFrameUnix = now - w.markPayloadLimitUsed(payloadLimit) - return payloadLimit -} - -func (w *writer) markPayloadLimitUsed(payloadLimit int) { - nextPayloadLimit := min(payloadLimit+framePayloadStep, maxPayload) - w.framePayloadLen = nextPayloadLimit - if w.lastFrameUnix == 0 { - w.lastFrameUnix = time.Now().Unix() - } -} - -func (w *writer) randomInt31() (int, error) { - var randomBytes [4]byte - _, err := io.ReadFull(rand.Reader, randomBytes[:]) - if err != nil { - return 0, err - } - return int(binary.LittleEndian.Uint32(randomBytes[:]) & 0x7fffffff), nil -} - -func (w *writer) Write(p []byte) (n int, err error) { - w.access.Lock() - defer w.access.Unlock() - payloadLimit := w.nextPayloadLimit() - if len(p) == 0 { - return 0, nil - } - // snell-server v5.0.1: FUN_00139670 encrypts every record of one encrypt call - // into a single malloc'd output buffer that the caller writes to the socket once. - recordCount := (len(p) + payloadLimit - 1) / payloadLimit - output := buf.NewSize(recordCount*(snell.HeaderCipherLen+snell.AEADTagLen) + len(p)) - defer output.Release() - for data := p; len(data) > 0; { - recordLen := min(len(data), payloadLimit) - err = w.sealRecordLocked(output, data[:recordLen], 0) - if err != nil { - return 0, err - } - data = data[recordLen:] - } - _, err = w.upstream.Write(output.Bytes()) - if err != nil { - return 0, err - } - return len(p), nil -} - -func (w *writer) WriteFirst(salt []byte, payload []byte) error { - w.access.Lock() - defer w.access.Unlock() - // snell-server v5.0.1: FUN_0013f020 prefixes the response byte and then calls - // FUN_00139670 once; that call reuses one payload limit for every emitted chunk - // and returns the salt plus all records in one buffer written to the socket once. - payloadLimit := w.nextPayloadLimit() - if len(payload) == 0 { - record := buf.NewSize(len(salt) + snell.HeaderCipherLen) - common.Must1(record.Write(salt)) - w.sealHeader(record.Extend(snell.HeaderCipherLen), 0, 0) - err := common.Error(w.upstream.Write(record.Bytes())) - record.Release() - return err - } - recordCount := (len(payload) + payloadLimit - 1) / payloadLimit - output := buf.NewSize(len(salt) + w.initialPaddingLen + recordCount*(snell.HeaderCipherLen+snell.AEADTagLen) + len(payload)) - defer output.Release() - common.Must1(output.Write(salt)) - paddingLen := w.initialPaddingLen - for data := payload; len(data) > 0; { - recordLen := min(len(data), payloadLimit) - err := w.sealRecordLocked(output, data[:recordLen], paddingLen) - if err != nil { - return err - } - paddingLen = 0 - data = data[recordLen:] - } - return common.Error(w.upstream.Write(output.Bytes())) -} - -func (w *writer) WriteFirstVectorised(salt []byte, buffers []*buf.Buffer, upstream N.VectorisedWriter) error { - payloadLen := buf.LenMulti(buffers) - var records []*buf.Buffer - defer func() { - buf.ReleaseMulti(records) - }() - w.access.Lock() - defer w.access.Unlock() - payloadLimit := w.nextPayloadLimit() - if payloadLen == 0 { - buf.ReleaseMulti(buffers) - record := buf.NewSize(len(salt) + snell.HeaderCipherLen) - common.Must1(record.Write(salt)) - w.sealHeader(record.Extend(snell.HeaderCipherLen), 0, 0) - records = append(records, record) - flushRecords := records - records = nil - return upstream.WriteVectorised(flushRecords) - } - saltPrefix := salt - paddingLen := w.initialPaddingLen - index := 0 - // snell-server v5.0.1: FUN_00139670 advances the growth window once per - // encrypt call and splits that call using the same payload limit. - for remainingPayload := payloadLen; remainingPayload > 0; { - for buffers[index].IsEmpty() { - buffers[index].Release() - index++ - } - recordLen := min(remainingPayload, payloadLimit) - buffer := buffers[index] - if len(saltPrefix) == 0 && paddingLen == 0 && buffer.Len() == recordLen { - record, err := w.makeBufferRecordLocked(buffer, 0) - if err != nil { - buffer.Release() - buf.ReleaseMulti(buffers[index+1:]) - return err - } - records = append(records, record) - index++ - } else { - payload := make([]byte, recordLen) - for copiedLen := 0; copiedLen < recordLen; { - buffer = buffers[index] - if buffer.IsEmpty() { - buffer.Release() - index++ - continue - } - copyLen := min(recordLen-copiedLen, buffer.Len()) - copy(payload[copiedLen:], buffer.Bytes()[:copyLen]) - buffer.Advance(copyLen) - copiedLen += copyLen - if buffer.IsEmpty() { - buffer.Release() - index++ - } - } - record := buf.NewSize(len(saltPrefix) + snell.HeaderCipherLen + paddingLen + recordLen + snell.AEADTagLen) - common.Must1(record.Write(saltPrefix)) - err := w.sealRecordLocked(record, payload, paddingLen) - if err != nil { - record.Release() - buf.ReleaseMulti(buffers[index:]) - return err - } - records = append(records, record) - } - remainingPayload -= recordLen - if len(saltPrefix) > 0 { - saltPrefix = nil - } - paddingLen = 0 - } - buf.ReleaseMulti(buffers[index:]) - flushRecords := records - records = nil - return upstream.WriteVectorised(flushRecords) -} - -func (w *writer) sealRecordLocked(output *buf.Buffer, payload []byte, paddingLen int) error { - if len(payload) > maxPayload || paddingLen > maxPayload { - panic("snell: v5 record exceeds maximum") - } - if len(payload) == 0 && paddingLen != 0 { - panic("snell: zero-length v5 record carries padding") - } - w.sealHeader(output.Extend(snell.HeaderCipherLen), paddingLen, len(payload)) - padding := output.Extend(paddingLen) - payloadCipher := output.Extend(len(payload) + snell.AEADTagLen) - copy(payloadCipher, payload) - w.cipher.Seal(payloadCipher[:0], w.nonce, payloadCipher[:len(payload)], nil) - snell.IncreaseNonce(w.nonce) - if paddingLen > 0 { - err := w.fillPadding(padding, payloadCipher) - if err != nil { - return err - } - limit := min(len(padding), len(payloadCipher)) - for index := 0; index < limit; index += 2 { - padding[index], payloadCipher[index] = payloadCipher[index], padding[index] - } - } - return nil -} - -func (w *writer) makeBufferRecordLocked(buffer *buf.Buffer, paddingLen int) (*buf.Buffer, error) { - dataLen := buffer.Len() - if dataLen > maxPayload || paddingLen > maxPayload { - panic("snell: v5 record exceeds maximum") - } - if dataLen == 0 && paddingLen != 0 { - panic("snell: zero-length v5 record carries padding") - } - prefix := buffer.ExtendHeader(snell.HeaderCipherLen + paddingLen) - header := prefix[:snell.HeaderCipherLen] - w.sealHeader(header, paddingLen, dataLen) - payloadCipher := buffer.From(snell.HeaderCipherLen + paddingLen) - buffer.Extend(snell.AEADTagLen) - w.cipher.Seal(payloadCipher[:0], w.nonce, payloadCipher, nil) - payloadCipher = buffer.From(snell.HeaderCipherLen + paddingLen) - snell.IncreaseNonce(w.nonce) - if paddingLen > 0 { - padding := prefix[snell.HeaderCipherLen:] - err := w.fillPadding(padding, payloadCipher) - if err != nil { - return nil, err - } - limit := min(len(padding), len(payloadCipher)) - for index := 0; index < limit; index += 2 { - padding[index], payloadCipher[index] = payloadCipher[index], padding[index] - } - } - return buffer, nil -} - -func (w *writer) WriteBuffer(buffer *buf.Buffer) error { - defer buffer.Release() - dataLen := buffer.Len() - if dataLen == 0 { - return nil - } - w.access.Lock() - defer w.access.Unlock() - payloadLimit := w.nextPayloadLimit() - if dataLen > payloadLimit { - recordCount := (dataLen + payloadLimit - 1) / payloadLimit - output := buf.NewSize(recordCount*(snell.HeaderCipherLen+snell.AEADTagLen) + dataLen) - defer output.Release() - for data := buffer.Bytes(); len(data) > 0; { - recordLen := min(len(data), payloadLimit) - err := w.sealRecordLocked(output, data[:recordLen], 0) - if err != nil { - return err - } - data = data[recordLen:] - } - return common.Error(w.upstream.Write(output.Bytes())) - } - record, err := w.makeBufferRecordLocked(buffer, 0) - if err != nil { - return err - } - defer record.Release() - return common.Error(w.upstream.Write(record.Bytes())) -} - -func (w *writer) WritePacketBuffer(buffer *buf.Buffer) error { - dataLen := buffer.Len() - w.access.Lock() - defer w.access.Unlock() - now := time.Now().Unix() - var payloadLimit int - if w.lastFrameUnix == 0 { - payloadLimit = frameSize - firstRecordOverhead - w.initialPaddingLen - } else if now-w.lastFrameUnix < framePayloadResetInterval { - payloadLimit = w.framePayloadLen - if payloadLimit == 0 { - payloadLimit = framePayloadStep - } - } else { - payloadLimit = framePayloadStep - } - if dataLen > payloadLimit { - buffer.Release() - return snell.ErrPayloadTooLarge - } - w.lastFrameUnix = now - w.markPayloadLimitUsed(payloadLimit) - record, err := w.makeBufferRecordLocked(buffer, 0) - if err != nil { - buffer.Release() - return err - } - defer record.Release() - return common.Error(w.upstream.Write(record.Bytes())) -} - -func (w *writer) WriteZeroChunk() error { - buffer := buf.NewSize(snell.HeaderCipherLen) - w.access.Lock() - defer w.access.Unlock() - // snell-server v5.0.1: FUN_0013def0 calls FUN_00139670 even for an empty - // payload, so EOF records consume one growth-window step. - w.nextPayloadLimit() - w.sealHeader(buffer.Extend(snell.HeaderCipherLen), 0, 0) - err := common.Error(w.upstream.Write(buffer.Bytes())) - buffer.Release() - return err -} - -func (w *writer) fillPadding(padding []byte, payloadCipher []byte) error { - if len(padding) == 0 { - return nil - } - oneBits := 0 - // snell-server v5.0.1: FUN_00138af0 counts ones only over complete - // 32-bit chunks, but computes zeros against the full payload cipher length. - payloadCountLen := len(payloadCipher) &^ 3 - for _, payloadByte := range payloadCipher[:payloadCountLen] { - oneBits += bits.OnesCount8(payloadByte) - } - zeroBits := 8*len(payloadCipher) - oneBits - if zeroBits <= 0 { - _, err := io.ReadFull(rand.Reader, padding) - return err - } - - ratio := float64(oneBits) / float64(zeroBits) - if ratio <= 0.5 || ratio >= 1.6 { - _, err := io.ReadFull(rand.Reader, padding) - return err - } - - targetRatioBase := 1.6 - if zeroBits < oneBits { - targetRatioBase = 0.4 - } - // snell-server v5.0.1: FUN_00138af0 derives the target padding ratio as - // base + rand()/2147483647.0/10, using the process rand() 31-bit range. - randomValue, err := w.randomInt31() - if err != nil { - return err - } - targetRatio := targetRatioBase + float64(randomValue)/2147483647.0/10 - targetOnes := int(float64(8*(len(payloadCipher)+len(padding)))*targetRatio/(targetRatio+1) - float64(oneBits)) - if targetOnes < 0 || targetOnes > 8*len(padding) { - _, err = io.ReadFull(rand.Reader, padding) - return err - } - return w.fillPaddingWithBitCount(padding, targetOnes) -} - -func (w *writer) fillPaddingWithBitCount(padding []byte, oneBits int) error { - totalBits := 8 * len(padding) - if oneBits < 0 || oneBits > totalBits { - panic("snell: invalid padding bit count") - } - bitset := make([]byte, totalBits) - for index := range oneBits { - bitset[index] = 1 - } - // snell-server v5.0.1: FUN_00138af0 shuffles from the front and maps rand() - // with division by RAND_MAX/remaining+1, preserving its modulo bias. - position := 0 - remaining := totalBits - for remaining != 1 { - randomValue, err := w.randomInt31() - if err != nil { - return err - } - divisor := 0 - if remaining != 0 { - divisor = 0x7fffffff / remaining - } - swapIndex := position + randomValue/(divisor+1) - bitset[swapIndex], bitset[position] = bitset[position], bitset[swapIndex] - position++ - remaining-- - } - clear(padding) - for index, bit := range bitset { - if bit == 1 { - padding[index/8] |= 1 << uint(index%8) - } - } - return nil -} - -func (w *writer) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - upstreamWriter, created := bufio.CreateVectorisedWriter(w.upstream) - if !created { - return nil, false - } - return w.CreateVectorisedWriterFor(upstreamWriter), true -} - -func (w *writer) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { - return &vectorisedWriter{writer: w, upstream: upstream} -} - -type vectorisedWriter struct { - writer *writer - upstream N.VectorisedWriter -} - -func (w *vectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { - payloadLen := buf.LenMulti(buffers) - if payloadLen == 0 { - buf.ReleaseMulti(buffers) - return nil - } - var records []*buf.Buffer - defer func() { - buf.ReleaseMulti(records) - }() - recordWriter := w.writer - recordWriter.access.Lock() - defer recordWriter.access.Unlock() - // snell-server v5.0.1: FUN_00139670 advances the growth window once per - // encrypt call and splits that call using the same payload limit. - payloadLimit := recordWriter.nextPayloadLimit() - index := 0 - for remainingPayload := payloadLen; remainingPayload > 0; { - for buffers[index].IsEmpty() { - buffers[index].Release() - index++ - } - recordLen := min(remainingPayload, payloadLimit) - buffer := buffers[index] - if buffer.Len() == recordLen { - record, err := recordWriter.makeBufferRecordLocked(buffer, 0) - if err != nil { - buffer.Release() - buf.ReleaseMulti(buffers[index+1:]) - return err - } - records = append(records, record) - index++ - } else { - payload := make([]byte, recordLen) - for copiedLen := 0; copiedLen < recordLen; { - buffer = buffers[index] - if buffer.IsEmpty() { - buffer.Release() - index++ - continue - } - copyLen := min(recordLen-copiedLen, buffer.Len()) - copy(payload[copiedLen:], buffer.Bytes()[:copyLen]) - buffer.Advance(copyLen) - copiedLen += copyLen - if buffer.IsEmpty() { - buffer.Release() - index++ - } - } - record := buf.NewSize(snell.HeaderCipherLen + recordLen + snell.AEADTagLen) - err := recordWriter.sealRecordLocked(record, payload, 0) - if err != nil { - record.Release() - buf.ReleaseMulti(buffers[index:]) - return err - } - records = append(records, record) - } - remainingPayload -= recordLen - } - buf.ReleaseMulti(buffers[index:]) - flushRecords := records - records = nil - return w.upstream.WriteVectorised(flushRecords) -} - -func (w *writer) CreatePacketVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { - return &packetVectorisedWriter{writer: w, upstream: upstream} -} - -type packetVectorisedWriter struct { - writer *writer - upstream N.VectorisedWriter -} - -func (w *packetVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { - var records []*buf.Buffer - defer func() { - buf.ReleaseMulti(records) - }() - recordWriter := w.writer - recordWriter.access.Lock() - defer recordWriter.access.Unlock() - for index, buffer := range buffers { - dataLen := buffer.Len() - if dataLen == 0 { - buffer.Release() - continue - } - now := time.Now().Unix() - var payloadLimit int - if recordWriter.lastFrameUnix == 0 { - payloadLimit = frameSize - firstRecordOverhead - recordWriter.initialPaddingLen - } else if now-recordWriter.lastFrameUnix < framePayloadResetInterval { - payloadLimit = recordWriter.framePayloadLen - if payloadLimit == 0 { - payloadLimit = framePayloadStep - } - } else { - payloadLimit = framePayloadStep - } - if dataLen > payloadLimit { - buffer.Release() - buf.ReleaseMulti(buffers[index+1:]) - return snell.ErrPayloadTooLarge - } - recordWriter.lastFrameUnix = now - recordWriter.markPayloadLimitUsed(payloadLimit) - record, err := recordWriter.makeBufferRecordLocked(buffer, 0) - if err != nil { - buffer.Release() - buf.ReleaseMulti(buffers[index+1:]) - return err - } - records = append(records, record) - } - if len(records) == 0 { - return nil - } - flushRecords := records - records = nil - return w.upstream.WriteVectorised(flushRecords) -} - -func (w *writer) FrontHeadroom() int { - return snell.HeaderCipherLen -} - -func (w *writer) RearHeadroom() int { - return snell.AEADTagLen -} - -func (w *writer) WriterMTU() int { - return maxPayload -} - -func (w *writer) Upstream() any { - return w.upstream -} - -var ( - _ N.ExtendedReader = (*reader)(nil) - _ N.ReadWaiter = (*reader)(nil) - _ N.ExtendedWriter = (*writer)(nil) - _ N.VectorisedWriteCreator = (*writer)(nil) - _ N.VectorisedWriter = (*vectorisedWriter)(nil) - _ N.VectorisedWriter = (*packetVectorisedWriter)(nil) - _ N.FrontHeadroom = (*writer)(nil) - _ N.RearHeadroom = (*writer)(nil) - _ N.WriterWithMTU = (*writer)(nil) -) diff --git a/proxy/snell/internal/singsnell/snellv5/reuse.go b/proxy/snell/internal/singsnell/snellv5/reuse.go deleted file mode 100644 index 97ab8df324a..00000000000 --- a/proxy/snell/internal/singsnell/snellv5/reuse.go +++ /dev/null @@ -1,482 +0,0 @@ -package snellv5 - -import ( - "context" - "crypto/rand" - "errors" - "io" - "net" - "sync" - "sync/atomic" - - snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" - "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" - E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -const ( - remoteEOFCode = reuse.RemoteEOFCode - remoteEOFMessage = reuse.RemoteEOFMessage -) - -type serverReuseSession[U comparable] struct { - net.Conn - service *Service - multiService *MultiService[U] - reader reuse.RecordReader - writer reuse.RecordWriter -} - -func (s *serverReuseSession[U]) Serve(ctx context.Context, source M.Socksaddr, onClose N.CloseHandlerFunc, record *buf.Buffer, request snell.Request) (err error) { - callOnClose := true - if onClose != nil { - defer func() { - if callOnClose { - onClose(err) - } - }() - } - for { - requestCtx := ctx - switch request.Command { - case snell.CommandConnect, snell.CommandConnectV2: - if s.multiService != nil { - requestCtx, err = s.multiService.authenticate(ctx, request) - if err != nil { - record.Release() - s.Conn.Close() - return err - } - } - case snell.CommandUDP: - if s.multiService != nil { - requestCtx, err = s.multiService.authenticate(ctx, request) - if err != nil { - record.Release() - s.Conn.Close() - return err - } - } - record.Release() - packetConn := &serverPacketConn{Conn: s.Conn, service: s.service, reader: s.reader, writer: s.writer} - err = packetConn.writeTunnelReply() - if err != nil { - s.Conn.Close() - return err - } - s.writer = packetConn.writer - callOnClose = false - s.service.handler.NewPacketConnectionEx(requestCtx, packetConn, source, M.Socksaddr{}, onClose) - return nil - case snell.CommandPing: - record.Release() - pong := [1]byte{snell.ReplyPong} - if s.writer == nil { - err = s.service.writePong(s.Conn) - } else { - _, err = s.writer.Write(pong[:]) - } - if err != nil { - s.Conn.Close() - return err - } - s.Conn.Close() - return nil - default: - record.Release() - s.Conn.Close() - return E.Extend(snell.ErrUnsupportedCommand, request.Command) - } - serverConn := &serverReuseConn[U]{Conn: s.Conn, session: s, completion: make(chan struct{})} - if record.IsEmpty() { - record.Release() - } else { - s.reader.SetCache(record) - } - s.service.handler.NewConnectionEx(requestCtx, serverConn, source, request.Destination, serverConn.completeLogicalConnection) - select { - case <-serverConn.completion: - case <-requestCtx.Done(): - s.Conn.Close() - return requestCtx.Err() - } - err = serverConn.CloseWrite() - if err != nil { - s.Conn.Close() - return err - } - if serverConn.aborted.Load() { - return nil - } - if !serverConn.readClosed.Load() { - // snell-server v5.0.1: after writing the EOF packet, FUN_0013f630 keeps - // consuming client records (forwarding them into the still writable - // outgoing socket) until the zero chunk reaches FUN_0013f500 -> - // FUN_0013ddb0 -> FUN_0013dca0, which resets the tunnel stage so the - // next record is parsed as a request. The handler has already released - // the upstream, so discard instead, capped like the client waiting state. - var discarded int - for { - var drainRecord *buf.Buffer - drainRecord, err = s.reader.NextRecord() - if err != nil { - break - } - discarded += drainRecord.Len() - drainRecord.Release() - if discarded >= reuse.WaitingDiscardLimit { - s.Conn.Close() - return E.New("snell: read too much data after request end") - } - } - if !errors.Is(err, io.EOF) { - s.Conn.Close() - return E.Cause(err, "drain request") - } - } - record, err = s.reader.ReadRecord() - if err != nil { - if errors.Is(err, io.EOF) { - return nil - } - s.Conn.Close() - return E.Cause(err, "read request") - } - request, err = s.service.readRequest(record) - if err != nil { - record.Release() - s.Conn.Close() - return E.Cause(err, "decode request") - } - } -} - -type serverReuseConn[U comparable] struct { - net.Conn - session *serverReuseSession[U] - - access sync.Mutex - closeWriteOnce sync.Once - closeWriteErr error - closeOnce sync.Once - closeErr error - readClosed atomic.Bool - aborted atomic.Bool - replyWritten bool - completeOnce sync.Once - completion chan struct{} -} - -func (c *serverReuseConn[U]) writeFirstResponse(payload []byte) error { - salt := make([]byte, snell.SaltLen) - _, err := io.ReadFull(rand.Reader, salt) - if err != nil { - return err - } - key := snell.DeriveKey(c.session.service.psk, salt) - aead, err := snell.NewAEAD(key) - if err != nil { - return err - } - nonce := make([]byte, snell.NonceLen) - reply := buf.NewSize(1 + len(payload)) - common.Must(reply.WriteByte(snell.ReplyTunnel)) - common.Must1(reply.Write(payload)) - writer, err := newWriter(c.session.Conn, aead, nonce) - if err != nil { - reply.Release() - return err - } - err = writer.WriteFirst(salt, reply.Bytes()) - reply.Release() - if err != nil { - return E.Cause(err, "write reply") - } - c.session.writer = writer - c.replyWritten = true - return nil -} - -func (c *serverReuseConn[U]) writeResponse(payload []byte) error { - if c.session.writer == nil { - return c.writeFirstResponse(payload) - } - reply := buf.NewSize(1 + len(payload)) - common.Must(reply.WriteByte(snell.ReplyTunnel)) - common.Must1(reply.Write(payload)) - defer reply.Release() - _, err := c.session.writer.Write(reply.Bytes()) - if err != nil { - return E.Cause(err, "write reply") - } - c.replyWritten = true - return nil -} - -func (c *serverReuseConn[U]) writeResponseBuffer(buffer *buf.Buffer) error { - if c.session.writer != nil { - buffer.ExtendHeader(1)[0] = snell.ReplyTunnel - err := c.session.writer.WriteBuffer(buffer) - if err != nil { - return E.Cause(err, "write reply") - } - c.replyWritten = true - return nil - } - buffer.ExtendHeader(1)[0] = snell.ReplyTunnel - salt := make([]byte, snell.SaltLen) - _, err := io.ReadFull(rand.Reader, salt) - if err != nil { - buffer.Release() - return err - } - key := snell.DeriveKey(c.session.service.psk, salt) - aead, err := snell.NewAEAD(key) - if err != nil { - buffer.Release() - return err - } - nonce := make([]byte, snell.NonceLen) - writer, err := newWriter(c.session.Conn, aead, nonce) - if err != nil { - buffer.Release() - return err - } - err = writer.WriteFirst(salt, buffer.Bytes()) - buffer.Release() - if err != nil { - return E.Cause(err, "write reply") - } - c.session.writer = writer - c.replyWritten = true - return nil -} - -func (c *serverReuseConn[U]) writeErrorResponse(code byte, messageText string) error { - message := []byte(messageText) - reply := buf.NewSize(3 + len(message)) - common.Must(reply.WriteByte(snell.ReplyError)) - common.Must(reply.WriteByte(code)) - common.Must(reply.WriteByte(byte(len(message)))) - common.Must1(reply.Write(message)) - defer reply.Release() - if c.session.writer == nil { - salt := make([]byte, snell.SaltLen) - _, err := io.ReadFull(rand.Reader, salt) - if err != nil { - return err - } - key := snell.DeriveKey(c.session.service.psk, salt) - aead, err := snell.NewAEAD(key) - if err != nil { - return err - } - nonce := make([]byte, snell.NonceLen) - writer, err := newWriter(c.session.Conn, aead, nonce) - if err != nil { - return err - } - err = writer.WriteFirst(salt, reply.Bytes()) - if err != nil { - return E.Cause(err, "write error reply") - } - c.session.writer = writer - return nil - } - _, err := c.session.writer.Write(reply.Bytes()) - if err != nil { - return E.Cause(err, "write error reply") - } - return nil -} - -func (c *serverReuseConn[U]) Read(p []byte) (int, error) { - n, err := c.session.reader.Read(p) - if errors.Is(err, io.EOF) { - c.readClosed.Store(true) - } - return n, err -} - -func (c *serverReuseConn[U]) ReadBuffer(buffer *buf.Buffer) error { - err := c.session.reader.ReadBuffer(buffer) - if errors.Is(err, io.EOF) { - c.readClosed.Store(true) - } - return err -} - -func (c *serverReuseConn[U]) Write(p []byte) (int, error) { - c.access.Lock() - defer c.access.Unlock() - if c.replyWritten { - return c.session.writer.Write(p) - } - err := c.writeResponse(p) - if err != nil { - return 0, err - } - return len(p), nil -} - -func (c *serverReuseConn[U]) WriteBuffer(buffer *buf.Buffer) error { - c.access.Lock() - defer c.access.Unlock() - if c.replyWritten { - return c.session.writer.WriteBuffer(buffer) - } - return c.writeResponseBuffer(buffer) -} - -func (c *serverReuseConn[U]) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - upstreamWriter, created := bufio.CreateVectorisedWriter(c.session.Conn) - if !created { - return nil, false - } - return &serverReuseVectorisedWriter[U]{conn: c, upstream: upstreamWriter}, true -} - -type serverReuseVectorisedWriter[U comparable] struct { - conn *serverReuseConn[U] - upstream N.VectorisedWriter -} - -func (w *serverReuseVectorisedWriter[U]) WriteVectorised(buffers []*buf.Buffer) error { - conn := w.conn - conn.access.Lock() - if conn.replyWritten { - recordWriter := conn.session.writer - conn.access.Unlock() - return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) - } - for index, buffer := range buffers { - if buffer.IsEmpty() { - buffer.Release() - continue - } - buffer.ExtendHeader(1)[0] = snell.ReplyTunnel - if conn.session.writer != nil { - recordWriter := conn.session.writer - err := recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers[index:]) - if err != nil { - conn.access.Unlock() - return err - } - conn.replyWritten = true - conn.access.Unlock() - return nil - } - salt := make([]byte, snell.SaltLen) - _, err := io.ReadFull(rand.Reader, salt) - if err != nil { - conn.access.Unlock() - buf.ReleaseMulti(buffers[index:]) - return err - } - key := snell.DeriveKey(conn.session.service.psk, salt) - aead, err := snell.NewAEAD(key) - if err != nil { - conn.access.Unlock() - buf.ReleaseMulti(buffers[index:]) - return err - } - nonce := make([]byte, snell.NonceLen) - recordWriter, err := newWriter(conn.session.Conn, aead, nonce) - if err != nil { - conn.access.Unlock() - buf.ReleaseMulti(buffers[index:]) - return err - } - err = recordWriter.WriteFirstVectorised(salt, buffers[index:], w.upstream) - if err != nil { - conn.access.Unlock() - return E.Cause(err, "write reply") - } - conn.session.writer = recordWriter - conn.replyWritten = true - conn.access.Unlock() - return nil - } - conn.access.Unlock() - return nil -} - -func (c *serverReuseConn[U]) CloseWrite() error { - c.closeWriteOnce.Do(func() { - c.access.Lock() - defer c.access.Unlock() - // snell-server v5.0.1: FUN_0013f020 reports upstream EOF before any - // payload as ReplyError 0x65 "Remote EOF", then aborts the reusable tunnel. - if !c.replyWritten { - c.closeWriteErr = c.writeErrorResponse(remoteEOFCode, remoteEOFMessage) - if c.closeWriteErr == nil { - c.aborted.Store(true) - c.closeWriteErr = c.session.Conn.Close() - } - return - } - c.closeWriteErr = c.session.writer.WriteZeroChunk() - }) - return c.closeWriteErr -} - -func (c *serverReuseConn[U]) Close() error { - c.closeOnce.Do(func() { - c.closeErr = c.CloseWrite() - c.completeLogicalConnection(c.closeErr) - }) - return c.closeErr -} - -func (c *serverReuseConn[U]) completeLogicalConnection(error) { - c.completeOnce.Do(func() { - close(c.completion) - }) -} - -func (c *serverReuseConn[U]) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { - return c.session.reader.InitializeReadWaiter(options) -} - -func (c *serverReuseConn[U]) WaitReadBuffer() (*buf.Buffer, error) { - buffer, err := c.session.reader.WaitReadBuffer() - if errors.Is(err, io.EOF) { - c.readClosed.Store(true) - } - return buffer, err -} - -func (c *serverReuseConn[U]) FrontHeadroom() int { - if c.replyWritten { - return snell.HeaderCipherLen - } - if c.session.writer != nil { - return 1 + snell.HeaderCipherLen - } - return 1 + snell.SaltLen + snell.HeaderCipherLen + maxInitialPaddingLen -} - -func (c *serverReuseConn[U]) RearHeadroom() int { - return snell.AEADTagLen -} - -func (c *serverReuseConn[U]) WriterMTU() int { - return maxPayload -} - -func (c *serverReuseConn[U]) Upstream() any { - return c.session.Conn -} - -var ( - _ N.ExtendedConn = (*serverReuseConn[struct{}])(nil) - _ N.ReadWaiter = (*serverReuseConn[struct{}])(nil) - _ N.VectorisedWriteCreator = (*serverReuseConn[struct{}])(nil) - _ N.VectorisedWriter = (*serverReuseVectorisedWriter[struct{}])(nil) - _ N.WriteCloser = (*serverReuseConn[struct{}])(nil) -) diff --git a/proxy/snell/internal/singsnell/snellv5/server.go b/proxy/snell/internal/singsnell/snellv5/server.go deleted file mode 100644 index 81a3605ce60..00000000000 --- a/proxy/snell/internal/singsnell/snellv5/server.go +++ /dev/null @@ -1,695 +0,0 @@ -package snellv5 - -import ( - "context" - "crypto/rand" - "encoding/binary" - "io" - "math" - "net" - "sync" - - snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/auth" - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" - E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -const ( - // snell-server v5.0.1: FUN_0014c020 initializes two 500000-entry Bloom generations - // with false-positive probability 1e-10 for first-record salt replay detection. - saltReplayCacheGenerationSize = 500000 - saltReplayCacheFalsePositiveRate = 1e-10 - saltReplayCacheMurmurHashDefaultSeed = 0x9747b28c -) - -var errDuplicateSalt = E.New("snell: duplicated salt") - -const ( - nonReusableEOFCode = 0xff - nonReusableEOFMessage = "end of file" -) - -type saltReplayCache struct { - access sync.Mutex - current int - counts [2]int - filters [2]saltReplayBloom -} - -type saltReplayBloom struct { - bits []byte - bitCount uint32 - hashCount int -} - -func (b *saltReplayBloom) Initialize(expectedEntries int, falsePositiveRate float64) { - bitsPerEntry := -math.Log(falsePositiveRate) / (math.Ln2 * math.Ln2) - b.bitCount = uint32(float64(expectedEntries) * bitsPerEntry) - b.hashCount = int(math.Ceil(bitsPerEntry * math.Ln2)) - b.bits = make([]byte, (b.bitCount+7)/8) -} - -func (b *saltReplayBloom) Contains(data []byte) bool { - firstHash := b.MurmurHash(data, saltReplayCacheMurmurHashDefaultSeed) - hashStep := b.MurmurHash(data, firstHash) - hashValue := firstHash - for index := 0; index < b.hashCount; index++ { - bitIndex := hashValue % b.bitCount - if b.bits[bitIndex>>3]&(1<<(bitIndex&7)) == 0 { - return false - } - hashValue += hashStep - } - return true -} - -func (b *saltReplayBloom) Add(data []byte) { - firstHash := b.MurmurHash(data, saltReplayCacheMurmurHashDefaultSeed) - hashStep := b.MurmurHash(data, firstHash) - hashValue := firstHash - for index := 0; index < b.hashCount; index++ { - bitIndex := hashValue % b.bitCount - b.bits[bitIndex>>3] |= 1 << (bitIndex & 7) - hashValue += hashStep - } -} - -func (b *saltReplayBloom) Clear() { - clear(b.bits) -} - -func (b *saltReplayBloom) MurmurHash(data []byte, seed uint32) uint32 { - hashValue := seed ^ uint32(len(data)) - for len(data) >= 4 { - chunk := binary.LittleEndian.Uint32(data) - chunk *= 0x5bd1e995 - chunk ^= chunk >> 24 - chunk *= 0x5bd1e995 - hashValue *= 0x5bd1e995 - hashValue ^= chunk - data = data[4:] - } - switch len(data) { - case 3: - hashValue ^= uint32(data[2]) << 16 - fallthrough - case 2: - hashValue ^= uint32(data[1]) << 8 - fallthrough - case 1: - hashValue ^= uint32(data[0]) - hashValue *= 0x5bd1e995 - } - hashValue ^= hashValue >> 13 - hashValue *= 0x5bd1e995 - hashValue ^= hashValue >> 15 - return hashValue -} - -func (c *saltReplayCache) CheckAndAdd(salt []byte) bool { - c.access.Lock() - defer c.access.Unlock() - if c.filters[0].bits == nil { - c.filters[0].Initialize(saltReplayCacheGenerationSize, saltReplayCacheFalsePositiveRate) - c.filters[1].Initialize(saltReplayCacheGenerationSize, saltReplayCacheFalsePositiveRate) - } - if c.filters[0].Contains(salt) || c.filters[1].Contains(salt) { - return true - } - c.filters[c.current].Add(salt) - c.counts[c.current]++ - if c.counts[c.current] >= saltReplayCacheGenerationSize { - c.counts[c.current] = 0 - c.current ^= 1 - c.filters[c.current].Clear() - } - return false -} - -type Service struct { - psk []byte - obfs snell.ObfsConfig - handler snell.Handler - saltCache saltReplayCache -} - -type ServiceOptions struct { - PSK []byte - ObfsMode snell.ObfsMode - Handler snell.Handler -} - -func NewService(options ServiceOptions) (*Service, error) { - if len(options.PSK) == 0 { - return nil, snell.ErrMissingPSK - } - switch options.ObfsMode { - case snell.ObfsModeNone, snell.ObfsModeHTTP: - case snell.ObfsModeTLS: - default: - return nil, E.New("snell: unknown obfs mode: ", int(options.ObfsMode)) - } - return &Service{ - psk: options.PSK, - obfs: snell.ObfsConfig{Mode: options.ObfsMode}, - handler: options.Handler, - }, nil -} - -func (s *Service) NewConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error { - err := s.newConnection(ctx, conn, source, onClose) - if err != nil { - return &snell.ServerError{Conn: conn, Source: source, Cause: err} - } - return nil -} - -type MultiService[U comparable] struct { - *Service - users map[string]U -} - -func NewMultiService[U comparable](options ServiceOptions) (*MultiService[U], error) { - service, err := NewService(options) - if err != nil { - return nil, err - } - return &MultiService[U]{Service: service, users: make(map[string]U)}, nil -} - -func (s *MultiService[U]) UpdateUsers(users []U, userKeys [][]byte) error { - if len(users) != len(userKeys) { - return E.New("snell: user/key count mismatch") - } - if len(users) == 0 { - return snell.ErrNoUsers - } - userMap := make(map[string]U, len(users)) - for index, user := range users { - key := userKeys[index] - if len(key) == 0 { - return snell.ErrBadUserKey - } - if len(key) > 255 { - return E.New("snell: user key too long") - } - keyString := string(key) - if _, loaded := userMap[keyString]; loaded { - return snell.ErrDuplicateUserKey - } - userMap[keyString] = user - } - s.users = userMap - return nil -} - -func (s *MultiService[U]) NewConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error { - err := s.newConnection(ctx, conn, source, onClose) - if err != nil { - return &snell.ServerError{Conn: conn, Source: source, Cause: err} - } - return nil -} - -func (s *MultiService[U]) authenticate(ctx context.Context, request snell.Request) (context.Context, error) { - user, loaded := s.users[string(request.ClientID)] - if !loaded { - return nil, snell.ErrBadUserKey - } - return auth.ContextWithUser(ctx, user), nil -} - -func (s *Service) readRequest(record *buf.Buffer) (snell.Request, error) { - prefix := make([]byte, 3) - _, err := io.ReadFull(record, prefix) - if err != nil { - return snell.Request{}, err - } - if prefix[0] != snell.RequestVersion { - return snell.Request{}, E.Extend(snell.ErrBadVersion, "request version ", prefix[0]) - } - request := snell.Request{Command: prefix[1]} - // snell-server v5.0.1: FUN_0013f630 handles SN_COMMAND_PING after the 3-byte - // request prefix is read and aborts with PONG data, without consuming prefix[2] - // client-id bytes. - if request.Command == snell.CommandPing { - return request, nil - } - clientIDLen := int(prefix[2]) - if clientIDLen > 0 { - request.ClientID = make([]byte, clientIDLen) - _, err = io.ReadFull(record, request.ClientID) - if err != nil { - return snell.Request{}, err - } - } - switch request.Command { - case snell.CommandConnect, snell.CommandConnectV2: - request.Destination, err = snell.ReadConnectAddress(record) - if err != nil { - return snell.Request{}, err - } - case snell.CommandUDP: - default: - return snell.Request{}, E.Extend(snell.ErrUnsupportedCommand, request.Command) - } - return request, nil -} - -func (s *Service) newConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error { - conn = s.obfs.ServerConn(conn) - salt := make([]byte, snell.SaltLen) - _, err := io.ReadFull(conn, salt) - if err != nil { - return err - } - key := snell.DeriveKey(s.psk, salt) - aead, err := snell.NewAEAD(key) - if err != nil { - return err - } - r := newReader(conn, aead, make([]byte, snell.NonceLen)) - record, err := r.ReadRecord() - if err != nil { - return E.Cause(err, "read request") - } - // snell-server v5.0.1: initial request path: checks the first record salt after decryption - // and aborts with "Duplicated salt detected" when it was recently seen. - if s.saltCache.CheckAndAdd(salt) { - record.Release() - return errDuplicateSalt - } - request, err := s.readRequest(record) - if err != nil { - record.Release() - return E.Cause(err, "decode request") - } - - switch request.Command { - case snell.CommandConnect: - serverConn := &serverConn{Conn: conn, service: s, reader: r} - if record.IsEmpty() { - record.Release() - } else { - r.SetCache(record) - } - s.handler.NewConnectionEx(ctx, serverConn, source, request.Destination, onClose) - return nil - case snell.CommandConnectV2: - reuseSession := &serverReuseSession[struct{}]{Conn: conn, service: s, reader: r} - return reuseSession.Serve(ctx, source, onClose, record, request) - case snell.CommandUDP: - record.Release() - packetConn := &serverPacketConn{Conn: conn, service: s, reader: r} - err = packetConn.writeTunnelReply() - if err != nil { - return err - } - s.handler.NewPacketConnectionEx(ctx, packetConn, source, M.Socksaddr{}, onClose) - return nil - case snell.CommandPing: - record.Release() - err = s.writePong(conn) - closeErr := conn.Close() - if err != nil { - return err - } - return closeErr - default: - record.Release() - return E.Extend(snell.ErrUnsupportedCommand, request.Command) - } -} - -func (s *MultiService[U]) newConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error { - conn = s.obfs.ServerConn(conn) - salt := make([]byte, snell.SaltLen) - _, err := io.ReadFull(conn, salt) - if err != nil { - return err - } - key := snell.DeriveKey(s.psk, salt) - aead, err := snell.NewAEAD(key) - if err != nil { - return err - } - r := newReader(conn, aead, make([]byte, snell.NonceLen)) - record, err := r.ReadRecord() - if err != nil { - return E.Cause(err, "read request") - } - // snell-server v5.0.1: initial request path: checks the first record salt after decryption - // and aborts with "Duplicated salt detected" when it was recently seen. - if s.saltCache.CheckAndAdd(salt) { - record.Release() - return errDuplicateSalt - } - request, err := s.readRequest(record) - if err != nil { - record.Release() - return E.Cause(err, "decode request") - } - - switch request.Command { - case snell.CommandConnect: - requestCtx, err := s.authenticate(ctx, request) - if err != nil { - record.Release() - return err - } - serverConn := &serverConn{Conn: conn, service: s.Service, reader: r} - if record.IsEmpty() { - record.Release() - } else { - r.SetCache(record) - } - s.handler.NewConnectionEx(requestCtx, serverConn, source, request.Destination, onClose) - return nil - case snell.CommandConnectV2: - reuseSession := &serverReuseSession[U]{Conn: conn, service: s.Service, multiService: s, reader: r} - return reuseSession.Serve(ctx, source, onClose, record, request) - case snell.CommandUDP: - requestCtx, err := s.authenticate(ctx, request) - if err != nil { - record.Release() - return err - } - record.Release() - packetConn := &serverPacketConn{Conn: conn, service: s.Service, reader: r} - err = packetConn.writeTunnelReply() - if err != nil { - return err - } - s.handler.NewPacketConnectionEx(requestCtx, packetConn, source, M.Socksaddr{}, onClose) - return nil - case snell.CommandPing: - record.Release() - err = s.writePong(conn) - closeErr := conn.Close() - if err != nil { - return err - } - return closeErr - default: - record.Release() - return E.Extend(snell.ErrUnsupportedCommand, request.Command) - } -} - -var ( - _ snell.Service = (*Service)(nil) - _ snell.Service = (*MultiService[int])(nil) -) - -type serverConn struct { - net.Conn - service *Service - reader *reader - - access sync.Mutex - writer *writer - - closeWriteOnce sync.Once - closeWriteErr error - closeOnce sync.Once - closeErr error -} - -func (c *serverConn) writeResponse(payload []byte) error { - salt := make([]byte, snell.SaltLen) - _, err := io.ReadFull(rand.Reader, salt) - if err != nil { - return err - } - key := snell.DeriveKey(c.service.psk, salt) - aead, err := snell.NewAEAD(key) - if err != nil { - return err - } - nonce := make([]byte, snell.NonceLen) - w, err := newWriter(c.Conn, aead, nonce) - if err != nil { - return err - } - reply := buf.NewSize(1 + len(payload)) - common.Must(reply.WriteByte(snell.ReplyTunnel)) - common.Must1(reply.Write(payload)) - err = w.WriteFirst(salt, reply.Bytes()) - reply.Release() - if err != nil { - return E.Cause(err, "write reply") - } - c.writer = w - return nil -} - -func (c *serverConn) writeErrorResponse(code byte, messageText string) error { - message := []byte(messageText) - reply := buf.NewSize(3 + len(message)) - common.Must(reply.WriteByte(snell.ReplyError)) - common.Must(reply.WriteByte(code)) - common.Must(reply.WriteByte(byte(len(message)))) - common.Must1(reply.Write(message)) - defer reply.Release() - salt := make([]byte, snell.SaltLen) - _, err := io.ReadFull(rand.Reader, salt) - if err != nil { - return err - } - key := snell.DeriveKey(c.service.psk, salt) - aead, err := snell.NewAEAD(key) - if err != nil { - return err - } - nonce := make([]byte, snell.NonceLen) - w, err := newWriter(c.Conn, aead, nonce) - if err != nil { - return err - } - err = w.WriteFirst(salt, reply.Bytes()) - if err != nil { - return E.Cause(err, "write error reply") - } - c.writer = w - return nil -} - -func (c *serverConn) writeResponseBuffer(buffer *buf.Buffer) error { - buffer.ExtendHeader(1)[0] = snell.ReplyTunnel - salt := make([]byte, snell.SaltLen) - _, err := io.ReadFull(rand.Reader, salt) - if err != nil { - buffer.Release() - return err - } - key := snell.DeriveKey(c.service.psk, salt) - aead, err := snell.NewAEAD(key) - if err != nil { - buffer.Release() - return err - } - nonce := make([]byte, snell.NonceLen) - writer, err := newWriter(c.Conn, aead, nonce) - if err != nil { - buffer.Release() - return err - } - err = writer.WriteFirst(salt, buffer.Bytes()) - buffer.Release() - if err != nil { - return E.Cause(err, "write reply") - } - c.writer = writer - return nil -} - -func (c *serverConn) Read(p []byte) (int, error) { - return c.reader.Read(p) -} - -func (c *serverConn) ReadBuffer(buffer *buf.Buffer) error { - return c.reader.ReadBuffer(buffer) -} - -func (c *serverConn) Write(p []byte) (int, error) { - if c.writer != nil { - return c.writer.Write(p) - } - c.access.Lock() - if c.writer != nil { - c.access.Unlock() - return c.writer.Write(p) - } - defer c.access.Unlock() - err := c.writeResponse(p) - if err != nil { - return 0, err - } - return len(p), nil -} - -func (c *serverConn) WriteBuffer(buffer *buf.Buffer) error { - if c.writer != nil { - return c.writer.WriteBuffer(buffer) - } - c.access.Lock() - if c.writer != nil { - c.access.Unlock() - return c.writer.WriteBuffer(buffer) - } - defer c.access.Unlock() - return c.writeResponseBuffer(buffer) -} - -func (c *serverConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - upstreamWriter, created := bufio.CreateVectorisedWriter(c.Conn) - if !created { - return nil, false - } - return &serverVectorisedWriter{conn: c, upstream: upstreamWriter}, true -} - -type serverVectorisedWriter struct { - conn *serverConn - upstream N.VectorisedWriter -} - -func (w *serverVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { - conn := w.conn - if conn.writer != nil { - return conn.writer.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) - } - conn.access.Lock() - if conn.writer != nil { - recordWriter := conn.writer - conn.access.Unlock() - return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) - } - for index, buffer := range buffers { - if buffer.IsEmpty() { - buffer.Release() - continue - } - buffer.ExtendHeader(1)[0] = snell.ReplyTunnel - salt := make([]byte, snell.SaltLen) - _, err := io.ReadFull(rand.Reader, salt) - if err != nil { - conn.access.Unlock() - buf.ReleaseMulti(buffers[index:]) - return err - } - key := snell.DeriveKey(conn.service.psk, salt) - aead, err := snell.NewAEAD(key) - if err != nil { - conn.access.Unlock() - buf.ReleaseMulti(buffers[index:]) - return err - } - nonce := make([]byte, snell.NonceLen) - recordWriter, err := newWriter(conn.Conn, aead, nonce) - if err != nil { - conn.access.Unlock() - buf.ReleaseMulti(buffers[index:]) - return err - } - err = recordWriter.WriteFirstVectorised(salt, buffers[index:], w.upstream) - if err != nil { - conn.access.Unlock() - return E.Cause(err, "write reply") - } - conn.writer = recordWriter - conn.access.Unlock() - return nil - } - conn.access.Unlock() - return nil -} - -func (c *serverConn) CloseWrite() error { - c.closeWriteOnce.Do(func() { - c.access.Lock() - defer c.access.Unlock() - if c.writer == nil { - // snell-server v5.0.1: FUN_0013f020 uses the generic network-error - // path for command-1 EOF before the first response; UV_EOF maps to - // ReplyError 0xff "end of file". - c.closeWriteErr = c.writeErrorResponse(nonReusableEOFCode, nonReusableEOFMessage) - closeErr := c.Close() - if c.closeWriteErr == nil { - c.closeWriteErr = closeErr - } - return - } - // snell-server v5.0.1: FUN_0013f020 closes non-reusable command-1 tunnels - // on upstream EOF instead of writing a Snell zero chunk. - c.closeWriteErr = c.Close() - }) - return c.closeWriteErr -} - -func (c *serverConn) Close() error { - c.closeOnce.Do(func() { - c.closeErr = c.Conn.Close() - }) - return c.closeErr -} - -func (c *serverConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { - return c.reader.InitializeReadWaiter(options) -} - -func (c *serverConn) WaitReadBuffer() (*buf.Buffer, error) { - return c.reader.WaitReadBuffer() -} - -func (c *serverConn) FrontHeadroom() int { - if c.writer == nil { - return 1 + snell.SaltLen + snell.HeaderCipherLen + maxInitialPaddingLen - } - return snell.HeaderCipherLen -} - -func (c *serverConn) RearHeadroom() int { - return snell.AEADTagLen -} - -func (c *serverConn) WriterMTU() int { - return maxPayload -} - -func (c *serverConn) Upstream() any { - return c.Conn -} - -func (s *Service) writePong(conn net.Conn) error { - salt := make([]byte, snell.SaltLen) - _, err := io.ReadFull(rand.Reader, salt) - if err != nil { - return err - } - key := snell.DeriveKey(s.psk, salt) - aead, err := snell.NewAEAD(key) - if err != nil { - return err - } - nonce := make([]byte, snell.NonceLen) - w, err := newWriter(conn, aead, nonce) - if err != nil { - return err - } - pong := [1]byte{snell.ReplyPong} - return w.WriteFirst(salt, pong[:]) -} - -var ( - _ N.ExtendedConn = (*serverConn)(nil) - _ N.ReadWaiter = (*serverConn)(nil) - _ N.VectorisedWriteCreator = (*serverConn)(nil) - _ N.VectorisedWriter = (*serverVectorisedWriter)(nil) - _ N.WriteCloser = (*serverConn)(nil) -) diff --git a/proxy/snell/internal/singsnell/snellv6/client.go b/proxy/snell/internal/singsnell/snellv6/client.go index 589fa077f14..a3e1921dca6 100644 --- a/proxy/snell/internal/singsnell/snellv6/client.go +++ b/proxy/snell/internal/singsnell/snellv6/client.go @@ -210,52 +210,8 @@ func (c *clientConn) WriteBuffer(buffer *buf.Buffer) error { return c.writeRequestBuffer(buffer) } -func (c *clientConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - upstreamWriter, created := bufio.CreateVectorisedWriter(c.Conn) - if !created { - return nil, false - } - return &clientVectorisedWriter{conn: c, upstream: upstreamWriter}, true -} -type clientVectorisedWriter struct { - conn *clientConn - upstream N.VectorisedWriter -} -func (w *clientVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { - conn := w.conn - if conn.writer != nil { - return conn.writer.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) - } - conn.access.Lock() - if conn.writer != nil { - recordWriter := conn.writer - conn.access.Unlock() - return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) - } - for index, buffer := range buffers { - if buffer.IsEmpty() { - buffer.Release() - continue - } - err := conn.writeRequestBuffer(buffer) - if err != nil { - conn.access.Unlock() - buf.ReleaseMulti(buffers[index+1:]) - return err - } - if index+1 < len(buffers) { - recordWriter := conn.writer - conn.access.Unlock() - return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers[index+1:]) - } - conn.access.Unlock() - return nil - } - conn.access.Unlock() - return nil -} func (c *clientConn) CloseWrite() error { c.closeWriteOnce.Do(func() { @@ -351,7 +307,6 @@ var ( _ N.ExtendedConn = (*clientConn)(nil) _ N.ReadWaiter = (*clientConn)(nil) _ N.ReadWaitCreator = (*clientConn)(nil) - _ N.VectorisedWriteCreator = (*clientConn)(nil) _ N.VectorisedWriter = (*clientVectorisedWriter)(nil) _ N.EarlyReader = (*clientConn)(nil) _ N.EarlyWriter = (*clientConn)(nil) diff --git a/proxy/snell/internal/singsnell/snellv6/packet.go b/proxy/snell/internal/singsnell/snellv6/packet.go index 28c0e422e6e..5792775b3aa 100644 --- a/proxy/snell/internal/singsnell/snellv6/packet.go +++ b/proxy/snell/internal/singsnell/snellv6/packet.go @@ -110,56 +110,8 @@ func (c *clientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksad return c.writer.WritePacketBuffer(buffer) } -func (c *clientPacketConn) CreatePacketBatchWriter() (N.PacketBatchWriter, bool) { - upstreamWriter, created := bufio.CreateVectorisedWriter(c.Conn) - if !created { - return nil, false - } - return &clientPacketBatchWriter{conn: c, upstream: upstreamWriter}, true -} -type clientPacketBatchWriter struct { - conn *clientPacketConn - upstream N.VectorisedWriter - access sync.Mutex - writer N.VectorisedWriter -} - -func (w *clientPacketBatchWriter) WritePacketBatch(buffers []*buf.Buffer, destinations []M.Socksaddr) error { - if len(buffers) == 0 || len(buffers) != len(destinations) { - buf.ReleaseMulti(buffers) - return os.ErrInvalid - } - w.access.Lock() - if w.writer == nil { - err := w.conn.writeRequest() - if err != nil { - w.access.Unlock() - buf.ReleaseMulti(buffers) - return err - } - _, err = w.conn.readReply() - if err != nil { - w.access.Unlock() - buf.ReleaseMulti(buffers) - return err - } - w.writer = w.conn.writer.CreatePacketVectorisedWriterFor(w.upstream) - } - recordWriter := w.writer - w.access.Unlock() - for index, buffer := range buffers { - header := buf.With(buffer.ExtendHeader(1 + w.conn.udpRequestAddrLen(destinations[index]))) - common.Must(header.WriteByte(snell.UDPCommandForward)) - err := snell.WriteUDPRequestAddress(header, destinations[index]) - if err != nil { - buf.ReleaseMulti(buffers) - return err - } - } - return recordWriter.WriteVectorised(buffers) -} func (c *clientPacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) { reader, err := c.readReply() @@ -329,54 +281,8 @@ func (c *serverPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksad return writer.WritePacketBuffer(buffer) } -func (c *serverPacketConn) CreatePacketBatchWriter() (N.PacketBatchWriter, bool) { - upstreamWriter, created := bufio.CreateVectorisedWriter(c.Conn) - if !created { - return nil, false - } - return &serverPacketBatchWriter{conn: c, upstream: upstreamWriter}, true -} -type serverPacketBatchWriter struct { - conn *serverPacketConn - upstream N.VectorisedWriter - access sync.Mutex - writer N.VectorisedWriter -} - -func (w *serverPacketBatchWriter) WritePacketBatch(buffers []*buf.Buffer, destinations []M.Socksaddr) error { - if len(buffers) == 0 || len(buffers) != len(destinations) { - buf.ReleaseMulti(buffers) - return os.ErrInvalid - } - w.access.Lock() - if w.writer == nil { - w.conn.writeAccess.Lock() - if w.conn.writer == nil { - err := w.conn.writeTunnelReply() - if err != nil { - w.conn.writeAccess.Unlock() - w.access.Unlock() - buf.ReleaseMulti(buffers) - return err - } - } - w.writer = w.conn.writer.CreatePacketVectorisedWriterFor(w.upstream) - w.conn.writeAccess.Unlock() - } - recordWriter := w.writer - w.access.Unlock() - for index, buffer := range buffers { - header := buf.With(buffer.ExtendHeader(w.conn.responseAddrLen(destinations[index]))) - err := snell.WriteUDPResponseAddress(header, destinations[index]) - if err != nil { - buf.ReleaseMulti(buffers) - return err - } - } - return recordWriter.WriteVectorised(buffers) -} func (c *serverPacketConn) writeTunnelReply() error { reply := [1]byte{snell.ReplyTunnel} @@ -468,12 +374,8 @@ func (c *serverPacketConn) WaitReadPacket() (*buf.Buffer, M.Socksaddr, error) { var ( _ N.PacketConn = (*clientPacketConn)(nil) _ N.PacketReadWaiter = (*clientPacketConn)(nil) - _ N.PacketBatchWriteCreator = (*clientPacketConn)(nil) _ N.WriterWithMTU = (*clientPacketConn)(nil) - _ N.PacketBatchWriter = (*clientPacketBatchWriter)(nil) _ N.PacketConn = (*serverPacketConn)(nil) _ N.PacketReadWaiter = (*serverPacketConn)(nil) - _ N.PacketBatchWriteCreator = (*serverPacketConn)(nil) _ N.WriterWithMTU = (*serverPacketConn)(nil) - _ N.PacketBatchWriter = (*serverPacketBatchWriter)(nil) ) diff --git a/proxy/snell/internal/singsnell/snellv6/record.go b/proxy/snell/internal/singsnell/snellv6/record.go index 042420ce66a..ce094013052 100644 --- a/proxy/snell/internal/singsnell/snellv6/record.go +++ b/proxy/snell/internal/singsnell/snellv6/record.go @@ -277,17 +277,7 @@ func (w *unshapedWriter) WritePacketBuffer(buffer *buf.Buffer) error { return w.WriteBuffer(buffer) } -func (w *unshapedWriter) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - upstreamWriter, created := bufio.CreateVectorisedWriter(w.upstream) - if !created { - return nil, false - } - return w.CreateVectorisedWriterFor(upstreamWriter), true -} -func (w *unshapedWriter) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { - return &vectorisedUnshapedWriter{writer: w, upstream: upstream} -} type vectorisedUnshapedWriter struct { writer *unshapedWriter @@ -295,40 +285,17 @@ type vectorisedUnshapedWriter struct { } func (w *vectorisedUnshapedWriter) WriteVectorised(buffers []*buf.Buffer) error { - var records []*buf.Buffer - defer func() { - buf.ReleaseMulti(records) - }() - recordWriter := w.writer - recordWriter.access.Lock() - defer recordWriter.access.Unlock() + // Compatibility fallback for sing v0.8.11 (no VectorisedWriteCreator chaining). for _, buffer := range buffers { - dataLen := buffer.Len() - if dataLen == 0 { - buffer.Release() + if buffer.IsEmpty() { continue } - for data := buffer.Bytes(); len(data) > 0; { - if len(data) == dataLen && dataLen <= maxPayload { - record := recordWriter.makeBufferRecordLocked(buffer) - buffer = nil - records = append(records, record) - break - } - recordLen := min(len(data), maxPayload) - records = append(records, recordWriter.makeSliceRecordLocked(data[:recordLen])) - data = data[recordLen:] - } - if buffer != nil { - buffer.Release() + _, err := w.Write(buffer.Bytes()) + if err != nil { + return err } } - if len(records) == 0 { - return nil - } - flushRecords := records - records = nil - return w.upstream.WriteVectorised(flushRecords) + return nil } func (w *unshapedWriter) CreatePacketVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { @@ -341,32 +308,17 @@ type packetVectorisedUnshapedWriter struct { } func (w *packetVectorisedUnshapedWriter) WriteVectorised(buffers []*buf.Buffer) error { - var records []*buf.Buffer - defer func() { - buf.ReleaseMulti(records) - }() - recordWriter := w.writer - recordWriter.access.Lock() - defer recordWriter.access.Unlock() - for index, buffer := range buffers { + // Compatibility fallback for sing v0.8.11 (no VectorisedWriteCreator chaining). + for _, buffer := range buffers { if buffer.IsEmpty() { - buffer.Release() continue } - if buffer.Len() > maxPayload { - buffer.Release() - buf.ReleaseMulti(buffers[index+1:]) - return snell.ErrPayloadTooLarge + _, err := w.Write(buffer.Bytes()) + if err != nil { + return err } - record := recordWriter.makeBufferRecordLocked(buffer) - records = append(records, record) - } - if len(records) == 0 { - return nil } - flushRecords := records - records = nil - return w.upstream.WriteVectorised(flushRecords) + return nil } func (w *unshapedWriter) WriteZeroChunk() error { @@ -503,17 +455,7 @@ func (w *rawWriter) WritePacketBuffer(buffer *buf.Buffer) error { return w.WriteBuffer(buffer) } -func (w *rawWriter) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - upstreamWriter, created := bufio.CreateVectorisedWriter(w.upstream) - if !created { - return nil, false - } - return w.CreateVectorisedWriterFor(upstreamWriter), true -} -func (w *rawWriter) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { - return &vectorisedRawWriter{writer: w, upstream: upstream} -} type vectorisedRawWriter struct { writer *rawWriter @@ -521,40 +463,17 @@ type vectorisedRawWriter struct { } func (w *vectorisedRawWriter) WriteVectorised(buffers []*buf.Buffer) error { - var records []*buf.Buffer - defer func() { - buf.ReleaseMulti(records) - }() - recordWriter := w.writer - recordWriter.access.Lock() - defer recordWriter.access.Unlock() + // Compatibility fallback for sing v0.8.11 (no VectorisedWriteCreator chaining). for _, buffer := range buffers { - dataLen := buffer.Len() - if dataLen == 0 { - buffer.Release() + if buffer.IsEmpty() { continue } - for data := buffer.Bytes(); len(data) > 0; { - if len(data) == dataLen && dataLen <= maxPayload { - record := recordWriter.makeBufferRecordLocked(buffer) - buffer = nil - records = append(records, record) - break - } - recordLen := min(len(data), maxPayload) - records = append(records, recordWriter.makeSliceRecordLocked(data[:recordLen])) - data = data[recordLen:] - } - if buffer != nil { - buffer.Release() + _, err := w.Write(buffer.Bytes()) + if err != nil { + return err } } - if len(records) == 0 { - return nil - } - flushRecords := records - records = nil - return w.upstream.WriteVectorised(flushRecords) + return nil } func (w *rawWriter) CreatePacketVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { @@ -567,32 +486,17 @@ type packetVectorisedRawWriter struct { } func (w *packetVectorisedRawWriter) WriteVectorised(buffers []*buf.Buffer) error { - var records []*buf.Buffer - defer func() { - buf.ReleaseMulti(records) - }() - recordWriter := w.writer - recordWriter.access.Lock() - defer recordWriter.access.Unlock() - for index, buffer := range buffers { + // Compatibility fallback for sing v0.8.11 (no VectorisedWriteCreator chaining). + for _, buffer := range buffers { if buffer.IsEmpty() { - buffer.Release() continue } - if buffer.Len() > maxPayload { - buffer.Release() - buf.ReleaseMulti(buffers[index+1:]) - return snell.ErrPayloadTooLarge + _, err := w.Write(buffer.Bytes()) + if err != nil { + return err } - record := recordWriter.makeBufferRecordLocked(buffer) - records = append(records, record) - } - if len(records) == 0 { - return nil } - flushRecords := records - records = nil - return w.upstream.WriteVectorised(flushRecords) + return nil } func (w *rawWriter) WriteZeroChunk() error { @@ -625,17 +529,14 @@ var ( _ N.ExtendedReader = (*unshapedReader)(nil) _ N.ReadWaiter = (*unshapedReader)(nil) _ N.ExtendedWriter = (*unshapedWriter)(nil) - _ N.VectorisedWriteCreator = (*unshapedWriter)(nil) _ N.VectorisedWriter = (*vectorisedUnshapedWriter)(nil) _ N.VectorisedWriter = (*packetVectorisedUnshapedWriter)(nil) _ N.FrontHeadroom = (*unshapedWriter)(nil) _ N.ExtendedReader = (*rawReader)(nil) _ N.ReadWaiter = (*rawReader)(nil) _ N.ExtendedWriter = (*rawWriter)(nil) - _ N.VectorisedWriteCreator = (*rawWriter)(nil) _ N.VectorisedWriter = (*vectorisedRawWriter)(nil) _ N.VectorisedWriter = (*packetVectorisedRawWriter)(nil) - _ N.VectorisedWriteCreator = (*shapedWriter)(nil) _ N.WriterWithMTU = (*unshapedWriter)(nil) _ N.WriterWithMTU = (*rawWriter)(nil) _ N.WriterWithMTU = (*shapedWriter)(nil) diff --git a/proxy/snell/internal/singsnell/snellv6/reuse.go b/proxy/snell/internal/singsnell/snellv6/reuse.go index 9cce732fcef..1a572d1ca72 100644 --- a/proxy/snell/internal/singsnell/snellv6/reuse.go +++ b/proxy/snell/internal/singsnell/snellv6/reuse.go @@ -316,9 +316,6 @@ func (c *reuseConn) WriteBuffer(buffer *buf.Buffer) error { return c.session.writer.WriteBuffer(buffer) } -func (c *reuseConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - return c.session.writer.CreateVectorisedWriter() -} func (c *reuseConn) CloseWrite() error { c.closeWriteOnce.Do(func() { @@ -456,398 +453,32 @@ func (c *reuseConn) RemoteAddr() net.Addr { return c.destination.TCPAddr() } -type serverReuseSession[U comparable] struct { - net.Conn - service *Service - multiService *MultiService[U] - reader reuse.RecordReader - writer reuse.RecordWriter -} -func (s *serverReuseSession[U]) Serve(ctx context.Context, source M.Socksaddr, onClose N.CloseHandlerFunc, record *buf.Buffer, request snell.Request) (err error) { - callOnClose := true - if onClose != nil { - defer func() { - if callOnClose { - onClose(err) - } - }() - } - for { - requestCtx := ctx - if s.multiService != nil && request.Command != snell.CommandPing { - requestCtx, err = s.multiService.authenticate(ctx, request) - if err != nil { - record.Release() - s.Conn.Close() - return err - } - } - switch request.Command { - case snell.CommandConnect, snell.CommandConnectV2: - case snell.CommandUDP: - // snell-server v6.0.0b4: FUN_00141bc0 rejects UDP tunnel requests when the - // first UDP datagram starts in the same decrypted frame as the tunnel request. - if !record.IsEmpty() { - record.Release() - s.Conn.Close() - return errInvalidUDPTunnelRequest - } - record.Release() - packetConn := &serverPacketConn{Conn: s.Conn, service: s.service, reader: s.reader, writer: s.writer} - err = packetConn.writeTunnelReply() - if err != nil { - s.Conn.Close() - return err - } - s.writer = packetConn.writer - callOnClose = false - s.service.handler.NewPacketConnectionEx(requestCtx, packetConn, source, M.Socksaddr{}, onClose) - return nil - case snell.CommandPing: - record.Release() - pong := [1]byte{snell.ReplyPong} - if s.writer == nil { - s.writer, err = writeFirstRecord(s.Conn, s.service.mode, s.service.psk, s.service.profile, pong[:]) - } else { - _, err = s.writer.Write(pong[:]) - } - if err != nil { - s.Conn.Close() - return err - } - s.Conn.Close() - return nil - default: - record.Release() - s.Conn.Close() - return E.Extend(snell.ErrUnsupportedCommand, request.Command) - } - serverConn := &serverReuseConn[U]{Conn: s.Conn, session: s, completion: make(chan struct{})} - if record.IsEmpty() { - record.Release() - } else { - s.reader.SetCache(record) - } - s.service.handler.NewConnectionEx(requestCtx, serverConn, source, request.Destination, serverConn.completeLogicalConnection) - select { - case <-serverConn.completion: - case <-requestCtx.Done(): - s.Conn.Close() - return requestCtx.Err() - } - err = serverConn.CloseWrite() - if err != nil { - s.Conn.Close() - return err - } - if serverConn.aborted.Load() { - return nil - } - if !serverConn.readClosed.Load() { - // snell-server v6.0.0b4: after writing the EOF packet, FUN_00141bc0 keeps - // consuming client records (forwarding them into the still writable - // outgoing socket) until the zero chunk reaches FUN_001419e0 -> - // FUN_001402c0 -> FUN_001401b0, which resets the tunnel stage so the - // next record is parsed as a request. The handler has already released - // the upstream, so discard instead, capped like the client waiting state. - var discarded int - for { - var drainRecord *buf.Buffer - drainRecord, err = s.reader.NextRecord() - if err != nil { - break - } - discarded += drainRecord.Len() - drainRecord.Release() - if discarded >= reuse.WaitingDiscardLimit { - s.Conn.Close() - return E.New("snell: read too much data after request end") - } - } - if !errors.Is(err, io.EOF) { - s.Conn.Close() - return E.Cause(err, "drain request") - } - } - record, err = s.reader.ReadRecord() - if err != nil { - if errors.Is(err, io.EOF) { - return nil - } - s.Conn.Close() - return E.Cause(err, "read request") - } - request, err = s.service.readRequest(record) - if err != nil { - record.Release() - s.Conn.Close() - return E.Cause(err, "decode request") - } - } -} -type serverReuseConn[U comparable] struct { - net.Conn - session *serverReuseSession[U] - - access sync.Mutex - closeWriteOnce sync.Once - closeWriteErr error - closeOnce sync.Once - closeErr error - readClosed atomic.Bool - aborted atomic.Bool - replyWritten bool - completeOnce sync.Once - completion chan struct{} -} -func (c *serverReuseConn[U]) writeResponse(payload []byte) error { - first := payload - if len(first) > maxPayload-1 { - first = payload[:maxPayload-1] - } - reply := make([]byte, 1+len(first)) - reply[0] = snell.ReplyTunnel - copy(reply[1:], first) - var err error - if c.session.writer == nil { - c.session.writer, err = writeFirstRecord(c.session.Conn, c.session.service.mode, c.session.service.psk, c.session.service.profile, reply) - } else { - _, err = c.session.writer.Write(reply) - } - if err != nil { - return E.Cause(err, "write reply") - } - c.replyWritten = true - if len(payload) > len(first) { - _, err = c.session.writer.Write(payload[len(first):]) - if err != nil { - return err - } - } - return nil -} -func (c *serverReuseConn[U]) writeResponseBuffer(buffer *buf.Buffer) error { - if c.session.writer != nil { - buffer.ExtendHeader(1)[0] = snell.ReplyTunnel - err := c.session.writer.WriteBuffer(buffer) - if err != nil { - return E.Cause(err, "write reply") - } - c.replyWritten = true - return nil - } - buffer.ExtendHeader(1)[0] = snell.ReplyTunnel - writer, err := writeFirstRecordBuffer(c.session.Conn, c.session.service.mode, c.session.service.psk, c.session.service.profile, buffer) - if err != nil { - return E.Cause(err, "write reply") - } - c.session.writer = writer - c.replyWritten = true - return nil -} -func (c *serverReuseConn[U]) writeErrorResponse() error { - message := []byte(remoteEOFMessage) - reply := make([]byte, 3+len(message)) - reply[0] = snell.ReplyError - reply[1] = remoteEOFCode - reply[2] = byte(len(message)) - copy(reply[3:], message) - var err error - if c.session.writer == nil { - c.session.writer, err = writeFirstRecord(c.session.Conn, c.session.service.mode, c.session.service.psk, c.session.service.profile, reply) - } else { - _, err = c.session.writer.Write(reply) - } - if err != nil { - return E.Cause(err, "write error reply") - } - return nil -} -func (c *serverReuseConn[U]) Read(p []byte) (int, error) { - n, err := c.session.reader.Read(p) - if errors.Is(err, io.EOF) { - c.readClosed.Store(true) - } - return n, err -} -func (c *serverReuseConn[U]) ReadBuffer(buffer *buf.Buffer) error { - err := c.session.reader.ReadBuffer(buffer) - if errors.Is(err, io.EOF) { - c.readClosed.Store(true) - } - return err -} -func (c *serverReuseConn[U]) Write(p []byte) (int, error) { - c.access.Lock() - defer c.access.Unlock() - if c.replyWritten { - return c.session.writer.Write(p) - } - err := c.writeResponse(p) - if err != nil { - return 0, err - } - return len(p), nil -} -func (c *serverReuseConn[U]) WriteBuffer(buffer *buf.Buffer) error { - c.access.Lock() - defer c.access.Unlock() - if c.replyWritten { - return c.session.writer.WriteBuffer(buffer) - } - return c.writeResponseBuffer(buffer) -} -func (c *serverReuseConn[U]) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - upstreamWriter, created := bufio.CreateVectorisedWriter(c.session.Conn) - if !created { - return nil, false - } - return &serverReuseVectorisedWriter[U]{conn: c, upstream: upstreamWriter}, true -} -type serverReuseVectorisedWriter[U comparable] struct { - conn *serverReuseConn[U] - upstream N.VectorisedWriter -} -func (w *serverReuseVectorisedWriter[U]) WriteVectorised(buffers []*buf.Buffer) error { - conn := w.conn - conn.access.Lock() - if conn.replyWritten { - recordWriter := conn.session.writer - conn.access.Unlock() - return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) - } - for index, buffer := range buffers { - if buffer.IsEmpty() { - buffer.Release() - continue - } - err := conn.writeResponseBuffer(buffer) - if err != nil { - conn.access.Unlock() - buf.ReleaseMulti(buffers[index+1:]) - return err - } - if index+1 < len(buffers) { - recordWriter := conn.session.writer - conn.access.Unlock() - return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers[index+1:]) - } - conn.access.Unlock() - return nil - } - conn.access.Unlock() - return nil -} -func (c *serverReuseConn[U]) CloseWrite() error { - c.closeWriteOnce.Do(func() { - c.access.Lock() - defer c.access.Unlock() - // snell-server v6.0.0b4: FUN_001414e0 -> FUN_00140390 reports upstream EOF - // before any payload as ReplyError 0x65 "Remote EOF", marks the context - // aborted, and FUN_00141390 closes the tunnel after the error data is written. - if !c.replyWritten { - c.closeWriteErr = c.writeErrorResponse() - if c.closeWriteErr == nil { - c.aborted.Store(true) - c.closeWriteErr = c.session.Conn.Close() - } - return - } - c.closeWriteErr = c.session.writer.WriteZeroChunk() - }) - return c.closeWriteErr -} -func (c *serverReuseConn[U]) Close() error { - c.closeOnce.Do(func() { - c.closeErr = c.CloseWrite() - c.completeLogicalConnection(c.closeErr) - }) - return c.closeErr -} -func (c *serverReuseConn[U]) completeLogicalConnection(error) { - c.completeOnce.Do(func() { - close(c.completion) - }) -} -func (c *serverReuseConn[U]) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { - return c.session.reader.InitializeReadWaiter(options) -} -func (c *serverReuseConn[U]) WaitReadBuffer() (*buf.Buffer, error) { - buffer, err := c.session.reader.WaitReadBuffer() - if errors.Is(err, io.EOF) { - c.readClosed.Store(true) - } - return buffer, err -} -func (c *serverReuseConn[U]) FrontHeadroom() int { - if c.replyWritten { - return c.session.writer.FrontHeadroom() - } - if c.session.writer != nil { - return 1 + c.session.writer.FrontHeadroom() - } - switch c.session.service.mode { - case ModeUnsafeRaw: - return 1 + snell.HeaderPlainLen - case ModeUnshaped: - return 1 + snell.SaltLen + snell.HeaderCipherLen - default: - return 1 + c.session.service.profile.saltBlockLen + c.session.service.profile.recordPrefixMax + snell.HeaderCipherLen + c.session.service.profile.padMaxHeadroom - } -} -func (c *serverReuseConn[U]) RearHeadroom() int { - if c.session.writer != nil { - return c.session.writer.RearHeadroom() - } - if c.session.service.mode == ModeUnsafeRaw { - return 0 - } - return snell.AEADTagLen -} -func (c *serverReuseConn[U]) WriterMTU() int { - if c.session.writer != nil { - return c.session.writer.WriterMTU() - } - if c.session.service.mode == ModeDefault { - return c.session.service.profile.chunkMax - } - return maxPayload -} -func (c *serverReuseConn[U]) Upstream() any { - return c.session.Conn -} var ( _ N.ExtendedConn = (*reuseConn)(nil) _ N.ReadWaitCreator = (*reuseConn)(nil) - _ N.VectorisedWriteCreator = (*reuseConn)(nil) _ N.EarlyReader = (*reuseConn)(nil) _ N.WriteCloser = (*reuseConn)(nil) _ N.ReadWaiter = (*reuseReadWaiter)(nil) - _ N.ExtendedConn = (*serverReuseConn[struct{}])(nil) - _ N.ReadWaiter = (*serverReuseConn[struct{}])(nil) - _ N.VectorisedWriteCreator = (*serverReuseConn[struct{}])(nil) - _ N.VectorisedWriter = (*serverReuseVectorisedWriter[struct{}])(nil) - _ N.WriteCloser = (*serverReuseConn[struct{}])(nil) ) diff --git a/proxy/snell/internal/singsnell/snellv6/server.go b/proxy/snell/internal/singsnell/snellv6/server.go deleted file mode 100644 index e2990787e6d..00000000000 --- a/proxy/snell/internal/singsnell/snellv6/server.go +++ /dev/null @@ -1,460 +0,0 @@ -package snellv6 - -import ( - "context" - "io" - "net" - "sync" - - snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" - "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" - "github.com/sagernet/sing/common/auth" - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" - E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -type Service struct { - psk []byte - mode Mode - profile *Profile - handler snell.Handler -} - -type ServerOptions struct { - PSK []byte - Mode Mode - Handler snell.Handler -} - -func NewService(options ServerOptions) (*Service, error) { - if len(options.PSK) == 0 { - return nil, snell.ErrMissingPSK - } - // snell-server v6.0.0b4: FUN_00138120: rejects PSKs outside the 12..255-byte range. - if len(options.PSK) < 12 || len(options.PSK) > 255 { - return nil, E.New("snell: psk length must be between 12 and 255 bytes") - } - if options.Mode != ModeDefault && options.Mode != ModeUnshaped && options.Mode != ModeUnsafeRaw { - return nil, E.New("snell: unknown v6 mode: ", int(options.Mode)) - } - service := &Service{psk: options.PSK, mode: options.Mode, handler: options.Handler} - if options.Mode == ModeDefault { - service.profile = NewProfile(options.PSK) - } - return service, nil -} - -func (s *Service) NewConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error { - err := s.newConnection(ctx, conn, source, onClose) - if err != nil { - return &snell.ServerError{Conn: conn, Source: source, Cause: err} - } - return nil -} - -type MultiService[U comparable] struct { - *Service - users map[string]U -} - -var errInvalidUDPTunnelRequest = E.New("snell: invalid udp tunnel request") - -func NewMultiService[U comparable](options ServerOptions) (*MultiService[U], error) { - service, err := NewService(options) - if err != nil { - return nil, err - } - return &MultiService[U]{Service: service, users: make(map[string]U)}, nil -} - -func (s *MultiService[U]) UpdateUsers(users []U, userKeys [][]byte) error { - if len(users) != len(userKeys) { - return E.New("snell: user/key count mismatch") - } - if len(users) == 0 { - return snell.ErrNoUsers - } - userMap := make(map[string]U, len(users)) - for index, user := range users { - key := userKeys[index] - if len(key) == 0 { - return snell.ErrBadUserKey - } - if len(key) > 255 { - return E.New("snell: user key too long") - } - keyString := string(key) - if _, loaded := userMap[keyString]; loaded { - return snell.ErrDuplicateUserKey - } - userMap[keyString] = user - } - s.users = userMap - return nil -} - -func (s *MultiService[U]) NewConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error { - err := s.newConnection(ctx, conn, source, onClose) - if err != nil { - return &snell.ServerError{Conn: conn, Source: source, Cause: err} - } - return nil -} - -func (s *MultiService[U]) authenticate(ctx context.Context, request snell.Request) (context.Context, error) { - user, loaded := s.users[string(request.ClientID)] - if !loaded { - return nil, snell.ErrBadUserKey - } - return auth.ContextWithUser(ctx, user), nil -} - -func (s *Service) readRequest(record *buf.Buffer) (snell.Request, error) { - prefix := make([]byte, 3) - _, err := io.ReadFull(record, prefix) - if err != nil { - return snell.Request{}, err - } - if prefix[0] != snell.RequestVersion { - return snell.Request{}, E.Extend(snell.ErrBadVersion, "request version ", prefix[0]) - } - request := snell.Request{Command: prefix[1]} - // snell-server v6.0.0b4: FUN_00141bc0 handles SN_COMMAND_PING immediately after - // the version/command bytes and aborts with PONG data, without consuming client-id bytes. - if request.Command == snell.CommandPing { - return request, nil - } - clientIDLen := int(prefix[2]) - if clientIDLen > 0 { - request.ClientID = make([]byte, clientIDLen) - _, err = io.ReadFull(record, request.ClientID) - if err != nil { - return snell.Request{}, err - } - } - switch request.Command { - case snell.CommandConnect, snell.CommandConnectV2: - request.Destination, err = snell.ReadConnectAddress(record) - if err != nil { - return snell.Request{}, err - } - case snell.CommandUDP: - default: - return snell.Request{}, E.Extend(snell.ErrUnsupportedCommand, request.Command) - } - return request, nil -} - -func (s *Service) newConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error { - reader, record, err := readFirstRecord(conn, s.mode, s.psk, s.profile, N.ReadWaitOptions{}) - if err != nil { - return E.Cause(err, "read request") - } - request, err := s.readRequest(record) - if err != nil { - record.Release() - return E.Cause(err, "decode request") - } - - switch request.Command { - case snell.CommandConnect: - serverConn := &serverConn{Conn: conn, service: s, reader: reader} - if record.IsEmpty() { - record.Release() - } else { - reader.SetCache(record) - } - s.handler.NewConnectionEx(ctx, serverConn, source, request.Destination, onClose) - return nil - case snell.CommandConnectV2: - reuseSession := &serverReuseSession[struct{}]{Conn: conn, service: s, reader: reader} - return reuseSession.Serve(ctx, source, onClose, record, request) - case snell.CommandUDP: - // snell-server v6.0.0b4: FUN_00141bc0 rejects UDP tunnel requests when the - // first UDP datagram starts in the same decrypted frame as the tunnel request. - if !record.IsEmpty() { - record.Release() - conn.Close() - return errInvalidUDPTunnelRequest - } - record.Release() - packetConn := &serverPacketConn{Conn: conn, service: s, reader: reader} - err = packetConn.writeTunnelReply() - if err != nil { - return err - } - s.handler.NewPacketConnectionEx(ctx, packetConn, source, M.Socksaddr{}, onClose) - return nil - case snell.CommandPing: - record.Release() - pong := [1]byte{snell.ReplyPong} - _, err = writeFirstRecord(conn, s.mode, s.psk, s.profile, pong[:]) - closeErr := conn.Close() - if err != nil { - return err - } - return closeErr - default: - record.Release() - return E.Extend(snell.ErrUnsupportedCommand, request.Command) - } -} - -func (s *MultiService[U]) newConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error { - reader, record, err := readFirstRecord(conn, s.mode, s.psk, s.profile, N.ReadWaitOptions{}) - if err != nil { - return E.Cause(err, "read request") - } - request, err := s.readRequest(record) - if err != nil { - record.Release() - return E.Cause(err, "decode request") - } - - switch request.Command { - case snell.CommandConnect: - requestCtx, err := s.authenticate(ctx, request) - if err != nil { - record.Release() - return err - } - serverConn := &serverConn{Conn: conn, service: s.Service, reader: reader} - if record.IsEmpty() { - record.Release() - } else { - reader.SetCache(record) - } - s.handler.NewConnectionEx(requestCtx, serverConn, source, request.Destination, onClose) - return nil - case snell.CommandConnectV2: - reuseSession := &serverReuseSession[U]{Conn: conn, service: s.Service, multiService: s, reader: reader} - return reuseSession.Serve(ctx, source, onClose, record, request) - case snell.CommandUDP: - requestCtx, err := s.authenticate(ctx, request) - if err != nil { - record.Release() - return err - } - // snell-server v6.0.0b4: FUN_00141bc0 rejects UDP tunnel requests when the - // first UDP datagram starts in the same decrypted frame as the tunnel request. - if !record.IsEmpty() { - record.Release() - conn.Close() - return errInvalidUDPTunnelRequest - } - record.Release() - packetConn := &serverPacketConn{Conn: conn, service: s.Service, reader: reader} - err = packetConn.writeTunnelReply() - if err != nil { - return err - } - s.handler.NewPacketConnectionEx(requestCtx, packetConn, source, M.Socksaddr{}, onClose) - return nil - case snell.CommandPing: - record.Release() - pong := [1]byte{snell.ReplyPong} - _, err = writeFirstRecord(conn, s.mode, s.psk, s.profile, pong[:]) - closeErr := conn.Close() - if err != nil { - return err - } - return closeErr - default: - record.Release() - return E.Extend(snell.ErrUnsupportedCommand, request.Command) - } -} - -var ( - _ snell.Service = (*Service)(nil) - _ snell.Service = (*MultiService[int])(nil) -) - -type serverConn struct { - net.Conn - service *Service - reader reuse.RecordReader - - access sync.Mutex - writer reuse.RecordWriter - - closeWriteOnce sync.Once - closeWriteErr error -} - -func (c *serverConn) writeResponse(payload []byte) error { - first := payload - if len(first) > maxPayload-1 { - first = payload[:maxPayload-1] - } - reply := make([]byte, 1+len(first)) - reply[0] = snell.ReplyTunnel - copy(reply[1:], first) - writer, err := writeFirstRecord(c.Conn, c.service.mode, c.service.psk, c.service.profile, reply) - if err != nil { - return E.Cause(err, "write reply") - } - c.writer = writer - if len(payload) > len(first) { - _, err = writer.Write(payload[len(first):]) - if err != nil { - return err - } - } - return nil -} - -func (c *serverConn) writeResponseBuffer(buffer *buf.Buffer) error { - buffer.ExtendHeader(1)[0] = snell.ReplyTunnel - writer, err := writeFirstRecordBuffer(c.Conn, c.service.mode, c.service.psk, c.service.profile, buffer) - if err != nil { - return E.Cause(err, "write reply") - } - c.writer = writer - return nil -} - -func (c *serverConn) Read(p []byte) (int, error) { - return c.reader.Read(p) -} - -func (c *serverConn) ReadBuffer(buffer *buf.Buffer) error { - return c.reader.ReadBuffer(buffer) -} - -func (c *serverConn) Write(p []byte) (int, error) { - c.access.Lock() - if c.writer != nil { - writer := c.writer - c.access.Unlock() - return writer.Write(p) - } - defer c.access.Unlock() - err := c.writeResponse(p) - if err != nil { - return 0, err - } - return len(p), nil -} - -func (c *serverConn) WriteBuffer(buffer *buf.Buffer) error { - c.access.Lock() - if c.writer != nil { - writer := c.writer - c.access.Unlock() - return writer.WriteBuffer(buffer) - } - defer c.access.Unlock() - return c.writeResponseBuffer(buffer) -} - -func (c *serverConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - upstreamWriter, created := bufio.CreateVectorisedWriter(c.Conn) - if !created { - return nil, false - } - return &serverVectorisedWriter{conn: c, upstream: upstreamWriter}, true -} - -type serverVectorisedWriter struct { - conn *serverConn - upstream N.VectorisedWriter -} - -func (w *serverVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { - conn := w.conn - conn.access.Lock() - if conn.writer != nil { - recordWriter := conn.writer - conn.access.Unlock() - return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) - } - for index, buffer := range buffers { - if buffer.IsEmpty() { - buffer.Release() - continue - } - err := conn.writeResponseBuffer(buffer) - if err != nil { - conn.access.Unlock() - buf.ReleaseMulti(buffers[index+1:]) - return err - } - if index+1 < len(buffers) { - recordWriter := conn.writer - conn.access.Unlock() - return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers[index+1:]) - } - conn.access.Unlock() - return nil - } - conn.access.Unlock() - return nil -} - -func (c *serverConn) CloseWrite() error { - c.closeWriteOnce.Do(func() { - c.access.Lock() - defer c.access.Unlock() - // snell-server v6.0.0b4 closes non-reusable command-1 tunnels on upstream EOF instead of writing a Snell zero chunk. - c.closeWriteErr = c.Conn.Close() - }) - return c.closeWriteErr -} - -func (c *serverConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { - return c.reader.InitializeReadWaiter(options) -} - -func (c *serverConn) WaitReadBuffer() (*buf.Buffer, error) { - return c.reader.WaitReadBuffer() -} - -func (c *serverConn) FrontHeadroom() int { - if c.writer != nil { - return c.writer.FrontHeadroom() - } - switch c.service.mode { - case ModeUnsafeRaw: - return 1 + snell.HeaderPlainLen - case ModeUnshaped: - return 1 + snell.SaltLen + snell.HeaderCipherLen - default: - return 1 + c.service.profile.saltBlockLen + c.service.profile.recordPrefixMax + snell.HeaderCipherLen + c.service.profile.padMaxHeadroom - } -} - -func (c *serverConn) RearHeadroom() int { - if c.writer != nil { - return c.writer.RearHeadroom() - } - if c.service.mode == ModeUnsafeRaw { - return 0 - } - return snell.AEADTagLen -} - -func (c *serverConn) WriterMTU() int { - if c.writer != nil { - return c.writer.WriterMTU() - } - if c.service.mode == ModeDefault { - return c.service.profile.chunkMax - } - return maxPayload -} - -func (c *serverConn) Upstream() any { - return c.Conn -} - -var ( - _ N.ExtendedConn = (*serverConn)(nil) - _ N.ReadWaiter = (*serverConn)(nil) - _ N.VectorisedWriteCreator = (*serverConn)(nil) - _ N.VectorisedWriter = (*serverVectorisedWriter)(nil) - _ N.WriteCloser = (*serverConn)(nil) -) diff --git a/proxy/snell/internal/singsnell/snellv6/shaped.go b/proxy/snell/internal/singsnell/snellv6/shaped.go index 10b73a6a721..901060d8f92 100644 --- a/proxy/snell/internal/singsnell/snellv6/shaped.go +++ b/proxy/snell/internal/singsnell/snellv6/shaped.go @@ -248,17 +248,7 @@ func (w *shapedWriter) WriteZeroChunk() error { return err } -func (w *shapedWriter) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - upstreamWriter, created := bufio.CreateVectorisedWriter(w.upstream) - if !created { - return nil, false - } - return w.CreateVectorisedWriterFor(upstreamWriter), true -} -func (w *shapedWriter) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { - return &vectorisedShapedWriter{writer: w, upstream: upstream} -} type vectorisedShapedWriter struct { writer *shapedWriter @@ -266,41 +256,17 @@ type vectorisedShapedWriter struct { } func (w *vectorisedShapedWriter) WriteVectorised(buffers []*buf.Buffer) error { - var records []*buf.Buffer - defer func() { - buf.ReleaseMulti(records) - }() - recordWriter := w.writer - recordWriter.access.Lock() - defer recordWriter.access.Unlock() + // Compatibility fallback for sing v0.8.11 (no VectorisedWriteCreator chaining). for _, buffer := range buffers { - dataLen := buffer.Len() - if dataLen == 0 { - buffer.Release() + if buffer.IsEmpty() { continue } - for data := buffer.Bytes(); len(data) > 0; { - payloadLimit := recordWriter.payloadLimitFor(time.Now()) - recordLen := min(len(data), payloadLimit) - if len(data) == dataLen && dataLen <= payloadLimit { - record := recordWriter.makeBufferRecord(buffer) - buffer = nil - records = append(records, record) - break - } - records = append(records, recordWriter.makeSliceRecord(data[:recordLen])) - data = data[recordLen:] - } - if buffer != nil { - buffer.Release() + _, err := w.Write(buffer.Bytes()) + if err != nil { + return err } } - if len(records) == 0 { - return nil - } - flushRecords := records - records = nil - return w.upstream.WriteVectorised(flushRecords) + return nil } var _ N.VectorisedWriter = (*vectorisedShapedWriter)(nil) @@ -315,48 +281,17 @@ type packetVectorisedShapedWriter struct { } func (w *packetVectorisedShapedWriter) WriteVectorised(buffers []*buf.Buffer) error { - var records []*buf.Buffer - defer func() { - buf.ReleaseMulti(records) - }() - recordWriter := w.writer - recordWriter.access.Lock() - defer recordWriter.access.Unlock() - for index, buffer := range buffers { + // Compatibility fallback for sing v0.8.11 (no VectorisedWriteCreator chaining). + for _, buffer := range buffers { if buffer.IsEmpty() { - buffer.Release() continue } - dataLen := buffer.Len() - nowUnix := time.Now().Unix() - chunkSize := recordWriter.chunkSize - if recordWriter.lastWriteUnix == 0 || nowUnix-recordWriter.lastWriteUnix > int64(recordWriter.profile.idleResetSec) { - chunkSize = recordWriter.profile.chunkInitial - } - if chunkSize == 0 { - chunkSize = recordWriter.profile.chunkInitial - } - payloadLimit := recordWriter.profile.chunkPayloadLimit(recordWriter.seq, chunkSize) - if recordWriter.seq == 0 { - payloadLimit = min(payloadLimit, recordWriter.profile.firstRecordCap) - } - payloadLimit = max(1, min(payloadLimit, maxPayload)) - if dataLen > payloadLimit { - buffer.Release() - buf.ReleaseMulti(buffers[index+1:]) - return snell.ErrPayloadTooLarge + _, err := w.Write(buffer.Bytes()) + if err != nil { + return err } - recordWriter.chunkSize = recordWriter.profile.nextChunkSize(chunkSize) - recordWriter.lastWriteUnix = nowUnix - record := recordWriter.makeBufferRecord(buffer) - records = append(records, record) - } - if len(records) == 0 { - return nil } - flushRecords := records - records = nil - return w.upstream.WriteVectorised(flushRecords) + return nil } var _ N.VectorisedWriter = (*packetVectorisedShapedWriter)(nil) From d27b71ae5b5badf476ed40d5ca2c6c4106f040a7 Mon Sep 17 00:00:00 2001 From: Minis <285644962+Minis233@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:31:40 +0800 Subject: [PATCH 08/15] fix(snell): reimport sing-snell with sing v0.8.11 shims Clean re-import of SagerNet/sing-snell client sources plus minimal compat stubs so CreateVectorisedWriter/PacketBatch* APIs degrade gracefully on sing v0.8.11. Server-only paths removed. --- .../internal/singsnell/snellv4/client.go | 40 +++++ .../singsnell/snellv4/compat_sing011.go | 21 +++ .../internal/singsnell/snellv4/packet.go | 13 ++ .../internal/singsnell/snellv4/record.go | 135 +++++++++++++-- .../snell/internal/singsnell/snellv4/reuse.go | 3 + .../internal/singsnell/snellv6/client.go | 40 +++++ .../singsnell/snellv6/compat_sing011.go | 21 +++ .../internal/singsnell/snellv6/packet.go | 162 ++---------------- .../internal/singsnell/snellv6/record.go | 136 ++++++++++++--- .../snell/internal/singsnell/snellv6/reuse.go | 3 + .../internal/singsnell/snellv6/shaped.go | 85 +++++++-- 11 files changed, 462 insertions(+), 197 deletions(-) create mode 100644 proxy/snell/internal/singsnell/snellv4/compat_sing011.go create mode 100644 proxy/snell/internal/singsnell/snellv6/compat_sing011.go diff --git a/proxy/snell/internal/singsnell/snellv4/client.go b/proxy/snell/internal/singsnell/snellv4/client.go index 2b017f627f4..06ff100d41a 100644 --- a/proxy/snell/internal/singsnell/snellv4/client.go +++ b/proxy/snell/internal/singsnell/snellv4/client.go @@ -203,8 +203,48 @@ func (c *clientConn) WriteBuffer(buffer *buf.Buffer) error { return c.writeRequestBuffer(buffer) } +func (c *clientConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + return nil, false +} +type clientVectorisedWriter struct { + conn *clientConn + upstream N.VectorisedWriter +} +func (w *clientVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { + conn := w.conn + if conn.writer != nil { + return conn.writer.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) + } + conn.access.Lock() + if conn.writer != nil { + recordWriter := conn.writer + conn.access.Unlock() + return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) + } + for index, buffer := range buffers { + if buffer.IsEmpty() { + buffer.Release() + continue + } + err := conn.writeRequestBuffer(buffer) + if err != nil { + conn.access.Unlock() + buf.ReleaseMulti(buffers[index+1:]) + return err + } + if index+1 < len(buffers) { + recordWriter := conn.writer + conn.access.Unlock() + return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers[index+1:]) + } + conn.access.Unlock() + return nil + } + conn.access.Unlock() + return nil +} func (c *clientConn) CloseWrite() error { c.closeWriteOnce.Do(func() { diff --git a/proxy/snell/internal/singsnell/snellv4/compat_sing011.go b/proxy/snell/internal/singsnell/snellv4/compat_sing011.go new file mode 100644 index 00000000000..016721ef77e --- /dev/null +++ b/proxy/snell/internal/singsnell/snellv4/compat_sing011.go @@ -0,0 +1,21 @@ +package snellv4 + +import ( + "github.com/sagernet/sing/common/buf" + M "github.com/sagernet/sing/common/metadata" +) + +// Stubs for sing APIs newer than v0.8.11. Methods that create these always +// return unsupported (nil, false) on this branch of exclave-core. + +type PacketBatchWriter interface { + WritePacketBatch(buffers []*buf.Buffer, destinations []M.Socksaddr) error +} + +type PacketBatchWriteCreator interface { + CreatePacketBatchWriter() (PacketBatchWriter, bool) +} + +type ConnectedPacketBatchWriteCreator interface { + CreateConnectedPacketBatchWriter() (PacketBatchWriter, bool) +} diff --git a/proxy/snell/internal/singsnell/snellv4/packet.go b/proxy/snell/internal/singsnell/snellv4/packet.go index 57c139c4b63..4cb7600b632 100644 --- a/proxy/snell/internal/singsnell/snellv4/packet.go +++ b/proxy/snell/internal/singsnell/snellv4/packet.go @@ -117,8 +117,21 @@ func (c *clientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksad return c.writer.WritePacketBuffer(buffer) } +func (c *clientPacketConn) CreatePacketBatchWriter() (PacketBatchWriter, bool) { + return nil, false +} + +type clientPacketBatchWriter struct { + conn *clientPacketConn + upstream N.VectorisedWriter + access sync.Mutex + writer N.VectorisedWriter +} +func (w *clientPacketBatchWriter) WritePacketBatch(buffers []*buf.Buffer, destinations []M.Socksaddr) error { + return nil +} func (c *clientPacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) { recordReader, err := c.readReply() diff --git a/proxy/snell/internal/singsnell/snellv4/record.go b/proxy/snell/internal/singsnell/snellv4/record.go index ea7ea2110f6..a8d4fee9191 100644 --- a/proxy/snell/internal/singsnell/snellv4/record.go +++ b/proxy/snell/internal/singsnell/snellv4/record.go @@ -618,40 +618,151 @@ func (w *writer) WriteBuffer(buffer *buf.Buffer) error { return w.writeBufferRecordLocked(buffer, paddingLen) } +func (w *writer) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + return nil, false +} +func (w *writer) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { + return nil +} +type vectorisedWriter struct { + writer *writer + upstream N.VectorisedWriter +} func (w *vectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { - // Compatibility fallback for sing v0.8.11 (no VectorisedWriteCreator chaining). - for _, buffer := range buffers { - if buffer.IsEmpty() { - continue + payloadLen := buf.LenMulti(buffers) + if payloadLen == 0 { + buf.ReleaseMulti(buffers) + return nil + } + var records []*buf.Buffer + defer func() { + buf.ReleaseMulti(records) + }() + recordWriter := w.writer + recordWriter.access.Lock() + defer recordWriter.access.Unlock() + err := recordWriter.initialize() + if err != nil { + buf.ReleaseMulti(buffers) + return err + } + nowUnix := time.Now().Unix() + payloadLimit := recordWriter.payloadLimitFor(nowUnix) + if payloadLimit <= 0 || payloadLimit > maxPayload { + panic("snell: invalid v4 payload limit") + } + // Surge 6.7.0 (11520): SNConnectorEngineAdapterV4::encryptData:outData: + // advances the growth window once per encrypt call and splits that call + // using the same payload limit. + recordWriter.advancePayloadLimit(payloadLimit, nowUnix) + index := 0 + for remainingPayload := payloadLen; remainingPayload > 0; { + for buffers[index].IsEmpty() { + buffers[index].Release() + index++ } - _, err := w.Write(buffer.Bytes()) - if err != nil { - return err + recordLen := min(remainingPayload, payloadLimit) + paddingLen := recordWriter.framePaddingLen(recordLen) + buffer := buffers[index] + if buffer.Len() == recordLen { + record, err := recordWriter.makeBufferRecordLocked(buffer, paddingLen) + if err != nil { + buffer.Release() + buf.ReleaseMulti(buffers[index+1:]) + return err + } + records = append(records, record) + index++ + } else { + payload := make([]byte, recordLen) + for copiedLen := 0; copiedLen < recordLen; { + buffer = buffers[index] + if buffer.IsEmpty() { + buffer.Release() + index++ + continue + } + copyLen := min(recordLen-copiedLen, buffer.Len()) + copy(payload[copiedLen:], buffer.Bytes()[:copyLen]) + buffer.Advance(copyLen) + copiedLen += copyLen + if buffer.IsEmpty() { + buffer.Release() + index++ + } + } + record, err := recordWriter.makeSliceRecordLocked(payload, paddingLen) + if err != nil { + buf.ReleaseMulti(buffers[index:]) + return err + } + records = append(records, record) } + remainingPayload -= recordLen } - return nil + buf.ReleaseMulti(buffers[index:]) + flushRecords := records + records = nil + return w.upstream.WriteVectorised(flushRecords) } func (w *writer) CreatePacketVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { return &packetVectorisedWriter{writer: w, upstream: upstream} } +type packetVectorisedWriter struct { + writer *writer + upstream N.VectorisedWriter +} func (w *packetVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { - // Compatibility fallback for sing v0.8.11 (no VectorisedWriteCreator chaining). - for _, buffer := range buffers { + var records []*buf.Buffer + defer func() { + buf.ReleaseMulti(records) + }() + recordWriter := w.writer + recordWriter.access.Lock() + defer recordWriter.access.Unlock() + err := recordWriter.initialize() + if err != nil { + buf.ReleaseMulti(buffers) + return err + } + for index, buffer := range buffers { if buffer.IsEmpty() { + buffer.Release() continue } - _, err := w.Write(buffer.Bytes()) + nowUnix := time.Now().Unix() + payloadLimit := recordWriter.payloadLimitFor(nowUnix) + if payloadLimit <= 0 || payloadLimit > maxPayload { + panic("snell: invalid v4 payload limit") + } + dataLen := buffer.Len() + if dataLen > payloadLimit { + buffer.Release() + buf.ReleaseMulti(buffers[index+1:]) + return snell.ErrPayloadTooLarge + } + recordWriter.advancePayloadLimit(payloadLimit, nowUnix) + paddingLen := recordWriter.framePaddingLen(dataLen) + record, err := recordWriter.makeBufferRecordLocked(buffer, paddingLen) if err != nil { + buffer.Release() + buf.ReleaseMulti(buffers[index+1:]) return err } + records = append(records, record) } - return nil + if len(records) == 0 { + return nil + } + flushRecords := records + records = nil + return w.upstream.WriteVectorised(flushRecords) } func (w *writer) WritePacketBuffer(buffer *buf.Buffer) error { diff --git a/proxy/snell/internal/singsnell/snellv4/reuse.go b/proxy/snell/internal/singsnell/snellv4/reuse.go index 4d5517f2166..950d91b7b69 100644 --- a/proxy/snell/internal/singsnell/snellv4/reuse.go +++ b/proxy/snell/internal/singsnell/snellv4/reuse.go @@ -312,6 +312,9 @@ func (c *reuseConn) WriteBuffer(buffer *buf.Buffer) error { return c.session.writer.WriteBuffer(buffer) } +func (c *reuseConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + return nil, false +} func (c *reuseConn) CloseWrite() error { c.closeWriteOnce.Do(func() { diff --git a/proxy/snell/internal/singsnell/snellv6/client.go b/proxy/snell/internal/singsnell/snellv6/client.go index a3e1921dca6..a9d7beb604f 100644 --- a/proxy/snell/internal/singsnell/snellv6/client.go +++ b/proxy/snell/internal/singsnell/snellv6/client.go @@ -210,8 +210,48 @@ func (c *clientConn) WriteBuffer(buffer *buf.Buffer) error { return c.writeRequestBuffer(buffer) } +func (c *clientConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + return nil, false +} +type clientVectorisedWriter struct { + conn *clientConn + upstream N.VectorisedWriter +} +func (w *clientVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { + conn := w.conn + if conn.writer != nil { + return conn.writer.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) + } + conn.access.Lock() + if conn.writer != nil { + recordWriter := conn.writer + conn.access.Unlock() + return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) + } + for index, buffer := range buffers { + if buffer.IsEmpty() { + buffer.Release() + continue + } + err := conn.writeRequestBuffer(buffer) + if err != nil { + conn.access.Unlock() + buf.ReleaseMulti(buffers[index+1:]) + return err + } + if index+1 < len(buffers) { + recordWriter := conn.writer + conn.access.Unlock() + return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers[index+1:]) + } + conn.access.Unlock() + return nil + } + conn.access.Unlock() + return nil +} func (c *clientConn) CloseWrite() error { c.closeWriteOnce.Do(func() { diff --git a/proxy/snell/internal/singsnell/snellv6/compat_sing011.go b/proxy/snell/internal/singsnell/snellv6/compat_sing011.go new file mode 100644 index 00000000000..ff3d19a629e --- /dev/null +++ b/proxy/snell/internal/singsnell/snellv6/compat_sing011.go @@ -0,0 +1,21 @@ +package snellv6 + +import ( + "github.com/sagernet/sing/common/buf" + M "github.com/sagernet/sing/common/metadata" +) + +// Stubs for sing APIs newer than v0.8.11. Methods that create these always +// return unsupported (nil, false) on this branch of exclave-core. + +type PacketBatchWriter interface { + WritePacketBatch(buffers []*buf.Buffer, destinations []M.Socksaddr) error +} + +type PacketBatchWriteCreator interface { + CreatePacketBatchWriter() (PacketBatchWriter, bool) +} + +type ConnectedPacketBatchWriteCreator interface { + CreateConnectedPacketBatchWriter() (PacketBatchWriter, bool) +} diff --git a/proxy/snell/internal/singsnell/snellv6/packet.go b/proxy/snell/internal/singsnell/snellv6/packet.go index 5792775b3aa..58cfbbe26f6 100644 --- a/proxy/snell/internal/singsnell/snellv6/packet.go +++ b/proxy/snell/internal/singsnell/snellv6/packet.go @@ -110,8 +110,21 @@ func (c *clientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksad return c.writer.WritePacketBuffer(buffer) } +func (c *clientPacketConn) CreatePacketBatchWriter() (PacketBatchWriter, bool) { + return nil, false +} + +type clientPacketBatchWriter struct { + conn *clientPacketConn + upstream N.VectorisedWriter + access sync.Mutex + writer N.VectorisedWriter +} +func (w *clientPacketBatchWriter) WritePacketBatch(buffers []*buf.Buffer, destinations []M.Socksaddr) error { + return nil +} func (c *clientPacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) { reader, err := c.readReply() @@ -208,168 +221,19 @@ func (c *clientPacketConn) WaitReadPacket() (*buf.Buffer, M.Socksaddr, error) { return record, destination, nil } -type serverPacketConn struct { - net.Conn - service *Service - reader reuse.RecordReader - readWaitOptions N.ReadWaitOptions - - writeAccess sync.Mutex - writer reuse.RecordWriter -} -func (c *serverPacketConn) responseAddrLen(source M.Socksaddr) int { - if source.Unwrap().Addr.Is4() { - return 1 + 4 + 2 - } - return 1 + 16 + 2 -} -func (c *serverPacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) { - record, err := c.reader.NextRecord() - if err != nil { - return M.Socksaddr{}, err - } - command, err := record.ReadByte() - if err != nil { - record.Release() - return M.Socksaddr{}, err - } - if command != snell.UDPCommandForward { - record.Release() - return M.Socksaddr{}, E.Extend(snell.ErrUnsupportedCommand, "udp ", command) - } - destination, err := snell.ReadUDPRequestAddress(record) - if err != nil { - record.Release() - return M.Socksaddr{}, err - } - if record.Len() > buffer.FreeLen() { - record.Release() - return M.Socksaddr{}, io.ErrShortBuffer - } - _, err = buffer.Write(record.Bytes()) - record.Release() - if err != nil { - return M.Socksaddr{}, err - } - return destination, nil -} -func (c *serverPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { - c.writeAccess.Lock() - if c.writer == nil { - err := c.writeTunnelReply() - if err != nil { - c.writeAccess.Unlock() - buffer.Release() - return err - } - } - writer := c.writer - c.writeAccess.Unlock() - header := buf.With(buffer.ExtendHeader(c.responseAddrLen(destination))) - err := snell.WriteUDPResponseAddress(header, destination) - if err != nil { - buffer.Release() - return err - } - if buffer.Len() > maxPayload { - buffer.Release() - return snell.ErrPayloadTooLarge - } - return writer.WritePacketBuffer(buffer) -} -func (c *serverPacketConn) writeTunnelReply() error { - reply := [1]byte{snell.ReplyTunnel} - if c.writer != nil { - _, err := c.writer.Write(reply[:]) - if err != nil { - return E.Cause(err, "write udp reply") - } - return nil - } - writer, err := writeFirstRecord(c.Conn, c.service.mode, c.service.psk, c.service.profile, reply[:]) - if err != nil { - return E.Cause(err, "write udp reply") - } - c.writer = writer - return nil -} -func (c *serverPacketConn) FrontHeadroom() int { - switch c.service.mode { - case ModeUnsafeRaw: - return snell.HeaderPlainLen + maxUDPResponseHeaderLen - case ModeUnshaped: - return snell.HeaderCipherLen + maxUDPResponseHeaderLen - default: - return c.service.profile.recordPrefixMax + snell.HeaderCipherLen + c.service.profile.padMaxHeadroom + maxUDPResponseHeaderLen - } -} -func (c *serverPacketConn) RearHeadroom() int { - if c.service.mode == ModeUnsafeRaw { - return 0 - } - return snell.AEADTagLen -} -func (c *serverPacketConn) WriterMTU() int { - switch c.service.mode { - case ModeUnsafeRaw, ModeUnshaped: - return maxPayload - maxUDPResponseHeaderLen - default: - payloadLimit := c.service.profile.chunkInitial - switch c.service.profile.chunkPolicy { - case 1: - payloadLimit = c.service.profile.chunkBuckets[0] - for _, chunkBucket := range c.service.profile.chunkBuckets[1:] { - payloadLimit = min(payloadLimit, chunkBucket) - } - case 2: - payloadLimit -= c.service.profile.chunkJitter - } - payloadLimit = max(0x40, min(payloadLimit, c.service.profile.chunkMax)) - return max(1, payloadLimit-maxUDPResponseHeaderLen) - } -} -func (c *serverPacketConn) Upstream() any { - return c.Conn -} -func (c *serverPacketConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { - c.readWaitOptions = options - c.reader.InitializeReadWaiter(options) - return false -} -func (c *serverPacketConn) WaitReadPacket() (*buf.Buffer, M.Socksaddr, error) { - record, err := c.reader.WaitReadBuffer() - if err != nil { - return nil, M.Socksaddr{}, err - } - command, err := record.ReadByte() - if err != nil { - record.Release() - return nil, M.Socksaddr{}, err - } - if command != snell.UDPCommandForward { - record.Release() - return nil, M.Socksaddr{}, E.Extend(snell.ErrUnsupportedCommand, "udp ", command) - } - destination, err := snell.ReadUDPRequestAddress(record) - if err != nil { - record.Release() - return nil, M.Socksaddr{}, err - } - return record, destination, nil -} var ( _ N.PacketConn = (*clientPacketConn)(nil) diff --git a/proxy/snell/internal/singsnell/snellv6/record.go b/proxy/snell/internal/singsnell/snellv6/record.go index ce094013052..6e474e2300f 100644 --- a/proxy/snell/internal/singsnell/snellv6/record.go +++ b/proxy/snell/internal/singsnell/snellv6/record.go @@ -277,7 +277,13 @@ func (w *unshapedWriter) WritePacketBuffer(buffer *buf.Buffer) error { return w.WriteBuffer(buffer) } +func (w *unshapedWriter) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + return nil, false +} +func (w *unshapedWriter) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { + return nil +} type vectorisedUnshapedWriter struct { writer *unshapedWriter @@ -285,17 +291,40 @@ type vectorisedUnshapedWriter struct { } func (w *vectorisedUnshapedWriter) WriteVectorised(buffers []*buf.Buffer) error { - // Compatibility fallback for sing v0.8.11 (no VectorisedWriteCreator chaining). + var records []*buf.Buffer + defer func() { + buf.ReleaseMulti(records) + }() + recordWriter := w.writer + recordWriter.access.Lock() + defer recordWriter.access.Unlock() for _, buffer := range buffers { - if buffer.IsEmpty() { + dataLen := buffer.Len() + if dataLen == 0 { + buffer.Release() continue } - _, err := w.Write(buffer.Bytes()) - if err != nil { - return err + for data := buffer.Bytes(); len(data) > 0; { + if len(data) == dataLen && dataLen <= maxPayload { + record := recordWriter.makeBufferRecordLocked(buffer) + buffer = nil + records = append(records, record) + break + } + recordLen := min(len(data), maxPayload) + records = append(records, recordWriter.makeSliceRecordLocked(data[:recordLen])) + data = data[recordLen:] + } + if buffer != nil { + buffer.Release() } } - return nil + if len(records) == 0 { + return nil + } + flushRecords := records + records = nil + return w.upstream.WriteVectorised(flushRecords) } func (w *unshapedWriter) CreatePacketVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { @@ -308,17 +337,32 @@ type packetVectorisedUnshapedWriter struct { } func (w *packetVectorisedUnshapedWriter) WriteVectorised(buffers []*buf.Buffer) error { - // Compatibility fallback for sing v0.8.11 (no VectorisedWriteCreator chaining). - for _, buffer := range buffers { + var records []*buf.Buffer + defer func() { + buf.ReleaseMulti(records) + }() + recordWriter := w.writer + recordWriter.access.Lock() + defer recordWriter.access.Unlock() + for index, buffer := range buffers { if buffer.IsEmpty() { + buffer.Release() continue } - _, err := w.Write(buffer.Bytes()) - if err != nil { - return err + if buffer.Len() > maxPayload { + buffer.Release() + buf.ReleaseMulti(buffers[index+1:]) + return snell.ErrPayloadTooLarge } + record := recordWriter.makeBufferRecordLocked(buffer) + records = append(records, record) } - return nil + if len(records) == 0 { + return nil + } + flushRecords := records + records = nil + return w.upstream.WriteVectorised(flushRecords) } func (w *unshapedWriter) WriteZeroChunk() error { @@ -455,7 +499,13 @@ func (w *rawWriter) WritePacketBuffer(buffer *buf.Buffer) error { return w.WriteBuffer(buffer) } +func (w *rawWriter) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + return nil, false +} +func (w *rawWriter) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { + return nil +} type vectorisedRawWriter struct { writer *rawWriter @@ -463,17 +513,40 @@ type vectorisedRawWriter struct { } func (w *vectorisedRawWriter) WriteVectorised(buffers []*buf.Buffer) error { - // Compatibility fallback for sing v0.8.11 (no VectorisedWriteCreator chaining). + var records []*buf.Buffer + defer func() { + buf.ReleaseMulti(records) + }() + recordWriter := w.writer + recordWriter.access.Lock() + defer recordWriter.access.Unlock() for _, buffer := range buffers { - if buffer.IsEmpty() { + dataLen := buffer.Len() + if dataLen == 0 { + buffer.Release() continue } - _, err := w.Write(buffer.Bytes()) - if err != nil { - return err + for data := buffer.Bytes(); len(data) > 0; { + if len(data) == dataLen && dataLen <= maxPayload { + record := recordWriter.makeBufferRecordLocked(buffer) + buffer = nil + records = append(records, record) + break + } + recordLen := min(len(data), maxPayload) + records = append(records, recordWriter.makeSliceRecordLocked(data[:recordLen])) + data = data[recordLen:] + } + if buffer != nil { + buffer.Release() } } - return nil + if len(records) == 0 { + return nil + } + flushRecords := records + records = nil + return w.upstream.WriteVectorised(flushRecords) } func (w *rawWriter) CreatePacketVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { @@ -486,17 +559,32 @@ type packetVectorisedRawWriter struct { } func (w *packetVectorisedRawWriter) WriteVectorised(buffers []*buf.Buffer) error { - // Compatibility fallback for sing v0.8.11 (no VectorisedWriteCreator chaining). - for _, buffer := range buffers { + var records []*buf.Buffer + defer func() { + buf.ReleaseMulti(records) + }() + recordWriter := w.writer + recordWriter.access.Lock() + defer recordWriter.access.Unlock() + for index, buffer := range buffers { if buffer.IsEmpty() { + buffer.Release() continue } - _, err := w.Write(buffer.Bytes()) - if err != nil { - return err + if buffer.Len() > maxPayload { + buffer.Release() + buf.ReleaseMulti(buffers[index+1:]) + return snell.ErrPayloadTooLarge } + record := recordWriter.makeBufferRecordLocked(buffer) + records = append(records, record) } - return nil + if len(records) == 0 { + return nil + } + flushRecords := records + records = nil + return w.upstream.WriteVectorised(flushRecords) } func (w *rawWriter) WriteZeroChunk() error { diff --git a/proxy/snell/internal/singsnell/snellv6/reuse.go b/proxy/snell/internal/singsnell/snellv6/reuse.go index 1a572d1ca72..e0b2c45f6ee 100644 --- a/proxy/snell/internal/singsnell/snellv6/reuse.go +++ b/proxy/snell/internal/singsnell/snellv6/reuse.go @@ -316,6 +316,9 @@ func (c *reuseConn) WriteBuffer(buffer *buf.Buffer) error { return c.session.writer.WriteBuffer(buffer) } +func (c *reuseConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + return nil, false +} func (c *reuseConn) CloseWrite() error { c.closeWriteOnce.Do(func() { diff --git a/proxy/snell/internal/singsnell/snellv6/shaped.go b/proxy/snell/internal/singsnell/snellv6/shaped.go index 901060d8f92..713674514e1 100644 --- a/proxy/snell/internal/singsnell/snellv6/shaped.go +++ b/proxy/snell/internal/singsnell/snellv6/shaped.go @@ -248,7 +248,13 @@ func (w *shapedWriter) WriteZeroChunk() error { return err } +func (w *shapedWriter) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + return nil, false +} +func (w *shapedWriter) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { + return nil +} type vectorisedShapedWriter struct { writer *shapedWriter @@ -256,17 +262,41 @@ type vectorisedShapedWriter struct { } func (w *vectorisedShapedWriter) WriteVectorised(buffers []*buf.Buffer) error { - // Compatibility fallback for sing v0.8.11 (no VectorisedWriteCreator chaining). + var records []*buf.Buffer + defer func() { + buf.ReleaseMulti(records) + }() + recordWriter := w.writer + recordWriter.access.Lock() + defer recordWriter.access.Unlock() for _, buffer := range buffers { - if buffer.IsEmpty() { + dataLen := buffer.Len() + if dataLen == 0 { + buffer.Release() continue } - _, err := w.Write(buffer.Bytes()) - if err != nil { - return err + for data := buffer.Bytes(); len(data) > 0; { + payloadLimit := recordWriter.payloadLimitFor(time.Now()) + recordLen := min(len(data), payloadLimit) + if len(data) == dataLen && dataLen <= payloadLimit { + record := recordWriter.makeBufferRecord(buffer) + buffer = nil + records = append(records, record) + break + } + records = append(records, recordWriter.makeSliceRecord(data[:recordLen])) + data = data[recordLen:] + } + if buffer != nil { + buffer.Release() } } - return nil + if len(records) == 0 { + return nil + } + flushRecords := records + records = nil + return w.upstream.WriteVectorised(flushRecords) } var _ N.VectorisedWriter = (*vectorisedShapedWriter)(nil) @@ -281,17 +311,48 @@ type packetVectorisedShapedWriter struct { } func (w *packetVectorisedShapedWriter) WriteVectorised(buffers []*buf.Buffer) error { - // Compatibility fallback for sing v0.8.11 (no VectorisedWriteCreator chaining). - for _, buffer := range buffers { + var records []*buf.Buffer + defer func() { + buf.ReleaseMulti(records) + }() + recordWriter := w.writer + recordWriter.access.Lock() + defer recordWriter.access.Unlock() + for index, buffer := range buffers { if buffer.IsEmpty() { + buffer.Release() continue } - _, err := w.Write(buffer.Bytes()) - if err != nil { - return err + dataLen := buffer.Len() + nowUnix := time.Now().Unix() + chunkSize := recordWriter.chunkSize + if recordWriter.lastWriteUnix == 0 || nowUnix-recordWriter.lastWriteUnix > int64(recordWriter.profile.idleResetSec) { + chunkSize = recordWriter.profile.chunkInitial } + if chunkSize == 0 { + chunkSize = recordWriter.profile.chunkInitial + } + payloadLimit := recordWriter.profile.chunkPayloadLimit(recordWriter.seq, chunkSize) + if recordWriter.seq == 0 { + payloadLimit = min(payloadLimit, recordWriter.profile.firstRecordCap) + } + payloadLimit = max(1, min(payloadLimit, maxPayload)) + if dataLen > payloadLimit { + buffer.Release() + buf.ReleaseMulti(buffers[index+1:]) + return snell.ErrPayloadTooLarge + } + recordWriter.chunkSize = recordWriter.profile.nextChunkSize(chunkSize) + recordWriter.lastWriteUnix = nowUnix + record := recordWriter.makeBufferRecord(buffer) + records = append(records, record) } - return nil + if len(records) == 0 { + return nil + } + flushRecords := records + records = nil + return w.upstream.WriteVectorised(flushRecords) } var _ N.VectorisedWriter = (*packetVectorisedShapedWriter)(nil) From dfbc3c8c45e3776f46c12485229bd41cadf53051 Mon Sep 17 00:00:00 2001 From: Minis <285644962+Minis233@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:34:03 +0800 Subject: [PATCH 09/15] fix(snell): drop leftover serverPacketConn asserts --- proxy/snell/internal/singsnell/snellv6/packet.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/proxy/snell/internal/singsnell/snellv6/packet.go b/proxy/snell/internal/singsnell/snellv6/packet.go index 58cfbbe26f6..97a48966c4f 100644 --- a/proxy/snell/internal/singsnell/snellv6/packet.go +++ b/proxy/snell/internal/singsnell/snellv6/packet.go @@ -239,7 +239,4 @@ var ( _ N.PacketConn = (*clientPacketConn)(nil) _ N.PacketReadWaiter = (*clientPacketConn)(nil) _ N.WriterWithMTU = (*clientPacketConn)(nil) - _ N.PacketConn = (*serverPacketConn)(nil) - _ N.PacketReadWaiter = (*serverPacketConn)(nil) - _ N.WriterWithMTU = (*serverPacketConn)(nil) ) From 6eaa1551bb34296a7c9d52c638ecc31245eeee20 Mon Sep 17 00:00:00 2001 From: Minis <285644962+Minis233@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:38:47 +0800 Subject: [PATCH 10/15] fix(snell): repair CreateVectorisedWriterFor and buffer helpers Restore RecordWriter.CreateVectorisedWriterFor, stub unsupported vectorised creators for sing v0.8.11, and replace NewBufferSize with buf.NewSize. --- .../singsnell/internal/reuse/reuse.go | 1 + .../internal/singsnell/snellv4/client.go | 25 ++----------------- .../internal/singsnell/snellv4/record.go | 2 +- .../snell/internal/singsnell/snellv4/reuse.go | 4 +++ .../internal/singsnell/snellv6/client.go | 25 ++----------------- .../internal/singsnell/snellv6/record.go | 4 +-- .../snell/internal/singsnell/snellv6/reuse.go | 4 +++ .../internal/singsnell/snellv6/shaped.go | 2 +- 8 files changed, 17 insertions(+), 50 deletions(-) diff --git a/proxy/snell/internal/singsnell/internal/reuse/reuse.go b/proxy/snell/internal/singsnell/internal/reuse/reuse.go index 64f72a0bf3a..2a776fe49ec 100644 --- a/proxy/snell/internal/singsnell/internal/reuse/reuse.go +++ b/proxy/snell/internal/singsnell/internal/reuse/reuse.go @@ -240,6 +240,7 @@ type RecordReader interface { type RecordWriter interface { N.ExtendedWriter + CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter CreatePacketVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter WritePacketBuffer(buffer *buf.Buffer) error WriteZeroChunk() error diff --git a/proxy/snell/internal/singsnell/snellv4/client.go b/proxy/snell/internal/singsnell/snellv4/client.go index 06ff100d41a..e37b6951c0a 100644 --- a/proxy/snell/internal/singsnell/snellv4/client.go +++ b/proxy/snell/internal/singsnell/snellv4/client.go @@ -213,36 +213,15 @@ type clientVectorisedWriter struct { } func (w *clientVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { - conn := w.conn - if conn.writer != nil { - return conn.writer.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) - } - conn.access.Lock() - if conn.writer != nil { - recordWriter := conn.writer - conn.access.Unlock() - return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) - } - for index, buffer := range buffers { + for _, buffer := range buffers { if buffer.IsEmpty() { - buffer.Release() continue } - err := conn.writeRequestBuffer(buffer) + err := w.conn.writer.WriteBuffer(buffer) if err != nil { - conn.access.Unlock() - buf.ReleaseMulti(buffers[index+1:]) return err } - if index+1 < len(buffers) { - recordWriter := conn.writer - conn.access.Unlock() - return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers[index+1:]) - } - conn.access.Unlock() - return nil } - conn.access.Unlock() return nil } diff --git a/proxy/snell/internal/singsnell/snellv4/record.go b/proxy/snell/internal/singsnell/snellv4/record.go index a8d4fee9191..2411fa128c3 100644 --- a/proxy/snell/internal/singsnell/snellv4/record.go +++ b/proxy/snell/internal/singsnell/snellv4/record.go @@ -108,7 +108,7 @@ func (r *reader) ReadRecord() (*buf.Buffer, error) { return nil, err } } - body := r.readWaitOptions.NewBufferSize(payloadLen + snell.AEADTagLen) + body := r.buf.NewSize(payloadLen + snell.AEADTagLen) _, err = body.ReadFullFrom(r.bufferedUpstream, payloadLen+snell.AEADTagLen) if err != nil { if padding != nil { diff --git a/proxy/snell/internal/singsnell/snellv4/reuse.go b/proxy/snell/internal/singsnell/snellv4/reuse.go index 950d91b7b69..22a8a8a91ef 100644 --- a/proxy/snell/internal/singsnell/snellv4/reuse.go +++ b/proxy/snell/internal/singsnell/snellv4/reuse.go @@ -315,6 +315,10 @@ func (c *reuseConn) WriteBuffer(buffer *buf.Buffer) error { func (c *reuseConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { return nil, false } +func (c *reuseConn) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { + return nil +} + func (c *reuseConn) CloseWrite() error { c.closeWriteOnce.Do(func() { diff --git a/proxy/snell/internal/singsnell/snellv6/client.go b/proxy/snell/internal/singsnell/snellv6/client.go index a9d7beb604f..565ca06e029 100644 --- a/proxy/snell/internal/singsnell/snellv6/client.go +++ b/proxy/snell/internal/singsnell/snellv6/client.go @@ -220,36 +220,15 @@ type clientVectorisedWriter struct { } func (w *clientVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { - conn := w.conn - if conn.writer != nil { - return conn.writer.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) - } - conn.access.Lock() - if conn.writer != nil { - recordWriter := conn.writer - conn.access.Unlock() - return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) - } - for index, buffer := range buffers { + for _, buffer := range buffers { if buffer.IsEmpty() { - buffer.Release() continue } - err := conn.writeRequestBuffer(buffer) + err := w.conn.writer.WriteBuffer(buffer) if err != nil { - conn.access.Unlock() - buf.ReleaseMulti(buffers[index+1:]) return err } - if index+1 < len(buffers) { - recordWriter := conn.writer - conn.access.Unlock() - return recordWriter.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers[index+1:]) - } - conn.access.Unlock() - return nil } - conn.access.Unlock() return nil } diff --git a/proxy/snell/internal/singsnell/snellv6/record.go b/proxy/snell/internal/singsnell/snellv6/record.go index 6e474e2300f..55bf628baa7 100644 --- a/proxy/snell/internal/singsnell/snellv6/record.go +++ b/proxy/snell/internal/singsnell/snellv6/record.go @@ -174,7 +174,7 @@ func (r *unshapedReader) read() (*buf.Buffer, error) { if payloadLen == 0 { return nil, io.EOF } - body := r.readWaitOptions.NewBufferSize(payloadLen + snell.AEADTagLen) + body := r.buf.NewSize(payloadLen + snell.AEADTagLen) _, err = body.ReadFullFrom(r.upstream, payloadLen+snell.AEADTagLen) if err != nil { body.Release() @@ -420,7 +420,7 @@ func (r *rawReader) read() (*buf.Buffer, error) { if payloadLen == 0 { return nil, io.EOF } - body := r.readWaitOptions.NewBufferSize(payloadLen) + body := r.buf.NewSize(payloadLen) _, err = body.ReadFullFrom(r.upstream, payloadLen) if err != nil { body.Release() diff --git a/proxy/snell/internal/singsnell/snellv6/reuse.go b/proxy/snell/internal/singsnell/snellv6/reuse.go index e0b2c45f6ee..c97ec23b0b3 100644 --- a/proxy/snell/internal/singsnell/snellv6/reuse.go +++ b/proxy/snell/internal/singsnell/snellv6/reuse.go @@ -319,6 +319,10 @@ func (c *reuseConn) WriteBuffer(buffer *buf.Buffer) error { func (c *reuseConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { return nil, false } +func (c *reuseConn) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { + return nil +} + func (c *reuseConn) CloseWrite() error { c.closeWriteOnce.Do(func() { diff --git a/proxy/snell/internal/singsnell/snellv6/shaped.go b/proxy/snell/internal/singsnell/snellv6/shaped.go index 713674514e1..2fcb44f1066 100644 --- a/proxy/snell/internal/singsnell/snellv6/shaped.go +++ b/proxy/snell/internal/singsnell/snellv6/shaped.go @@ -455,7 +455,7 @@ func (r *shapedReader) read() (*buf.Buffer, error) { return nil, err } } - body := r.readWaitOptions.NewBufferSize(payloadLen + snell.AEADTagLen) + body := r.buf.NewSize(payloadLen + snell.AEADTagLen) _, err = body.ReadFullFrom(r.upstream, payloadLen+snell.AEADTagLen) if err != nil { if padding != nil { From 0e63c3dc474c743e0d1d33f5c6c4c073504e9a19 Mon Sep 17 00:00:00 2001 From: Minis <285644962+Minis233@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:41:02 +0800 Subject: [PATCH 11/15] fix(snell): use buf.NewSize instead of r.buf.NewSize --- proxy/snell/internal/singsnell/snellv4/packet.go | 2 -- proxy/snell/internal/singsnell/snellv4/record.go | 2 +- proxy/snell/internal/singsnell/snellv6/packet.go | 2 -- proxy/snell/internal/singsnell/snellv6/record.go | 4 ++-- proxy/snell/internal/singsnell/snellv6/reuse.go | 1 - proxy/snell/internal/singsnell/snellv6/shaped.go | 3 +-- 6 files changed, 4 insertions(+), 10 deletions(-) diff --git a/proxy/snell/internal/singsnell/snellv4/packet.go b/proxy/snell/internal/singsnell/snellv4/packet.go index 4cb7600b632..9f3c5e1b9dd 100644 --- a/proxy/snell/internal/singsnell/snellv4/packet.go +++ b/proxy/snell/internal/singsnell/snellv4/packet.go @@ -3,14 +3,12 @@ package snellv4 import ( "io" "net" - "os" "sync" snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" diff --git a/proxy/snell/internal/singsnell/snellv4/record.go b/proxy/snell/internal/singsnell/snellv4/record.go index 2411fa128c3..6a4fc803559 100644 --- a/proxy/snell/internal/singsnell/snellv4/record.go +++ b/proxy/snell/internal/singsnell/snellv4/record.go @@ -108,7 +108,7 @@ func (r *reader) ReadRecord() (*buf.Buffer, error) { return nil, err } } - body := r.buf.NewSize(payloadLen + snell.AEADTagLen) + body := buf.NewSize(payloadLen + snell.AEADTagLen) _, err = body.ReadFullFrom(r.bufferedUpstream, payloadLen+snell.AEADTagLen) if err != nil { if padding != nil { diff --git a/proxy/snell/internal/singsnell/snellv6/packet.go b/proxy/snell/internal/singsnell/snellv6/packet.go index 97a48966c4f..472bc12e46a 100644 --- a/proxy/snell/internal/singsnell/snellv6/packet.go +++ b/proxy/snell/internal/singsnell/snellv6/packet.go @@ -3,14 +3,12 @@ package snellv6 import ( "io" "net" - "os" "sync" snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" diff --git a/proxy/snell/internal/singsnell/snellv6/record.go b/proxy/snell/internal/singsnell/snellv6/record.go index 55bf628baa7..24a752158e6 100644 --- a/proxy/snell/internal/singsnell/snellv6/record.go +++ b/proxy/snell/internal/singsnell/snellv6/record.go @@ -174,7 +174,7 @@ func (r *unshapedReader) read() (*buf.Buffer, error) { if payloadLen == 0 { return nil, io.EOF } - body := r.buf.NewSize(payloadLen + snell.AEADTagLen) + body := buf.NewSize(payloadLen + snell.AEADTagLen) _, err = body.ReadFullFrom(r.upstream, payloadLen+snell.AEADTagLen) if err != nil { body.Release() @@ -420,7 +420,7 @@ func (r *rawReader) read() (*buf.Buffer, error) { if payloadLen == 0 { return nil, io.EOF } - body := r.buf.NewSize(payloadLen) + body := buf.NewSize(payloadLen) _, err = body.ReadFullFrom(r.upstream, payloadLen) if err != nil { body.Release() diff --git a/proxy/snell/internal/singsnell/snellv6/reuse.go b/proxy/snell/internal/singsnell/snellv6/reuse.go index c97ec23b0b3..d0091b56528 100644 --- a/proxy/snell/internal/singsnell/snellv6/reuse.go +++ b/proxy/snell/internal/singsnell/snellv6/reuse.go @@ -12,7 +12,6 @@ import ( snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" diff --git a/proxy/snell/internal/singsnell/snellv6/shaped.go b/proxy/snell/internal/singsnell/snellv6/shaped.go index 2fcb44f1066..8b9a094d6cd 100644 --- a/proxy/snell/internal/singsnell/snellv6/shaped.go +++ b/proxy/snell/internal/singsnell/snellv6/shaped.go @@ -9,7 +9,6 @@ import ( snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" ) @@ -455,7 +454,7 @@ func (r *shapedReader) read() (*buf.Buffer, error) { return nil, err } } - body := r.buf.NewSize(payloadLen + snell.AEADTagLen) + body := buf.NewSize(payloadLen + snell.AEADTagLen) _, err = body.ReadFullFrom(r.upstream, payloadLen+snell.AEADTagLen) if err != nil { if padding != nil { From c68fb71908318867f8dd1a889d48d6c2692cc983 Mon Sep 17 00:00:00 2001 From: Minis <285644962+Minis233@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:43:34 +0800 Subject: [PATCH 12/15] fix(snell): drop unused bufio imports in record.go --- proxy/snell/internal/singsnell/snellv4/record.go | 1 - proxy/snell/internal/singsnell/snellv6/record.go | 1 - 2 files changed, 2 deletions(-) diff --git a/proxy/snell/internal/singsnell/snellv4/record.go b/proxy/snell/internal/singsnell/snellv4/record.go index 6a4fc803559..a69c426b61e 100644 --- a/proxy/snell/internal/singsnell/snellv4/record.go +++ b/proxy/snell/internal/singsnell/snellv4/record.go @@ -14,7 +14,6 @@ import ( snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" ) diff --git a/proxy/snell/internal/singsnell/snellv6/record.go b/proxy/snell/internal/singsnell/snellv6/record.go index 24a752158e6..3e79bca4f1a 100644 --- a/proxy/snell/internal/singsnell/snellv6/record.go +++ b/proxy/snell/internal/singsnell/snellv6/record.go @@ -9,7 +9,6 @@ import ( snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" ) From fb4f26d8c1cf9fadd3b0671a595048d04b1c3807 Mon Sep 17 00:00:00 2001 From: Minis <285644962+Minis233@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:46:12 +0800 Subject: [PATCH 13/15] fix(snell): ToSocksAddr and unused err in outbound --- proxy/snell/outbound.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/proxy/snell/outbound.go b/proxy/snell/outbound.go index f3f3fa4e830..98b3e953105 100644 --- a/proxy/snell/outbound.go +++ b/proxy/snell/outbound.go @@ -118,16 +118,13 @@ func (o *Outbound) getClient(dialer internet.Dialer) (snellClient, error) { psk := []byte(o.psk) userKey := []byte(o.userKey) - var ( - client snellClient - err error - ) + var client snellClient if o.version == 6 { mode, err := snellv6.ParseMode(o.mode) if err != nil { return nil, err } - client, err = snellv6.NewClient(snellv6.ClientOptions{ + c, err := snellv6.NewClient(snellv6.ClientOptions{ PSK: psk, UserKey: userKey, Mode: mode, @@ -138,12 +135,13 @@ func (o *Outbound) getClient(dialer internet.Dialer) (snellClient, error) { if err != nil { return nil, err } + client = c } else { obfsMode, err := snellproto.ParseObfsMode(o.obfs) if err != nil { return nil, err } - client, err = snellv4.NewClient(snellv4.ClientOptions{ + c, err := snellv4.NewClient(snellv4.ClientOptions{ PSK: psk, UserKey: userKey, Reuse: o.reuse, @@ -155,6 +153,7 @@ func (o *Outbound) getClient(dialer internet.Dialer) (snellClient, error) { if err != nil { return nil, err } + client = c } o.client = client return client, nil @@ -176,7 +175,7 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int " (v", o.version, ")").WriteToLog(session.ExportIDToError(ctx)) detachedCtx := core.ToBackgroundDetachedContext(ctx) - target := singbridge.ToSocksaddr(destination) + target := singbridge.ToSocksAddr(destination) if destination.Network == v2net.Network_TCP { serverConn, err := client.DialContext(detachedCtx, target) From 265a5d1da2f82d34e1beff55c6d0a37cd45d4691 Mon Sep 17 00:00:00 2001 From: Minis <285644962+Minis233@users.noreply.github.com> Date: Sat, 11 Jul 2026 12:29:17 +0800 Subject: [PATCH 14/15] =?UTF-8?q?refactor(snell):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20direct=20sing-snell,=20v4/v6=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Import github.com/sagernet/sing-snell directly; bump sing to the unstable version required by sing-snell (add HandshakeTimeout to singbridge TLS wrapper). - Support versions 4 and 6 only (drop v3/v5). - Rename JSON field obfs → obfsMode; strict exact-match parsing (none|http|tls; no "off" alias; case-sensitive). - Reject non-empty v4-only fields on v6 and vice versa. - Add userPSK (sing-box private extension; optional). - Allow empty PSK (servers may accept it). - Remove server-speaks-first first-payload logic (Snell is client-first). - Resolve UDP domain destinations to IP before write (Snell UDP has no domain ATYP). Snell v6 remains beta upstream; exposed with that understanding. --- common/singbridge/tls.go | 9 + go.mod | 3 +- infra/conf/v4/snell.go | 80 +- proxy/snell/NOTICE | 4 +- proxy/snell/config.pb.go | 39 +- proxy/snell/config.proto | 15 +- proxy/snell/internal/singsnell/address.go | 97 --- proxy/snell/internal/singsnell/crypto.go | 31 - .../singsnell/internal/reuse/reuse.go | 273 ------ proxy/snell/internal/singsnell/obfs.go | 685 --------------- proxy/snell/internal/singsnell/request.go | 97 --- proxy/snell/internal/singsnell/snell.go | 83 -- .../internal/singsnell/snellv4/client.go | 307 ------- .../singsnell/snellv4/compat_sing011.go | 21 - .../internal/singsnell/snellv4/packet.go | 233 ----- .../internal/singsnell/snellv4/record.go | 824 ------------------ .../snell/internal/singsnell/snellv4/reuse.go | 465 ---------- .../internal/singsnell/snellv6/client.go | 333 ------- .../singsnell/snellv6/compat_sing011.go | 21 - .../internal/singsnell/snellv6/handshake.go | 216 ----- .../internal/singsnell/snellv6/mixing.go | 54 -- .../snell/internal/singsnell/snellv6/mode.go | 41 - .../internal/singsnell/snellv6/packet.go | 240 ----- .../internal/singsnell/snellv6/profile.go | 522 ----------- .../internal/singsnell/snellv6/record.go | 630 ------------- .../snell/internal/singsnell/snellv6/reuse.go | 490 ----------- .../snell/internal/singsnell/snellv6/salt.go | 56 -- .../internal/singsnell/snellv6/shaped.go | 484 ---------- proxy/snell/outbound.go | 162 ++-- 29 files changed, 220 insertions(+), 6295 deletions(-) delete mode 100644 proxy/snell/internal/singsnell/address.go delete mode 100644 proxy/snell/internal/singsnell/crypto.go delete mode 100644 proxy/snell/internal/singsnell/internal/reuse/reuse.go delete mode 100644 proxy/snell/internal/singsnell/obfs.go delete mode 100644 proxy/snell/internal/singsnell/request.go delete mode 100644 proxy/snell/internal/singsnell/snell.go delete mode 100644 proxy/snell/internal/singsnell/snellv4/client.go delete mode 100644 proxy/snell/internal/singsnell/snellv4/compat_sing011.go delete mode 100644 proxy/snell/internal/singsnell/snellv4/packet.go delete mode 100644 proxy/snell/internal/singsnell/snellv4/record.go delete mode 100644 proxy/snell/internal/singsnell/snellv4/reuse.go delete mode 100644 proxy/snell/internal/singsnell/snellv6/client.go delete mode 100644 proxy/snell/internal/singsnell/snellv6/compat_sing011.go delete mode 100644 proxy/snell/internal/singsnell/snellv6/handshake.go delete mode 100644 proxy/snell/internal/singsnell/snellv6/mixing.go delete mode 100644 proxy/snell/internal/singsnell/snellv6/mode.go delete mode 100644 proxy/snell/internal/singsnell/snellv6/packet.go delete mode 100644 proxy/snell/internal/singsnell/snellv6/profile.go delete mode 100644 proxy/snell/internal/singsnell/snellv6/record.go delete mode 100644 proxy/snell/internal/singsnell/snellv6/reuse.go delete mode 100644 proxy/snell/internal/singsnell/snellv6/salt.go delete mode 100644 proxy/snell/internal/singsnell/snellv6/shaped.go diff --git a/common/singbridge/tls.go b/common/singbridge/tls.go index 08adfd6ec62..60fbd37c279 100644 --- a/common/singbridge/tls.go +++ b/common/singbridge/tls.go @@ -3,6 +3,7 @@ package singbridge import ( "context" "crypto/tls" + "time" singtls "github.com/sagernet/sing/common/tls" @@ -50,6 +51,14 @@ func (c *tlsConfigWrapper) Client(_ net.Conn) (singtls.Conn, error) { panic("invalid") } +// HandshakeTimeout / SetHandshakeTimeout satisfy sing v0.8.12+ tls.Config. +// stdlib crypto/tls.Config has no handshake timeout field, so these are no-ops. +func (c *tlsConfigWrapper) HandshakeTimeout() time.Duration { + return 0 +} + +func (c *tlsConfigWrapper) SetHandshakeTimeout(_ time.Duration) {} + func (c *tlsConfigWrapper) Clone() singtls.Config { panic("invalid") } diff --git a/go.mod b/go.mod index 21adde4fe29..ca6998af59e 100644 --- a/go.mod +++ b/go.mod @@ -22,11 +22,12 @@ require ( github.com/pires/go-proxyproto v0.15.0 github.com/quic-go/quic-go v0.60.0 github.com/refraction-networking/utls v1.8.3-0.20260623165621-880e27d8b0e5 - github.com/sagernet/sing v0.8.11 + github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d github.com/sagernet/sing-mux v0.3.5 github.com/sagernet/sing-quic v0.6.3 github.com/sagernet/sing-shadowsocks v0.2.9 github.com/sagernet/sing-shadowsocks2 v0.2.2 + github.com/sagernet/sing-snell v0.0.0-20260709045721-90e5a65e1f85 github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 github.com/stretchr/testify v1.11.1 github.com/v2fly/BrowserBridge v0.0.0-20210430233438-0570fc1d7d08 diff --git a/infra/conf/v4/snell.go b/infra/conf/v4/snell.go index ff66f863c57..3d7d877b940 100644 --- a/infra/conf/v4/snell.go +++ b/infra/conf/v4/snell.go @@ -7,36 +7,88 @@ import ( "github.com/exclavenetwork/exclave-core/v5/proxy/snell" ) +// SnellClientConfig is the JSON configuration for Snell outbound. +// +// Validation is strict: +// - version: 4 or 6 only (0 defaults to 4) +// - obfsMode: exact "none" | "http" | "tls" (empty defaults to "none"; "off" is not accepted) +// - mode (v6 only): exact "default" | "unshaped" | "unsafe-raw" (empty defaults to "default") +// - v4-only fields must be empty on v6, and v6-only fields must be empty on v4 +// - empty PSK is allowed +// - userPSK is a sing-box private extension (optional) type SnellClientConfig struct { - Address *cfgcommon.Address `json:"address"` - Port uint16 `json:"port"` - PSK string `json:"psk"` - Obfs string `json:"obfs"` - ObfsHost string `json:"obfsHost"` - Version uint32 `json:"version"` - Reuse bool `json:"reuse"` - Mode string `json:"mode"` + Address *cfgcommon.Address `json:"address"` + Port uint16 `json:"port"` + PSK string `json:"psk"` + // UserPSK is only compatible with sing-box Snell server. + UserPSK string `json:"userPSK"` + ObfsMode string `json:"obfsMode"` + // ObfsHost is v4-only. + ObfsHost string `json:"obfsHost"` + Version uint32 `json:"version"` + Reuse bool `json:"reuse"` + // Mode is v6-only. + Mode string `json:"mode"` } func (c *SnellClientConfig) Build() (proto.Message, error) { if c.Address == nil { - return nil, newError("missing server address") - } - if c.PSK == "" { - return nil, newError("missing psk") + return nil, newError("Snell: missing server address") } + version := c.Version if version == 0 { version = 4 } + if version != 4 && version != 6 { + return nil, newError("Snell: version must be 4 or 6, got ", version) + } + + obfsMode := c.ObfsMode + if obfsMode == "" { + obfsMode = "none" + } + switch obfsMode { + case "none", "http", "tls": + default: + return nil, newError(`Snell: obfsMode must be "none", "http" or "tls", got `, c.ObfsMode) + } + + mode := c.Mode + switch version { + case 6: + if mode == "" { + mode = "default" + } + switch mode { + case "default", "unshaped", "unsafe-raw": + default: + return nil, newError(`Snell: mode must be "default", "unshaped" or "unsafe-raw", got `, c.Mode) + } + // v4-only fields: obfsHost, and non-default obfuscation + if c.ObfsHost != "" { + return nil, newError("Snell: obfsHost is only valid for version 4") + } + if c.ObfsMode != "" && c.ObfsMode != "none" { + return nil, newError("Snell: obfsMode is only valid for version 4 (v6 has no obfuscation)") + } + obfsMode = "none" + case 4: + if c.Mode != "" { + return nil, newError("Snell: mode is only valid for version 6") + } + mode = "" + } + return &snell.ClientConfig{ Address: c.Address.Build(), Port: uint32(c.Port), Psk: c.PSK, - Obfs: c.Obfs, + UserPsk: c.UserPSK, + ObfsMode: obfsMode, ObfsHost: c.ObfsHost, Version: version, Reuse: c.Reuse, - Mode: c.Mode, + Mode: mode, }, nil } diff --git a/proxy/snell/NOTICE b/proxy/snell/NOTICE index 263c604515d..223bf28237f 100644 --- a/proxy/snell/NOTICE +++ b/proxy/snell/NOTICE @@ -1,2 +1,2 @@ -Snell protocol implementation inlined from https://github.com/SagerNet/sing-snell (GPL-3.0) -Supports Snell v4 (via snellv4) and v6 (via snellv6), matching sing-box testing branch. +Snell outbound uses https://github.com/SagerNet/sing-snell (GPL-3.0). +Supports Snell v4 and v6. userPSK is a sing-box private extension. diff --git a/proxy/snell/config.pb.go b/proxy/snell/config.pb.go index 1ffb9f8f5ee..76e40e638a7 100644 --- a/proxy/snell/config.pb.go +++ b/proxy/snell/config.pb.go @@ -27,17 +27,20 @@ type ClientConfig struct { state protoimpl.MessageState `protogen:"open.v1"` Address *net.IPOrDomain `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` - Psk string `protobuf:"bytes,3,opt,name=psk,proto3" json:"psk,omitempty"` - // none | http | tls (also accept off) - Obfs string `protobuf:"bytes,4,opt,name=obfs,proto3" json:"obfs,omitempty"` - // 1-6 (legacy 1-2 disabled in client; 3-5 via snellv4, 6 via snellv6) + // Pre-shared key. Empty string is allowed (server may accept it). + Psk string `protobuf:"bytes,3,opt,name=psk,proto3" json:"psk,omitempty"` + // "none" | "http" | "tls" (exact match; empty defaults to "none") + ObfsMode string `protobuf:"bytes,4,opt,name=obfs_mode,json=obfsMode,proto3" json:"obfs_mode,omitempty"` + // 4 or 6 only Version uint32 `protobuf:"varint,5,opt,name=version,proto3" json:"version,omitempty"` - // enable connection reuse (v4+) + // connection reuse Reuse bool `protobuf:"varint,6,opt,name=reuse,proto3" json:"reuse,omitempty"` - // optional obfs host (default bing.com / cloudfront.net per mode) + // v4-only: optional obfs host (empty uses library defaults) ObfsHost string `protobuf:"bytes,7,opt,name=obfs_host,json=obfsHost,proto3" json:"obfs_host,omitempty"` - // v6 only: default | unshaped | unsafe-raw - Mode string `protobuf:"bytes,8,opt,name=mode,proto3" json:"mode,omitempty"` + // v6-only: "default" | "unshaped" | "unsafe-raw" (exact match; empty defaults to "default") + Mode string `protobuf:"bytes,8,opt,name=mode,proto3" json:"mode,omitempty"` + // sing-box private extension (userPSK), only compatible with sing-box Snell server. + UserPsk string `protobuf:"bytes,9,opt,name=user_psk,json=userPsk,proto3" json:"user_psk,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -93,9 +96,9 @@ func (x *ClientConfig) GetPsk() string { return "" } -func (x *ClientConfig) GetObfs() string { +func (x *ClientConfig) GetObfsMode() string { if x != nil { - return x.Obfs + return x.ObfsMode } return "" } @@ -128,20 +131,28 @@ func (x *ClientConfig) GetMode() string { return "" } +func (x *ClientConfig) GetUserPsk() string { + if x != nil { + return x.UserPsk + } + return "" +} + var File_proxy_snell_config_proto protoreflect.FileDescriptor const file_proxy_snell_config_proto_rawDesc = "" + "\n" + - "\x18proxy/snell/config.proto\x12\x18exclave.core.proxy.snell\x1a common/protoext/extensions.proto\x1a\x18common/net/address.proto\"\xff\x01\n" + + "\x18proxy/snell/config.proto\x12\x18exclave.core.proxy.snell\x1a common/protoext/extensions.proto\x1a\x18common/net/address.proto\"\xa3\x02\n" + "\fClientConfig\x12=\n" + "\aaddress\x18\x01 \x01(\v2#.exclave.core.common.net.IPOrDomainR\aaddress\x12\x12\n" + "\x04port\x18\x02 \x01(\rR\x04port\x12\x10\n" + - "\x03psk\x18\x03 \x01(\tR\x03psk\x12\x12\n" + - "\x04obfs\x18\x04 \x01(\tR\x04obfs\x12\x18\n" + + "\x03psk\x18\x03 \x01(\tR\x03psk\x12\x1b\n" + + "\tobfs_mode\x18\x04 \x01(\tR\bobfsMode\x12\x18\n" + "\aversion\x18\x05 \x01(\rR\aversion\x12\x14\n" + "\x05reuse\x18\x06 \x01(\bR\x05reuse\x12\x1b\n" + "\tobfs_host\x18\a \x01(\tR\bobfsHost\x12\x12\n" + - "\x04mode\x18\b \x01(\tR\x04mode:\x15\x82\xb5\x18\x11\n" + + "\x04mode\x18\b \x01(\tR\x04mode\x12\x19\n" + + "\buser_psk\x18\t \x01(\tR\auserPsk:\x15\x82\xb5\x18\x11\n" + "\boutbound\x12\x05snellB\x88\x01\n" + "2com.github.exclavenetwork.exclave.core.proxy.snellP\x01Z5github.com/exclavenetwork/exclave-core/v5/proxy/snell\xaa\x02\x18Exclave.Core.Proxy.Snellb\x06proto3" diff --git a/proxy/snell/config.proto b/proxy/snell/config.proto index b9cfdd32eca..2b2227e2ad2 100644 --- a/proxy/snell/config.proto +++ b/proxy/snell/config.proto @@ -15,15 +15,18 @@ message ClientConfig { exclave.core.common.net.IPOrDomain address = 1; uint32 port = 2; + // Pre-shared key. Empty string is allowed (server may accept it). string psk = 3; - // none | http | tls (also accept off) - string obfs = 4; - // 1-6 (legacy 1-2 disabled in client; 3-5 via snellv4, 6 via snellv6) + // "none" | "http" | "tls" (exact match; empty defaults to "none") + string obfs_mode = 4; + // 4 or 6 only uint32 version = 5; - // enable connection reuse (v4+) + // connection reuse bool reuse = 6; - // optional obfs host (default bing.com / cloudfront.net per mode) + // v4-only: optional obfs host (empty uses library defaults) string obfs_host = 7; - // v6 only: default | unshaped | unsafe-raw + // v6-only: "default" | "unshaped" | "unsafe-raw" (exact match; empty defaults to "default") string mode = 8; + // sing-box private extension (userPSK), only compatible with sing-box Snell server. + string user_psk = 9; } diff --git a/proxy/snell/internal/singsnell/address.go b/proxy/snell/internal/singsnell/address.go deleted file mode 100644 index 1a93fb74349..00000000000 --- a/proxy/snell/internal/singsnell/address.go +++ /dev/null @@ -1,97 +0,0 @@ -package snell - -import ( - "encoding/binary" - "io" - - "github.com/sagernet/sing/common/buf" - E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" -) - -var udpIPSerializer = M.NewSerializer( - M.AddressFamilyByte(AddressTypeIPv4, M.AddressFamilyIPv4), - M.AddressFamilyByte(AddressTypeIPv6, M.AddressFamilyIPv6), -) - -// Surge 6.7.0 (11520): SNConnectorV4::targetHandshakeData: CONNECT targets are host strings even for literal IPs. -func WriteConnectAddress(buffer *buf.Buffer, destination M.Socksaddr) error { - host := destination.AddrString() - if len(host) > 255 { - return E.New("snell: host too long: ", host) - } - err := M.WriteSocksString(buffer, host) - if err != nil { - return err - } - binary.BigEndian.PutUint16(buffer.Extend(2), destination.Port) - return nil -} - -func ReadConnectAddress(reader io.Reader) (M.Socksaddr, error) { - host, err := M.ReadSockString(reader) - if err != nil { - return M.Socksaddr{}, err - } - var portBytes [2]byte - _, err = io.ReadFull(reader, portBytes[:]) - if err != nil { - return M.Socksaddr{}, err - } - return M.ParseSocksaddrHostPort(host, binary.BigEndian.Uint16(portBytes[:])), nil -} - -func WriteUDPRequestAddress(buffer *buf.Buffer, destination M.Socksaddr) error { - // Surge 6.7.0 (11520): SGUDPConnectorSnellV4::sendData:toHostname:port:metadata: writes 0x00 before literal IP UDP targets. - if destination.IsIP() { - err := buffer.WriteByte(0x00) - if err != nil { - return err - } - return udpIPSerializer.WriteAddrPort(buffer, destination.Unwrap()) - } else { - host := destination.Fqdn - if len(host) == 0 || len(host) > 255 { - return E.New("snell: invalid udp host: ", host) - } - err := M.WriteSocksString(buffer, host) - if err != nil { - return err - } - } - binary.BigEndian.PutUint16(buffer.Extend(2), destination.Port) - return nil -} - -func ReadUDPRequestAddress(buffer *buf.Buffer) (M.Socksaddr, error) { - first, err := buffer.ReadByte() - if err != nil { - return M.Socksaddr{}, err - } - if first != 0x00 { - host, hostErr := buffer.ReadBytes(int(first)) - if hostErr != nil { - return M.Socksaddr{}, hostErr - } - port, portErr := buffer.ReadBytes(2) - if portErr != nil { - return M.Socksaddr{}, portErr - } - return M.ParseSocksaddrHostPort(string(host), binary.BigEndian.Uint16(port)), nil - } - return udpIPSerializer.ReadAddrPort(buffer) -} - -// snell-server v6.0.0b4: FUN_001431c0: UDP response sources are emitted as literal IPs only. -func WriteUDPResponseAddress(buffer *buf.Buffer, source M.Socksaddr) error { - source = source.Unwrap() - addr := source.Addr - if !addr.IsValid() { - return E.New("snell: udp response source is not an ip: ", source) - } - return udpIPSerializer.WriteAddrPort(buffer, source) -} - -func ReadUDPResponseAddress(buffer *buf.Buffer) (M.Socksaddr, error) { - return udpIPSerializer.ReadAddrPort(buffer) -} diff --git a/proxy/snell/internal/singsnell/crypto.go b/proxy/snell/internal/singsnell/crypto.go deleted file mode 100644 index 0c4ca5c56cf..00000000000 --- a/proxy/snell/internal/singsnell/crypto.go +++ /dev/null @@ -1,31 +0,0 @@ -package snell - -import ( - "crypto/aes" - "crypto/cipher" - - "golang.org/x/crypto/argon2" -) - -// Surge 6.7.0 (11520): FUN_1000123d0: AEAD keys are derived with these Argon2id parameters. -func DeriveKey(psk []byte, salt []byte) []byte { - return argon2.IDKey(psk, salt, 3, 8, 1, 32)[:16] -} - -func NewAEAD(key []byte) (cipher.AEAD, error) { - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - return cipher.NewGCM(block) -} - -// Surge 6.7.0 (11520): FUN_100012944: nonce is u64le(counter) || 0x00000000. -func IncreaseNonce(nonce []byte) { - for index := range nonce { - nonce[index]++ - if nonce[index] != 0 { - return - } - } -} diff --git a/proxy/snell/internal/singsnell/internal/reuse/reuse.go b/proxy/snell/internal/singsnell/internal/reuse/reuse.go deleted file mode 100644 index 2a776fe49ec..00000000000 --- a/proxy/snell/internal/singsnell/internal/reuse/reuse.go +++ /dev/null @@ -1,273 +0,0 @@ -package reuse - -import ( - "io" - "sync" - "sync/atomic" - "time" - - snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" - "github.com/sagernet/sing/common/buf" - E "github.com/sagernet/sing/common/exceptions" - N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/common/x/list" -) - -const ( - PoolSize = 10 - PoolMaxAge = 180 * time.Second - PoolTimerInterval = 20 * time.Second - // Surge 6.7.0 (11520): SNConnectorV4::readServerEOFIfNotInReadState: closes a waiting connector after 0x80001 bytes are read after client EOF. - WaitingDiscardLimit = 0x80001 - // snell-server v5.0.1: FUN_0013f020: writes ReplyError code 0x65 with message "Remote EOF" when the server side closes before replying. - RemoteEOFCode = 0x65 - RemoteEOFMessage = "Remote EOF" -) - -type State uint8 - -const ( - StateReady State = iota - StateActive - StateWaiting - StateClosed -) - -type Session interface { - io.Closer - ReuseState() *atomic.Uint32 -} - -type poolEntry[S Session] struct { - session S - time time.Time -} - -type Pool[S Session] struct { - access sync.Mutex - closed bool - entries list.List[*poolEntry[S]] - stop chan struct{} - ticking bool - drainWg sync.WaitGroup -} - -func (p *Pool[S]) Init() { - p.access.Lock() - if p.stop == nil { - p.entries.Init() - p.stop = make(chan struct{}) - } - p.access.Unlock() -} - -func (p *Pool[S]) IsClosed() bool { - p.access.Lock() - closed := p.closed - p.access.Unlock() - return closed -} - -func (p *Pool[S]) Take() (session S, found bool, closed bool) { - p.access.Lock() - closed = p.closed - var expired []S - now := time.Now() - if !closed { - for element := p.entries.Front(); element != nil; { - next := element.Next() - entry := element.Value - state := State(entry.session.ReuseState().Load()) - if state == StateClosed { - p.entries.Remove(element) - element = next - continue - } - if now.Sub(entry.time) > PoolMaxAge { - p.entries.Remove(element) - expired = append(expired, entry.session) - element = next - continue - } - if !found && state == StateReady && entry.session.ReuseState().CompareAndSwap(uint32(StateReady), uint32(StateActive)) { - session = entry.session - found = true - p.entries.Remove(element) - element = next - continue - } - element = next - } - } - p.access.Unlock() - for _, expiredSession := range expired { - expiredSession.Close() - } - return -} - -func (p *Pool[S]) MoveToPool(session S, state State, drain bool) bool { - p.access.Lock() - closed := p.closed - var expired []S - now := time.Now() - if !closed { - for element := p.entries.Front(); element != nil; { - next := element.Next() - entry := element.Value - entryState := State(entry.session.ReuseState().Load()) - if entryState == StateClosed { - p.entries.Remove(element) - element = next - continue - } - if now.Sub(entry.time) > PoolMaxAge { - p.entries.Remove(element) - expired = append(expired, entry.session) - element = next - continue - } - element = next - } - } - var added bool - if !closed && p.stop != nil && p.entries.Len() < PoolSize { - p.entries.PushBack(&poolEntry[S]{session: session, time: now}) - if state == StateWaiting && drain { - p.drainWg.Add(1) - } - added = true - if !p.ticking { - p.ticking = true - go p.ExpireLoop() - } - } - p.access.Unlock() - for _, expiredSession := range expired { - expiredSession.Close() - } - if !added { - session.Close() - } - return added -} - -func (p *Pool[S]) ExpireLoop() { - ticker := time.NewTicker(PoolTimerInterval) - defer ticker.Stop() - for { - select { - case <-ticker.C: - var expired []S - now := time.Now() - p.access.Lock() - if p.closed { - p.ticking = false - p.access.Unlock() - return - } - for element := p.entries.Front(); element != nil; { - next := element.Next() - entry := element.Value - state := State(entry.session.ReuseState().Load()) - if state == StateClosed { - p.entries.Remove(element) - element = next - continue - } - if now.Sub(entry.time) > PoolMaxAge { - p.entries.Remove(element) - expired = append(expired, entry.session) - element = next - continue - } - element = next - } - if p.entries.Len() == 0 { - p.ticking = false - p.access.Unlock() - for _, expiredSession := range expired { - expiredSession.Close() - } - return - } - p.access.Unlock() - for _, expiredSession := range expired { - expiredSession.Close() - } - case <-p.stop: - return - } - } -} - -func (p *Pool[S]) DrainDone() { - p.drainWg.Done() -} - -func (p *Pool[S]) Close() error { - p.access.Lock() - if p.closed { - p.access.Unlock() - return nil - } - p.closed = true - var sessions []S - for element := p.entries.Front(); element != nil; element = element.Next() { - sessions = append(sessions, element.Value.session) - } - p.entries.Init() - if p.stop != nil { - close(p.stop) - } - p.access.Unlock() - var closeErr error - for _, session := range sessions { - closeErr = E.Errors(closeErr, session.Close()) - } - p.drainWg.Wait() - return closeErr -} - -type RecordReader interface { - N.ExtendedReader - N.ReadWaiter - ReadRecord() (*buf.Buffer, error) - NextRecord() (*buf.Buffer, error) - SetCache(*buf.Buffer) - ReleaseCache() -} - -type RecordWriter interface { - N.ExtendedWriter - CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter - CreatePacketVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter - WritePacketBuffer(buffer *buf.Buffer) error - WriteZeroChunk() error - FrontHeadroom() int - RearHeadroom() int - WriterMTU() int - Upstream() any -} - -func ParseReply(record *buf.Buffer) (*buf.Buffer, error) { - reply, err := record.ReadByte() - if err != nil { - record.Release() - return nil, err - } - switch reply { - case snell.ReplyTunnel: - if record.IsEmpty() { - record.Release() - return nil, nil - } - return record, nil - case snell.ReplyError: - defer record.Release() - return nil, snell.ReadServerError(record) - default: - record.Release() - return nil, E.Extend(snell.ErrUnexpectedReply, reply) - } -} diff --git a/proxy/snell/internal/singsnell/obfs.go b/proxy/snell/internal/singsnell/obfs.go deleted file mode 100644 index 729251d9f66..00000000000 --- a/proxy/snell/internal/singsnell/obfs.go +++ /dev/null @@ -1,685 +0,0 @@ -package snell - -import ( - "bufio" - "crypto/rand" - "encoding/base64" - "encoding/binary" - "fmt" - "io" - "math/big" - "net" - "strings" - "sync" - "time" - - E "github.com/sagernet/sing/common/exceptions" -) - -type ObfsMode int - -const ( - ObfsModeNone ObfsMode = iota - ObfsModeHTTP - ObfsModeTLS -) - -const ( - // Surge 6.7.0 (11520): SGObfsHelper::initWithPolicy: uses these legacy obfs default hosts. - DefaultObfsHost = "bing.com" - DefaultTLSObfsHost = "cloudfront.net" -) - -const ( - // Surge 6.7.0 (11520): SGObfsHelperTLS::encodeObfsData: embeds at most 0x400 bytes in the ClientHello and chunks later records at 0x4000. - tlsObfsFirstClientPayloadLen = 0x400 - tlsObfsRecordPayloadLen = 0x4000 -) - -var ( - httpObfsClientFingerprintOnce sync.Once - httpObfsClientFingerprintErr error - httpObfsClientKey string - httpObfsClientUserAgent string - - httpObfsServerResponseOnce sync.Once - httpObfsServerResponseErr error - httpObfsServerResponse []byte -) - -type ObfsConfig struct { - Mode ObfsMode - Host string -} - -func ParseObfsMode(name string) (ObfsMode, error) { - switch strings.ToLower(name) { - case "", "none": - return ObfsModeNone, nil - case "http": - return ObfsModeHTTP, nil - case "tls": - return ObfsModeTLS, nil - default: - return 0, E.New("snell: unknown obfs mode: ", name) - } -} - -func (m ObfsMode) String() string { - switch m { - case ObfsModeNone: - return "none" - case ObfsModeHTTP: - return "http" - case ObfsModeTLS: - return "tls" - default: - panic("snell: invalid obfs mode") - } -} - -func (c ObfsConfig) ClientConn(conn net.Conn) net.Conn { - switch c.Mode { - case ObfsModeNone: - return conn - case ObfsModeHTTP: - return &httpObfsClientConn{Conn: conn, config: c} - case ObfsModeTLS: - return &tlsObfsClientConn{Conn: conn, config: c, firstResponse: true} - default: - panic("snell: invalid obfs mode") - } -} - -func (c ObfsConfig) ServerConn(conn net.Conn) net.Conn { - switch c.Mode { - case ObfsModeNone: - return conn - case ObfsModeHTTP: - return &httpObfsServerConn{Conn: conn, config: c} - case ObfsModeTLS: - return &tlsObfsServerConn{Conn: conn, config: c, firstRequest: true, firstResponse: true} - default: - panic("snell: invalid obfs mode") - } -} - -func (c ObfsConfig) WriteClientRequest(w io.Writer) error { - return c.WriteClientRequestWithPayload(w, nil) -} - -func (c ObfsConfig) WriteClientRequestWithPayload(w io.Writer, payload []byte) error { - switch c.Mode { - case ObfsModeHTTP: - httpObfsClientFingerprintOnce.Do(func() { - var keyBytes [16]byte - _, readErr := io.ReadFull(rand.Reader, keyBytes[:]) - if readErr != nil { - httpObfsClientFingerprintErr = E.Cause(readErr, "generate http obfs websocket key") - return - } - osMinorDelta, randomErr := rand.Int(rand.Reader, big.NewInt(6)) - if randomErr != nil { - httpObfsClientFingerprintErr = E.Cause(randomErr, "generate http obfs user agent") - return - } - firefoxVersionDelta, randomErr := rand.Int(rand.Reader, big.NewInt(43)) - if randomErr != nil { - httpObfsClientFingerprintErr = E.Cause(randomErr, "generate http obfs user agent") - return - } - httpObfsClientKey = base64.StdEncoding.EncodeToString(keyBytes[:]) - // Surge 6.7.0 (11520): SNObfsHelperHTTP init creates one Firefox-style user agent per helper class lifetime. - httpObfsClientUserAgent = fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.%d; rv:64.0) Gecko/20100101 Firefox/%d.0", int(osMinorDelta.Int64())+9, int(firefoxVersionDelta.Int64())+22) - }) - if httpObfsClientFingerprintErr != nil { - return httpObfsClientFingerprintErr - } - host := c.Host - if host == "" { - host = DefaultObfsHost - } - var request []byte - if len(payload) == 0 { - request = fmt.Appendf(request, "GET / HTTP/1.1\r\nHost: %s\r\nUser-Agent: %s\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: %s\r\n\r\n", host, httpObfsClientUserAgent, httpObfsClientKey) - } else { - request = fmt.Appendf(request, "GET / HTTP/1.1\r\nHost: %s\r\nUser-Agent: %s\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nContent-Length: %d\r\nSec-WebSocket-Key: %s\r\n\r\n", host, httpObfsClientUserAgent, len(payload), httpObfsClientKey) - request = append(request, payload...) - } - _, err := w.Write(request) - if err != nil { - return E.Cause(err, "write http obfs request") - } - return nil - case ObfsModeTLS: - host := c.Host - if host == "" { - host = DefaultTLSObfsHost - } - firstPayload := payload - if len(firstPayload) > tlsObfsFirstClientPayloadLen { - firstPayload = payload[:tlsObfsFirstClientPayloadLen] - } - random := make([]byte, 28) - _, err := io.ReadFull(rand.Reader, random) - if err != nil { - return err - } - sessionID := make([]byte, 32) - _, err = io.ReadFull(rand.Reader, sessionID) - if err != nil { - return err - } - // Surge 6.7.0 (11520): SGObfsHelperTLS::encodeObfsData: sends the first Snell record in the ClientHello - // session_ticket extension and caps that embedded payload at 0x400 bytes. - out := make([]byte, 0, 0xd9+len(firstPayload)+len(host)) - out = append(out, 0x16, 0x03, 0x01) - out = binary.BigEndian.AppendUint16(out, uint16(212+len(firstPayload)+len(host))) - out = append(out, 0x01, 0x00) - out = binary.BigEndian.AppendUint16(out, uint16(208+len(firstPayload)+len(host))) - out = append(out, 0x03, 0x03) - out = binary.BigEndian.AppendUint32(out, uint32(time.Now().Unix())) - out = append(out, random...) - out = append(out, 0x20) - out = append(out, sessionID...) - out = binary.BigEndian.AppendUint16(out, 0x0038) - out = append(out, - 0xc0, 0x2c, 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f, - 0x00, 0x9e, 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, 0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a, - 0xc0, 0x14, 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, 0x00, 0x9d, 0x00, 0x9c, 0x00, 0x3d, - 0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0xff, - ) - out = append(out, 0x01, 0x00) - out = binary.BigEndian.AppendUint16(out, uint16(79+len(firstPayload)+len(host))) - out = binary.BigEndian.AppendUint16(out, 0x0023) - out = binary.BigEndian.AppendUint16(out, uint16(len(firstPayload))) - out = append(out, firstPayload...) - out = binary.BigEndian.AppendUint16(out, 0x0000) - out = binary.BigEndian.AppendUint16(out, uint16(len(host)+5)) - out = binary.BigEndian.AppendUint16(out, uint16(len(host)+3)) - out = append(out, 0x00) - out = binary.BigEndian.AppendUint16(out, uint16(len(host))) - out = append(out, host...) - out = append(out, - 0x00, 0x0b, 0x00, 0x04, 0x03, 0x01, 0x00, 0x02, - 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18, - 0x00, 0x0d, 0x00, 0x20, 0x00, 0x1e, 0x06, 0x01, 0x06, 0x02, 0x06, 0x03, 0x05, - 0x01, 0x05, 0x02, 0x05, 0x03, 0x04, 0x01, 0x04, 0x02, 0x04, 0x03, 0x03, 0x01, - 0x03, 0x02, 0x03, 0x03, 0x02, 0x01, 0x02, 0x02, 0x02, 0x03, - 0x00, 0x16, 0x00, 0x00, - 0x00, 0x17, 0x00, 0x00, - ) - _, err = w.Write(out) - if err != nil { - return E.Cause(err, "write tls obfs request") - } - for payload = payload[len(firstPayload):]; len(payload) > 0; { - payloadLen := min(len(payload), tlsObfsRecordPayloadLen) - record := make([]byte, 0, 5+payloadLen) - record = append(record, 0x17, 0x03, 0x03) - record = binary.BigEndian.AppendUint16(record, uint16(payloadLen)) - record = append(record, payload[:payloadLen]...) - _, err = w.Write(record) - if err != nil { - return E.Cause(err, "write tls obfs request payload") - } - payload = payload[payloadLen:] - } - return nil - case ObfsModeNone: - if len(payload) == 0 { - return nil - } - _, err := w.Write(payload) - if err != nil { - return E.Cause(err, "write obfs payload") - } - return nil - default: - panic("snell: invalid obfs mode") - } -} - -func (c ObfsConfig) ReadClientResponse(r io.Reader) (io.Reader, error) { - switch c.Mode { - case ObfsModeNone, ObfsModeTLS: - return r, nil - case ObfsModeHTTP: - reader := bufio.NewReader(r) - terminator := [4]byte{'\r', '\n', '\r', '\n'} - matched := 0 - for { - value, err := reader.ReadByte() - if err != nil { - return nil, E.Cause(err, "read http obfs response") - } - if value == terminator[matched] { - matched++ - if matched == len(terminator) { - return reader, nil - } - continue - } - if value == terminator[0] { - matched = 1 - } else { - matched = 0 - } - } - default: - panic("snell: invalid obfs mode") - } -} - -func (c ObfsConfig) ReadServerRequest(r io.Reader) (io.Reader, error) { - switch c.Mode { - case ObfsModeNone, ObfsModeTLS: - return r, nil - case ObfsModeHTTP: - reader := bufio.NewReader(r) - terminator := [4]byte{'\r', '\n', '\r', '\n'} - matched := 0 - for { - value, err := reader.ReadByte() - if err != nil { - return nil, E.Cause(err, "read http obfs request") - } - if value == terminator[matched] { - matched++ - if matched == len(terminator) { - return reader, nil - } - continue - } - if value == terminator[0] { - matched = 1 - } else { - matched = 0 - } - } - default: - panic("snell: invalid obfs mode") - } -} - -func (c ObfsConfig) WriteServerResponse(w io.Writer) error { - return c.WriteServerResponseWithPayload(w, nil) -} - -func (c ObfsConfig) WriteServerResponseWithPayload(w io.Writer, payload []byte) error { - switch c.Mode { - case ObfsModeHTTP: - httpObfsServerResponseOnce.Do(func() { - var acceptBytes [16]byte - _, readErr := io.ReadFull(rand.Reader, acceptBytes[:]) - if readErr != nil { - httpObfsServerResponseErr = E.Cause(readErr, "generate http obfs accept") - return - } - versionMinorDelta, randomErr := rand.Int(rand.Reader, big.NewInt(14)) - if randomErr != nil { - httpObfsServerResponseErr = E.Cause(randomErr, "generate http obfs server version") - return - } - versionPatchDelta, randomErr := rand.Int(rand.Reader, big.NewInt(12)) - if randomErr != nil { - httpObfsServerResponseErr = E.Cause(randomErr, "generate http obfs server version") - return - } - accept := base64.StdEncoding.EncodeToString(acceptBytes[:]) - // Surge 6.7.0 (11520): SNObfsHelperServer initializes and reuses one fake 101 response. - httpObfsServerResponse = fmt.Appendf(nil, "HTTP/1.1 101 Switching Protocols\r\nServer: nginx/1.%d.%d\r\nDate: %s\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: %s\r\n\r\n", int(versionMinorDelta.Int64()), int(versionPatchDelta.Int64()), time.Now().Format("Mon, 02 Jan 2006 15:04:05 GMT"), accept) - }) - if httpObfsServerResponseErr != nil { - return httpObfsServerResponseErr - } - response := append([]byte(nil), httpObfsServerResponse...) - if len(payload) > 0 { - response = append(response, payload...) - } - _, err := w.Write(response) - if err != nil { - return E.Cause(err, "write http obfs response") - } - return nil - case ObfsModeTLS: - random := make([]byte, 28) - _, err := io.ReadFull(rand.Reader, random) - if err != nil { - return err - } - sessionID := make([]byte, 32) - _, err = io.ReadFull(rand.Reader, sessionID) - if err != nil { - return err - } - firstPayload := payload - if len(firstPayload) > tlsObfsRecordPayloadLen { - firstPayload = payload[:tlsObfsRecordPayloadLen] - } - // Surge 6.7.0 (11520): SGObfsHelperTLS::decodeObfsData: expects this fake ServerHello, a CCS record, - // then the first Snell payload in a TLS handshake record. - out := make([]byte, 0, 107+len(firstPayload)) - out = append(out, 0x16, 0x03, 0x01) - out = binary.BigEndian.AppendUint16(out, 91) - out = append(out, 0x02, 0x00, 0x00, 0x57, 0x03, 0x03) - out = binary.BigEndian.AppendUint32(out, uint32(time.Now().Unix())) - out = append(out, random...) - out = append(out, 0x20) - out = append(out, sessionID...) - out = append(out, - 0xcc, 0xa8, 0x00, - 0x00, 0x00, - 0xff, 0x01, 0x00, 0x01, 0x00, - 0x00, 0x17, 0x00, 0x00, - 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, - 0x14, 0x03, 0x03, 0x00, 0x01, 0x01, - 0x16, 0x03, 0x03, - ) - out = binary.BigEndian.AppendUint16(out, uint16(len(firstPayload))) - out = append(out, firstPayload...) - _, err = w.Write(out) - if err != nil { - return E.Cause(err, "write tls obfs response") - } - for payload = payload[len(firstPayload):]; len(payload) > 0; { - payloadLen := min(len(payload), tlsObfsRecordPayloadLen) - record := make([]byte, 0, 5+payloadLen) - record = append(record, 0x17, 0x03, 0x03) - record = binary.BigEndian.AppendUint16(record, uint16(payloadLen)) - record = append(record, payload[:payloadLen]...) - _, err = w.Write(record) - if err != nil { - return E.Cause(err, "write tls obfs response payload") - } - payload = payload[payloadLen:] - } - return nil - case ObfsModeNone: - if len(payload) == 0 { - return nil - } - _, err := w.Write(payload) - if err != nil { - return E.Cause(err, "write obfs payload") - } - return nil - default: - panic("snell: invalid obfs mode") - } -} - -type httpObfsClientConn struct { - net.Conn - config ObfsConfig - - readAccess sync.Mutex - writeAccess sync.Mutex - reader io.Reader - wrote bool -} - -func (c *httpObfsClientConn) Read(p []byte) (int, error) { - c.readAccess.Lock() - if c.reader == nil { - reader, err := c.config.ReadClientResponse(c.Conn) - if err != nil { - c.readAccess.Unlock() - return 0, err - } - c.reader = reader - } - reader := c.reader - c.readAccess.Unlock() - return reader.Read(p) -} - -func (c *httpObfsClientConn) Write(p []byte) (int, error) { - c.writeAccess.Lock() - defer c.writeAccess.Unlock() - if !c.wrote { - c.wrote = true - err := c.config.WriteClientRequestWithPayload(c.Conn, p) - if err != nil { - return 0, err - } - return len(p), nil - } - return c.Conn.Write(p) -} - -type httpObfsServerConn struct { - net.Conn - config ObfsConfig - - readAccess sync.Mutex - writeAccess sync.Mutex - reader io.Reader - wrote bool -} - -func (c *httpObfsServerConn) Read(p []byte) (int, error) { - c.readAccess.Lock() - if c.reader == nil { - reader, err := c.config.ReadServerRequest(c.Conn) - if err != nil { - c.readAccess.Unlock() - return 0, err - } - c.reader = reader - } - reader := c.reader - c.readAccess.Unlock() - return reader.Read(p) -} - -func (c *httpObfsServerConn) Write(p []byte) (int, error) { - c.writeAccess.Lock() - defer c.writeAccess.Unlock() - if !c.wrote { - c.wrote = true - err := c.config.WriteServerResponseWithPayload(c.Conn, p) - if err != nil { - return 0, err - } - return len(p), nil - } - return c.Conn.Write(p) -} - -type tlsObfsClientConn struct { - net.Conn - config ObfsConfig - - readAccess sync.Mutex - writeAccess sync.Mutex - readRemaining int - firstResponse bool - wrote bool -} - -func (c *tlsObfsClientConn) Read(p []byte) (int, error) { - if len(p) == 0 { - return 0, nil - } - c.readAccess.Lock() - defer c.readAccess.Unlock() - if c.readRemaining > 0 { - payloadLen := min(c.readRemaining, len(p)) - n, err := io.ReadFull(c.Conn, p[:payloadLen]) - c.readRemaining -= n - return n, err - } - if c.firstResponse { - c.firstResponse = false - return c.readRecordPayload(p, 105) - } - return c.readRecordPayload(p, 3) -} - -func (c *tlsObfsClientConn) readRecordPayload(p []byte, discardLen int) (int, error) { - _, err := io.CopyN(io.Discard, c.Conn, int64(discardLen)) - if err != nil { - return 0, err - } - var lengthBytes [2]byte - _, err = io.ReadFull(c.Conn, lengthBytes[:]) - if err != nil { - return 0, err - } - payloadLen := int(binary.BigEndian.Uint16(lengthBytes[:])) - if payloadLen == 0 { - return 0, nil - } - if payloadLen > len(p) { - n, err := io.ReadFull(c.Conn, p) - c.readRemaining = payloadLen - n - return n, err - } - return io.ReadFull(c.Conn, p[:payloadLen]) -} - -func (c *tlsObfsClientConn) Write(p []byte) (int, error) { - if len(p) == 0 { - return 0, nil - } - c.writeAccess.Lock() - defer c.writeAccess.Unlock() - written := 0 - if !c.wrote { - c.wrote = true - firstPayload := p - if len(firstPayload) > tlsObfsFirstClientPayloadLen { - firstPayload = p[:tlsObfsFirstClientPayloadLen] - } - err := c.config.WriteClientRequestWithPayload(c.Conn, firstPayload) - if err != nil { - return 0, err - } - written += len(firstPayload) - p = p[len(firstPayload):] - } - for len(p) > 0 { - payloadLen := min(len(p), tlsObfsRecordPayloadLen) - record := make([]byte, 0, 5+payloadLen) - record = append(record, 0x17, 0x03, 0x03) - record = binary.BigEndian.AppendUint16(record, uint16(payloadLen)) - record = append(record, p[:payloadLen]...) - _, err := c.Conn.Write(record) - if err != nil { - return written, E.Cause(err, "write tls obfs payload") - } - written += payloadLen - p = p[payloadLen:] - } - return written, nil -} - -type tlsObfsServerConn struct { - net.Conn - config ObfsConfig - - readAccess sync.Mutex - writeAccess sync.Mutex - readRemaining int - firstRequest bool - sessionTicketDone bool - firstResponse bool -} - -func (c *tlsObfsServerConn) Read(p []byte) (int, error) { - if len(p) == 0 { - return 0, nil - } - c.readAccess.Lock() - defer c.readAccess.Unlock() - if c.readRemaining > 0 { - payloadLen := min(c.readRemaining, len(p)) - n, err := io.ReadFull(c.Conn, p[:payloadLen]) - c.readRemaining -= n - return n, err - } - if c.firstRequest { - c.firstRequest = false - return c.readRecordPayload(p, 9*16-4) - } - if !c.sessionTicketDone { - c.sessionTicketDone = true - _, err := io.CopyN(io.Discard, c.Conn, 7) - if err != nil { - return 0, err - } - var lengthBytes [2]byte - _, err = io.ReadFull(c.Conn, lengthBytes[:]) - if err != nil { - return 0, err - } - _, err = io.CopyN(io.Discard, c.Conn, int64(binary.BigEndian.Uint16(lengthBytes[:]))) - if err != nil { - return 0, err - } - _, err = io.CopyN(io.Discard, c.Conn, 4*16+2) - if err != nil { - return 0, err - } - } - return c.readRecordPayload(p, 3) -} - -func (c *tlsObfsServerConn) readRecordPayload(p []byte, discardLen int) (int, error) { - _, err := io.CopyN(io.Discard, c.Conn, int64(discardLen)) - if err != nil { - return 0, err - } - var lengthBytes [2]byte - _, err = io.ReadFull(c.Conn, lengthBytes[:]) - if err != nil { - return 0, err - } - payloadLen := int(binary.BigEndian.Uint16(lengthBytes[:])) - if payloadLen == 0 { - return 0, nil - } - if payloadLen > len(p) { - n, err := io.ReadFull(c.Conn, p) - c.readRemaining = payloadLen - n - return n, err - } - return io.ReadFull(c.Conn, p[:payloadLen]) -} - -func (c *tlsObfsServerConn) Write(p []byte) (int, error) { - if len(p) == 0 { - return 0, nil - } - c.writeAccess.Lock() - defer c.writeAccess.Unlock() - written := 0 - if c.firstResponse { - c.firstResponse = false - firstPayload := p - if len(firstPayload) > tlsObfsRecordPayloadLen { - firstPayload = p[:tlsObfsRecordPayloadLen] - } - err := c.config.WriteServerResponseWithPayload(c.Conn, firstPayload) - if err != nil { - return 0, err - } - written += len(firstPayload) - p = p[len(firstPayload):] - } - for len(p) > 0 { - payloadLen := min(len(p), tlsObfsRecordPayloadLen) - record := make([]byte, 0, 5+payloadLen) - record = append(record, 0x17, 0x03, 0x03) - record = binary.BigEndian.AppendUint16(record, uint16(payloadLen)) - record = append(record, p[:payloadLen]...) - _, err := c.Conn.Write(record) - if err != nil { - return written, E.Cause(err, "write tls obfs payload") - } - written += payloadLen - p = p[payloadLen:] - } - return written, nil -} diff --git a/proxy/snell/internal/singsnell/request.go b/proxy/snell/internal/singsnell/request.go deleted file mode 100644 index f4eadf736a4..00000000000 --- a/proxy/snell/internal/singsnell/request.go +++ /dev/null @@ -1,97 +0,0 @@ -package snell - -import ( - "io" - - "github.com/sagernet/sing/common/buf" - E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" -) - -type Request struct { - Command byte - ClientID []byte - Destination M.Socksaddr -} - -func (r Request) Write(buffer *buf.Buffer) error { - if len(r.ClientID) > 255 { - return E.New("snell: client id too long") - } - _, err := buffer.Write([]byte{RequestVersion, r.Command, byte(len(r.ClientID))}) - if err != nil { - return err - } - if len(r.ClientID) > 0 { - _, err = buffer.Write(r.ClientID) - if err != nil { - return err - } - } - switch r.Command { - case CommandConnect, CommandConnectV2: - return WriteConnectAddress(buffer, r.Destination) - case CommandPing, CommandUDP: - return nil - default: - return E.Extend(ErrUnsupportedCommand, r.Command) - } -} - -func (r Request) Len() int { - requestLen := 3 + len(r.ClientID) - switch r.Command { - case CommandConnect, CommandConnectV2: - return requestLen + 1 + len(r.Destination.AddrString()) + 2 - default: - return requestLen - } -} - -func ReadRequest(reader io.Reader) (Request, error) { - prefix := make([]byte, 3) - _, err := io.ReadFull(reader, prefix) - if err != nil { - return Request{}, err - } - if prefix[0] != RequestVersion { - return Request{}, E.Extend(ErrBadVersion, "request version ", prefix[0]) - } - clientIDLen := int(prefix[2]) - request := Request{Command: prefix[1]} - if clientIDLen > 0 { - request.ClientID = make([]byte, clientIDLen) - _, err = io.ReadFull(reader, request.ClientID) - if err != nil { - return Request{}, err - } - } - switch request.Command { - case CommandConnect, CommandConnectV2: - request.Destination, err = ReadConnectAddress(reader) - if err != nil { - return Request{}, err - } - case CommandPing, CommandUDP: - default: - return Request{}, E.Extend(ErrUnsupportedCommand, request.Command) - } - return request, nil -} - -func ReadServerError(record *buf.Buffer) error { - errorCode, err := record.ReadByte() - if err != nil { - return err - } - message, err := M.ReadSockString(record) - if err != nil { - return err - } - // Surge 6.7.0 (11520): SNConnectorV4::socket:didReadData: rejects server-error replies unless the frame is - // exactly code, message length, and message bytes. - if !record.IsEmpty() { - return E.New("snell: server error reply has trailing data") - } - return E.New("snell: server error ", errorCode, ": ", message) -} diff --git a/proxy/snell/internal/singsnell/snell.go b/proxy/snell/internal/singsnell/snell.go deleted file mode 100644 index eb6eb12aed1..00000000000 --- a/proxy/snell/internal/singsnell/snell.go +++ /dev/null @@ -1,83 +0,0 @@ -package snell - -import ( - "context" - "net" - - E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -const ( - HeaderVersion = 0x04 - HeaderPlainLen = 7 - AEADTagLen = 16 - HeaderCipherLen = HeaderPlainLen + AEADTagLen - SaltLen = 16 - NonceLen = 12 - MaxPayloadLen = 0x3fff -) - -const RequestVersion = 0x01 - -const ( - CommandPing = 0x00 - CommandConnect = 0x01 - CommandConnectV2 = 0x05 - CommandUDP = 0x06 -) - -const ( - ReplyTunnel = 0x00 - ReplyPong = 0x01 - ReplyError = 0x02 -) - -const ( - UDPCommandForward = 0x01 - AddressTypeIPv4 = 0x04 - AddressTypeIPv6 = 0x06 -) - -var ( - ErrBadVersion = E.New("snell: bad header version") - ErrReservedNonZero = E.New("snell: reserved header octet is non-zero") - ErrPayloadTooLarge = E.New("snell: payload length exceeds maximum") - ErrUnsupportedCommand = E.New("snell: unsupported command") - ErrUnexpectedReply = E.New("snell: unexpected reply opcode") - ErrMissingPSK = E.New("snell: missing pre-shared key") - ErrNoUsers = E.New("snell: no users") - ErrBadUserKey = E.New("snell: bad user key") - ErrDuplicateUserKey = E.New("snell: duplicate user key") -) - -type Method interface { - DialConn(conn net.Conn, destination M.Socksaddr) (net.Conn, error) - DialEarlyConn(conn net.Conn, destination M.Socksaddr) net.Conn - DialPacketConn(conn net.Conn) (N.NetPacketConn, error) -} - -type Handler interface { - N.TCPConnectionHandlerEx - N.UDPConnectionHandlerEx -} - -type Service interface { - NewConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error -} - -// snell-server v6.0.0b4: FUN_00141bc0: malformed handshakes are torn down abruptly. -type ServerError struct { - net.Conn - Source M.Socksaddr - Cause error -} - -func (e *ServerError) Unwrap() error { - return e.Cause -} - -func (e *ServerError) Error() string { - return "snell: serve " + e.Source.String() + ": " + e.Cause.Error() -} diff --git a/proxy/snell/internal/singsnell/snellv4/client.go b/proxy/snell/internal/singsnell/snellv4/client.go deleted file mode 100644 index e37b6951c0a..00000000000 --- a/proxy/snell/internal/singsnell/snellv4/client.go +++ /dev/null @@ -1,307 +0,0 @@ -package snellv4 - -import ( - "net" - "sync" - - snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" - "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" - E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -type Client struct { - psk []byte - userKey []byte - reuse bool - obfs snell.ObfsConfig - dialer N.Dialer - server M.Socksaddr - - pool reuse.Pool[*reuseSession] -} - -type ClientOptions struct { - PSK []byte - UserKey []byte - Reuse bool - ObfsMode snell.ObfsMode - ObfsHost string - Dialer N.Dialer - Server M.Socksaddr -} - -func NewClient(options ClientOptions) (*Client, error) { - if len(options.PSK) == 0 { - return nil, snell.ErrMissingPSK - } - if len(options.UserKey) > 255 { - return nil, E.New("snell: user key too long") - } - switch options.ObfsMode { - case snell.ObfsModeNone, snell.ObfsModeHTTP, snell.ObfsModeTLS: - default: - return nil, E.New("snell: unknown obfs mode: ", int(options.ObfsMode)) - } - client := &Client{ - psk: options.PSK, - userKey: options.UserKey, - reuse: options.Reuse, - obfs: snell.ObfsConfig{Mode: options.ObfsMode, Host: options.ObfsHost}, - dialer: options.Dialer, - server: options.Server, - } - if options.Reuse { - client.pool.Init() - } - return client, nil -} - -func (c *Client) DialConn(conn net.Conn, destination M.Socksaddr) (net.Conn, error) { - clientConn := &clientConn{client: c, Conn: c.obfs.ClientConn(conn), destination: destination} - return clientConn, clientConn.writeRequest(nil) -} - -func (c *Client) DialEarlyConn(conn net.Conn, destination M.Socksaddr) net.Conn { - return &clientConn{client: c, Conn: c.obfs.ClientConn(conn), destination: destination} -} - -func (c *Client) DialPacketConn(conn net.Conn) (N.NetPacketConn, error) { - return bufio.NewNetPacketConn(&clientPacketConn{Conn: c.obfs.ClientConn(conn), client: c}), nil -} - -var _ snell.Method = (*Client)(nil) - -type clientConn struct { - net.Conn - client *Client - destination M.Socksaddr - - access sync.Mutex - reader *reader - writer *writer - readWaitOptions N.ReadWaitOptions - closeWriteOnce sync.Once - closeWriteErr error -} - -func (c *clientConn) writeRequest(payload []byte) error { - // Surge 6.7.0 (11520): SNConnectorV4::targetHandshakeData: writes command 5 for v4/v5 TCP - // handshakes even when connector reuse is disabled. - requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: c.client.userKey, Destination: c.destination} - request := buf.NewSize(requestPayload.Len() + len(payload)) - err := requestPayload.Write(request) - if err != nil { - request.Release() - return err - } - if len(payload) > 0 { - common.Must1(request.Write(payload)) - } - defer request.Release() - - recordWriter := &writer{ - upstream: c.Conn, - psk: c.client.psk, - } - _, err = recordWriter.Write(request.Bytes()) - if err != nil { - return E.Cause(err, "write request") - } - c.writer = recordWriter - return nil -} - -func (c *clientConn) writeRequestBuffer(buffer *buf.Buffer) error { - requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: c.client.userKey, Destination: c.destination} - request := buf.With(buffer.ExtendHeader(requestPayload.Len())) - err := requestPayload.Write(request) - if err != nil { - buffer.Release() - return err - } - recordWriter := &writer{ - upstream: c.Conn, - psk: c.client.psk, - } - err = recordWriter.WriteBuffer(buffer) - if err != nil { - return E.Cause(err, "write request") - } - c.writer = recordWriter - return nil -} - -func (c *clientConn) readResponse() error { - if c.reader != nil { - return nil - } - recordReader := &reader{upstream: c.Conn, psk: c.client.psk} - recordReader.InitializeReadWaiter(c.readWaitOptions) - record, err := recordReader.ReadRecord() - if err != nil { - return E.Cause(err, "read reply") - } - cached, err := reuse.ParseReply(record) - if err != nil { - return err - } - if cached != nil { - recordReader.SetCache(cached) - } - c.reader = recordReader - return nil -} - -func (c *clientConn) Read(p []byte) (int, error) { - err := c.readResponse() - if err != nil { - return 0, err - } - return c.reader.Read(p) -} - -func (c *clientConn) ReadBuffer(buffer *buf.Buffer) error { - err := c.readResponse() - if err != nil { - return err - } - return c.reader.ReadBuffer(buffer) -} - -func (c *clientConn) Write(p []byte) (int, error) { - if c.writer != nil { - return c.writer.Write(p) - } - c.access.Lock() - if c.writer != nil { - c.access.Unlock() - return c.writer.Write(p) - } - defer c.access.Unlock() - err := c.writeRequest(p) - if err != nil { - return 0, err - } - return len(p), nil -} - -func (c *clientConn) WriteBuffer(buffer *buf.Buffer) error { - if c.writer != nil { - return c.writer.WriteBuffer(buffer) - } - c.access.Lock() - if c.writer != nil { - c.access.Unlock() - return c.writer.WriteBuffer(buffer) - } - defer c.access.Unlock() - return c.writeRequestBuffer(buffer) -} - -func (c *clientConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - return nil, false -} - -type clientVectorisedWriter struct { - conn *clientConn - upstream N.VectorisedWriter -} - -func (w *clientVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { - for _, buffer := range buffers { - if buffer.IsEmpty() { - continue - } - err := w.conn.writer.WriteBuffer(buffer) - if err != nil { - return err - } - } - return nil -} - -func (c *clientConn) CloseWrite() error { - c.closeWriteOnce.Do(func() { - c.access.Lock() - defer c.access.Unlock() - if c.writer == nil { - c.closeWriteErr = c.writeRequest(nil) - if c.closeWriteErr != nil { - return - } - } - c.closeWriteErr = c.writer.WriteZeroChunk() - }) - return c.closeWriteErr -} - -func (c *clientConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { - c.readWaitOptions = options - if c.reader != nil { - c.reader.InitializeReadWaiter(options) - } - return false -} - -func (c *clientConn) WaitReadBuffer() (*buf.Buffer, error) { - err := c.readResponse() - if err != nil { - return nil, err - } - return c.reader.WaitReadBuffer() -} - -func (c *clientConn) CreateReadWaiter() (N.ReadWaiter, bool) { - return c, true -} - -func (c *clientConn) FrontHeadroom() int { - if c.writer != nil { - return c.writer.FrontHeadroom() - } - requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: c.client.userKey, Destination: c.destination} - return requestPayload.Len() + snell.SaltLen + snell.HeaderCipherLen + maxInitialPaddingLen -} - -func (c *clientConn) RearHeadroom() int { - return snell.AEADTagLen -} - -func (c *clientConn) WriterMTU() int { - return maxPayload -} - -func (c *clientConn) NeedHandshakeForRead() bool { - return c.reader == nil -} - -func (c *clientConn) NeedHandshakeForWrite() bool { - return c.writer == nil -} - -func (c *clientConn) NeedAdditionalReadDeadline() bool { - return c.reader == nil -} - -func (c *clientConn) Upstream() any { - return c.Conn -} - -func (c *clientConn) RemoteAddr() net.Addr { - return c.destination.TCPAddr() -} - -var ( - _ N.ExtendedConn = (*clientConn)(nil) - _ N.ReadWaiter = (*clientConn)(nil) - _ N.ReadWaitCreator = (*clientConn)(nil) - _ N.VectorisedWriter = (*clientVectorisedWriter)(nil) - _ N.EarlyReader = (*clientConn)(nil) - _ N.EarlyWriter = (*clientConn)(nil) - _ N.WriteCloser = (*clientConn)(nil) -) diff --git a/proxy/snell/internal/singsnell/snellv4/compat_sing011.go b/proxy/snell/internal/singsnell/snellv4/compat_sing011.go deleted file mode 100644 index 016721ef77e..00000000000 --- a/proxy/snell/internal/singsnell/snellv4/compat_sing011.go +++ /dev/null @@ -1,21 +0,0 @@ -package snellv4 - -import ( - "github.com/sagernet/sing/common/buf" - M "github.com/sagernet/sing/common/metadata" -) - -// Stubs for sing APIs newer than v0.8.11. Methods that create these always -// return unsupported (nil, false) on this branch of exclave-core. - -type PacketBatchWriter interface { - WritePacketBatch(buffers []*buf.Buffer, destinations []M.Socksaddr) error -} - -type PacketBatchWriteCreator interface { - CreatePacketBatchWriter() (PacketBatchWriter, bool) -} - -type ConnectedPacketBatchWriteCreator interface { - CreateConnectedPacketBatchWriter() (PacketBatchWriter, bool) -} diff --git a/proxy/snell/internal/singsnell/snellv4/packet.go b/proxy/snell/internal/singsnell/snellv4/packet.go deleted file mode 100644 index 9f3c5e1b9dd..00000000000 --- a/proxy/snell/internal/singsnell/snellv4/packet.go +++ /dev/null @@ -1,233 +0,0 @@ -package snellv4 - -import ( - "io" - "net" - "sync" - - snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" - "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/buf" - E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -const ( - maxUDPRequestHeaderLen = 1 + 1 + 255 + 2 - minUDPPayloadLimit = frameSize - resetRecordOverhead -) - -type clientPacketConn struct { - net.Conn - client *Client - - writeAccess sync.Mutex - writer *writer - - readAccess sync.Mutex - reader *reader - readWaitOptions N.ReadWaitOptions -} - -func (c *clientPacketConn) udpRequestAddrLen(destination M.Socksaddr) int { - if destination.IsIP() { - if destination.Unwrap().Addr.Is4() { - return 1 + 1 + 4 + 2 - } - return 1 + 1 + 16 + 2 - } - return 1 + len(destination.Fqdn) + 2 -} - -func (c *clientPacketConn) writeRequest() error { - c.writeAccess.Lock() - defer c.writeAccess.Unlock() - if c.writer != nil { - return nil - } - recordWriter := &writer{ - upstream: c.Conn, - psk: c.client.psk, - } - requestPayload := snell.Request{Command: snell.CommandUDP, ClientID: c.client.userKey} - request := buf.NewSize(requestPayload.Len()) - err := requestPayload.Write(request) - if err != nil { - request.Release() - return err - } - _, err = recordWriter.Write(request.Bytes()) - request.Release() - if err != nil { - return E.Cause(err, "write udp request") - } - c.writer = recordWriter - return nil -} - -func (c *clientPacketConn) readReply() (*reader, error) { - c.readAccess.Lock() - defer c.readAccess.Unlock() - if c.reader != nil { - return c.reader, nil - } - recordReader := &reader{upstream: c.Conn, psk: c.client.psk} - recordReader.InitializeReadWaiter(c.readWaitOptions) - record, err := recordReader.ReadRecord() - if err != nil { - return nil, E.Cause(err, "read udp reply") - } - cached, err := reuse.ParseReply(record) - if err != nil { - return nil, err - } - if cached != nil { - recordReader.SetCache(cached) - } - c.reader = recordReader - return recordReader, nil -} - -func (c *clientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { - err := c.writeRequest() - if err != nil { - buffer.Release() - return err - } - _, err = c.readReply() - if err != nil { - buffer.Release() - return err - } - header := buf.With(buffer.ExtendHeader(1 + c.udpRequestAddrLen(destination))) - common.Must(header.WriteByte(snell.UDPCommandForward)) - err = snell.WriteUDPRequestAddress(header, destination) - if err != nil { - buffer.Release() - return err - } - if buffer.Len() > maxPayload { - buffer.Release() - return snell.ErrPayloadTooLarge - } - return c.writer.WritePacketBuffer(buffer) -} - -func (c *clientPacketConn) CreatePacketBatchWriter() (PacketBatchWriter, bool) { - return nil, false -} - -type clientPacketBatchWriter struct { - conn *clientPacketConn - upstream N.VectorisedWriter - - access sync.Mutex - writer N.VectorisedWriter -} - -func (w *clientPacketBatchWriter) WritePacketBatch(buffers []*buf.Buffer, destinations []M.Socksaddr) error { - return nil -} - -func (c *clientPacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) { - recordReader, err := c.readReply() - if err != nil { - return M.Socksaddr{}, err - } - for { - record, err := recordReader.NextRecord() - if err != nil { - return M.Socksaddr{}, err - } - if record.Len() < 8 { - record.Release() - return M.Socksaddr{}, E.New("snell: udp response too short") - } - // Surge 6.7.0 (11520): SGUDPConnectorSnellV4::socket:didReadData: silently ignores UDP - // response frames whose address type is neither IPv4 nor IPv6. - addressType := record.Bytes()[0] - if addressType != snell.AddressTypeIPv4 && addressType != snell.AddressTypeIPv6 { - record.Release() - continue - } - destination, err := snell.ReadUDPResponseAddress(record) - if err != nil { - record.Release() - return M.Socksaddr{}, err - } - if record.Len() > buffer.FreeLen() { - record.Release() - return M.Socksaddr{}, io.ErrShortBuffer - } - _, err = buffer.Write(record.Bytes()) - record.Release() - if err != nil { - return M.Socksaddr{}, err - } - return destination, nil - } -} - -func (c *clientPacketConn) FrontHeadroom() int { - return snell.HeaderCipherLen + maxUDPRequestHeaderLen -} - -func (c *clientPacketConn) RearHeadroom() int { - return snell.AEADTagLen -} - -func (c *clientPacketConn) WriterMTU() int { - return minUDPPayloadLimit - maxUDPRequestHeaderLen -} - -func (c *clientPacketConn) Upstream() any { - return c.Conn -} - -func (c *clientPacketConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { - c.readAccess.Lock() - defer c.readAccess.Unlock() - c.readWaitOptions = options - if c.reader != nil { - c.reader.InitializeReadWaiter(options) - } - return false -} - -func (c *clientPacketConn) WaitReadPacket() (*buf.Buffer, M.Socksaddr, error) { - recordReader, err := c.readReply() - if err != nil { - return nil, M.Socksaddr{}, err - } - for { - record, err := recordReader.WaitReadBuffer() - if err != nil { - return nil, M.Socksaddr{}, err - } - if record.Len() < 8 { - record.Release() - return nil, M.Socksaddr{}, E.New("snell: udp response too short") - } - // Surge 6.7.0 (11520): SGUDPConnectorSnellV4::socket:didReadData: silently ignores UDP - // response frames whose address type is neither IPv4 nor IPv6. - addressType := record.Bytes()[0] - if addressType != snell.AddressTypeIPv4 && addressType != snell.AddressTypeIPv6 { - record.Release() - continue - } - destination, err := snell.ReadUDPResponseAddress(record) - if err != nil { - record.Release() - return nil, M.Socksaddr{}, err - } - return record, destination, nil - } -} - -var ( - _ N.PacketConn = (*clientPacketConn)(nil) - _ N.PacketReadWaiter = (*clientPacketConn)(nil) - _ N.WriterWithMTU = (*clientPacketConn)(nil) -) diff --git a/proxy/snell/internal/singsnell/snellv4/record.go b/proxy/snell/internal/singsnell/snellv4/record.go deleted file mode 100644 index a69c426b61e..00000000000 --- a/proxy/snell/internal/singsnell/snellv4/record.go +++ /dev/null @@ -1,824 +0,0 @@ -package snellv4 - -import ( - standardbufio "bufio" - "crypto/cipher" - "crypto/rand" - "encoding/binary" - "io" - "math/big" - "math/bits" - "sync" - "time" - - snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/buf" - E "github.com/sagernet/sing/common/exceptions" - N "github.com/sagernet/sing/common/network" -) - -const ( - // Surge 6.7.0 (11520): SNConnectorEngineAdapterV4::encryptData:outData: uses these v4 frame sizing constants. - maxPayload = snell.MaxPayloadLen - frameSize = 1460 - firstRecordOverhead = 55 - resetRecordOverhead = 39 - initialPaddingMin = 0x100 - initialPaddingSpan = 0x100 - maxInitialPaddingLen = initialPaddingMin + initialPaddingSpan - 1 - // Surge 6.7.0 (11520): FUN_100012944 compares time(0) deltas against 0x1f. - framePayloadResetInterval = 31 -) - -type reader struct { - upstream io.Reader - bufferedUpstream *standardbufio.Reader - psk []byte - cipher cipher.AEAD - nonce []byte - cache *buf.Buffer - readWaitOptions N.ReadWaitOptions -} - -func (r *reader) initialize() error { - if r.cipher != nil { - return nil - } - if r.bufferedUpstream == nil { - r.bufferedUpstream = standardbufio.NewReader(r.upstream) - } - salt := make([]byte, snell.SaltLen) - _, err := io.ReadFull(r.bufferedUpstream, salt) - if err != nil { - return err - } - aead, err := snell.NewAEAD(snell.DeriveKey(r.psk, salt)) - if err != nil { - return err - } - r.cipher = aead - r.nonce = make([]byte, snell.NonceLen) - return nil -} - -func (r *reader) ReadRecord() (*buf.Buffer, error) { - err := r.initialize() - if err != nil { - return nil, err - } - headerCipher := buf.NewSize(snell.HeaderCipherLen) - _, err = headerCipher.ReadFullFrom(r.bufferedUpstream, snell.HeaderCipherLen) - if err != nil { - headerCipher.Release() - return nil, err - } - _, err = r.cipher.Open(headerCipher.Index(0), r.nonce, headerCipher.Bytes(), nil) - if err != nil { - headerCipher.Release() - return nil, E.Cause(err, "open record header") - } - snell.IncreaseNonce(r.nonce) - header := headerCipher.To(snell.HeaderPlainLen) - if header[0] != snell.HeaderVersion { - headerCipher.Release() - return nil, E.Extend(snell.ErrBadVersion, header[0]) - } - paddingLen := int(binary.BigEndian.Uint16(header[3:5])) - payloadLen := int(binary.BigEndian.Uint16(header[5:7])) - headerCipher.Release() - - if payloadLen == 0 { - // Surge 6.7.0 (11520): SNConnectorEngineAdapterV4::decryptData:outData: - // returns "ZERO CHUNK with remaining data." when the EOF record leaves - // bytes in the internal decrypt input buffer. - if r.bufferedUpstream.Buffered() > 0 { - return nil, E.New("snell: zero chunk has trailing data") - } - return nil, io.EOF - } - - var padding *buf.Buffer - if paddingLen > 0 { - padding = buf.NewSize(paddingLen) - _, err = padding.ReadFullFrom(r.bufferedUpstream, paddingLen) - if err != nil { - padding.Release() - return nil, err - } - } - body := buf.NewSize(payloadLen + snell.AEADTagLen) - _, err = body.ReadFullFrom(r.bufferedUpstream, payloadLen+snell.AEADTagLen) - if err != nil { - if padding != nil { - padding.Release() - } - body.Release() - return nil, err - } - if padding != nil { - paddingBytes := padding.Bytes() - payloadCipher := body.Bytes() - limit := min(len(paddingBytes), len(payloadCipher)) - for index := 0; index < limit; index += 2 { - paddingBytes[index], payloadCipher[index] = payloadCipher[index], paddingBytes[index] - } - padding.Release() - } - payloadCipher := body.Bytes() - _, err = r.cipher.Open(payloadCipher[:0], r.nonce, payloadCipher, nil) - if err != nil { - body.Release() - return nil, E.Cause(err, "open record payload") - } - snell.IncreaseNonce(r.nonce) - body.Truncate(payloadLen) - r.readWaitOptions.PostReturn(body) - return body, nil -} - -func (r *reader) NextRecord() (*buf.Buffer, error) { - record := r.cache - if record != nil { - r.cache = nil - if record.IsEmpty() { - record.Release() - } else { - return record, nil - } - } - return r.ReadRecord() -} - -func (r *reader) SetCache(cache *buf.Buffer) { - r.cache = cache -} - -func (r *reader) ReleaseCache() { - if r.cache != nil { - r.cache.Release() - r.cache = nil - } -} - -func (r *reader) Read(p []byte) (n int, err error) { - for { - if r.cache != nil { - if r.cache.IsEmpty() { - r.cache.Release() - r.cache = nil - } else { - n = copy(p, r.cache.Bytes()) - r.cache.Advance(n) - return - } - } - r.cache, err = r.ReadRecord() - if err != nil { - return - } - } -} - -func (r *reader) ReadBuffer(buffer *buf.Buffer) error { - for { - if r.cache != nil { - if r.cache.IsEmpty() { - r.cache.Release() - r.cache = nil - } else { - n, _ := buffer.Write(r.cache.Bytes()) - r.cache.Advance(n) - return nil - } - } - var err error - r.cache, err = r.ReadRecord() - if err != nil { - return err - } - } -} - -func (r *reader) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { - r.readWaitOptions = options - return false -} - -func (r *reader) WaitReadBuffer() (buffer *buf.Buffer, err error) { - for { - if r.cache == nil { - return r.ReadRecord() - } - buffer = r.cache - r.cache = nil - if buffer.IsEmpty() { - buffer.Release() - continue - } - if buffer.Start() >= r.readWaitOptions.FrontHeadroom && buffer.FreeLen() >= r.readWaitOptions.RearHeadroom { - return - } - buffer = r.readWaitOptions.Copy(buffer) - return buffer, nil - } -} - -func (r *reader) Upstream() any { - return r.upstream -} - -type writer struct { - upstream io.Writer - psk []byte - cipher cipher.AEAD - nonce []byte - salt []byte - saltSent bool - initialPaddingLen int - payloadLimit int - lastWriteUnix int64 - access sync.Mutex -} - -func (w *writer) initialize() error { - if w.cipher != nil { - return nil - } - salt := make([]byte, snell.SaltLen) - _, err := io.ReadFull(rand.Reader, salt) - if err != nil { - return err - } - aead, err := snell.NewAEAD(snell.DeriveKey(w.psk, salt)) - if err != nil { - return err - } - paddingDelta, err := rand.Int(rand.Reader, big.NewInt(initialPaddingSpan)) - if err != nil { - return err - } - w.cipher = aead - w.nonce = make([]byte, snell.NonceLen) - w.salt = salt - w.initialPaddingLen = initialPaddingMin + int(paddingDelta.Int64()) - return nil -} - -func (w *writer) payloadLimitFor(nowUnix int64) int { - if !w.saltSent { - return frameSize - firstRecordOverhead - w.initialPaddingLen - } - if w.lastWriteUnix != 0 && nowUnix-w.lastWriteUnix < framePayloadResetInterval { - if w.payloadLimit > 0 { - return w.payloadLimit - } - return frameSize - resetRecordOverhead - } - return frameSize - resetRecordOverhead -} - -func (w *writer) advancePayloadLimit(payloadLimit int, nowUnix int64) { - w.lastWriteUnix = nowUnix - if payloadLimit <= maxPayload-1 { - nextPayloadLimit := payloadLimit + frameSize - resetRecordOverhead - w.payloadLimit = min(nextPayloadLimit, maxPayload) - } else { - w.payloadLimit = maxPayload - } -} - -func (w *writer) framePaddingLen(payloadLen int) int { - if w.saltSent || payloadLen == 0 { - return 0 - } - return w.initialPaddingLen -} - -func (w *writer) writeBytesLocked(p []byte) (n int, err error) { - err = w.initialize() - if err != nil { - return 0, err - } - nowUnix := time.Now().Unix() - payloadLimit := w.payloadLimitFor(nowUnix) - if payloadLimit <= 0 || payloadLimit > maxPayload { - panic("snell: invalid v4 payload limit") - } - // Surge 6.7.0 (11520): SNConnectorEngineAdapterV4::encryptData:outData: updates the next payload limit once for - // each encryptData call, splits that call using the same limit, and returns the salt plus every - // record in one output buffer that the caller writes to the socket once. - w.advancePayloadLimit(payloadLimit, nowUnix) - recordCount := (len(p) + payloadLimit - 1) / payloadLimit - totalLen := recordCount*(snell.HeaderCipherLen+snell.AEADTagLen) + len(p) - if !w.saltSent { - totalLen += snell.SaltLen + w.initialPaddingLen - } - output := buf.NewSize(totalLen) - defer output.Release() - for data := p; len(data) > 0; { - recordLen := min(len(data), payloadLimit) - err = w.sealRecordLocked(output, data[:recordLen], w.framePaddingLen(recordLen)) - if err != nil { - return 0, err - } - data = data[recordLen:] - } - _, err = w.upstream.Write(output.Bytes()) - if err != nil { - return 0, err - } - return len(p), nil -} - -func (w *writer) sealRecordLocked(output *buf.Buffer, payload []byte, paddingLen int) error { - if len(payload) > maxPayload || paddingLen > maxPayload { - panic("snell: v4 record exceeds maximum") - } - if len(payload) == 0 && paddingLen != 0 { - panic("snell: zero-length v4 record carries padding") - } - if !w.saltSent { - common.Must1(output.Write(w.salt)) - w.saltSent = true - } - header := output.Extend(snell.HeaderCipherLen) - header[0] = snell.HeaderVersion - header[1] = 0 - header[2] = 0 - binary.BigEndian.PutUint16(header[3:5], uint16(paddingLen)) - binary.BigEndian.PutUint16(header[5:7], uint16(len(payload))) - w.cipher.Seal(header[:0], w.nonce, header[:snell.HeaderPlainLen], nil) - snell.IncreaseNonce(w.nonce) - padding := output.Extend(paddingLen) - if len(payload) > 0 { - payloadCipher := output.Extend(len(payload) + snell.AEADTagLen) - copy(payloadCipher, payload) - w.cipher.Seal(payloadCipher[:0], w.nonce, payloadCipher[:len(payload)], nil) - snell.IncreaseNonce(w.nonce) - if paddingLen > 0 { - err := w.fillPadding(padding, payloadCipher) - if err != nil { - return err - } - limit := min(len(padding), len(payloadCipher)) - for index := 0; index < limit; index += 2 { - padding[index], payloadCipher[index] = payloadCipher[index], padding[index] - } - } - } - return nil -} - -func (w *writer) makeSliceRecordLocked(payload []byte, paddingLen int) (*buf.Buffer, error) { - err := w.initialize() - if err != nil { - return nil, err - } - saltLen := 0 - if !w.saltSent { - saltLen = snell.SaltLen - } - payloadCipherLen := 0 - if len(payload) > 0 { - payloadCipherLen = len(payload) + snell.AEADTagLen - } - out := buf.NewSize(saltLen + snell.HeaderCipherLen + paddingLen + payloadCipherLen) - err = w.sealRecordLocked(out, payload, paddingLen) - if err != nil { - out.Release() - return nil, err - } - return out, nil -} - -func (w *writer) writeSliceRecordLocked(payload []byte, paddingLen int) error { - out, err := w.makeSliceRecordLocked(payload, paddingLen) - if err != nil { - return err - } - return w.writeRecordLocked(out) -} - -func (w *writer) writeRecordLocked(record *buf.Buffer) error { - defer record.Release() - return common.Error(w.upstream.Write(record.Bytes())) -} - -func (w *writer) randomInt31() (int, error) { - var randomBytes [4]byte - _, err := io.ReadFull(rand.Reader, randomBytes[:]) - if err != nil { - return 0, err - } - return int(binary.LittleEndian.Uint32(randomBytes[:]) & 0x7fffffff), nil -} - -func (w *writer) makeBufferRecordLocked(buffer *buf.Buffer, paddingLen int) (*buf.Buffer, error) { - err := w.initialize() - if err != nil { - return nil, err - } - dataLen := buffer.Len() - if dataLen > maxPayload || paddingLen > maxPayload { - panic("snell: v4 record exceeds maximum") - } - if dataLen == 0 && paddingLen != 0 { - panic("snell: zero-length v4 record carries padding") - } - saltLen := 0 - if !w.saltSent { - saltLen = snell.SaltLen - } - frontLen := saltLen + snell.HeaderCipherLen + paddingLen - prefix := buffer.ExtendHeader(frontLen) - if saltLen > 0 { - copy(prefix[:saltLen], w.salt) - w.saltSent = true - } - header := prefix[saltLen : saltLen+snell.HeaderCipherLen] - header[0] = snell.HeaderVersion - header[1] = 0 - header[2] = 0 - binary.BigEndian.PutUint16(header[3:5], uint16(paddingLen)) - binary.BigEndian.PutUint16(header[5:7], uint16(dataLen)) - w.cipher.Seal(header[:0], w.nonce, header[:snell.HeaderPlainLen], nil) - snell.IncreaseNonce(w.nonce) - if dataLen > 0 { - payloadCipher := buffer.From(frontLen) - buffer.Extend(snell.AEADTagLen) - w.cipher.Seal(payloadCipher[:0], w.nonce, payloadCipher[:dataLen], nil) - snell.IncreaseNonce(w.nonce) - if paddingLen > 0 { - padding := prefix[saltLen+snell.HeaderCipherLen:] - payloadCipher = buffer.From(frontLen) - err = w.fillPadding(padding, payloadCipher) - if err != nil { - return nil, err - } - limit := min(len(padding), len(payloadCipher)) - for index := 0; index < limit; index += 2 { - padding[index], payloadCipher[index] = payloadCipher[index], padding[index] - } - } - } - return buffer, nil -} - -func (w *writer) writeBufferRecordLocked(buffer *buf.Buffer, paddingLen int) error { - record, err := w.makeBufferRecordLocked(buffer, paddingLen) - if err != nil { - return err - } - return w.writeRecordLocked(record) -} - -func (w *writer) fillPadding(padding []byte, payloadCipher []byte) error { - if len(padding) == 0 { - return nil - } - payloadOnes := 0 - // Surge 6.7.0 (11520): FUN_100012b24 counts ones only over complete - // 32-bit chunks, but computes zeros against the full payload cipher length. - payloadCountLen := len(payloadCipher) &^ 3 - for _, payloadByte := range payloadCipher[:payloadCountLen] { - payloadOnes += bits.OnesCount8(payloadByte) - } - payloadZeros := 8*len(payloadCipher) - payloadOnes - if payloadZeros <= 0 { - _, err := io.ReadFull(rand.Reader, padding) - return err - } - - ratio := float64(payloadOnes) / float64(payloadZeros) - if ratio <= 0.5 || ratio >= 1.6 { - _, err := io.ReadFull(rand.Reader, padding) - return err - } - - targetRatioBase := 1.6 - if payloadZeros < payloadOnes { - targetRatioBase = 0.4 - } - // Surge 6.7.0 (11520): FUN_100012944 derives the target padding ratio as - // base + rand()/2147483647.0/10, using the process rand() 31-bit range. - randomValue, err := w.randomInt31() - if err != nil { - return err - } - targetRatio := targetRatioBase + float64(randomValue)/2147483647.0/10 - totalBits := 8 * (len(padding) + len(payloadCipher)) - targetOnes := int(float64(totalBits)*(targetRatio/(targetRatio+1)) - float64(payloadOnes)) - if targetOnes < 0 || targetOnes > 8*len(padding) { - _, err = io.ReadFull(rand.Reader, padding) - return err - } - return w.fillPaddingWithBitCount(padding, targetOnes) -} - -func (w *writer) fillPaddingWithBitCount(padding []byte, oneBits int) error { - totalBits := 8 * len(padding) - if oneBits < 0 || oneBits > totalBits { - panic("snell: invalid padding bit count") - } - bitset := make([]byte, totalBits) - for index := range oneBits { - bitset[index] = 1 - } - // Surge 6.7.0 (11520): FUN_100012eb8 shuffles from the front and maps - // rand() with division by RAND_MAX/remaining+1, preserving its modulo bias. - position := 0 - remaining := totalBits - for remaining != 1 { - randomValue, err := w.randomInt31() - if err != nil { - return err - } - divisor := 0 - if remaining != 0 { - divisor = 0x7fffffff / remaining - } - swapIndex := position + randomValue/(divisor+1) - bitset[swapIndex], bitset[position] = bitset[position], bitset[swapIndex] - position++ - remaining-- - } - clear(padding) - for index, bit := range bitset { - if bit == 1 { - padding[index/8] |= 1 << uint(index%8) - } - } - return nil -} - -func (w *writer) Write(p []byte) (n int, err error) { - w.access.Lock() - defer w.access.Unlock() - if len(p) == 0 { - err = w.initialize() - if err != nil { - return 0, err - } - nowUnix := time.Now().Unix() - payloadLimit := w.payloadLimitFor(nowUnix) - if payloadLimit <= 0 || payloadLimit > maxPayload { - panic("snell: invalid v4 payload limit") - } - // Surge 6.7.0 (11520): FUN_100012944 advances the growth window before - // calling FUN_100012b24 even when encrypting a zero-length EOF record. - w.advancePayloadLimit(payloadLimit, nowUnix) - return 0, w.writeSliceRecordLocked(nil, 0) - } - return w.writeBytesLocked(p) -} - -func (w *writer) WriteZeroChunk() error { - w.access.Lock() - defer w.access.Unlock() - err := w.initialize() - if err != nil { - return err - } - nowUnix := time.Now().Unix() - payloadLimit := w.payloadLimitFor(nowUnix) - if payloadLimit <= 0 || payloadLimit > maxPayload { - panic("snell: invalid v4 payload limit") - } - // Surge 6.7.0 (11520): FUN_100012944 advances the growth window before - // calling FUN_100012b24 even when encrypting a zero-length EOF record. - w.advancePayloadLimit(payloadLimit, nowUnix) - return w.writeSliceRecordLocked(nil, 0) -} - -func (w *writer) WriteBuffer(buffer *buf.Buffer) error { - defer buffer.Release() - dataLen := buffer.Len() - if dataLen == 0 { - return nil - } - w.access.Lock() - defer w.access.Unlock() - err := w.initialize() - if err != nil { - return err - } - nowUnix := time.Now().Unix() - payloadLimit := w.payloadLimitFor(nowUnix) - if payloadLimit <= 0 || payloadLimit > maxPayload { - panic("snell: invalid v4 payload limit") - } - if dataLen > payloadLimit { - _, err = w.writeBytesLocked(buffer.Bytes()) - return err - } - w.advancePayloadLimit(payloadLimit, nowUnix) - paddingLen := w.framePaddingLen(dataLen) - return w.writeBufferRecordLocked(buffer, paddingLen) -} - -func (w *writer) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - return nil, false -} - -func (w *writer) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { - return nil -} - -type vectorisedWriter struct { - writer *writer - upstream N.VectorisedWriter -} - -func (w *vectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { - payloadLen := buf.LenMulti(buffers) - if payloadLen == 0 { - buf.ReleaseMulti(buffers) - return nil - } - var records []*buf.Buffer - defer func() { - buf.ReleaseMulti(records) - }() - recordWriter := w.writer - recordWriter.access.Lock() - defer recordWriter.access.Unlock() - err := recordWriter.initialize() - if err != nil { - buf.ReleaseMulti(buffers) - return err - } - nowUnix := time.Now().Unix() - payloadLimit := recordWriter.payloadLimitFor(nowUnix) - if payloadLimit <= 0 || payloadLimit > maxPayload { - panic("snell: invalid v4 payload limit") - } - // Surge 6.7.0 (11520): SNConnectorEngineAdapterV4::encryptData:outData: - // advances the growth window once per encrypt call and splits that call - // using the same payload limit. - recordWriter.advancePayloadLimit(payloadLimit, nowUnix) - index := 0 - for remainingPayload := payloadLen; remainingPayload > 0; { - for buffers[index].IsEmpty() { - buffers[index].Release() - index++ - } - recordLen := min(remainingPayload, payloadLimit) - paddingLen := recordWriter.framePaddingLen(recordLen) - buffer := buffers[index] - if buffer.Len() == recordLen { - record, err := recordWriter.makeBufferRecordLocked(buffer, paddingLen) - if err != nil { - buffer.Release() - buf.ReleaseMulti(buffers[index+1:]) - return err - } - records = append(records, record) - index++ - } else { - payload := make([]byte, recordLen) - for copiedLen := 0; copiedLen < recordLen; { - buffer = buffers[index] - if buffer.IsEmpty() { - buffer.Release() - index++ - continue - } - copyLen := min(recordLen-copiedLen, buffer.Len()) - copy(payload[copiedLen:], buffer.Bytes()[:copyLen]) - buffer.Advance(copyLen) - copiedLen += copyLen - if buffer.IsEmpty() { - buffer.Release() - index++ - } - } - record, err := recordWriter.makeSliceRecordLocked(payload, paddingLen) - if err != nil { - buf.ReleaseMulti(buffers[index:]) - return err - } - records = append(records, record) - } - remainingPayload -= recordLen - } - buf.ReleaseMulti(buffers[index:]) - flushRecords := records - records = nil - return w.upstream.WriteVectorised(flushRecords) -} - -func (w *writer) CreatePacketVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { - return &packetVectorisedWriter{writer: w, upstream: upstream} -} - -type packetVectorisedWriter struct { - writer *writer - upstream N.VectorisedWriter -} - -func (w *packetVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { - var records []*buf.Buffer - defer func() { - buf.ReleaseMulti(records) - }() - recordWriter := w.writer - recordWriter.access.Lock() - defer recordWriter.access.Unlock() - err := recordWriter.initialize() - if err != nil { - buf.ReleaseMulti(buffers) - return err - } - for index, buffer := range buffers { - if buffer.IsEmpty() { - buffer.Release() - continue - } - nowUnix := time.Now().Unix() - payloadLimit := recordWriter.payloadLimitFor(nowUnix) - if payloadLimit <= 0 || payloadLimit > maxPayload { - panic("snell: invalid v4 payload limit") - } - dataLen := buffer.Len() - if dataLen > payloadLimit { - buffer.Release() - buf.ReleaseMulti(buffers[index+1:]) - return snell.ErrPayloadTooLarge - } - recordWriter.advancePayloadLimit(payloadLimit, nowUnix) - paddingLen := recordWriter.framePaddingLen(dataLen) - record, err := recordWriter.makeBufferRecordLocked(buffer, paddingLen) - if err != nil { - buffer.Release() - buf.ReleaseMulti(buffers[index+1:]) - return err - } - records = append(records, record) - } - if len(records) == 0 { - return nil - } - flushRecords := records - records = nil - return w.upstream.WriteVectorised(flushRecords) -} - -func (w *writer) WritePacketBuffer(buffer *buf.Buffer) error { - defer buffer.Release() - dataLen := buffer.Len() - w.access.Lock() - defer w.access.Unlock() - err := w.initialize() - if err != nil { - return err - } - nowUnix := time.Now().Unix() - payloadLimit := w.payloadLimitFor(nowUnix) - if payloadLimit <= 0 || payloadLimit > maxPayload { - panic("snell: invalid v4 payload limit") - } - if dataLen > payloadLimit { - return snell.ErrPayloadTooLarge - } - w.advancePayloadLimit(payloadLimit, nowUnix) - paddingLen := w.framePaddingLen(dataLen) - frontHeadroom := snell.HeaderCipherLen + paddingLen - if !w.saltSent { - frontHeadroom += snell.SaltLen - } - if buffer.Start() < frontHeadroom || buffer.FreeLen() < snell.AEADTagLen { - return w.writeSliceRecordLocked(buffer.Bytes(), paddingLen) - } - return w.writeBufferRecordLocked(buffer, paddingLen) -} - -func (w *writer) FrontHeadroom() int { - if w.saltSent { - return snell.HeaderCipherLen - } - return snell.SaltLen + snell.HeaderCipherLen + maxInitialPaddingLen -} - -func (w *writer) RearHeadroom() int { - return snell.AEADTagLen -} - -func (w *writer) WriterMTU() int { - return maxPayload -} - -func (w *writer) Upstream() any { - return w.upstream -} - -var ( - _ N.ExtendedReader = (*reader)(nil) - _ N.ReadWaiter = (*reader)(nil) - _ N.ExtendedWriter = (*writer)(nil) - _ N.VectorisedWriter = (*vectorisedWriter)(nil) - _ N.VectorisedWriter = (*packetVectorisedWriter)(nil) - _ N.FrontHeadroom = (*writer)(nil) - _ N.RearHeadroom = (*writer)(nil) - _ N.WriterWithMTU = (*writer)(nil) -) diff --git a/proxy/snell/internal/singsnell/snellv4/reuse.go b/proxy/snell/internal/singsnell/snellv4/reuse.go deleted file mode 100644 index 22a8a8a91ef..00000000000 --- a/proxy/snell/internal/singsnell/snellv4/reuse.go +++ /dev/null @@ -1,465 +0,0 @@ -package snellv4 - -import ( - "context" - "errors" - "io" - "net" - "sync" - "sync/atomic" - "time" - - snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" - "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" - "github.com/sagernet/sing/common/buf" - E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -func (c *Client) DialContext(ctx context.Context, destination M.Socksaddr) (net.Conn, error) { - if c.reuse { - session, err := c.reuseSession(ctx) - if err != nil { - return nil, err - } - conn, err := session.DialConn(destination) - if err != nil { - session.Close() - return nil, err - } - return conn, nil - } - if c.pool.IsClosed() { - return nil, net.ErrClosed - } - if c.dialer == nil { - return nil, E.New("snell: missing dialer") - } - conn, err := c.dialer.DialContext(ctx, N.NetworkTCP, c.server) - if err != nil { - return nil, err - } - proxyConn, err := c.DialConn(conn, destination) - if err != nil { - conn.Close() - return nil, err - } - return proxyConn, nil -} - -func (c *Client) reuseSession(ctx context.Context) (*reuseSession, error) { - session, found, closed := c.pool.Take() - if closed { - return nil, net.ErrClosed - } - if c.dialer == nil { - return nil, E.New("snell: missing dialer") - } - if found { - return session, nil - } - conn, err := c.dialer.DialContext(ctx, N.NetworkTCP, c.server) - if err != nil { - return nil, err - } - if c.pool.IsClosed() { - conn.Close() - return nil, net.ErrClosed - } - session = c.newReuseSession(conn) - session.state.Store(uint32(reuse.StateActive)) - return session, nil -} - -func (c *Client) Close() error { - return c.pool.Close() -} - -type reuseSession struct { - net.Conn - client *Client - - state atomic.Uint32 - reader *reader - writer *writer -} - -func (c *Client) newReuseSession(conn net.Conn) *reuseSession { - return &reuseSession{Conn: c.obfs.ClientConn(conn), client: c} -} - -func (s *reuseSession) ReuseState() *atomic.Uint32 { - return &s.state -} - -func (s *reuseSession) DialConn(destination M.Socksaddr) (net.Conn, error) { - state := reuse.State(s.state.Load()) - if state == reuse.StateClosed { - return nil, net.ErrClosed - } - if state != reuse.StateActive { - return nil, E.New("snell: reuse session is busy") - } - - requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: s.client.userKey, Destination: destination} - request := buf.NewSize(requestPayload.Len()) - err := requestPayload.Write(request) - if err != nil { - request.Release() - s.Release(false) - return nil, err - } - if s.writer == nil { - s.writer = &writer{ - upstream: s.Conn, - psk: s.client.psk, - } - } - _, err = s.writer.Write(request.Bytes()) - request.Release() - if err != nil { - s.Release(false) - return nil, E.Cause(err, "write request") - } - return &reuseConn{Conn: s.Conn, session: s, destination: destination}, nil -} - -func (s *reuseSession) Release(reusable bool) { - if !reusable { - s.Close() - return - } - if !s.state.CompareAndSwap(uint32(reuse.StateActive), uint32(reuse.StateReady)) { - if reuse.State(s.state.Load()) != reuse.StateClosed { - s.Close() - } - return - } - s.client.pool.MoveToPool(s, reuse.StateReady, false) -} - -func (s *reuseSession) Close() error { - if reuse.State(s.state.Swap(uint32(reuse.StateClosed))) == reuse.StateClosed { - return nil - } - return s.Conn.Close() -} - -func (s *reuseSession) startDrain() { - if !s.state.CompareAndSwap(uint32(reuse.StateActive), uint32(reuse.StateWaiting)) { - if reuse.State(s.state.Load()) != reuse.StateClosed { - s.Close() - } - return - } - if !s.client.pool.MoveToPool(s, reuse.StateWaiting, true) { - return - } - go s.drain() -} - -func (s *reuseSession) drain() { - defer s.client.pool.DrainDone() - var discarded int - for { - record, err := s.reader.NextRecord() - if errors.Is(err, io.EOF) { - s.state.CompareAndSwap(uint32(reuse.StateWaiting), uint32(reuse.StateReady)) - return - } - if err != nil { - s.Close() - return - } - // Surge 6.7.0 (11520): SNConnectorV4::socket:didReadData: discards data - // received while waiting for server EOF and closes after 0x80001 bytes. - discarded += record.Len() - record.Release() - if discarded >= reuse.WaitingDiscardLimit { - s.Close() - return - } - } -} - -type reuseConn struct { - net.Conn - session *reuseSession - destination M.Socksaddr - - closeWriteOnce sync.Once - closeWriteErr error - closeOnce sync.Once - closeErr error - replyRead atomic.Bool - readWaitOptions N.ReadWaitOptions - closed atomic.Bool - readActionCount atomic.Int32 - readClosed atomic.Bool -} - -func (c *reuseConn) readResponse() error { - if c.replyRead.Load() { - return nil - } - if c.session.reader == nil { - c.session.reader = &reader{upstream: c.session.Conn, psk: c.session.client.psk} - c.session.reader.InitializeReadWaiter(c.readWaitOptions) - } - record, err := c.session.reader.ReadRecord() - if err != nil { - return E.Cause(err, "read reply") - } - cached, err := reuse.ParseReply(record) - if err != nil { - return err - } - if cached != nil { - c.session.reader.SetCache(cached) - } - c.replyRead.Store(true) - return nil -} - -func (c *reuseConn) Read(p []byte) (int, error) { - if c.closed.Load() { - return 0, net.ErrClosed - } - c.readActionCount.Add(1) - defer c.readActionCount.Add(-1) - if c.closed.Load() { - return 0, net.ErrClosed - } - err := c.readResponse() - if err != nil { - return 0, err - } - var discarded int - for { - n, err := c.session.reader.Read(p) - if c.closed.Load() { - discarded += n - if errors.Is(err, io.EOF) { - c.readClosed.Store(true) - c.session.state.CompareAndSwap(uint32(reuse.StateWaiting), uint32(reuse.StateReady)) - return 0, err - } - if err != nil { - c.session.Close() - return 0, err - } - if discarded >= reuse.WaitingDiscardLimit { - c.session.Close() - return 0, E.New("snell: read too much data after client EOF") - } - continue - } - if errors.Is(err, io.EOF) { - c.readClosed.Store(true) - } - return n, err - } -} - -func (c *reuseConn) ReadBuffer(buffer *buf.Buffer) error { - if c.closed.Load() { - return net.ErrClosed - } - c.readActionCount.Add(1) - defer c.readActionCount.Add(-1) - if c.closed.Load() { - return net.ErrClosed - } - err := c.readResponse() - if err != nil { - return err - } - var discarded int - for { - bufferLen := buffer.Len() - err = c.session.reader.ReadBuffer(buffer) - if c.closed.Load() { - discarded += buffer.Len() - bufferLen - buffer.Truncate(bufferLen) - if errors.Is(err, io.EOF) { - c.readClosed.Store(true) - c.session.state.CompareAndSwap(uint32(reuse.StateWaiting), uint32(reuse.StateReady)) - return err - } - if err != nil { - c.session.Close() - return err - } - if discarded >= reuse.WaitingDiscardLimit { - c.session.Close() - return E.New("snell: read too much data after client EOF") - } - continue - } - if errors.Is(err, io.EOF) { - c.readClosed.Store(true) - } - return err - } -} - -func (c *reuseConn) Write(p []byte) (int, error) { - return c.session.writer.Write(p) -} - -func (c *reuseConn) WriteBuffer(buffer *buf.Buffer) error { - return c.session.writer.WriteBuffer(buffer) -} - -func (c *reuseConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - return nil, false -} -func (c *reuseConn) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { - return nil -} - - -func (c *reuseConn) CloseWrite() error { - c.closeWriteOnce.Do(func() { - c.closeWriteErr = c.session.writer.WriteZeroChunk() - }) - return c.closeWriteErr -} - -func (c *reuseConn) Close() error { - c.closeOnce.Do(func() { - c.closed.Store(true) - if !c.replyRead.Load() { - c.session.Release(false) - return - } - c.closeErr = c.CloseWrite() - if c.closeErr != nil { - c.session.Release(false) - return - } - c.session.Conn.SetReadDeadline(time.Time{}) - c.session.Conn.SetWriteDeadline(time.Time{}) - if c.readClosed.Load() { - c.session.Release(true) - return - } - // Surge 6.7.0 (11520): SNConnectorV4::readServerEOFIfNotInReadState: starts waiting-state EOF - // reads when _socketReadActionCounter is 0. - if c.readActionCount.Load() > 0 { - if !c.session.state.CompareAndSwap(uint32(reuse.StateActive), uint32(reuse.StateWaiting)) { - if reuse.State(c.session.state.Load()) != reuse.StateClosed { - c.session.Close() - } - return - } - poolState := reuse.StateWaiting - if c.readClosed.Load() { - c.session.state.CompareAndSwap(uint32(reuse.StateWaiting), uint32(reuse.StateReady)) - poolState = reuse.StateReady - } - if !c.session.client.pool.MoveToPool(c.session, poolState, false) { - return - } - return - } - c.session.startDrain() - }) - return c.closeErr -} - -func (c *reuseConn) CreateReadWaiter() (N.ReadWaiter, bool) { - return &reuseReadWaiter{conn: c}, true -} - -type reuseReadWaiter struct { - conn *reuseConn -} - -func (w *reuseReadWaiter) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { - w.conn.readWaitOptions = options - if w.conn.session.reader != nil { - w.conn.session.reader.InitializeReadWaiter(options) - } - return false -} - -func (w *reuseReadWaiter) WaitReadBuffer() (*buf.Buffer, error) { - if w.conn.closed.Load() { - return nil, net.ErrClosed - } - w.conn.readActionCount.Add(1) - defer w.conn.readActionCount.Add(-1) - if w.conn.closed.Load() { - return nil, net.ErrClosed - } - err := w.conn.readResponse() - if err != nil { - return nil, err - } - var discarded int - for { - buffer, err := w.conn.session.reader.WaitReadBuffer() - if w.conn.closed.Load() { - if buffer != nil { - discarded += buffer.Len() - buffer.Release() - } - if errors.Is(err, io.EOF) { - w.conn.readClosed.Store(true) - w.conn.session.state.CompareAndSwap(uint32(reuse.StateWaiting), uint32(reuse.StateReady)) - return nil, err - } - if err != nil { - w.conn.session.Close() - return nil, err - } - if discarded >= reuse.WaitingDiscardLimit { - w.conn.session.Close() - return nil, E.New("snell: read too much data after client EOF") - } - continue - } - if errors.Is(err, io.EOF) { - w.conn.readClosed.Store(true) - } - return buffer, err - } -} - -func (c *reuseConn) FrontHeadroom() int { - return c.session.writer.FrontHeadroom() -} - -func (c *reuseConn) RearHeadroom() int { - return snell.AEADTagLen -} - -func (c *reuseConn) WriterMTU() int { - return maxPayload -} - -func (c *reuseConn) NeedHandshakeForRead() bool { - return !c.replyRead.Load() -} - -func (c *reuseConn) NeedAdditionalReadDeadline() bool { - return !c.replyRead.Load() -} - -func (c *reuseConn) Upstream() any { - return c.session.Conn -} - -func (c *reuseConn) RemoteAddr() net.Addr { - return c.destination.TCPAddr() -} - -var ( - _ N.ExtendedConn = (*reuseConn)(nil) - _ N.ReadWaitCreator = (*reuseConn)(nil) - _ N.EarlyReader = (*reuseConn)(nil) - _ N.WriteCloser = (*reuseConn)(nil) - _ N.ReadWaiter = (*reuseReadWaiter)(nil) -) diff --git a/proxy/snell/internal/singsnell/snellv6/client.go b/proxy/snell/internal/singsnell/snellv6/client.go deleted file mode 100644 index 565ca06e029..00000000000 --- a/proxy/snell/internal/singsnell/snellv6/client.go +++ /dev/null @@ -1,333 +0,0 @@ -package snellv6 - -import ( - "net" - "sync" - - snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" - "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" - E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -type Client struct { - psk []byte - userKey []byte - mode Mode - reuse bool - profile *Profile - dialer N.Dialer - server M.Socksaddr - - pool reuse.Pool[*reuseSession] -} - -type ClientOptions struct { - PSK []byte - UserKey []byte - Mode Mode - Reuse bool - Dialer N.Dialer - Server M.Socksaddr -} - -func NewClient(options ClientOptions) (*Client, error) { - if len(options.PSK) == 0 { - return nil, snell.ErrMissingPSK - } - if len(options.UserKey) > 255 { - return nil, E.New("snell: user key too long") - } - if options.Mode != ModeDefault && options.Mode != ModeUnshaped && options.Mode != ModeUnsafeRaw { - return nil, E.New("snell: unknown v6 mode: ", int(options.Mode)) - } - client := &Client{ - psk: options.PSK, - userKey: options.UserKey, - mode: options.Mode, - reuse: options.Reuse, - dialer: options.Dialer, - server: options.Server, - } - if options.Mode == ModeDefault { - client.profile = NewProfile(options.PSK) - } - if options.Reuse { - client.pool.Init() - } - return client, nil -} - -func (c *Client) DialConn(conn net.Conn, destination M.Socksaddr) (net.Conn, error) { - clientConn := &clientConn{client: c, Conn: conn, destination: destination} - return clientConn, clientConn.writeRequest(nil) -} - -func (c *Client) DialEarlyConn(conn net.Conn, destination M.Socksaddr) net.Conn { - return &clientConn{client: c, Conn: conn, destination: destination} -} - -func (c *Client) DialPacketConn(conn net.Conn) (N.NetPacketConn, error) { - return bufio.NewNetPacketConn(&clientPacketConn{Conn: conn, client: c}), nil -} - -var _ snell.Method = (*Client)(nil) - -type clientConn struct { - net.Conn - client *Client - destination M.Socksaddr - - access sync.Mutex - reader reuse.RecordReader - writer reuse.RecordWriter - readWaitOptions N.ReadWaitOptions - closeWriteOnce sync.Once - closeWriteErr error -} - -func (c *clientConn) requestPayload(payload []byte) (*buf.Buffer, error) { - // Surge 6.7.0 (11520): SNConnectorV4::targetHandshakeData: writes command 5 - // for v6 TCP handshakes even when connector reuse is disabled. - requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: c.client.userKey, Destination: c.destination} - request := buf.NewSize(requestPayload.Len() + len(payload)) - err := requestPayload.Write(request) - if err != nil { - request.Release() - return nil, err - } - if len(payload) > 0 { - common.Must1(request.Write(payload)) - } - return request, nil -} - -func (c *clientConn) writeRequest(payload []byte) error { - request, err := c.requestPayload(payload) - if err != nil { - return err - } - defer request.Release() - data := request.Bytes() - first := data - if len(first) > maxPayload { - first = data[:maxPayload] - } - writer, err := writeFirstRecord(c.Conn, c.client.mode, c.client.psk, c.client.profile, first) - if err != nil { - return E.Cause(err, "write request") - } - c.writer = writer - if len(data) > len(first) { - _, err = writer.Write(data[len(first):]) - if err != nil { - return E.Cause(err, "write request") - } - } - return nil -} - -func (c *clientConn) writeRequestBuffer(buffer *buf.Buffer) error { - requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: c.client.userKey, Destination: c.destination} - request := buf.With(buffer.ExtendHeader(requestPayload.Len())) - err := requestPayload.Write(request) - if err != nil { - buffer.Release() - return err - } - writer, err := writeFirstRecordBuffer(c.Conn, c.client.mode, c.client.psk, c.client.profile, buffer) - if err != nil { - return E.Cause(err, "write request") - } - c.writer = writer - return nil -} - -func (c *clientConn) readResponse() error { - if c.reader != nil { - return nil - } - reader, record, err := readFirstRecord(c.Conn, c.client.mode, c.client.psk, c.client.profile, c.readWaitOptions) - if err != nil { - return E.Cause(err, "read reply") - } - cached, err := reuse.ParseReply(record) - if err != nil { - return err - } - reader.SetCache(cached) - c.reader = reader - return nil -} - -func (c *clientConn) Read(p []byte) (int, error) { - err := c.readResponse() - if err != nil { - return 0, err - } - return c.reader.Read(p) -} - -func (c *clientConn) ReadBuffer(buffer *buf.Buffer) error { - err := c.readResponse() - if err != nil { - return err - } - return c.reader.ReadBuffer(buffer) -} - -func (c *clientConn) Write(p []byte) (int, error) { - if c.writer != nil { - return c.writer.Write(p) - } - c.access.Lock() - if c.writer != nil { - c.access.Unlock() - return c.writer.Write(p) - } - defer c.access.Unlock() - err := c.writeRequest(p) - if err != nil { - return 0, err - } - return len(p), nil -} - -func (c *clientConn) WriteBuffer(buffer *buf.Buffer) error { - if c.writer != nil { - return c.writer.WriteBuffer(buffer) - } - c.access.Lock() - if c.writer != nil { - c.access.Unlock() - return c.writer.WriteBuffer(buffer) - } - defer c.access.Unlock() - return c.writeRequestBuffer(buffer) -} - -func (c *clientConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - return nil, false -} - -type clientVectorisedWriter struct { - conn *clientConn - upstream N.VectorisedWriter -} - -func (w *clientVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { - for _, buffer := range buffers { - if buffer.IsEmpty() { - continue - } - err := w.conn.writer.WriteBuffer(buffer) - if err != nil { - return err - } - } - return nil -} - -func (c *clientConn) CloseWrite() error { - c.closeWriteOnce.Do(func() { - c.access.Lock() - defer c.access.Unlock() - if c.writer == nil { - c.closeWriteErr = c.writeRequest(nil) - if c.closeWriteErr != nil { - return - } - } - c.closeWriteErr = c.writer.WriteZeroChunk() - }) - return c.closeWriteErr -} - -func (c *clientConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { - c.readWaitOptions = options - if c.reader != nil { - c.reader.InitializeReadWaiter(options) - } - return false -} - -func (c *clientConn) WaitReadBuffer() (*buf.Buffer, error) { - err := c.readResponse() - if err != nil { - return nil, err - } - return c.reader.WaitReadBuffer() -} - -func (c *clientConn) CreateReadWaiter() (N.ReadWaiter, bool) { - return c, true -} - -func (c *clientConn) FrontHeadroom() int { - if c.writer != nil { - return c.writer.FrontHeadroom() - } - requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: c.client.userKey, Destination: c.destination} - switch c.client.mode { - case ModeUnsafeRaw: - return requestPayload.Len() + snell.HeaderPlainLen - case ModeUnshaped: - return requestPayload.Len() + snell.SaltLen + snell.HeaderCipherLen - default: - return requestPayload.Len() + c.client.profile.saltBlockLen + c.client.profile.recordPrefixMax + snell.HeaderCipherLen + c.client.profile.padMaxHeadroom - } -} - -func (c *clientConn) RearHeadroom() int { - if c.writer != nil { - return c.writer.RearHeadroom() - } - if c.client.mode == ModeUnsafeRaw { - return 0 - } - return snell.AEADTagLen -} - -func (c *clientConn) WriterMTU() int { - if c.writer != nil { - return c.writer.WriterMTU() - } - if c.client.mode == ModeDefault { - return c.client.profile.chunkMax - } - return maxPayload -} - -func (c *clientConn) NeedHandshakeForRead() bool { - return c.reader == nil -} - -func (c *clientConn) NeedHandshakeForWrite() bool { - return c.writer == nil -} - -func (c *clientConn) NeedAdditionalReadDeadline() bool { - return c.reader == nil -} - -func (c *clientConn) Upstream() any { - return c.Conn -} - -func (c *clientConn) RemoteAddr() net.Addr { - return c.destination.TCPAddr() -} - -var ( - _ N.ExtendedConn = (*clientConn)(nil) - _ N.ReadWaiter = (*clientConn)(nil) - _ N.ReadWaitCreator = (*clientConn)(nil) - _ N.VectorisedWriter = (*clientVectorisedWriter)(nil) - _ N.EarlyReader = (*clientConn)(nil) - _ N.EarlyWriter = (*clientConn)(nil) - _ N.WriteCloser = (*clientConn)(nil) -) diff --git a/proxy/snell/internal/singsnell/snellv6/compat_sing011.go b/proxy/snell/internal/singsnell/snellv6/compat_sing011.go deleted file mode 100644 index ff3d19a629e..00000000000 --- a/proxy/snell/internal/singsnell/snellv6/compat_sing011.go +++ /dev/null @@ -1,21 +0,0 @@ -package snellv6 - -import ( - "github.com/sagernet/sing/common/buf" - M "github.com/sagernet/sing/common/metadata" -) - -// Stubs for sing APIs newer than v0.8.11. Methods that create these always -// return unsupported (nil, false) on this branch of exclave-core. - -type PacketBatchWriter interface { - WritePacketBatch(buffers []*buf.Buffer, destinations []M.Socksaddr) error -} - -type PacketBatchWriteCreator interface { - CreatePacketBatchWriter() (PacketBatchWriter, bool) -} - -type ConnectedPacketBatchWriteCreator interface { - CreateConnectedPacketBatchWriter() (PacketBatchWriter, bool) -} diff --git a/proxy/snell/internal/singsnell/snellv6/handshake.go b/proxy/snell/internal/singsnell/snellv6/handshake.go deleted file mode 100644 index cf24b31b45c..00000000000 --- a/proxy/snell/internal/singsnell/snellv6/handshake.go +++ /dev/null @@ -1,216 +0,0 @@ -package snellv6 - -import ( - "crypto/rand" - "io" - "time" - - snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" - "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/buf" - N "github.com/sagernet/sing/common/network" -) - -func writeFirstRecord(conn io.Writer, mode Mode, psk []byte, profile *Profile, payload []byte) (reuse.RecordWriter, error) { - switch mode { - case ModeUnsafeRaw: - writer := newRawWriter(conn) - // Surge 6.7.0 (11520): FUN_1000154e0: chunks unsafe-raw payloads larger than the - // 16-bit record payload field instead of truncating the length. - if len(payload) > maxPayload { - _, err := writer.Write(payload) - if err != nil { - return nil, err - } - return writer, nil - } - out := buf.NewSize(snell.HeaderPlainLen + len(payload)) - putHeader(out.Extend(snell.HeaderPlainLen), 0, len(payload)) - common.Must1(out.Write(payload)) - _, err := conn.Write(out.Bytes()) - out.Release() - if err != nil { - return nil, err - } - return writer, nil - case ModeUnshaped: - salt := make([]byte, snell.SaltLen) - _, err := io.ReadFull(rand.Reader, salt) - if err != nil { - return nil, err - } - key := snell.DeriveKey(psk, salt) - aead, err := snell.NewAEAD(key) - if err != nil { - return nil, err - } - nonce := make([]byte, snell.NonceLen) - recordCount := 1 - if len(payload) > 0 { - recordCount = (len(payload) + maxPayload - 1) / maxPayload - } - payloadCipherLen := len(payload) - if len(payload) > 0 { - payloadCipherLen += recordCount * snell.AEADTagLen - } - out := buf.NewSize(snell.SaltLen + recordCount*snell.HeaderCipherLen + payloadCipherLen) - common.Must1(out.Write(salt)) - w := newUnshapedWriter(conn, aead, nonce) - // Surge 6.7.0 (11520): FUN_10001596c: emits the salt once, then splits the first - // unshaped payload into 0xffff-byte records. - for remaining := payload; ; { - recordLen := min(len(remaining), maxPayload) - w.sealHeader(out.Extend(snell.HeaderCipherLen), recordLen) - if recordLen > 0 { - aead.Seal(out.Extend(recordLen + snell.AEADTagLen)[:0], nonce, remaining[:recordLen], nil) - snell.IncreaseNonce(nonce) - remaining = remaining[recordLen:] - if len(remaining) > 0 { - continue - } - } - break - } - _, err = conn.Write(out.Bytes()) - out.Release() - if err != nil { - return nil, err - } - return w, nil - case ModeDefault: - salt := make([]byte, snell.SaltLen) - _, err := io.ReadFull(rand.Reader, salt) - if err != nil { - return nil, err - } - aead, err := snell.NewAEAD(snell.DeriveKey(psk, salt)) - if err != nil { - return nil, err - } - writer := newShapedWriter(conn, profile, salt, aead, make([]byte, snell.NonceLen)) - if len(payload) == 0 { - writer.payloadLimitFor(time.Now()) - record := writer.makeSliceRecord(nil) - _, err = conn.Write(record.Bytes()) - record.Release() - } else { - _, err = writer.Write(payload) - } - if err != nil { - return nil, err - } - return writer, nil - default: - panic("snell: invalid v6 mode") - } -} - -func writeFirstRecordBuffer(conn io.Writer, mode Mode, psk []byte, profile *Profile, buffer *buf.Buffer) (reuse.RecordWriter, error) { - switch mode { - case ModeUnsafeRaw: - writer := newRawWriter(conn) - err := writer.WriteBuffer(buffer) - if err != nil { - return nil, err - } - return writer, nil - case ModeUnshaped: - if buffer.Len() > maxPayload { - writer, err := writeFirstRecord(conn, mode, psk, profile, buffer.Bytes()) - buffer.Release() - if err != nil { - return nil, err - } - return writer, nil - } - salt := make([]byte, snell.SaltLen) - _, err := io.ReadFull(rand.Reader, salt) - if err != nil { - buffer.Release() - return nil, err - } - key := snell.DeriveKey(psk, salt) - aead, err := snell.NewAEAD(key) - if err != nil { - buffer.Release() - return nil, err - } - nonce := make([]byte, snell.NonceLen) - record := buffer.ExtendHeader(snell.SaltLen + snell.HeaderCipherLen) - copy(record[:snell.SaltLen], salt) - writer := newUnshapedWriter(conn, aead, nonce) - writer.sealHeader(record[snell.SaltLen:], buffer.Len()-(snell.SaltLen+snell.HeaderCipherLen)) - payloadCipher := buffer.From(snell.SaltLen + snell.HeaderCipherLen) - buffer.Extend(snell.AEADTagLen) - aead.Seal(payloadCipher[:0], nonce, payloadCipher, nil) - snell.IncreaseNonce(nonce) - _, err = conn.Write(buffer.Bytes()) - buffer.Release() - if err != nil { - return nil, err - } - return writer, nil - case ModeDefault: - salt := make([]byte, snell.SaltLen) - _, err := io.ReadFull(rand.Reader, salt) - if err != nil { - buffer.Release() - return nil, err - } - aead, err := snell.NewAEAD(snell.DeriveKey(psk, salt)) - if err != nil { - buffer.Release() - return nil, err - } - writer := newShapedWriter(conn, profile, salt, aead, make([]byte, snell.NonceLen)) - err = writer.WriteBuffer(buffer) - if err != nil { - return nil, err - } - return writer, nil - default: - panic("snell: invalid v6 mode") - } -} - -func readFirstRecord(conn io.Reader, mode Mode, psk []byte, profile *Profile, readWaitOptions N.ReadWaitOptions) (reuse.RecordReader, *buf.Buffer, error) { - switch mode { - case ModeUnsafeRaw: - r := newRawReader(conn) - r.InitializeReadWaiter(readWaitOptions) - record, err := r.ReadRecord() - if err != nil { - return nil, nil, err - } - return r, record, nil - case ModeUnshaped: - salt := make([]byte, snell.SaltLen) - _, err := io.ReadFull(conn, salt) - if err != nil { - return nil, nil, err - } - key := snell.DeriveKey(psk, salt) - aead, err := snell.NewAEAD(key) - if err != nil { - return nil, nil, err - } - r := newUnshapedReader(conn, aead, make([]byte, snell.NonceLen)) - r.InitializeReadWaiter(readWaitOptions) - record, err := r.ReadRecord() - if err != nil { - return nil, nil, err - } - return r, record, nil - case ModeDefault: - reader := newShapedReader(conn, psk, profile) - reader.InitializeReadWaiter(readWaitOptions) - record, err := reader.ReadRecord() - if err != nil { - return nil, nil, err - } - return reader, record, nil - default: - panic("snell: invalid v6 mode") - } -} diff --git a/proxy/snell/internal/singsnell/snellv6/mixing.go b/proxy/snell/internal/singsnell/snellv6/mixing.go deleted file mode 100644 index ae478e83e6f..00000000000 --- a/proxy/snell/internal/singsnell/snellv6/mixing.go +++ /dev/null @@ -1,54 +0,0 @@ -package snellv6 - -func (p *Profile) mixPaddingPayload(seq uint32, padding []byte, payloadCipher []byte) { - n := min(len(padding), len(payloadCipher)) - if n == 0 { - return - } - for round := uint32(0); round < p.mixRounds; round++ { - switch p.mixMode { - case 0: - p.mixFixedStride(round, padding, payloadCipher, n) - case 1: - p.mixAlternatingBlock(round, padding, payloadCipher, n) - case 2: - p.mixPRFStride(seq, round, padding, payloadCipher, n) - } - } -} - -func (p *Profile) mixFixedStride(round uint32, padding []byte, payloadCipher []byte, n int) { - stride := max(p.mixStride+int(round%3), 1) - if stride == 1 { - for i := range n { - padding[i], payloadCipher[i] = payloadCipher[i], padding[i] - } - return - } - for off := p.mixOffsetBase % stride; off < n; off += stride { - padding[off], payloadCipher[off] = payloadCipher[off], padding[off] - } -} - -func (p *Profile) mixAlternatingBlock(round uint32, padding []byte, payloadCipher []byte, n int) { - block := p.mixBlock - for off := int(round&1) * block; off+block <= n; off += block * 2 { - for i := off; i < off+block; i++ { - padding[i], payloadCipher[i] = payloadCipher[i], padding[i] - } - } -} - -func (p *Profile) mixPRFStride(seq uint32, round uint32, padding []byte, payloadCipher []byte, n int) { - stride := max(p.mixStride+int(round%3), 1) - off := int((uint64(p.namespaces.prf32(labelMixOffset, seq, round)) + uint64(p.mixOffsetBase)) % uint64(stride)) - if stride == 1 { - for i := range n { - padding[i], payloadCipher[i] = payloadCipher[i], padding[i] - } - return - } - for ; off < n; off += stride { - padding[off], payloadCipher[off] = payloadCipher[off], padding[off] - } -} diff --git a/proxy/snell/internal/singsnell/snellv6/mode.go b/proxy/snell/internal/singsnell/snellv6/mode.go deleted file mode 100644 index d89b470f1eb..00000000000 --- a/proxy/snell/internal/singsnell/snellv6/mode.go +++ /dev/null @@ -1,41 +0,0 @@ -package snellv6 - -import ( - E "github.com/sagernet/sing/common/exceptions" -) - -type Mode int - -const ( - ModeDefault Mode = iota - ModeUnshaped - ModeUnsafeRaw -) - -func ParseMode(name string) (Mode, error) { - switch name { - case "", "default": - return ModeDefault, nil - case "unshaped": - return ModeUnshaped, nil - case "unsafe-raw": - return ModeUnsafeRaw, nil - default: - return 0, E.New("snell: unknown v6 mode: ", name) - } -} - -func (m Mode) String() string { - switch m { - case ModeDefault: - return "default" - case ModeUnshaped: - return "unshaped" - case ModeUnsafeRaw: - return "unsafe-raw" - default: - panic("snell: invalid v6 mode") - } -} - -const maxPayload = 0xffff diff --git a/proxy/snell/internal/singsnell/snellv6/packet.go b/proxy/snell/internal/singsnell/snellv6/packet.go deleted file mode 100644 index 472bc12e46a..00000000000 --- a/proxy/snell/internal/singsnell/snellv6/packet.go +++ /dev/null @@ -1,240 +0,0 @@ -package snellv6 - -import ( - "io" - "net" - "sync" - - snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" - "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/buf" - E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -const ( - maxUDPRequestHeaderLen = 1 + 1 + 255 + 2 - maxUDPResponseHeaderLen = 1 + 16 + 2 -) - -func (c *clientPacketConn) udpRequestAddrLen(destination M.Socksaddr) int { - if destination.IsIP() { - if destination.Unwrap().Addr.Is4() { - return 1 + 1 + 4 + 2 - } - return 1 + 1 + 16 + 2 - } - return 1 + len(destination.Fqdn) + 2 -} - -type clientPacketConn struct { - net.Conn - client *Client - - writeAccess sync.Mutex - writer reuse.RecordWriter - - readAccess sync.Mutex - reader reuse.RecordReader - - readWaitOptions N.ReadWaitOptions -} - -func (c *clientPacketConn) writeRequest() error { - c.writeAccess.Lock() - defer c.writeAccess.Unlock() - if c.writer != nil { - return nil - } - requestPayload := snell.Request{Command: snell.CommandUDP, ClientID: c.client.userKey} - request := buf.NewSize(requestPayload.Len()) - err := requestPayload.Write(request) - if err != nil { - request.Release() - return err - } - writer, err := writeFirstRecord(c.Conn, c.client.mode, c.client.psk, c.client.profile, request.Bytes()) - request.Release() - if err != nil { - return E.Cause(err, "write udp request") - } - c.writer = writer - return nil -} - -func (c *clientPacketConn) readReply() (reuse.RecordReader, error) { - c.readAccess.Lock() - defer c.readAccess.Unlock() - if c.reader != nil { - return c.reader, nil - } - reader, record, err := readFirstRecord(c.Conn, c.client.mode, c.client.psk, c.client.profile, c.readWaitOptions) - if err != nil { - return nil, E.Cause(err, "read udp reply") - } - cached, err := reuse.ParseReply(record) - if err != nil { - return nil, err - } - reader.SetCache(cached) - c.reader = reader - return reader, nil -} - -func (c *clientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { - err := c.writeRequest() - if err != nil { - buffer.Release() - return err - } - _, err = c.readReply() - if err != nil { - buffer.Release() - return err - } - header := buf.With(buffer.ExtendHeader(1 + c.udpRequestAddrLen(destination))) - common.Must(header.WriteByte(snell.UDPCommandForward)) - err = snell.WriteUDPRequestAddress(header, destination) - if err != nil { - buffer.Release() - return err - } - if buffer.Len() > maxPayload { - buffer.Release() - return snell.ErrPayloadTooLarge - } - return c.writer.WritePacketBuffer(buffer) -} - -func (c *clientPacketConn) CreatePacketBatchWriter() (PacketBatchWriter, bool) { - return nil, false -} - -type clientPacketBatchWriter struct { - conn *clientPacketConn - upstream N.VectorisedWriter - - access sync.Mutex - writer N.VectorisedWriter -} - -func (w *clientPacketBatchWriter) WritePacketBatch(buffers []*buf.Buffer, destinations []M.Socksaddr) error { - return nil -} - -func (c *clientPacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) { - reader, err := c.readReply() - if err != nil { - return M.Socksaddr{}, err - } - record, err := reader.NextRecord() - if err != nil { - return M.Socksaddr{}, err - } - destination, err := snell.ReadUDPResponseAddress(record) - if err != nil { - record.Release() - return M.Socksaddr{}, err - } - if record.Len() > buffer.FreeLen() { - record.Release() - return M.Socksaddr{}, io.ErrShortBuffer - } - _, err = buffer.Write(record.Bytes()) - record.Release() - if err != nil { - return M.Socksaddr{}, err - } - return destination, nil -} - -func (c *clientPacketConn) FrontHeadroom() int { - switch c.client.mode { - case ModeUnsafeRaw: - return snell.HeaderPlainLen + maxUDPRequestHeaderLen - case ModeUnshaped: - return snell.HeaderCipherLen + maxUDPRequestHeaderLen - default: - return c.client.profile.recordPrefixMax + snell.HeaderCipherLen + c.client.profile.padMaxHeadroom + maxUDPRequestHeaderLen - } -} - -func (c *clientPacketConn) RearHeadroom() int { - if c.client.mode == ModeUnsafeRaw { - return 0 - } - return snell.AEADTagLen -} - -func (c *clientPacketConn) WriterMTU() int { - switch c.client.mode { - case ModeUnsafeRaw, ModeUnshaped: - return maxPayload - maxUDPRequestHeaderLen - default: - payloadLimit := c.client.profile.chunkInitial - switch c.client.profile.chunkPolicy { - case 1: - payloadLimit = c.client.profile.chunkBuckets[0] - for _, chunkBucket := range c.client.profile.chunkBuckets[1:] { - payloadLimit = min(payloadLimit, chunkBucket) - } - case 2: - payloadLimit -= c.client.profile.chunkJitter - } - payloadLimit = max(0x40, min(payloadLimit, c.client.profile.chunkMax)) - return max(1, payloadLimit-maxUDPRequestHeaderLen) - } -} - -func (c *clientPacketConn) Upstream() any { - return c.Conn -} - -func (c *clientPacketConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { - c.readAccess.Lock() - defer c.readAccess.Unlock() - c.readWaitOptions = options - if c.reader != nil { - c.reader.InitializeReadWaiter(options) - } - return false -} - -func (c *clientPacketConn) WaitReadPacket() (*buf.Buffer, M.Socksaddr, error) { - reader, err := c.readReply() - if err != nil { - return nil, M.Socksaddr{}, err - } - record, err := reader.WaitReadBuffer() - if err != nil { - return nil, M.Socksaddr{}, err - } - destination, err := snell.ReadUDPResponseAddress(record) - if err != nil { - record.Release() - return nil, M.Socksaddr{}, err - } - return record, destination, nil -} - - - - - - - - - - - - - - - -var ( - _ N.PacketConn = (*clientPacketConn)(nil) - _ N.PacketReadWaiter = (*clientPacketConn)(nil) - _ N.WriterWithMTU = (*clientPacketConn)(nil) -) diff --git a/proxy/snell/internal/singsnell/snellv6/profile.go b/proxy/snell/internal/singsnell/snellv6/profile.go deleted file mode 100644 index 32953422fc1..00000000000 --- a/proxy/snell/internal/singsnell/snellv6/profile.go +++ /dev/null @@ -1,522 +0,0 @@ -package snellv6 - -import ( - "encoding/binary" - "math/bits" - - snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" - - "golang.org/x/crypto/blake2b" -) - -// Surge 6.7.0 (11520): FUN_1000130ac: derives v6 profile values with this SplitMix64 gamma. -const goldenGamma = 0x9e3779b97f4a7c15 - -const saltLen = 16 - -const ( - // Surge 6.7.0 (11520): FUN_1000130ac: uses these PRF labels for v6 profile derivation. - labelPadding = 0 - labelBitPercent = 1 - labelMotif = 2 - labelMixOffset = 3 - labelSalt = 3 - labelProfileID = 5 - labelGenerator = 6 - labelPadMin = 7 - labelPadMax = 8 - labelPadCount = 9 - labelPadInterval = 10 - labelSmallLimit = 11 - labelBitMin = 12 - labelBitMax = 13 - labelPrefixMin = 14 - labelPrefixMax = 15 - labelMixMode = 16 - labelMixRounds = 17 - labelMixStride = 18 - labelMixOffsetBase = 19 - labelMixBlock = 20 - labelChunkPolicy = 21 - labelChunkInitial = 22 - labelChunkFirstCap = 22 - labelChunkMax = 23 - labelChunkStep = 24 - labelChunkJitter = 25 - labelChunkBucket = 26 - labelIdleReset = 27 - labelWritePolicy = 28 - labelWriteFirst = 29 - labelWriteBucket = 30 - labelWriteSeq = 31 - labelWriteJitter = 32 - labelRecordPrefix = 33 - labelPayloadPad = 34 - labelWriteTarget = 35 - labelWriteJitterV = 36 - labelWriteNext = 37 - labelChunkSize = 38 - labelChunkJitterV = 39 -) - -const ( - // Surge 6.7.0 (11520): FUN_1000130ac: uses these domain separators for handshake and chunk profile values. - handshakeDomain = 0x7053 - mixHandshakeDomain = 0x51a7 - chunkInitialDomain = 0xf17c -) - -const ( - // Surge 6.7.0 (11520): FUN_1000130ac: uses these namespace seeds for v6 profile derivation. - nsSeedProfile = 0xb46c2e7d9a1538f1 - nsSeedPrefix = 0x5d9217c083e64ab9 - nsSeedMotif = 0xa71f0c54d8396e2b - nsSeedSalt = 0x3e8a91b52740f6cd - nsSeedMix = 0xc9f4260b7d1e835a - nsSeedChunk = 0x62d0b5e19c4a783f - nsSeedWrite = 0x917b3c48e6a205d4 -) - -const ( - // Surge 6.7.0 (11520): FUN_100014910/FUN_1000149c4: uses these arithmetic constants for v6 profile PRFs. - domainMul = 0xd6e8feb86659fd93 - namespaceAdd = 0xa0761d6478bd642f - coefB = 0x589965cc75374cc3 - addB = 0x33a213ec50ffe2e9 - coefA = 0xe7037ed1a0b428db - addA = 0x8f3907f7b2b80c35 - maxExtraPadding = 0x02da -) - -// Surge 6.7.0 (11520): FUN_1000130ac: profileSeed is the BLAKE2b prefix mixed with the PSK to derive the profile secret. -var profileSeed = [...]byte{ - 0x8d, 0x41, 0xa7, 0x13, 0x5c, 0xe2, 0x09, 0xbb, 0x70, 0x2f, 0xd6, 0x94, - 0x33, 0x18, 0xc0, 0x6e, 0x4a, 0x91, 0x25, 0xfd, 0xb8, 0x03, 0x77, 0xac, -} - -// Surge 6.7.0 (11520): FUN_100014f98: generator 0 maps low nibbles through this bit-rotation table. -var bitRotateTable = [...]byte{ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, - 0x03, 0x05, 0x09, 0x11, 0x21, 0x41, 0x81, 0x06, 0x0a, 0x12, 0x22, 0x42, 0x82, 0x0c, 0x18, 0x24, - 0x07, 0x0b, 0x13, 0x23, 0x43, 0x83, 0x0d, 0x19, 0x31, 0x61, 0xc1, 0x0e, 0x1c, 0x38, 0x70, 0xe0, - 0x0f, 0x17, 0x27, 0x47, 0x87, 0x1b, 0x33, 0x63, 0xc3, 0x1d, 0x39, 0x71, 0xe1, 0x3c, 0x78, 0xf0, - 0xf8, 0xf4, 0xec, 0xdc, 0xbc, 0x7c, 0xf2, 0xe6, 0xce, 0x9e, 0x3e, 0xf1, 0xe3, 0xc7, 0x8f, 0x1f, - 0xfc, 0xfa, 0xf6, 0xee, 0xde, 0xbe, 0x7e, 0xf9, 0xf5, 0xed, 0xdd, 0xbd, 0x7d, 0xf3, 0xe7, 0xdb, - 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, -} - -func splitmix64(x uint64) uint64 { - x ^= x >> 30 - x *= 0xbf58476d1ce4e5b9 - x ^= x >> 27 - x *= 0x94d049bb133111eb - x ^= x >> 31 - return x -} - -func prf32Fold(namespace uint64, label uint32, a uint64, b uint64) uint32 { - x := namespace ^ (b*coefB + addB) ^ (uint64(label) * goldenGamma) ^ (a*coefA + addA) - y := splitmix64(x) - return uint32(y ^ (y >> 32)) -} - -func pick(raw uint32, lo int, hi int) int { - if hi < lo { - panic("snell: invalid profile range") - } - return lo + int(uint64(raw)%uint64(hi-lo+1)) -} - -func pickU32(raw uint32, lo uint32, hi uint32) uint32 { - if hi < lo { - panic("snell: invalid profile range") - } - return lo + raw%(hi-lo+1) -} - -type namespaces struct { - profile uint64 - prefix uint64 - motif uint64 - salt uint64 - mix uint64 - chunk uint64 - write uint64 -} - -func deriveNamespace(secret []byte, label uint32, seedConst uint64) uint64 { - s0 := binary.LittleEndian.Uint64(secret[0:]) - s1 := binary.LittleEndian.Uint64(secret[8:]) - s2 := binary.LittleEndian.Uint64(secret[16:]) - s3 := binary.LittleEndian.Uint64(secret[24:]) - mixed := uint64(label)*domainMul ^ - (seedConst + namespaceAdd) ^ - s0 ^ - (s1 + goldenGamma) ^ - bits.RotateLeft64(s2, 17) ^ - bits.RotateLeft64(s3, -11) - return splitmix64(mixed) -} - -func newNamespaces(secret []byte) namespaces { - return namespaces{ - profile: deriveNamespace(secret, labelProfileID, nsSeedProfile), - prefix: deriveNamespace(secret, labelPadding, nsSeedPrefix), - motif: deriveNamespace(secret, labelMotif, nsSeedMotif), - salt: deriveNamespace(secret, labelSalt, nsSeedSalt), - mix: deriveNamespace(secret, labelMixMode, nsSeedMix), - chunk: deriveNamespace(secret, labelChunkPolicy, nsSeedChunk), - write: deriveNamespace(secret, labelWritePolicy, nsSeedWrite), - } -} - -func (n namespaces) forLabel(label uint32) uint64 { - switch label { - case labelPadding, labelBitPercent, labelPrefixMin, labelPrefixMax, labelRecordPrefix, labelPayloadPad: - return n.prefix - case labelMotif: - return n.motif - case labelMixOffset, labelMixMode, labelMixRounds, labelMixStride, labelMixOffsetBase, labelMixBlock: - return n.mix - case labelChunkPolicy, labelChunkInitial, labelChunkMax, labelChunkStep, labelChunkJitter, labelChunkBucket, labelChunkSize, labelChunkJitterV: - return n.chunk - case labelWritePolicy, labelWriteFirst, labelWriteBucket, labelWriteSeq, labelWriteJitter, labelWriteTarget, labelWriteJitterV, labelWriteNext: - return n.write - default: - return n.profile - } -} - -func (n namespaces) prf32(label uint32, a uint32, b uint32) uint32 { - return prf32Fold(n.forLabel(label), label, uint64(a), uint64(b)) -} - -func (n namespaces) prfBytes(label uint32, a uint32, output []byte) { - seed := n.forLabel(label) ^ - (uint64(a)*domainMul + 0xb57de1f3f82cb33f) ^ - (uint64(label) * 0xa24baed4963ee407) ^ - (uint64(len(output))*0x165667b19e3779f9 + 0x0d4cd3e7b14a36d7) - offset := 0 - for offset < len(output) { - seed += goldenGamma - block := splitmix64(seed) - for shift := 0; shift < 64 && offset < len(output); shift += 8 { - output[offset] = byte(block >> shift) - offset++ - } - } -} - -func (n namespaces) prfStatic(label uint32, domain uint32) uint32 { - return prf32Fold(n.forLabel(label), label, 0, uint64(domain)) -} - -type Profile struct { - namespaces namespaces - - generator uint32 - padMin int - padMax int - padCount uint32 - padInterval uint32 - smallLimit int - bitMin uint32 - bitMax uint32 - prefixMinRecord int - prefixMaxRecord int - mixMode uint32 - mixRounds uint32 - mixStride int - mixOffsetBase int - mixBlock int - chunkPolicy uint32 - chunkInitial int - firstRecordCap int - chunkMax int - chunkStep int - chunkJitter int - idleResetSec int - writePolicy uint32 - writeFirst uint32 - chunkBuckets [8]int - writeBuckets [8]int - writeSeq [8]int - writeJitter int - writeJitterPct int - g1 int - g2 int - g3 int - g4 int - g5 int - g6 int - - saltBlockLen int - mixStrideHandshake int - mixRoundsHandshake uint32 - - recordPrefixMax int - padMaxHeadroom int -} - -func profileSecret(psk []byte) [32]byte { - input := make([]byte, len(profileSeed)+len(psk)) - copy(input, profileSeed[:]) - copy(input[len(profileSeed):], psk) - return blake2b.Sum256(input) -} - -func NewProfile(psk []byte) *Profile { - secret := profileSecret(psk) - ns := newNamespaces(secret[:]) - - padMin := pick(ns.prfStatic(labelPadMin, 0), 0x18, 0xa0) - padMax := min(padMin+pick(ns.prfStatic(labelPadMax, 0), 0xa0, 0x3c0), maxExtraPadding) - - prefixMinHS := pick(ns.prfStatic(labelPrefixMin, handshakeDomain), 0x10, 0x60) - prefixMaxHS := min(prefixMinHS+pick(ns.prfStatic(labelPrefixMax, handshakeDomain), 0x10, 0xa0), 0x80) - prefixMinHS = min(prefixMinHS, prefixMaxHS) - saltPrefixLen := pick(ns.prfStatic(labelRecordPrefix, handshakeDomain), prefixMinHS, prefixMaxHS) - saltBlockLen := saltLen + saltPrefixLen - - mixRoundsHS := pickU32(ns.prfStatic(labelMixRounds, mixHandshakeDomain), 1, 4) - mixStrideHS := pick(ns.prfStatic(labelMixStride, mixHandshakeDomain), 0x11, 0xfb) - - prefixMinRecord := pick(ns.prfStatic(labelPrefixMin, 0), 0x08, 0x50) - prefixMaxRecord := min(prefixMinRecord+pick(ns.prfStatic(labelPrefixMax, 0), 0x10, 0xa0), 0x80) - prefixMinRecord = min(prefixMinRecord, prefixMaxRecord) - - chunkInitial := max(0x60, min(pick(ns.prfStatic(labelChunkInitial, 0), 0x200, 0x05b4), 0x05b4)) - firstRecordCap := max(0x100, min(pick(ns.prfStatic(labelChunkFirstCap, chunkInitialDomain), 0x100, 0x300), min(chunkInitial, 0x300))) - chunkMax := max(pick(ns.prfStatic(labelChunkMax, 0), 0x2000, 0x3fff), chunkInitial) - - profile := &Profile{ - namespaces: ns, - generator: ns.prfStatic(labelGenerator, 0) & 3, - padMin: padMin, - padMax: padMax, - padCount: pickU32(ns.prfStatic(labelPadCount, 0), 2, 8), - padInterval: pickU32(ns.prfStatic(labelPadInterval, 0), 2, 0x0b), - smallLimit: pick(ns.prfStatic(labelSmallLimit, 0), 0x60, 0x300), - bitMin: pickU32(ns.prfStatic(labelBitMin, 0), 0x18, 0x29), - bitMax: pickU32(ns.prfStatic(labelBitMax, 0), 0x3a, 0x4c), - prefixMinRecord: prefixMinRecord, - prefixMaxRecord: prefixMaxRecord, - mixMode: ns.prfStatic(labelMixMode, 0) % 3, - mixRounds: pickU32(ns.prfStatic(labelMixRounds, 0), 1, 3), - mixStride: pick(ns.prfStatic(labelMixStride, 0), 2, 13), - mixOffsetBase: pick(ns.prfStatic(labelMixOffsetBase, 0), 0, 15), - mixBlock: pick(ns.prfStatic(labelMixBlock, 0), 8, 0x40), - chunkPolicy: ns.prfStatic(labelChunkPolicy, 0) % 3, - chunkInitial: chunkInitial, - firstRecordCap: firstRecordCap, - chunkMax: chunkMax, - chunkStep: min(pick(ns.prfStatic(labelChunkStep, 0), 0x400, 0x1000), 0x0b68), - chunkJitter: min(pick(ns.prfStatic(labelChunkJitter, 0), 0x10, 0xc0), 0x0b6), - idleResetSec: pick(ns.prfStatic(labelIdleReset, 0), 0x0c, 0x5a), - writePolicy: ns.prfStatic(labelWritePolicy, 0) % 3, - writeFirst: pickU32(ns.prfStatic(labelWriteFirst, 0), 4, 8), - writeJitter: pick(ns.prfStatic(labelWriteJitter, 0), 0x08, 0x60), - writeJitterPct: pick(ns.prfStatic(labelWritePolicy, 0x504c), 8, 0x30), - g1: pick(ns.prfStatic(labelGenerator, 1), 0x18, 0x80), - g2: pick(ns.prfStatic(labelGenerator, 2), 0x10, 0x60), - g3: pick(ns.prfStatic(labelGenerator, 3), 0x10, 0x60), - g4: pick(ns.prfStatic(labelGenerator, 4), 0x00, 0x09), - g5: pick(ns.prfStatic(labelGenerator, 5), 0x01, 0x08), - g6: pick(ns.prfStatic(labelGenerator, 6), 0x07, 0x17), - saltBlockLen: saltBlockLen, - mixStrideHandshake: mixStrideHS, - mixRoundsHandshake: mixRoundsHS, - recordPrefixMax: prefixMaxRecord, - padMaxHeadroom: padMax + maxExtraPadding, - } - - for index := range 8 { - profile.chunkBuckets[index] = pick(ns.prfStatic(labelChunkBucket, uint32(index)), 0x1000, chunkMax) - profile.writeBuckets[index] = pick(ns.prfStatic(labelWriteBucket, uint32(index)), 0x140, 0x05b4) - profile.writeSeq[index] = pick(ns.prfStatic(labelWriteSeq, uint32(index)), 0x168, 0x05b4) - } - - return profile -} - -func (p *Profile) recordPrefixLen(seq uint32) int { - return pick(p.namespaces.prf32(labelRecordPrefix, seq, 0), p.prefixMinRecord, p.prefixMaxRecord) -} - -func (p *Profile) chunkPayloadLimit(seq uint32, chunkSize int) int { - if chunkSize == 0 { - chunkSize = p.chunkInitial - } - switch p.chunkPolicy { - case 1: - chunkSize = p.chunkBuckets[p.namespaces.prf32(labelChunkSize, seq, uint32(chunkSize))%uint32(len(p.chunkBuckets))] - case 2: - rawJitter := p.namespaces.prf32(labelChunkJitterV, seq, uint32(chunkSize)) - chunkSize += int(rawJitter%uint32(p.chunkJitter*2+1)) - p.chunkJitter - } - return max(0x40, min(chunkSize, p.chunkMax)) -} - -func (p *Profile) nextChunkSize(chunkSize int) int { - if chunkSize == 0 { - return p.chunkInitial - } - return min(chunkSize+p.chunkStep, p.chunkMax) -} - -func (p *Profile) paddingLen(seq uint32, payloadLen int, prefixLen int, saltPrefixLen int, totalLen int) int { - paddingLen := 0 - if seq < p.padCount || (payloadLen > 0 && payloadLen <= p.smallLimit) || (p.padInterval > 0 && seq%p.padInterval == 0) { - paddingLen = pick(p.namespaces.prf32(labelPayloadPad, seq, uint32(payloadLen)), p.padMin, p.padMax) - } - payloadTagLen := 0 - if payloadLen > 0 { - payloadTagLen = snell.AEADTagLen - } - frameLen := totalLen + prefixLen + snell.HeaderCipherLen + paddingLen + payloadLen + payloadTagLen - targetLen := p.writeTarget(seq, frameLen) - if targetLen > frameLen { - paddingLen += min(targetLen-frameLen, maxExtraPadding) - } - if totalLen > 0 { - paddingLen = p.firstRecordPaddingLen(paddingLen, prefixLen, saltPrefixLen, payloadLen) - } - return min(paddingLen, 0xffff) -} - -func (p *Profile) writeTarget(seq uint32, frameLen int) int { - if frameLen > 0x05b3 { - if frameLen <= 0xffff { - return frameLen - } - return 0xffff - } - var targetLen int - if seq < p.writeFirst { - targetLen = p.writeSeq[seq] - } else { - targetLen = p.writeBuckets[p.namespaces.prf32(labelWriteTarget, seq, uint32(frameLen))%uint32(len(p.writeBuckets))] - } - if p.writePolicy == 2 { - rawJitter := p.namespaces.prf32(labelWriteJitterV, seq, 0) - jitter := int(rawJitter%uint32(p.writeJitter*2+1)) - p.writeJitter - targetLen = max(targetLen+jitter, 1) - } - spread := min(maxExtraPadding, int(uint64(frameLen)*uint64(p.writeJitterPct)/100)) - if p.namespaces.prf32(labelWriteTarget, seq, uint32(spread))&1 == 0 { - if targetLen+spread <= 0xffff { - targetLen += spread - } else { - targetLen = 0xffff - } - } else { - halfSpread := spread >> 1 - if targetLen > halfSpread { - targetLen -= halfSpread - } - } - for targetLen >= 0 && frameLen > targetLen { - nextLen := p.writeBuckets[p.namespaces.prf32(labelWriteNext, seq, uint32(targetLen))%uint32(len(p.writeBuckets))] - if nextLen <= targetLen { - nextLen = targetLen + p.padMax - if nextLen > 0xffff { - return 0xffff - } - } - targetLen = nextLen - } - return targetLen -} - -func (p *Profile) firstRecordPaddingLen(paddingLen int, prefixLen int, saltPrefixLen int, payloadLen int) int { - overheadLen := saltPrefixLen + prefixLen + paddingLen - thresholdInput := payloadLen + 0x37 - if payloadLen == 0 { - thresholdInput = payloadLen + 0x27 - } - threshold := max((thresholdInput*25+0x4a)/75, 0xc0) - if overheadLen >= threshold { - return paddingLen - } - targetPaddingLen := threshold - saltPrefixLen - prefixLen - maxPaddingLen := p.padMax + maxExtraPadding - if maxPaddingLen <= targetPaddingLen { - targetPaddingLen = maxPaddingLen - } - if targetPaddingLen > 0xfffe { - return paddingLen - } - return targetPaddingLen -} - -func (p *Profile) fillPadding(seq uint32, output []byte) { - if len(output) == 0 { - return - } - p.namespaces.prfBytes(labelPadding, seq, output) - switch p.generator { - case 0: - bitCount := pickU32(p.namespaces.prf32(labelBitPercent, seq, 0), p.bitMin, p.bitMax) - rotate := 1 - scaled := int(bitCount) * 8 - if scaled > 0x31 { - rotate = 7 - if scaled <= 0x02ed { - rotate = (scaled + 0x32) / 100 - } - } - for index, value := range output { - raw := byte(int(value) + index) - tableRow := max(rotate, 1) - tableValue := bitRotateTable[tableRow*16+int((raw^value)&0x0f)] - output[index] = bits.RotateLeft8(tableValue, int((raw^(value>>4))&7)) - } - case 1: - for index, value := range output { - bucket := int(value) % (p.g1 + p.g2 + p.g3) - switch { - case bucket < p.g1: - // Surge 6.7.0 (11520): FUN_100014f98: truncates the byte/index mix before - // reducing it into the printable range. - output[index] = byte(pick(uint32(byte(int(value)+index)), 0x20, 0x7e)) - case bucket < p.g1+p.g2: - output[index] = byte(pick(uint32(byte(int(value)^index)), 0x80, 0xbf)) - default: - output[index] = byte(pick(uint32(byte(int(value)+index*7)), 0xc0, 0xff)) - } - } - case 2: - for index, value := range output { - low := (int(value&0x0f) + p.g4 + (index & 1)) % 10 - high := (int(value) + ((index & 3) << 4) + 0x30) & 0xf0 - output[index] = byte(high | low) - } - case 3: - motif := make([]byte, 32) - p.namespaces.prfBytes(labelMotif, seq, motif) - motifLen := p.g5 * 4 - if motifLen == 0 { - motifLen = 4 - } - period := max(p.g6, 5) - blockOffset := 0 - motifOffset := 0 - for index, value := range output { - switch { - case blockOffset < period-3: - output[index] = byte((p.g5+3)*index) ^ motif[motifOffset%len(motif)] - case blockOffset < period-1: - output[index] = byte(0x30 | (int(value) % 10)) - } - blockOffset++ - if blockOffset == period { - blockOffset = 0 - } - motifOffset++ - if motifOffset == motifLen { - motifOffset = 0 - } - } - } -} diff --git a/proxy/snell/internal/singsnell/snellv6/record.go b/proxy/snell/internal/singsnell/snellv6/record.go deleted file mode 100644 index 3e79bca4f1a..00000000000 --- a/proxy/snell/internal/singsnell/snellv6/record.go +++ /dev/null @@ -1,630 +0,0 @@ -package snellv6 - -import ( - "crypto/cipher" - "encoding/binary" - "io" - "sync" - - snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/buf" - E "github.com/sagernet/sing/common/exceptions" - N "github.com/sagernet/sing/common/network" -) - -// Surge 6.7.0 (11520): FUN_100015c54/FUN_1000156e0: non-default records require zero padding length. -var errRecordPadding = E.New("snell: unexpected padding in non-default record") - -func parseHeader(header []byte) (paddingLen int, payloadLen int, err error) { - if header[0] != snell.HeaderVersion { - return 0, 0, E.Extend(snell.ErrBadVersion, header[0]) - } - if header[1] != 0 || header[2] != 0 { - return 0, 0, snell.ErrReservedNonZero - } - return int(binary.BigEndian.Uint16(header[3:5])), int(binary.BigEndian.Uint16(header[5:7])), nil -} - -func putHeader(header []byte, paddingLen int, payloadLen int) { - header[0] = snell.HeaderVersion - header[1] = 0 - header[2] = 0 - binary.BigEndian.PutUint16(header[3:5], uint16(paddingLen)) - binary.BigEndian.PutUint16(header[5:7], uint16(payloadLen)) -} - -type baseReader struct { - upstream io.Reader - readFunc func() (*buf.Buffer, error) - cache *buf.Buffer - readWaitOptions N.ReadWaitOptions -} - -func (r *baseReader) ReadRecord() (*buf.Buffer, error) { - return r.readFunc() -} - -func (r *baseReader) NextRecord() (*buf.Buffer, error) { - record := r.cache - if record != nil { - r.cache = nil - if record.IsEmpty() { - record.Release() - } else { - return record, nil - } - } - return r.ReadRecord() -} - -func (r *baseReader) SetCache(cache *buf.Buffer) { - r.cache = cache -} - -func (r *baseReader) ReleaseCache() { - if r.cache != nil { - r.cache.Release() - r.cache = nil - } -} - -func (r *baseReader) Read(p []byte) (n int, err error) { - for { - if r.cache != nil { - if r.cache.IsEmpty() { - r.cache.Release() - r.cache = nil - } else { - n = copy(p, r.cache.Bytes()) - r.cache.Advance(n) - return - } - } - r.cache, err = r.ReadRecord() - if err != nil { - return - } - } -} - -func (r *baseReader) ReadBuffer(buffer *buf.Buffer) error { - for { - if r.cache != nil { - if r.cache.IsEmpty() { - r.cache.Release() - r.cache = nil - } else { - n, _ := buffer.Write(r.cache.Bytes()) - r.cache.Advance(n) - return nil - } - } - var err error - r.cache, err = r.ReadRecord() - if err != nil { - return err - } - } -} - -func (r *baseReader) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { - r.readWaitOptions = options - return false -} - -func (r *baseReader) WaitReadBuffer() (buffer *buf.Buffer, err error) { - for { - if r.cache == nil { - return r.ReadRecord() - } - buffer = r.cache - r.cache = nil - if buffer.IsEmpty() { - buffer.Release() - continue - } - if buffer.Start() >= r.readWaitOptions.FrontHeadroom && buffer.FreeLen() >= r.readWaitOptions.RearHeadroom { - return - } - buffer = r.readWaitOptions.Copy(buffer) - return buffer, nil - } -} - -func (r *baseReader) Upstream() any { - return r.upstream -} - -type unshapedReader struct { - baseReader - cipher cipher.AEAD - nonce []byte -} - -func newUnshapedReader(upstream io.Reader, aead cipher.AEAD, nonce []byte) *unshapedReader { - r := &unshapedReader{cipher: aead, nonce: nonce} - r.upstream = upstream - r.readFunc = r.read - return r -} - -func (r *unshapedReader) read() (*buf.Buffer, error) { - headerCipher := buf.NewSize(snell.HeaderCipherLen) - _, err := headerCipher.ReadFullFrom(r.upstream, snell.HeaderCipherLen) - if err != nil { - headerCipher.Release() - return nil, err - } - _, err = r.cipher.Open(headerCipher.Index(0), r.nonce, headerCipher.Bytes(), nil) - if err != nil { - headerCipher.Release() - return nil, E.Cause(err, "open record header") - } - snell.IncreaseNonce(r.nonce) - paddingLen, payloadLen, err := parseHeader(headerCipher.To(snell.HeaderPlainLen)) - headerCipher.Release() - if err != nil { - return nil, err - } - if paddingLen != 0 { - return nil, errRecordPadding - } - if payloadLen == 0 { - return nil, io.EOF - } - body := buf.NewSize(payloadLen + snell.AEADTagLen) - _, err = body.ReadFullFrom(r.upstream, payloadLen+snell.AEADTagLen) - if err != nil { - body.Release() - return nil, err - } - _, err = r.cipher.Open(body.Index(0), r.nonce, body.Bytes(), nil) - if err != nil { - body.Release() - return nil, E.Cause(err, "open record payload") - } - snell.IncreaseNonce(r.nonce) - body.Truncate(payloadLen) - r.readWaitOptions.PostReturn(body) - return body, nil -} - -type unshapedWriter struct { - upstream io.Writer - cipher cipher.AEAD - nonce []byte - access sync.Mutex -} - -func newUnshapedWriter(upstream io.Writer, aead cipher.AEAD, nonce []byte) *unshapedWriter { - return &unshapedWriter{upstream: upstream, cipher: aead, nonce: nonce} -} - -func (w *unshapedWriter) sealHeader(header []byte, payloadLen int) { - putHeader(header, 0, payloadLen) - w.cipher.Seal(header[:0], w.nonce, header[:snell.HeaderPlainLen], nil) - snell.IncreaseNonce(w.nonce) -} - -func (w *unshapedWriter) Write(p []byte) (n int, err error) { - if len(p) == 0 { - return 0, nil - } - w.access.Lock() - defer w.access.Unlock() - // snell-server v6.0.0b4: FUN_0013d580 splits one encrypt call at 0xffff and - // reallocs every record into a single output buffer written to the socket once. - recordCount := (len(p) + maxPayload - 1) / maxPayload - output := buf.NewSize(recordCount*(snell.HeaderCipherLen+snell.AEADTagLen) + len(p)) - defer output.Release() - for data := p; len(data) > 0; { - recordLen := min(len(data), maxPayload) - w.sealHeader(output.Extend(snell.HeaderCipherLen), recordLen) - w.cipher.Seal(output.Extend(recordLen+snell.AEADTagLen)[:0], w.nonce, data[:recordLen], nil) - snell.IncreaseNonce(w.nonce) - data = data[recordLen:] - } - _, err = w.upstream.Write(output.Bytes()) - if err != nil { - return 0, err - } - return len(p), nil -} - -func (w *unshapedWriter) makeSliceRecordLocked(payload []byte) *buf.Buffer { - record := buf.NewSize(snell.HeaderCipherLen + len(payload) + snell.AEADTagLen) - w.sealHeader(record.Extend(snell.HeaderCipherLen), len(payload)) - common.Must1(record.Write(payload)) - w.cipher.Seal(record.From(snell.HeaderCipherLen)[:0], w.nonce, record.From(snell.HeaderCipherLen), nil) - record.Extend(snell.AEADTagLen) - snell.IncreaseNonce(w.nonce) - return record -} - -func (w *unshapedWriter) makeBufferRecordLocked(buffer *buf.Buffer) *buf.Buffer { - dataLen := buffer.Len() - header := buffer.ExtendHeader(snell.HeaderCipherLen) - w.sealHeader(header, dataLen) - payloadCipher := buffer.From(snell.HeaderCipherLen) - buffer.Extend(snell.AEADTagLen) - w.cipher.Seal(payloadCipher[:0], w.nonce, payloadCipher[:dataLen], nil) - snell.IncreaseNonce(w.nonce) - return buffer -} - -func (w *unshapedWriter) WriteBuffer(buffer *buf.Buffer) error { - defer buffer.Release() - dataLen := buffer.Len() - if dataLen == 0 { - return nil - } - if dataLen > maxPayload { - return common.Error(w.Write(buffer.Bytes())) - } - w.access.Lock() - defer w.access.Unlock() - w.makeBufferRecordLocked(buffer) - return common.Error(w.upstream.Write(buffer.Bytes())) -} - -func (w *unshapedWriter) WritePacketBuffer(buffer *buf.Buffer) error { - if buffer.Len() > maxPayload { - buffer.Release() - return snell.ErrPayloadTooLarge - } - return w.WriteBuffer(buffer) -} - -func (w *unshapedWriter) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - return nil, false -} - -func (w *unshapedWriter) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { - return nil -} - -type vectorisedUnshapedWriter struct { - writer *unshapedWriter - upstream N.VectorisedWriter -} - -func (w *vectorisedUnshapedWriter) WriteVectorised(buffers []*buf.Buffer) error { - var records []*buf.Buffer - defer func() { - buf.ReleaseMulti(records) - }() - recordWriter := w.writer - recordWriter.access.Lock() - defer recordWriter.access.Unlock() - for _, buffer := range buffers { - dataLen := buffer.Len() - if dataLen == 0 { - buffer.Release() - continue - } - for data := buffer.Bytes(); len(data) > 0; { - if len(data) == dataLen && dataLen <= maxPayload { - record := recordWriter.makeBufferRecordLocked(buffer) - buffer = nil - records = append(records, record) - break - } - recordLen := min(len(data), maxPayload) - records = append(records, recordWriter.makeSliceRecordLocked(data[:recordLen])) - data = data[recordLen:] - } - if buffer != nil { - buffer.Release() - } - } - if len(records) == 0 { - return nil - } - flushRecords := records - records = nil - return w.upstream.WriteVectorised(flushRecords) -} - -func (w *unshapedWriter) CreatePacketVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { - return &packetVectorisedUnshapedWriter{writer: w, upstream: upstream} -} - -type packetVectorisedUnshapedWriter struct { - writer *unshapedWriter - upstream N.VectorisedWriter -} - -func (w *packetVectorisedUnshapedWriter) WriteVectorised(buffers []*buf.Buffer) error { - var records []*buf.Buffer - defer func() { - buf.ReleaseMulti(records) - }() - recordWriter := w.writer - recordWriter.access.Lock() - defer recordWriter.access.Unlock() - for index, buffer := range buffers { - if buffer.IsEmpty() { - buffer.Release() - continue - } - if buffer.Len() > maxPayload { - buffer.Release() - buf.ReleaseMulti(buffers[index+1:]) - return snell.ErrPayloadTooLarge - } - record := recordWriter.makeBufferRecordLocked(buffer) - records = append(records, record) - } - if len(records) == 0 { - return nil - } - flushRecords := records - records = nil - return w.upstream.WriteVectorised(flushRecords) -} - -func (w *unshapedWriter) WriteZeroChunk() error { - buffer := buf.NewSize(snell.HeaderCipherLen) - w.access.Lock() - defer w.access.Unlock() - w.sealHeader(buffer.Extend(snell.HeaderCipherLen), 0) - err := common.Error(w.upstream.Write(buffer.Bytes())) - buffer.Release() - return err -} - -func (w *unshapedWriter) FrontHeadroom() int { - return snell.HeaderCipherLen -} - -func (w *unshapedWriter) RearHeadroom() int { - return snell.AEADTagLen -} - -func (w *unshapedWriter) WriterMTU() int { - return maxPayload -} - -func (w *unshapedWriter) Upstream() any { - return w.upstream -} - -type rawReader struct { - baseReader -} - -func newRawReader(upstream io.Reader) *rawReader { - r := &rawReader{} - r.upstream = upstream - r.readFunc = r.read - return r -} - -func (r *rawReader) read() (*buf.Buffer, error) { - header := buf.NewSize(snell.HeaderPlainLen) - _, err := header.ReadFullFrom(r.upstream, snell.HeaderPlainLen) - if err != nil { - header.Release() - return nil, err - } - paddingLen, payloadLen, err := parseHeader(header.Bytes()) - header.Release() - if err != nil { - return nil, err - } - if paddingLen != 0 { - return nil, errRecordPadding - } - if payloadLen == 0 { - return nil, io.EOF - } - body := buf.NewSize(payloadLen) - _, err = body.ReadFullFrom(r.upstream, payloadLen) - if err != nil { - body.Release() - return nil, err - } - r.readWaitOptions.PostReturn(body) - return body, nil -} - -type rawWriter struct { - upstream io.Writer - access sync.Mutex -} - -func newRawWriter(upstream io.Writer) *rawWriter { - return &rawWriter{upstream: upstream} -} - -func (w *rawWriter) Write(p []byte) (n int, err error) { - if len(p) == 0 { - return 0, nil - } - // snell-server v6.0.0b4: FUN_0013d010 splits one encrypt call at 0xffff and - // reallocs every record into a single output buffer written to the socket once. - recordCount := (len(p) + maxPayload - 1) / maxPayload - output := buf.NewSize(recordCount*snell.HeaderPlainLen + len(p)) - defer output.Release() - for data := p; len(data) > 0; { - recordLen := min(len(data), maxPayload) - putHeader(output.Extend(snell.HeaderPlainLen), 0, recordLen) - common.Must1(output.Write(data[:recordLen])) - data = data[recordLen:] - } - w.access.Lock() - defer w.access.Unlock() - _, err = w.upstream.Write(output.Bytes()) - if err != nil { - return 0, err - } - return len(p), nil -} - -func (w *rawWriter) makeSliceRecordLocked(payload []byte) *buf.Buffer { - record := buf.NewSize(snell.HeaderPlainLen + len(payload)) - putHeader(record.Extend(snell.HeaderPlainLen), 0, len(payload)) - common.Must1(record.Write(payload)) - return record -} - -func (w *rawWriter) makeBufferRecordLocked(buffer *buf.Buffer) *buf.Buffer { - dataLen := buffer.Len() - putHeader(buffer.ExtendHeader(snell.HeaderPlainLen), 0, dataLen) - return buffer -} - -func (w *rawWriter) WriteBuffer(buffer *buf.Buffer) error { - defer buffer.Release() - dataLen := buffer.Len() - if dataLen == 0 { - return nil - } - if dataLen > maxPayload { - return common.Error(w.Write(buffer.Bytes())) - } - w.access.Lock() - defer w.access.Unlock() - w.makeBufferRecordLocked(buffer) - return common.Error(w.upstream.Write(buffer.Bytes())) -} - -func (w *rawWriter) WritePacketBuffer(buffer *buf.Buffer) error { - if buffer.Len() > maxPayload { - buffer.Release() - return snell.ErrPayloadTooLarge - } - return w.WriteBuffer(buffer) -} - -func (w *rawWriter) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - return nil, false -} - -func (w *rawWriter) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { - return nil -} - -type vectorisedRawWriter struct { - writer *rawWriter - upstream N.VectorisedWriter -} - -func (w *vectorisedRawWriter) WriteVectorised(buffers []*buf.Buffer) error { - var records []*buf.Buffer - defer func() { - buf.ReleaseMulti(records) - }() - recordWriter := w.writer - recordWriter.access.Lock() - defer recordWriter.access.Unlock() - for _, buffer := range buffers { - dataLen := buffer.Len() - if dataLen == 0 { - buffer.Release() - continue - } - for data := buffer.Bytes(); len(data) > 0; { - if len(data) == dataLen && dataLen <= maxPayload { - record := recordWriter.makeBufferRecordLocked(buffer) - buffer = nil - records = append(records, record) - break - } - recordLen := min(len(data), maxPayload) - records = append(records, recordWriter.makeSliceRecordLocked(data[:recordLen])) - data = data[recordLen:] - } - if buffer != nil { - buffer.Release() - } - } - if len(records) == 0 { - return nil - } - flushRecords := records - records = nil - return w.upstream.WriteVectorised(flushRecords) -} - -func (w *rawWriter) CreatePacketVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { - return &packetVectorisedRawWriter{writer: w, upstream: upstream} -} - -type packetVectorisedRawWriter struct { - writer *rawWriter - upstream N.VectorisedWriter -} - -func (w *packetVectorisedRawWriter) WriteVectorised(buffers []*buf.Buffer) error { - var records []*buf.Buffer - defer func() { - buf.ReleaseMulti(records) - }() - recordWriter := w.writer - recordWriter.access.Lock() - defer recordWriter.access.Unlock() - for index, buffer := range buffers { - if buffer.IsEmpty() { - buffer.Release() - continue - } - if buffer.Len() > maxPayload { - buffer.Release() - buf.ReleaseMulti(buffers[index+1:]) - return snell.ErrPayloadTooLarge - } - record := recordWriter.makeBufferRecordLocked(buffer) - records = append(records, record) - } - if len(records) == 0 { - return nil - } - flushRecords := records - records = nil - return w.upstream.WriteVectorised(flushRecords) -} - -func (w *rawWriter) WriteZeroChunk() error { - buffer := buf.NewSize(snell.HeaderPlainLen) - putHeader(buffer.Extend(snell.HeaderPlainLen), 0, 0) - w.access.Lock() - defer w.access.Unlock() - err := common.Error(w.upstream.Write(buffer.Bytes())) - buffer.Release() - return err -} - -func (w *rawWriter) FrontHeadroom() int { - return snell.HeaderPlainLen -} - -func (w *rawWriter) RearHeadroom() int { - return 0 -} - -func (w *rawWriter) WriterMTU() int { - return maxPayload -} - -func (w *rawWriter) Upstream() any { - return w.upstream -} - -var ( - _ N.ExtendedReader = (*unshapedReader)(nil) - _ N.ReadWaiter = (*unshapedReader)(nil) - _ N.ExtendedWriter = (*unshapedWriter)(nil) - _ N.VectorisedWriter = (*vectorisedUnshapedWriter)(nil) - _ N.VectorisedWriter = (*packetVectorisedUnshapedWriter)(nil) - _ N.FrontHeadroom = (*unshapedWriter)(nil) - _ N.ExtendedReader = (*rawReader)(nil) - _ N.ReadWaiter = (*rawReader)(nil) - _ N.ExtendedWriter = (*rawWriter)(nil) - _ N.VectorisedWriter = (*vectorisedRawWriter)(nil) - _ N.VectorisedWriter = (*packetVectorisedRawWriter)(nil) - _ N.WriterWithMTU = (*unshapedWriter)(nil) - _ N.WriterWithMTU = (*rawWriter)(nil) - _ N.WriterWithMTU = (*shapedWriter)(nil) -) diff --git a/proxy/snell/internal/singsnell/snellv6/reuse.go b/proxy/snell/internal/singsnell/snellv6/reuse.go deleted file mode 100644 index d0091b56528..00000000000 --- a/proxy/snell/internal/singsnell/snellv6/reuse.go +++ /dev/null @@ -1,490 +0,0 @@ -package snellv6 - -import ( - "context" - "errors" - "io" - "net" - "sync" - "sync/atomic" - "time" - - snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" - "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/internal/reuse" - "github.com/sagernet/sing/common/buf" - E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -const ( - remoteEOFCode = reuse.RemoteEOFCode - remoteEOFMessage = reuse.RemoteEOFMessage -) - -func (c *Client) DialContext(ctx context.Context, destination M.Socksaddr) (net.Conn, error) { - if c.reuse { - session, err := c.reuseSession(ctx) - if err != nil { - return nil, err - } - conn, err := session.DialConn(destination) - if err != nil { - session.Close() - return nil, err - } - return conn, nil - } - if c.pool.IsClosed() { - return nil, net.ErrClosed - } - if c.dialer == nil { - return nil, E.New("snell: missing dialer") - } - conn, err := c.dialer.DialContext(ctx, N.NetworkTCP, c.server) - if err != nil { - return nil, err - } - proxyConn, err := c.DialConn(conn, destination) - if err != nil { - conn.Close() - return nil, err - } - return proxyConn, nil -} - -func (c *Client) reuseSession(ctx context.Context) (*reuseSession, error) { - session, found, closed := c.pool.Take() - if closed { - return nil, net.ErrClosed - } - if c.dialer == nil { - return nil, E.New("snell: missing dialer") - } - if found { - return session, nil - } - conn, err := c.dialer.DialContext(ctx, N.NetworkTCP, c.server) - if err != nil { - return nil, err - } - if c.pool.IsClosed() { - conn.Close() - return nil, net.ErrClosed - } - session = c.newReuseSession(conn) - session.state.Store(uint32(reuse.StateActive)) - return session, nil -} - -func (c *Client) Close() error { - return c.pool.Close() -} - -type reuseSession struct { - net.Conn - client *Client - - state atomic.Uint32 - reader reuse.RecordReader - writer reuse.RecordWriter -} - -func (c *Client) newReuseSession(conn net.Conn) *reuseSession { - return &reuseSession{Conn: conn, client: c} -} - -func (s *reuseSession) ReuseState() *atomic.Uint32 { - return &s.state -} - -func (s *reuseSession) DialConn(destination M.Socksaddr) (net.Conn, error) { - state := reuse.State(s.state.Load()) - if state == reuse.StateClosed { - return nil, net.ErrClosed - } - if state != reuse.StateActive { - return nil, E.New("snell: reuse session is busy") - } - - requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: s.client.userKey, Destination: destination} - request := buf.NewSize(requestPayload.Len()) - err := requestPayload.Write(request) - if err != nil { - request.Release() - s.Release(false) - return nil, err - } - if s.writer == nil { - s.writer, err = writeFirstRecord(s.Conn, s.client.mode, s.client.psk, s.client.profile, request.Bytes()) - } else { - _, err = s.writer.Write(request.Bytes()) - } - request.Release() - if err != nil { - s.Release(false) - return nil, E.Cause(err, "write request") - } - return &reuseConn{Conn: s.Conn, session: s, destination: destination}, nil -} - -func (s *reuseSession) Release(reusable bool) { - if !reusable { - s.Close() - return - } - if !s.state.CompareAndSwap(uint32(reuse.StateActive), uint32(reuse.StateReady)) { - if reuse.State(s.state.Load()) != reuse.StateClosed { - s.Close() - } - return - } - s.client.pool.MoveToPool(s, reuse.StateReady, false) -} - -func (s *reuseSession) Close() error { - if reuse.State(s.state.Swap(uint32(reuse.StateClosed))) == reuse.StateClosed { - return nil - } - return s.Conn.Close() -} - -func (s *reuseSession) startDrain() { - if !s.state.CompareAndSwap(uint32(reuse.StateActive), uint32(reuse.StateWaiting)) { - if reuse.State(s.state.Load()) != reuse.StateClosed { - s.Close() - } - return - } - if !s.client.pool.MoveToPool(s, reuse.StateWaiting, true) { - return - } - go s.drain() -} - -func (s *reuseSession) drain() { - defer s.client.pool.DrainDone() - var discarded int - for { - record, err := s.reader.NextRecord() - if errors.Is(err, io.EOF) { - s.state.CompareAndSwap(uint32(reuse.StateWaiting), uint32(reuse.StateReady)) - return - } - if err != nil { - s.Close() - return - } - // Surge 6.7.0 (11520): SNConnectorV4::socket:didReadData: discards data - // received while waiting for server EOF and closes after 0x80001 bytes. - discarded += record.Len() - record.Release() - if discarded >= reuse.WaitingDiscardLimit { - s.Close() - return - } - } -} - -type reuseConn struct { - net.Conn - session *reuseSession - destination M.Socksaddr - - closeWriteOnce sync.Once - closeWriteErr error - closeOnce sync.Once - closeErr error - replyRead atomic.Bool - readWaitOptions N.ReadWaitOptions - closed atomic.Bool - readActionCount atomic.Int32 - readClosed atomic.Bool -} - -func (c *reuseConn) readResponse() error { - if c.replyRead.Load() { - return nil - } - var record *buf.Buffer - var err error - if c.session.reader == nil { - c.session.reader, record, err = readFirstRecord(c.session.Conn, c.session.client.mode, c.session.client.psk, c.session.client.profile, c.readWaitOptions) - } else { - record, err = c.session.reader.ReadRecord() - } - if err != nil { - return E.Cause(err, "read reply") - } - cached, err := reuse.ParseReply(record) - if err != nil { - return err - } - c.session.reader.SetCache(cached) - c.replyRead.Store(true) - return nil -} - -func (c *reuseConn) Read(p []byte) (int, error) { - if c.closed.Load() { - return 0, net.ErrClosed - } - c.readActionCount.Add(1) - defer c.readActionCount.Add(-1) - if c.closed.Load() { - return 0, net.ErrClosed - } - err := c.readResponse() - if err != nil { - return 0, err - } - var discarded int - for { - n, err := c.session.reader.Read(p) - if c.closed.Load() { - discarded += n - if errors.Is(err, io.EOF) { - c.readClosed.Store(true) - c.session.state.CompareAndSwap(uint32(reuse.StateWaiting), uint32(reuse.StateReady)) - return 0, err - } - if err != nil { - c.session.Close() - return 0, err - } - if discarded >= reuse.WaitingDiscardLimit { - c.session.Close() - return 0, E.New("snell: read too much data after client EOF") - } - continue - } - if errors.Is(err, io.EOF) { - c.readClosed.Store(true) - } - return n, err - } -} - -func (c *reuseConn) ReadBuffer(buffer *buf.Buffer) error { - if c.closed.Load() { - return net.ErrClosed - } - c.readActionCount.Add(1) - defer c.readActionCount.Add(-1) - if c.closed.Load() { - return net.ErrClosed - } - err := c.readResponse() - if err != nil { - return err - } - var discarded int - for { - bufferLen := buffer.Len() - err = c.session.reader.ReadBuffer(buffer) - if c.closed.Load() { - discarded += buffer.Len() - bufferLen - buffer.Truncate(bufferLen) - if errors.Is(err, io.EOF) { - c.readClosed.Store(true) - c.session.state.CompareAndSwap(uint32(reuse.StateWaiting), uint32(reuse.StateReady)) - return err - } - if err != nil { - c.session.Close() - return err - } - if discarded >= reuse.WaitingDiscardLimit { - c.session.Close() - return E.New("snell: read too much data after client EOF") - } - continue - } - if errors.Is(err, io.EOF) { - c.readClosed.Store(true) - } - return err - } -} - -func (c *reuseConn) Write(p []byte) (int, error) { - return c.session.writer.Write(p) -} - -func (c *reuseConn) WriteBuffer(buffer *buf.Buffer) error { - return c.session.writer.WriteBuffer(buffer) -} - -func (c *reuseConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - return nil, false -} -func (c *reuseConn) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { - return nil -} - - -func (c *reuseConn) CloseWrite() error { - c.closeWriteOnce.Do(func() { - c.closeWriteErr = c.session.writer.WriteZeroChunk() - }) - return c.closeWriteErr -} - -func (c *reuseConn) Close() error { - c.closeOnce.Do(func() { - c.closed.Store(true) - if !c.replyRead.Load() { - c.session.Release(false) - return - } - c.closeErr = c.CloseWrite() - if c.closeErr != nil { - c.session.Release(false) - return - } - c.session.Conn.SetReadDeadline(time.Time{}) - c.session.Conn.SetWriteDeadline(time.Time{}) - if c.readClosed.Load() { - c.session.Release(true) - return - } - // Surge 6.7.0 (11520): SNConnectorV4::readServerEOFIfNotInReadState: starts waiting-state EOF - // reads when _socketReadActionCounter is 0. - if c.readActionCount.Load() > 0 { - if !c.session.state.CompareAndSwap(uint32(reuse.StateActive), uint32(reuse.StateWaiting)) { - if reuse.State(c.session.state.Load()) != reuse.StateClosed { - c.session.Close() - } - return - } - poolState := reuse.StateWaiting - if c.readClosed.Load() { - c.session.state.CompareAndSwap(uint32(reuse.StateWaiting), uint32(reuse.StateReady)) - poolState = reuse.StateReady - } - if !c.session.client.pool.MoveToPool(c.session, poolState, false) { - return - } - return - } - c.session.startDrain() - }) - return c.closeErr -} - -func (c *reuseConn) CreateReadWaiter() (N.ReadWaiter, bool) { - return &reuseReadWaiter{conn: c}, true -} - -type reuseReadWaiter struct { - conn *reuseConn -} - -func (w *reuseReadWaiter) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { - w.conn.readWaitOptions = options - if w.conn.session.reader != nil { - w.conn.session.reader.InitializeReadWaiter(options) - } - return false -} - -func (w *reuseReadWaiter) WaitReadBuffer() (*buf.Buffer, error) { - if w.conn.closed.Load() { - return nil, net.ErrClosed - } - w.conn.readActionCount.Add(1) - defer w.conn.readActionCount.Add(-1) - if w.conn.closed.Load() { - return nil, net.ErrClosed - } - err := w.conn.readResponse() - if err != nil { - return nil, err - } - var discarded int - for { - buffer, err := w.conn.session.reader.WaitReadBuffer() - if w.conn.closed.Load() { - if buffer != nil { - discarded += buffer.Len() - buffer.Release() - } - if errors.Is(err, io.EOF) { - w.conn.readClosed.Store(true) - w.conn.session.state.CompareAndSwap(uint32(reuse.StateWaiting), uint32(reuse.StateReady)) - return nil, err - } - if err != nil { - w.conn.session.Close() - return nil, err - } - if discarded >= reuse.WaitingDiscardLimit { - w.conn.session.Close() - return nil, E.New("snell: read too much data after client EOF") - } - continue - } - if errors.Is(err, io.EOF) { - w.conn.readClosed.Store(true) - } - return buffer, err - } -} - -func (c *reuseConn) FrontHeadroom() int { - return c.session.writer.FrontHeadroom() -} - -func (c *reuseConn) RearHeadroom() int { - return c.session.writer.RearHeadroom() -} - -func (c *reuseConn) WriterMTU() int { - return c.session.writer.WriterMTU() -} - -func (c *reuseConn) NeedHandshakeForRead() bool { - return !c.replyRead.Load() -} - -func (c *reuseConn) NeedAdditionalReadDeadline() bool { - return !c.replyRead.Load() -} - -func (c *reuseConn) Upstream() any { - return c.session.Conn -} - -func (c *reuseConn) RemoteAddr() net.Addr { - return c.destination.TCPAddr() -} - - - - - - - - - - - - - - - - - - - - - - - -var ( - _ N.ExtendedConn = (*reuseConn)(nil) - _ N.ReadWaitCreator = (*reuseConn)(nil) - _ N.EarlyReader = (*reuseConn)(nil) - _ N.WriteCloser = (*reuseConn)(nil) - _ N.ReadWaiter = (*reuseReadWaiter)(nil) -) diff --git a/proxy/snell/internal/singsnell/snellv6/salt.go b/proxy/snell/internal/singsnell/snellv6/salt.go deleted file mode 100644 index 106852bb408..00000000000 --- a/proxy/snell/internal/singsnell/snellv6/salt.go +++ /dev/null @@ -1,56 +0,0 @@ -package snellv6 - -// Surge 6.7.0 (11520): FUN_100015274/FUN_1000140bc: shuffles and masks v6 default-mode salts with this namespace xor. -const saltNSXor = 0xdaa66d2c7ddf743f - -func saltShufflePRF(nsSalt uint64, domain uint32, index uint32) uint32 { - rdx := uint64(index)*coefB + addB - rdi := nsSalt ^ saltNSXor - rsi := uint64(domain)*coefA + addA - y := splitmix64((rdx ^ rdi) ^ rsi) - return uint32(y ^ (y >> 32)) -} - -func shufflePerm(nsSalt uint64, rounds uint8, length int) []byte { - out := make([]byte, length) - for index := range out { - out[index] = byte(index) - } - if length == 0 { - return out - } - if rounds == 0 { - rounds = 1 - } - for round := uint32(0); round < uint32(rounds); round++ { - domain := uint32(mixHandshakeDomain) + round - for i := range length { - span := uint64(length - i) - raw := uint64(saltShufflePRF(nsSalt, domain, uint32(i))) - j := i + int(raw%span) - out[i], out[j] = out[j], out[i] - } - } - return out -} - -func saltMask(nsSalt uint64, mixStride uint8, index uint32) byte { - prf := prf32Fold(nsSalt, labelMotif, uint64(mixHandshakeDomain), uint64(index)) - return byte(index)*mixStride ^ byte(prf) -} - -func (p *Profile) extractSalt(block []byte) [saltLen]byte { - perm := shufflePerm(p.namespaces.salt, byte(p.mixRoundsHandshake), len(block)) - var out [saltLen]byte - for i := range saltLen { - out[i] = saltMask(p.namespaces.salt, byte(p.mixStrideHandshake), uint32(i)) ^ block[perm[i]] - } - return out -} - -func (p *Profile) writeSaltBlock(salt []byte, block []byte) { - perm := shufflePerm(p.namespaces.salt, byte(p.mixRoundsHandshake), len(block)) - for i := range saltLen { - block[perm[i]] = saltMask(p.namespaces.salt, byte(p.mixStrideHandshake), uint32(i)) ^ salt[i] - } -} diff --git a/proxy/snell/internal/singsnell/snellv6/shaped.go b/proxy/snell/internal/singsnell/snellv6/shaped.go deleted file mode 100644 index 8b9a094d6cd..00000000000 --- a/proxy/snell/internal/singsnell/snellv6/shaped.go +++ /dev/null @@ -1,484 +0,0 @@ -package snellv6 - -import ( - "crypto/cipher" - "encoding/binary" - "io" - "sync" - "time" - - snell "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" - "github.com/sagernet/sing/common/buf" - E "github.com/sagernet/sing/common/exceptions" - N "github.com/sagernet/sing/common/network" -) - -type shapedWriter struct { - upstream io.Writer - profile *Profile - cipher cipher.AEAD - salt []byte - nonce []byte - seq uint32 - saltSent bool - chunkSize int - lastWriteUnix int64 - access sync.Mutex -} - -func newShapedWriter(upstream io.Writer, profile *Profile, salt []byte, aead cipher.AEAD, nonce []byte) *shapedWriter { - return &shapedWriter{upstream: upstream, profile: profile, cipher: aead, salt: salt, nonce: nonce} -} - -func (w *shapedWriter) makeSliceRecord(payload []byte) *buf.Buffer { - prefixLen := w.profile.recordPrefixLen(w.seq) - saltBlockLen := 0 - saltPrefixLen := 0 - if !w.saltSent { - saltBlockLen = w.profile.saltBlockLen - saltPrefixLen = saltBlockLen - saltLen - } - paddingLen := w.profile.paddingLen(w.seq, len(payload), prefixLen, saltPrefixLen, saltBlockLen) - payloadCipherLen := 0 - if len(payload) > 0 { - payloadCipherLen = len(payload) + snell.AEADTagLen - } - out := buf.NewSize(saltBlockLen + prefixLen + snell.HeaderCipherLen + paddingLen + payloadCipherLen) - - if saltBlockLen > 0 { - block := out.Extend(saltBlockLen) - // Surge 6.7.0 (11520): FUN_100014610: fills the salt block with shaped padding - // at sentinel sequence 0xffffffff before overwriting the salt positions. - w.profile.fillPadding(0xffffffff, block) - w.profile.writeSaltBlock(w.salt, block) - w.saltSent = true - } - prefix := out.Extend(prefixLen) - w.profile.fillPadding(w.seq, prefix) - - header := out.Extend(snell.HeaderCipherLen) - putHeader(header, paddingLen, len(payload)) - w.cipher.Seal(header[:0], w.nonce, header[:snell.HeaderPlainLen], prefix) - snell.IncreaseNonce(w.nonce) - - padding := out.Extend(paddingLen) - w.profile.fillPadding(w.seq, padding) - if len(payload) > 0 { - region := out.Extend(payloadCipherLen) - copy(region, payload) - w.cipher.Seal(region[:0], w.nonce, region[:len(payload)], padding) - snell.IncreaseNonce(w.nonce) - w.profile.mixPaddingPayload(w.seq, padding, region) - } - w.seq++ - return out -} - -func (w *shapedWriter) makeBufferRecord(buffer *buf.Buffer) *buf.Buffer { - dataLen := buffer.Len() - prefixLen := w.profile.recordPrefixLen(w.seq) - saltBlockLen := 0 - saltPrefixLen := 0 - if !w.saltSent { - saltBlockLen = w.profile.saltBlockLen - saltPrefixLen = saltBlockLen - saltLen - } - paddingLen := w.profile.paddingLen(w.seq, dataLen, prefixLen, saltPrefixLen, saltBlockLen) - frontLen := saltBlockLen + prefixLen + snell.HeaderCipherLen + paddingLen - front := buffer.ExtendHeader(frontLen) - if saltBlockLen > 0 { - block := front[:saltBlockLen] - // Surge 6.7.0 (11520): FUN_100014610: fills the salt block with shaped padding - // at sentinel sequence 0xffffffff before overwriting the salt positions. - w.profile.fillPadding(0xffffffff, block) - w.profile.writeSaltBlock(w.salt, block) - w.saltSent = true - } - prefix := front[saltBlockLen : saltBlockLen+prefixLen] - w.profile.fillPadding(w.seq, prefix) - - header := front[saltBlockLen+prefixLen : saltBlockLen+prefixLen+snell.HeaderCipherLen] - putHeader(header, paddingLen, dataLen) - w.cipher.Seal(header[:0], w.nonce, header[:snell.HeaderPlainLen], prefix) - snell.IncreaseNonce(w.nonce) - - padding := front[saltBlockLen+prefixLen+snell.HeaderCipherLen:] - w.profile.fillPadding(w.seq, padding) - if dataLen > 0 { - region := buffer.From(frontLen) - buffer.Extend(snell.AEADTagLen) - w.cipher.Seal(region[:0], w.nonce, region[:dataLen], padding) - snell.IncreaseNonce(w.nonce) - region = buffer.From(frontLen) - w.profile.mixPaddingPayload(w.seq, padding, region) - } - w.seq++ - return buffer -} - -func (w *shapedWriter) writeRecords(records []*buf.Buffer) error { - if len(records) == 1 { - _, err := w.upstream.Write(records[0].Bytes()) - return err - } - out := buf.NewSize(buf.LenMulti(records)) - defer out.Release() - for _, record := range records { - copy(out.Extend(record.Len()), record.Bytes()) - } - _, err := w.upstream.Write(out.Bytes()) - return err -} - -func (w *shapedWriter) payloadLimitFor(now time.Time) int { - nowUnix := now.Unix() - // Surge 6.7.0 (11520): FUN_1000142d4/FUN_100014610: v6 writer idle reset is second-granular. - if w.lastWriteUnix == 0 || nowUnix-w.lastWriteUnix > int64(w.profile.idleResetSec) { - w.chunkSize = w.profile.chunkInitial - } - if w.chunkSize == 0 { - w.chunkSize = w.profile.chunkInitial - } - payloadLimit := w.profile.chunkPayloadLimit(w.seq, w.chunkSize) - if w.seq == 0 { - payloadLimit = min(payloadLimit, w.profile.firstRecordCap) - } - w.chunkSize = w.profile.nextChunkSize(w.chunkSize) - w.lastWriteUnix = nowUnix - return max(1, min(payloadLimit, maxPayload)) -} - -func (w *shapedWriter) Write(p []byte) (n int, err error) { - w.access.Lock() - defer w.access.Unlock() - now := time.Now() - originalLen := len(p) - var records []*buf.Buffer - defer func() { - buf.ReleaseMulti(records) - }() - for len(p) > 0 { - payloadLimit := w.payloadLimitFor(now) - recordLen := min(len(p), payloadLimit) - records = append(records, w.makeSliceRecord(p[:recordLen])) - p = p[recordLen:] - } - if len(records) == 0 { - return 0, nil - } - err = w.writeRecords(records) - if err != nil { - return 0, err - } - n = originalLen - return -} - -func (w *shapedWriter) WriteBuffer(buffer *buf.Buffer) error { - defer buffer.Release() - dataLen := buffer.Len() - if dataLen == 0 { - return nil - } - w.access.Lock() - defer w.access.Unlock() - now := time.Now() - payloadLimit := w.payloadLimitFor(now) - if dataLen <= payloadLimit { - w.makeBufferRecord(buffer) - _, err := w.upstream.Write(buffer.Bytes()) - return err - } - var records []*buf.Buffer - defer func() { - buf.ReleaseMulti(records) - }() - for data := buffer.Bytes(); len(data) > 0; { - recordLen := min(len(data), payloadLimit) - records = append(records, w.makeSliceRecord(data[:recordLen])) - data = data[recordLen:] - if len(data) > 0 { - payloadLimit = w.payloadLimitFor(now) - } - } - return w.writeRecords(records) -} - -func (w *shapedWriter) WritePacketBuffer(buffer *buf.Buffer) error { - dataLen := buffer.Len() - if dataLen == 0 { - buffer.Release() - return nil - } - w.access.Lock() - defer w.access.Unlock() - nowUnix := time.Now().Unix() - chunkSize := w.chunkSize - if w.lastWriteUnix == 0 || nowUnix-w.lastWriteUnix > int64(w.profile.idleResetSec) { - chunkSize = w.profile.chunkInitial - } - if chunkSize == 0 { - chunkSize = w.profile.chunkInitial - } - payloadLimit := w.profile.chunkPayloadLimit(w.seq, chunkSize) - if w.seq == 0 { - payloadLimit = min(payloadLimit, w.profile.firstRecordCap) - } - payloadLimit = max(1, min(payloadLimit, maxPayload)) - if dataLen > payloadLimit { - buffer.Release() - return snell.ErrPayloadTooLarge - } - w.chunkSize = w.profile.nextChunkSize(chunkSize) - w.lastWriteUnix = nowUnix - record := w.makeBufferRecord(buffer) - _, err := w.upstream.Write(record.Bytes()) - record.Release() - return err -} - -func (w *shapedWriter) WriteZeroChunk() error { - w.access.Lock() - defer w.access.Unlock() - w.payloadLimitFor(time.Now()) - record := w.makeSliceRecord(nil) - defer record.Release() - _, err := w.upstream.Write(record.Bytes()) - return err -} - -func (w *shapedWriter) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - return nil, false -} - -func (w *shapedWriter) CreateVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { - return nil -} - -type vectorisedShapedWriter struct { - writer *shapedWriter - upstream N.VectorisedWriter -} - -func (w *vectorisedShapedWriter) WriteVectorised(buffers []*buf.Buffer) error { - var records []*buf.Buffer - defer func() { - buf.ReleaseMulti(records) - }() - recordWriter := w.writer - recordWriter.access.Lock() - defer recordWriter.access.Unlock() - for _, buffer := range buffers { - dataLen := buffer.Len() - if dataLen == 0 { - buffer.Release() - continue - } - for data := buffer.Bytes(); len(data) > 0; { - payloadLimit := recordWriter.payloadLimitFor(time.Now()) - recordLen := min(len(data), payloadLimit) - if len(data) == dataLen && dataLen <= payloadLimit { - record := recordWriter.makeBufferRecord(buffer) - buffer = nil - records = append(records, record) - break - } - records = append(records, recordWriter.makeSliceRecord(data[:recordLen])) - data = data[recordLen:] - } - if buffer != nil { - buffer.Release() - } - } - if len(records) == 0 { - return nil - } - flushRecords := records - records = nil - return w.upstream.WriteVectorised(flushRecords) -} - -var _ N.VectorisedWriter = (*vectorisedShapedWriter)(nil) - -func (w *shapedWriter) CreatePacketVectorisedWriterFor(upstream N.VectorisedWriter) N.VectorisedWriter { - return &packetVectorisedShapedWriter{writer: w, upstream: upstream} -} - -type packetVectorisedShapedWriter struct { - writer *shapedWriter - upstream N.VectorisedWriter -} - -func (w *packetVectorisedShapedWriter) WriteVectorised(buffers []*buf.Buffer) error { - var records []*buf.Buffer - defer func() { - buf.ReleaseMulti(records) - }() - recordWriter := w.writer - recordWriter.access.Lock() - defer recordWriter.access.Unlock() - for index, buffer := range buffers { - if buffer.IsEmpty() { - buffer.Release() - continue - } - dataLen := buffer.Len() - nowUnix := time.Now().Unix() - chunkSize := recordWriter.chunkSize - if recordWriter.lastWriteUnix == 0 || nowUnix-recordWriter.lastWriteUnix > int64(recordWriter.profile.idleResetSec) { - chunkSize = recordWriter.profile.chunkInitial - } - if chunkSize == 0 { - chunkSize = recordWriter.profile.chunkInitial - } - payloadLimit := recordWriter.profile.chunkPayloadLimit(recordWriter.seq, chunkSize) - if recordWriter.seq == 0 { - payloadLimit = min(payloadLimit, recordWriter.profile.firstRecordCap) - } - payloadLimit = max(1, min(payloadLimit, maxPayload)) - if dataLen > payloadLimit { - buffer.Release() - buf.ReleaseMulti(buffers[index+1:]) - return snell.ErrPayloadTooLarge - } - recordWriter.chunkSize = recordWriter.profile.nextChunkSize(chunkSize) - recordWriter.lastWriteUnix = nowUnix - record := recordWriter.makeBufferRecord(buffer) - records = append(records, record) - } - if len(records) == 0 { - return nil - } - flushRecords := records - records = nil - return w.upstream.WriteVectorised(flushRecords) -} - -var _ N.VectorisedWriter = (*packetVectorisedShapedWriter)(nil) - -func (w *shapedWriter) FrontHeadroom() int { - headroom := w.profile.recordPrefixMax + snell.HeaderCipherLen + w.profile.padMaxHeadroom - if !w.saltSent { - headroom += w.profile.saltBlockLen - } - return headroom -} - -func (w *shapedWriter) RearHeadroom() int { - return snell.AEADTagLen -} - -func (w *shapedWriter) WriterMTU() int { - return w.profile.chunkMax -} - -func (w *shapedWriter) Upstream() any { - return w.upstream -} - -type shapedReader struct { - baseReader - psk []byte - profile *Profile - cipher cipher.AEAD - nonce []byte - seq uint32 -} - -func newShapedReader(upstream io.Reader, psk []byte, profile *Profile) *shapedReader { - r := &shapedReader{psk: psk, profile: profile, nonce: make([]byte, snell.NonceLen)} - r.upstream = upstream - r.readFunc = r.read - return r -} - -func (r *shapedReader) read() (*buf.Buffer, error) { - if r.cipher == nil { - block := make([]byte, r.profile.saltBlockLen) - _, err := io.ReadFull(r.upstream, block) - if err != nil { - return nil, err - } - salt := r.profile.extractSalt(block) - aead, err := snell.NewAEAD(snell.DeriveKey(r.psk, salt[:])) - if err != nil { - return nil, err - } - r.cipher = aead - } - - prefixLen := r.profile.recordPrefixLen(r.seq) - head := buf.NewSize(prefixLen + snell.HeaderCipherLen) - _, err := head.ReadFullFrom(r.upstream, prefixLen+snell.HeaderCipherLen) - if err != nil { - head.Release() - return nil, err - } - prefix := head.To(prefixLen) - headerCipher := head.Range(prefixLen, prefixLen+snell.HeaderCipherLen) - _, err = r.cipher.Open(headerCipher[:0], r.nonce, headerCipher, prefix) - if err != nil { - head.Release() - return nil, E.Cause(err, "open shaped header") - } - snell.IncreaseNonce(r.nonce) - if headerCipher[0] != snell.HeaderVersion { - head.Release() - return nil, E.Extend(snell.ErrBadVersion, headerCipher[0]) - } - // Surge 6.7.0 (11520): FUN_100013abc: default-shaped reader ignores the two reserved header bytes. - paddingLen := int(binary.BigEndian.Uint16(headerCipher[3:5])) - payloadLen := int(binary.BigEndian.Uint16(headerCipher[5:7])) - head.Release() - seq := r.seq - r.seq++ - - if payloadLen == 0 { - if paddingLen > 0 { - discard := buf.NewSize(paddingLen) - _, err = discard.ReadFullFrom(r.upstream, paddingLen) - discard.Release() - if err != nil { - return nil, err - } - } - return nil, io.EOF - } - - var padding *buf.Buffer - if paddingLen > 0 { - padding = buf.NewSize(paddingLen) - _, err = padding.ReadFullFrom(r.upstream, paddingLen) - if err != nil { - padding.Release() - return nil, err - } - } - body := buf.NewSize(payloadLen + snell.AEADTagLen) - _, err = body.ReadFullFrom(r.upstream, payloadLen+snell.AEADTagLen) - if err != nil { - if padding != nil { - padding.Release() - } - body.Release() - return nil, err - } - var paddingBytes []byte - if padding != nil { - paddingBytes = padding.Bytes() - } - payloadCipher := body.Bytes() - r.profile.mixPaddingPayload(seq, paddingBytes, payloadCipher) - _, err = r.cipher.Open(payloadCipher[:0], r.nonce, payloadCipher, paddingBytes) - if padding != nil { - padding.Release() - } - if err != nil { - body.Release() - return nil, E.Cause(err, "open shaped payload") - } - snell.IncreaseNonce(r.nonce) - body.Truncate(payloadLen) - r.readWaitOptions.PostReturn(body) - return body, nil -} diff --git a/proxy/snell/outbound.go b/proxy/snell/outbound.go index 98b3e953105..802d11b7ccf 100644 --- a/proxy/snell/outbound.go +++ b/proxy/snell/outbound.go @@ -3,12 +3,13 @@ package snell import ( "context" "net" - "strings" + "net/netip" "sync" - snellproto "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell" - "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/snellv4" - "github.com/exclavenetwork/exclave-core/v5/proxy/snell/internal/singsnell/snellv6" + "github.com/sagernet/sing-snell" + "github.com/sagernet/sing-snell/snellv4" + "github.com/sagernet/sing-snell/snellv6" + "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" @@ -16,12 +17,9 @@ import ( core "github.com/exclavenetwork/exclave-core/v5" "github.com/exclavenetwork/exclave-core/v5/app/proxyman/outbound" "github.com/exclavenetwork/exclave-core/v5/common" - "github.com/exclavenetwork/exclave-core/v5/common/buf" - "github.com/exclavenetwork/exclave-core/v5/common/bytespool" v2net "github.com/exclavenetwork/exclave-core/v5/common/net" "github.com/exclavenetwork/exclave-core/v5/common/session" "github.com/exclavenetwork/exclave-core/v5/common/singbridge" - "github.com/exclavenetwork/exclave-core/v5/proxy" "github.com/exclavenetwork/exclave-core/v5/transport" "github.com/exclavenetwork/exclave-core/v5/transport/internet" ) @@ -32,20 +30,19 @@ func init() { })) } -// snellClient matches SagerNet sing-snell client surface used by sing-box. +// snellClient is the subset of SagerNet sing-snell clients used here. type snellClient interface { DialContext(ctx context.Context, destination M.Socksaddr) (net.Conn, error) DialPacketConn(conn net.Conn) (N.NetPacketConn, error) Close() error } -// Outbound implements Snell v4/v6 client outbound (SagerNet sing-snell). -// Versions 3/5 map onto the v4 client path where supported by the library. +// Outbound is a Snell v4 / v6 client outbound. type Outbound struct { serverAddr v2net.Destination psk string - userKey string - obfs string + userPSK string + obfsMode string obfsHost string version int reuse bool @@ -58,26 +55,47 @@ func NewClient(ctx context.Context, config *ClientConfig) (*Outbound, error) { if config.Address == nil { return nil, newError("missing server address") } - if config.Psk == "" { - return nil, newError("missing psk") - } version := int(config.Version) if version == 0 { version = 4 } - // sing-snell currently exposes dedicated clients for v4 and v6. - // Accept 3/4/5 → v4 client, 6 → v6 client. - if version < 3 || version > 6 { - return nil, newError("unsupported snell version ", version, " (want 3-6)") + if version != 4 && version != 6 { + return nil, newError("unsupported snell version ", version, " (want 4 or 6)") + } + + obfsMode := config.ObfsMode + if obfsMode == "" { + obfsMode = "none" } - obfs := strings.ToLower(config.Obfs) - switch obfs { - case "", "off", "none": - obfs = "none" - case "http", "tls": + switch obfsMode { + case "none", "http", "tls": default: - return nil, newError("invalid snell obfs ", config.Obfs) + return nil, newError(`invalid snell obfsMode `, config.ObfsMode, ` (want "none", "http" or "tls")`) + } + + mode := config.Mode + if version == 6 { + if mode == "" { + mode = "default" + } + switch mode { + case "default", "unshaped", "unsafe-raw": + default: + return nil, newError(`invalid snell mode `, config.Mode) + } + if config.ObfsHost != "" { + return nil, newError("obfsHost is only valid for version 4") + } + if config.ObfsMode != "" && config.ObfsMode != "none" { + return nil, newError("obfsMode is only valid for version 4") + } + obfsMode = "none" + } else if config.Mode != "" { + return nil, newError("mode is only valid for version 6") } + + // Empty PSK is allowed: Snell derives keys via Argon2id and servers may accept empty PSK. + return &Outbound{ serverAddr: v2net.Destination{ Address: config.Address.AsAddress(), @@ -85,11 +103,12 @@ func NewClient(ctx context.Context, config *ClientConfig) (*Outbound, error) { Network: v2net.Network_TCP, }, psk: config.Psk, - obfs: obfs, + userPSK: config.UserPsk, + obfsMode: obfsMode, obfsHost: config.ObfsHost, version: version, reuse: config.Reuse, - mode: config.Mode, + mode: mode, }, nil } @@ -110,13 +129,13 @@ func (o *Outbound) getClient(dialer internet.Dialer) (snellClient, error) { return nil, newError("transport layer enabled") } if streamSettings := handler.StreamSettings(); streamSettings != nil && streamSettings.SecurityType != "" { - return nil, newError("tls/security enabled on streamSettings; snell uses built-in crypto (use obfs=tls for fake TLS)") + return nil, newError("tls/security enabled on streamSettings; snell uses built-in crypto (use obfsMode=tls for fake TLS on v4)") } sdialer := singbridge.NewDialerWrapper(dialer) server := M.ParseSocksaddr(o.serverAddr.NetAddr()) psk := []byte(o.psk) - userKey := []byte(o.userKey) + userKey := []byte(o.userPSK) var client snellClient if o.version == 6 { @@ -137,7 +156,7 @@ func (o *Outbound) getClient(dialer internet.Dialer) (snellClient, error) { } client = c } else { - obfsMode, err := snellproto.ParseObfsMode(o.obfs) + obfs, err := snell.ParseObfsMode(o.obfsMode) if err != nil { return nil, err } @@ -145,7 +164,7 @@ func (o *Outbound) getClient(dialer internet.Dialer) (snellClient, error) { PSK: psk, UserKey: userKey, Reuse: o.reuse, - ObfsMode: obfsMode, + ObfsMode: obfs, ObfsHost: o.obfsHost, Dialer: sdialer, Server: server, @@ -175,37 +194,19 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int " (v", o.version, ")").WriteToLog(session.ExportIDToError(ctx)) detachedCtx := core.ToBackgroundDetachedContext(ctx) - target := singbridge.ToSocksAddr(destination) if destination.Network == v2net.Network_TCP { - serverConn, err := client.DialContext(detachedCtx, target) + // Snell is client-speaks-first (CONNECT header before any application payload). + // No server-speaks-first first-payload handling is required. + serverConn, err := client.DialContext(detachedCtx, singbridge.ToSocksAddr(destination)) if err != nil { return err } - - var firstPayload []byte - if reader, ok := link.Reader.(buf.TimeoutReader); ok { - if mb, _ := reader.ReadMultiBufferTimeout(proxy.FirstPayloadTimeout); mb != nil { - length := mb.Len() - firstPayload = bytespool.Alloc(length) - mb, _ = buf.SplitBytes(mb, firstPayload) - firstPayload = firstPayload[:length] - buf.ReleaseMulti(mb) - } - } - if len(firstPayload) > 0 { - _, err = serverConn.Write(firstPayload) - bytespool.Free(firstPayload) - if err != nil { - _ = serverConn.Close() - return singbridge.ReturnError(err) - } - } - return singbridge.ReturnError(bufio.CopyConn(detachedCtx, singbridge.NewPipeConnWrapper(link), serverConn)) } - // UDP-over-TCP: dial raw TCP to snell server, then DialPacketConn (sing-box pattern). + // UDP-over-TCP. + // Snell UDP framing only carries IPv4/IPv6 (no domain ATYP). Resolve domains before write. raw, err := dialer.Dial(detachedCtx, o.serverAddr) if err != nil { return err @@ -215,13 +216,64 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int _ = raw.Close() return err } + + var resolve domainResolveFunc + if ob.Resolver != nil { + resolve = func(ctx context.Context, domain string) (net.IP, error) { + addr := ob.Resolver(ctx, domain) + if !addr.Family().IsIP() { + return nil, newError("resolver returned non-IP for ", domain) + } + return addr.IP(), nil + } + } else { + resolve = func(ctx context.Context, domain string) (net.IP, error) { + ips, err := net.DefaultResolver.LookupIP(ctx, "ip", domain) + if err != nil { + return nil, err + } + if len(ips) == 0 { + return nil, newError("no IP for domain ", domain) + } + return ips[0], nil + } + } + return singbridge.ReturnError(bufio.CopyPacketConn( detachedCtx, singbridge.NewPacketConnWrapper(link, destination), - bufio.NewPacketConn(pc), + &udpIPOnlyPacketConn{NetPacketConn: pc, ctx: detachedCtx, resolve: resolve}, )) } +type domainResolveFunc func(ctx context.Context, domain string) (net.IP, error) + +// udpIPOnlyPacketConn resolves domain destinations to IP for Snell UDP writes. +// Read path is unchanged: servers reply with IP addresses only. +type udpIPOnlyPacketConn struct { + N.NetPacketConn + ctx context.Context + resolve domainResolveFunc +} + +func (c *udpIPOnlyPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { + // Snell UDP address encoding only supports IPv4/IPv6, not domain names. + if destination.IsDomain() { + ip, err := c.resolve(c.ctx, destination.Fqdn) + if err != nil { + buffer.Release() + return err + } + addr, ok := netip.AddrFromSlice(ip) + if !ok { + buffer.Release() + return newError("invalid IP for domain ", destination.Fqdn) + } + destination = M.SocksaddrFrom(addr, destination.Port) + } + return c.NetPacketConn.WritePacket(buffer, destination) +} + func (o *Outbound) InterfaceUpdate() { _ = o.Close() } From 6c3f90c3975e3855e3ac3e19453df6ca791f86e4 Mon Sep 17 00:00:00 2001 From: dyhkwong <50692134+dyhkwong@users.noreply.github.com> Date: Sat, 11 Jul 2026 15:52:42 +0800 Subject: [PATCH 15/15] update Todo: UDP domain address --- common/singbridge/tls.go | 8 +- go.mod | 2 +- go.sum | 6 +- infra/conf/v4/snell.go | 87 +++----------- proxy/snell/NOTICE | 2 - proxy/snell/config.pb.go | 69 +++++------- proxy/snell/config.proto | 17 +-- proxy/snell/outbound.go | 238 ++++++++++++++------------------------- 8 files changed, 139 insertions(+), 290 deletions(-) delete mode 100644 proxy/snell/NOTICE diff --git a/common/singbridge/tls.go b/common/singbridge/tls.go index 60fbd37c279..592345a57c5 100644 --- a/common/singbridge/tls.go +++ b/common/singbridge/tls.go @@ -51,13 +51,13 @@ func (c *tlsConfigWrapper) Client(_ net.Conn) (singtls.Conn, error) { panic("invalid") } -// HandshakeTimeout / SetHandshakeTimeout satisfy sing v0.8.12+ tls.Config. -// stdlib crypto/tls.Config has no handshake timeout field, so these are no-ops. func (c *tlsConfigWrapper) HandshakeTimeout() time.Duration { - return 0 + panic("invalid") } -func (c *tlsConfigWrapper) SetHandshakeTimeout(_ time.Duration) {} +func (c *tlsConfigWrapper) SetHandshakeTimeout(_ time.Duration) { + panic("invalid") +} func (c *tlsConfigWrapper) Clone() singtls.Config { panic("invalid") diff --git a/go.mod b/go.mod index ca6998af59e..5b41c58b868 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/sagernet/sing-quic v0.6.3 github.com/sagernet/sing-shadowsocks v0.2.9 github.com/sagernet/sing-shadowsocks2 v0.2.2 - github.com/sagernet/sing-snell v0.0.0-20260709045721-90e5a65e1f85 + github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 github.com/stretchr/testify v1.11.1 github.com/v2fly/BrowserBridge v0.0.0-20210430233438-0570fc1d7d08 diff --git a/go.sum b/go.sum index 2aa89ccf343..33117591631 100644 --- a/go.sum +++ b/go.sum @@ -225,8 +225,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= -github.com/sagernet/sing v0.8.11 h1:AKZRvjFPHtAXwGCjOJrzAQPiZxr8mobhuSUqkHf+VQw= -github.com/sagernet/sing v0.8.11/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA= +github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d h1:BhsQU0Iug1tU4xR52cjm8Sc+LBo+KwdyLTRn3ie9moo= +github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA= github.com/sagernet/sing-mux v0.3.5 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONfhI= github.com/sagernet/sing-mux v0.3.5/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= github.com/sagernet/sing-quic v0.6.3 h1:0wSPqCNJsC7CadB5GZS1aPkyU27aRwHPd5SlsijbBNw= @@ -235,6 +235,8 @@ github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4 github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8= github.com/sagernet/sing-shadowsocks2 v0.2.2 h1:GUN6ttP3NSJL/bFJJZZezp9/o6DQdG9qWyQdRvOy2LY= github.com/sagernet/sing-shadowsocks2 v0.2.2/go.mod h1:ZAYv1VFjvnMK4QQJltuvoN52biUN76TdNwzyDjSRrzc= +github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb h1:VvU2/PZqP5tbKTDq0BxkhRO8ZnKI4UJzziakgBiP2Qg= +github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb/go.mod h1:PcwzX/Xvqky0EP3kGt8OCjYb3R1pydenPHNQZcPZmXY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= diff --git a/infra/conf/v4/snell.go b/infra/conf/v4/snell.go index 3d7d877b940..03838e1c47d 100644 --- a/infra/conf/v4/snell.go +++ b/infra/conf/v4/snell.go @@ -7,88 +7,31 @@ import ( "github.com/exclavenetwork/exclave-core/v5/proxy/snell" ) -// SnellClientConfig is the JSON configuration for Snell outbound. -// -// Validation is strict: -// - version: 4 or 6 only (0 defaults to 4) -// - obfsMode: exact "none" | "http" | "tls" (empty defaults to "none"; "off" is not accepted) -// - mode (v6 only): exact "default" | "unshaped" | "unsafe-raw" (empty defaults to "default") -// - v4-only fields must be empty on v6, and v6-only fields must be empty on v4 -// - empty PSK is allowed -// - userPSK is a sing-box private extension (optional) type SnellClientConfig struct { - Address *cfgcommon.Address `json:"address"` - Port uint16 `json:"port"` - PSK string `json:"psk"` - // UserPSK is only compatible with sing-box Snell server. - UserPSK string `json:"userPSK"` - ObfsMode string `json:"obfsMode"` - // ObfsHost is v4-only. - ObfsHost string `json:"obfsHost"` - Version uint32 `json:"version"` - Reuse bool `json:"reuse"` - // Mode is v6-only. - Mode string `json:"mode"` + Address *cfgcommon.Address `json:"address"` + Port uint16 `json:"port"` + PSK string `json:"psk"` + UserKey string `json:"userKey"` + Version uint32 `json:"version"` + Reuse bool `json:"reuse"` + ObfsMode string `json:"obfsMode"` + ObfsHost string `json:"obfsHost"` + Mode string `json:"mode"` } func (c *SnellClientConfig) Build() (proto.Message, error) { if c.Address == nil { - return nil, newError("Snell: missing server address") + return nil, newError("missing server address") } - - version := c.Version - if version == 0 { - version = 4 - } - if version != 4 && version != 6 { - return nil, newError("Snell: version must be 4 or 6, got ", version) - } - - obfsMode := c.ObfsMode - if obfsMode == "" { - obfsMode = "none" - } - switch obfsMode { - case "none", "http", "tls": - default: - return nil, newError(`Snell: obfsMode must be "none", "http" or "tls", got `, c.ObfsMode) - } - - mode := c.Mode - switch version { - case 6: - if mode == "" { - mode = "default" - } - switch mode { - case "default", "unshaped", "unsafe-raw": - default: - return nil, newError(`Snell: mode must be "default", "unshaped" or "unsafe-raw", got `, c.Mode) - } - // v4-only fields: obfsHost, and non-default obfuscation - if c.ObfsHost != "" { - return nil, newError("Snell: obfsHost is only valid for version 4") - } - if c.ObfsMode != "" && c.ObfsMode != "none" { - return nil, newError("Snell: obfsMode is only valid for version 4 (v6 has no obfuscation)") - } - obfsMode = "none" - case 4: - if c.Mode != "" { - return nil, newError("Snell: mode is only valid for version 6") - } - mode = "" - } - return &snell.ClientConfig{ Address: c.Address.Build(), Port: uint32(c.Port), Psk: c.PSK, - UserPsk: c.UserPSK, - ObfsMode: obfsMode, - ObfsHost: c.ObfsHost, - Version: version, + UserKey: c.UserKey, + Version: c.Version, Reuse: c.Reuse, - Mode: mode, + ObfsMode: c.ObfsMode, + ObfsHost: c.ObfsHost, + Mode: c.Mode, }, nil } diff --git a/proxy/snell/NOTICE b/proxy/snell/NOTICE deleted file mode 100644 index 223bf28237f..00000000000 --- a/proxy/snell/NOTICE +++ /dev/null @@ -1,2 +0,0 @@ -Snell outbound uses https://github.com/SagerNet/sing-snell (GPL-3.0). -Supports Snell v4 and v6. userPSK is a sing-box private extension. diff --git a/proxy/snell/config.pb.go b/proxy/snell/config.pb.go index 76e40e638a7..684d16fb505 100644 --- a/proxy/snell/config.pb.go +++ b/proxy/snell/config.pb.go @@ -1,9 +1,3 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc v4.24.4 -// source: proxy/snell/config.proto - package snell import ( @@ -24,23 +18,16 @@ const ( ) type ClientConfig struct { - state protoimpl.MessageState `protogen:"open.v1"` - Address *net.IPOrDomain `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` - Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` - // Pre-shared key. Empty string is allowed (server may accept it). - Psk string `protobuf:"bytes,3,opt,name=psk,proto3" json:"psk,omitempty"` - // "none" | "http" | "tls" (exact match; empty defaults to "none") - ObfsMode string `protobuf:"bytes,4,opt,name=obfs_mode,json=obfsMode,proto3" json:"obfs_mode,omitempty"` - // 4 or 6 only - Version uint32 `protobuf:"varint,5,opt,name=version,proto3" json:"version,omitempty"` - // connection reuse - Reuse bool `protobuf:"varint,6,opt,name=reuse,proto3" json:"reuse,omitempty"` - // v4-only: optional obfs host (empty uses library defaults) - ObfsHost string `protobuf:"bytes,7,opt,name=obfs_host,json=obfsHost,proto3" json:"obfs_host,omitempty"` - // v6-only: "default" | "unshaped" | "unsafe-raw" (exact match; empty defaults to "default") - Mode string `protobuf:"bytes,8,opt,name=mode,proto3" json:"mode,omitempty"` - // sing-box private extension (userPSK), only compatible with sing-box Snell server. - UserPsk string `protobuf:"bytes,9,opt,name=user_psk,json=userPsk,proto3" json:"user_psk,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Address *net.IPOrDomain `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` + Psk string `protobuf:"bytes,3,opt,name=psk,proto3" json:"psk,omitempty"` + Version uint32 `protobuf:"varint,4,opt,name=version,proto3" json:"version,omitempty"` + UserKey string `protobuf:"bytes,5,opt,name=user_key,json=userKey,proto3" json:"user_key,omitempty"` + Reuse bool `protobuf:"varint,6,opt,name=reuse,proto3" json:"reuse,omitempty"` + ObfsMode string `protobuf:"bytes,7,opt,name=obfs_mode,json=obfsMode,proto3" json:"obfs_mode,omitempty"` + ObfsHost string `protobuf:"bytes,8,opt,name=obfs_host,json=obfsHost,proto3" json:"obfs_host,omitempty"` + Mode string `protobuf:"bytes,9,opt,name=mode,proto3" json:"mode,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -96,18 +83,18 @@ func (x *ClientConfig) GetPsk() string { return "" } -func (x *ClientConfig) GetObfsMode() string { +func (x *ClientConfig) GetVersion() uint32 { if x != nil { - return x.ObfsMode + return x.Version } - return "" + return 0 } -func (x *ClientConfig) GetVersion() uint32 { +func (x *ClientConfig) GetUserKey() string { if x != nil { - return x.Version + return x.UserKey } - return 0 + return "" } func (x *ClientConfig) GetReuse() bool { @@ -117,23 +104,23 @@ func (x *ClientConfig) GetReuse() bool { return false } -func (x *ClientConfig) GetObfsHost() string { +func (x *ClientConfig) GetObfsMode() string { if x != nil { - return x.ObfsHost + return x.ObfsMode } return "" } -func (x *ClientConfig) GetMode() string { +func (x *ClientConfig) GetObfsHost() string { if x != nil { - return x.Mode + return x.ObfsHost } return "" } -func (x *ClientConfig) GetUserPsk() string { +func (x *ClientConfig) GetMode() string { if x != nil { - return x.UserPsk + return x.Mode } return "" } @@ -146,13 +133,13 @@ const file_proxy_snell_config_proto_rawDesc = "" + "\fClientConfig\x12=\n" + "\aaddress\x18\x01 \x01(\v2#.exclave.core.common.net.IPOrDomainR\aaddress\x12\x12\n" + "\x04port\x18\x02 \x01(\rR\x04port\x12\x10\n" + - "\x03psk\x18\x03 \x01(\tR\x03psk\x12\x1b\n" + - "\tobfs_mode\x18\x04 \x01(\tR\bobfsMode\x12\x18\n" + - "\aversion\x18\x05 \x01(\rR\aversion\x12\x14\n" + + "\x03psk\x18\x03 \x01(\tR\x03psk\x12\x18\n" + + "\aversion\x18\x04 \x01(\rR\aversion\x12\x19\n" + + "\buser_key\x18\x05 \x01(\tR\auserKey\x12\x14\n" + "\x05reuse\x18\x06 \x01(\bR\x05reuse\x12\x1b\n" + - "\tobfs_host\x18\a \x01(\tR\bobfsHost\x12\x12\n" + - "\x04mode\x18\b \x01(\tR\x04mode\x12\x19\n" + - "\buser_psk\x18\t \x01(\tR\auserPsk:\x15\x82\xb5\x18\x11\n" + + "\tobfs_mode\x18\a \x01(\tR\bobfsMode\x12\x1b\n" + + "\tobfs_host\x18\b \x01(\tR\bobfsHost\x12\x12\n" + + "\x04mode\x18\t \x01(\tR\x04mode:\x15\x82\xb5\x18\x11\n" + "\boutbound\x12\x05snellB\x88\x01\n" + "2com.github.exclavenetwork.exclave.core.proxy.snellP\x01Z5github.com/exclavenetwork/exclave-core/v5/proxy/snell\xaa\x02\x18Exclave.Core.Proxy.Snellb\x06proto3" diff --git a/proxy/snell/config.proto b/proxy/snell/config.proto index 2b2227e2ad2..784ec58b138 100644 --- a/proxy/snell/config.proto +++ b/proxy/snell/config.proto @@ -15,18 +15,11 @@ message ClientConfig { exclave.core.common.net.IPOrDomain address = 1; uint32 port = 2; - // Pre-shared key. Empty string is allowed (server may accept it). string psk = 3; - // "none" | "http" | "tls" (exact match; empty defaults to "none") - string obfs_mode = 4; - // 4 or 6 only - uint32 version = 5; - // connection reuse + uint32 version = 4; + string user_key = 5; bool reuse = 6; - // v4-only: optional obfs host (empty uses library defaults) - string obfs_host = 7; - // v6-only: "default" | "unshaped" | "unsafe-raw" (exact match; empty defaults to "default") - string mode = 8; - // sing-box private extension (userPSK), only compatible with sing-box Snell server. - string user_psk = 9; + string obfs_mode = 7; + string obfs_host = 8; + string mode = 9; } diff --git a/proxy/snell/outbound.go b/proxy/snell/outbound.go index 802d11b7ccf..6b32f18a1d5 100644 --- a/proxy/snell/outbound.go +++ b/proxy/snell/outbound.go @@ -3,16 +3,14 @@ package snell import ( "context" "net" - "net/netip" "sync" - "github.com/sagernet/sing-snell" + snell "github.com/sagernet/sing-snell" "github.com/sagernet/sing-snell/snellv4" "github.com/sagernet/sing-snell/snellv6" - "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/network" core "github.com/exclavenetwork/exclave-core/v5" "github.com/exclavenetwork/exclave-core/v5/app/proxyman/outbound" @@ -30,23 +28,26 @@ func init() { })) } -// snellClient is the subset of SagerNet sing-snell clients used here. +var ( + _ snellClient = (*snellv4.Client)(nil) + _ snellClient = (*snellv6.Client)(nil) +) + type snellClient interface { - DialContext(ctx context.Context, destination M.Socksaddr) (net.Conn, error) - DialPacketConn(conn net.Conn) (N.NetPacketConn, error) + DialContext(ctx context.Context, destination metadata.Socksaddr) (net.Conn, error) + DialPacketConn(conn net.Conn) (network.NetPacketConn, error) Close() error } -// Outbound is a Snell v4 / v6 client outbound. type Outbound struct { serverAddr v2net.Destination - psk string - userPSK string - obfsMode string + psk []byte + userKey []byte + obfsMode snell.ObfsMode obfsHost string - version int + version uint32 reuse bool - mode string + mode snellv6.Mode client snellClient clientAccess sync.Mutex } @@ -55,61 +56,63 @@ func NewClient(ctx context.Context, config *ClientConfig) (*Outbound, error) { if config.Address == nil { return nil, newError("missing server address") } - version := int(config.Version) - if version == 0 { - version = 4 - } - if version != 4 && version != 6 { - return nil, newError("unsupported snell version ", version, " (want 4 or 6)") + outbound := &Outbound{ + serverAddr: v2net.Destination{ + Address: config.Address.AsAddress(), + Port: v2net.Port(config.Port), + Network: v2net.Network_TCP, + }, + psk: []byte(config.Psk), + userKey: []byte(config.UserKey), + version: config.Version, + reuse: config.Reuse, } - obfsMode := config.ObfsMode - if obfsMode == "" { - obfsMode = "none" - } - switch obfsMode { - case "none", "http", "tls": - default: - return nil, newError(`invalid snell obfsMode `, config.ObfsMode, ` (want "none", "http" or "tls")`) + if outbound.version != 4 && outbound.version != 6 { + return nil, newError("unsupported snell version: ", outbound.version) } - mode := config.Mode - if version == 6 { - if mode == "" { - mode = "default" + if outbound.version == 4 { + switch config.ObfsMode { + case "", "none": + outbound.obfsMode = snell.ObfsModeNone + if config.ObfsHost != "" { + return nil, newError(`invalid obfsHost for obfsMode "none"`) + } + case "http": + outbound.obfsMode = snell.ObfsModeHTTP + outbound.obfsHost = config.ObfsHost + case "tls": + outbound.obfsMode = snell.ObfsModeTLS + outbound.obfsHost = config.ObfsHost + default: + return nil, newError("invalid snell obfsMode: ", config.ObfsMode) } - switch mode { - case "default", "unshaped", "unsafe-raw": + if config.Mode != "" { + return nil, newError("mode is only valid for version 6") + } + } + + if outbound.version == 6 { + switch config.Mode { + case "", "default": + outbound.mode = snellv6.ModeDefault + case "unshaped": + outbound.mode = snellv6.ModeUnshaped + case "unsafe-raw": + outbound.mode = snellv6.ModeUnsafeRaw default: - return nil, newError(`invalid snell mode `, config.Mode) + return nil, newError("invalid snell mode: ", config.Mode) } if config.ObfsHost != "" { return nil, newError("obfsHost is only valid for version 4") } - if config.ObfsMode != "" && config.ObfsMode != "none" { + if config.ObfsMode != "" { return nil, newError("obfsMode is only valid for version 4") } - obfsMode = "none" - } else if config.Mode != "" { - return nil, newError("mode is only valid for version 6") } - // Empty PSK is allowed: Snell derives keys via Argon2id and servers may accept empty PSK. - - return &Outbound{ - serverAddr: v2net.Destination{ - Address: config.Address.AsAddress(), - Port: v2net.Port(config.Port), - Network: v2net.Network_TCP, - }, - psk: config.Psk, - userPSK: config.UserPsk, - obfsMode: obfsMode, - obfsHost: config.ObfsHost, - version: version, - reuse: config.Reuse, - mode: mode, - }, nil + return outbound, nil } func (o *Outbound) getClient(dialer internet.Dialer) (snellClient, error) { @@ -129,50 +132,33 @@ func (o *Outbound) getClient(dialer internet.Dialer) (snellClient, error) { return nil, newError("transport layer enabled") } if streamSettings := handler.StreamSettings(); streamSettings != nil && streamSettings.SecurityType != "" { - return nil, newError("tls/security enabled on streamSettings; snell uses built-in crypto (use obfsMode=tls for fake TLS on v4)") + return nil, newError("tls enabled") } - sdialer := singbridge.NewDialerWrapper(dialer) - server := M.ParseSocksaddr(o.serverAddr.NetAddr()) - psk := []byte(o.psk) - userKey := []byte(o.userPSK) - var client snellClient + var err error if o.version == 6 { - mode, err := snellv6.ParseMode(o.mode) - if err != nil { - return nil, err - } - c, err := snellv6.NewClient(snellv6.ClientOptions{ - PSK: psk, - UserKey: userKey, - Mode: mode, + client, err = snellv6.NewClient(snellv6.ClientOptions{ + PSK: o.psk, + UserKey: o.userKey, + Mode: o.mode, Reuse: o.reuse, - Dialer: sdialer, - Server: server, + Dialer: singbridge.NewDialerWrapper(dialer), + Server: metadata.ParseSocksaddr(o.serverAddr.NetAddr()), }) - if err != nil { - return nil, err - } - client = c } else { - obfs, err := snell.ParseObfsMode(o.obfsMode) - if err != nil { - return nil, err - } - c, err := snellv4.NewClient(snellv4.ClientOptions{ - PSK: psk, - UserKey: userKey, + client, err = snellv4.NewClient(snellv4.ClientOptions{ + PSK: o.psk, + UserKey: o.userKey, Reuse: o.reuse, - ObfsMode: obfs, + ObfsMode: o.obfsMode, ObfsHost: o.obfsHost, - Dialer: sdialer, - Server: server, + Dialer: singbridge.NewDialerWrapper(dialer), + Server: metadata.ParseSocksaddr(o.serverAddr.NetAddr()), }) - if err != nil { - return nil, err - } - client = c + } + if err != nil { + return nil, err } o.client = client return client, nil @@ -190,88 +176,28 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int } destination := ob.Target - newError("tunneling request to ", destination, " via snell://", o.serverAddr.NetAddr(), - " (v", o.version, ")").WriteToLog(session.ExportIDToError(ctx)) + newError("tunneling request to ", destination, " via ", o.serverAddr.NetAddr()).WriteToLog(session.ExportIDToError(ctx)) detachedCtx := core.ToBackgroundDetachedContext(ctx) if destination.Network == v2net.Network_TCP { - // Snell is client-speaks-first (CONNECT header before any application payload). - // No server-speaks-first first-payload handling is required. serverConn, err := client.DialContext(detachedCtx, singbridge.ToSocksAddr(destination)) if err != nil { return err } return singbridge.ReturnError(bufio.CopyConn(detachedCtx, singbridge.NewPipeConnWrapper(link), serverConn)) - } - - // UDP-over-TCP. - // Snell UDP framing only carries IPv4/IPv6 (no domain ATYP). Resolve domains before write. - raw, err := dialer.Dial(detachedCtx, o.serverAddr) - if err != nil { - return err - } - pc, err := client.DialPacketConn(raw) - if err != nil { - _ = raw.Close() - return err - } - - var resolve domainResolveFunc - if ob.Resolver != nil { - resolve = func(ctx context.Context, domain string) (net.IP, error) { - addr := ob.Resolver(ctx, domain) - if !addr.Family().IsIP() { - return nil, newError("resolver returned non-IP for ", domain) - } - return addr.IP(), nil - } } else { - resolve = func(ctx context.Context, domain string) (net.IP, error) { - ips, err := net.DefaultResolver.LookupIP(ctx, "ip", domain) - if err != nil { - return nil, err - } - if len(ips) == 0 { - return nil, newError("no IP for domain ", domain) - } - return ips[0], nil - } - } - - return singbridge.ReturnError(bufio.CopyPacketConn( - detachedCtx, - singbridge.NewPacketConnWrapper(link, destination), - &udpIPOnlyPacketConn{NetPacketConn: pc, ctx: detachedCtx, resolve: resolve}, - )) -} - -type domainResolveFunc func(ctx context.Context, domain string) (net.IP, error) - -// udpIPOnlyPacketConn resolves domain destinations to IP for Snell UDP writes. -// Read path is unchanged: servers reply with IP addresses only. -type udpIPOnlyPacketConn struct { - N.NetPacketConn - ctx context.Context - resolve domainResolveFunc -} - -func (c *udpIPOnlyPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { - // Snell UDP address encoding only supports IPv4/IPv6, not domain names. - if destination.IsDomain() { - ip, err := c.resolve(c.ctx, destination.Fqdn) + rawConn, err := dialer.Dial(detachedCtx, o.serverAddr) if err != nil { - buffer.Release() return err } - addr, ok := netip.AddrFromSlice(ip) - if !ok { - buffer.Release() - return newError("invalid IP for domain ", destination.Fqdn) + serverConn, err := client.DialPacketConn(rawConn) + if err != nil { + rawConn.Close() + return err } - destination = M.SocksaddrFrom(addr, destination.Port) + return singbridge.ReturnError(bufio.CopyPacketConn(detachedCtx, singbridge.NewPacketConnWrapper(link, destination), serverConn)) } - return c.NetPacketConn.WritePacket(buffer, destination) } func (o *Outbound) InterfaceUpdate() {