From 5f2c29848ea82f100e2cb20e924d0cb68312da2f Mon Sep 17 00:00:00 2001 From: hungrybirder Date: Thu, 5 Nov 2026 09:44:45 +0800 Subject: [PATCH 1/4] feat: add snell version 6 outbound Port the Snell v6 client/server implementation from SagerNet/sing-snell into transport/singsnell (GPL-3.0-or-later), with imports rewritten to github.com/metacubex/sing and a small compat layer for interfaces that the pinned sing release does not provide. Builtins newer than the Go version declared in go.mod are replaced with equivalents. The snell outbound accepts `version: 6` and a `mode` option (default / unshaped / unsafe-raw) that must match the server. TCP, UDP over TCP and connection reuse are supported; http/tls simple-obfs is rejected for v6 since the protocol replaced it with PSK-derived traffic shaping, while shadow-tls/restls/jls wrappers still apply. Co-Authored-By: Claude Fable 5.1 --- adapter/outbound/snell.go | 137 ++- adapter/outbound/snell_v6_test.go | 218 ++++ docs/config.yaml | 19 +- transport/singsnell/LICENSE | 14 + transport/singsnell/README.md | 13 + transport/singsnell/address.go | 97 ++ transport/singsnell/compat.go | 56 + transport/singsnell/crypto.go | 31 + transport/singsnell/internal/reuse/reuse.go | 291 +++++ transport/singsnell/request.go | 97 ++ transport/singsnell/snell.go | 94 ++ transport/singsnell/snellv6/client.go | 369 ++++++ transport/singsnell/snellv6/handshake.go | 216 ++++ transport/singsnell/snellv6/loopback_test.go | 238 ++++ transport/singsnell/snellv6/mixing.go | 56 + transport/singsnell/snellv6/mode.go | 41 + transport/singsnell/snellv6/packet.go | 447 +++++++ transport/singsnell/snellv6/profile.go | 523 +++++++++ transport/singsnell/snellv6/record.go | 654 +++++++++++ transport/singsnell/snellv6/reuse.go | 1031 +++++++++++++++++ transport/singsnell/snellv6/salt.go | 56 + transport/singsnell/snellv6/server.go | 463 ++++++++ transport/singsnell/snellv6/shaped.go | 463 ++++++++ transport/singsnell/snellv6/streaming_test.go | 105 ++ transport/snell/snell.go | 1 + 25 files changed, 5719 insertions(+), 11 deletions(-) create mode 100644 adapter/outbound/snell_v6_test.go create mode 100644 transport/singsnell/LICENSE create mode 100644 transport/singsnell/README.md create mode 100644 transport/singsnell/address.go create mode 100644 transport/singsnell/compat.go create mode 100644 transport/singsnell/crypto.go create mode 100644 transport/singsnell/internal/reuse/reuse.go create mode 100644 transport/singsnell/request.go create mode 100644 transport/singsnell/snell.go create mode 100644 transport/singsnell/snellv6/client.go create mode 100644 transport/singsnell/snellv6/handshake.go create mode 100644 transport/singsnell/snellv6/loopback_test.go create mode 100644 transport/singsnell/snellv6/mixing.go create mode 100644 transport/singsnell/snellv6/mode.go create mode 100644 transport/singsnell/snellv6/packet.go create mode 100644 transport/singsnell/snellv6/profile.go create mode 100644 transport/singsnell/snellv6/record.go create mode 100644 transport/singsnell/snellv6/reuse.go create mode 100644 transport/singsnell/snellv6/salt.go create mode 100644 transport/singsnell/snellv6/server.go create mode 100644 transport/singsnell/snellv6/shaped.go create mode 100644 transport/singsnell/snellv6/streaming_test.go diff --git a/adapter/outbound/snell.go b/adapter/outbound/snell.go index 30ef8f96eb..be20274d9c 100644 --- a/adapter/outbound/snell.go +++ b/adapter/outbound/snell.go @@ -2,6 +2,7 @@ package outbound import ( "context" + "errors" "fmt" "net" "strconv" @@ -13,7 +14,11 @@ import ( "github.com/metacubex/mihomo/transport/restls" "github.com/metacubex/mihomo/transport/shadowtls" obfs "github.com/metacubex/mihomo/transport/simple-obfs" + "github.com/metacubex/mihomo/transport/singsnell/snellv6" "github.com/metacubex/mihomo/transport/snell" + + M "github.com/metacubex/sing/common/metadata" + singN "github.com/metacubex/sing/common/network" ) type Snell struct { @@ -21,6 +26,7 @@ type Snell struct { option *SnellOption psk []byte pool *snell.Pool + v6 *snellv6.Client obfsOption *simpleObfsOption shadowTLSOption *shadowtls.ShadowTLSOption restlsConfig *restls.Config @@ -38,11 +44,19 @@ type SnellOption struct { UDP bool `proxy:"udp,omitempty"` Version int `proxy:"version,omitempty"` Reuse bool `proxy:"reuse,omitempty"` + Mode string `proxy:"mode,omitempty"` // v6 only: default / unshaped / unsafe-raw ObfsOpts map[string]any `proxy:"obfs-opts,omitempty"` ClientFingerprint string `proxy:"client-fingerprint,omitempty"` } -func (s *Snell) streamConnContext(ctx context.Context, c net.Conn) (*snell.Snell, error) { +var ( + errSnellV6UDPStream = errors.New("snell v6 udp must be created with ListenPacketContext") + errSnellV6ListenPacket = errors.New("snell v6 dialer does not support ListenPacket") +) + +// obfsConnContext wraps the raw server connection with the configured +// obfuscation layer, which is shared by every Snell version. +func (s *Snell) obfsConnContext(ctx context.Context, c net.Conn) (net.Conn, error) { var err error switch s.obfsOption.Mode { case "tls": @@ -66,11 +80,22 @@ func (s *Snell) streamConnContext(ctx context.Context, c net.Conn) (*snell.Snell return nil, err } } + return c, nil +} + +func (s *Snell) streamConnContext(ctx context.Context, c net.Conn) (*snell.Snell, error) { + c, err := s.obfsConnContext(ctx, c) + if err != nil { + return nil, err + } return snell.StreamConn(c, s.psk, s.version), nil } // StreamConnContext implements C.ProxyAdapter func (s *Snell) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { + if s.version == snell.Version6 { + return s.streamV6ConnContext(ctx, c, metadata) + } c, err := s.streamConnContext(ctx, c) if err != nil { return nil, err @@ -79,6 +104,25 @@ func (s *Snell) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M return c, err } +func (s *Snell) streamV6ConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) { + if metadata.NetWork == C.UDP { + return nil, errSnellV6UDPStream + } + c, err = s.obfsConnContext(ctx, c) + if err != nil { + return nil, err + } + destination := M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort) + if N.NeedHandshake(c) { + return s.v6.DialEarlyConn(c, destination), nil + } + if ctx.Done() != nil { + done := N.SetupContextForConn(ctx, c) + defer done(&err) + } + return s.v6.DialConn(c, destination) +} + func (s *Snell) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (err error) { if ctx.Done() != nil { done := N.SetupContextForConn(ctx, c) @@ -101,6 +145,15 @@ func (s *Snell) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C. // DialContext implements C.ProxyAdapter func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { if s.reuse { + if s.version == snell.Version6 { + // the v6 client owns its own session pool and dials through v6Dialer + c, err := s.v6.DialContext(ctx, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", s.addr, err) + } + return NewConn(c, s), nil + } + c, err := s.pool.Get() if err != nil { return nil, err @@ -143,6 +196,19 @@ func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata) ( safeConnClose(c, err) }(c) + if s.version == snell.Version6 { + c, err = s.obfsConnContext(ctx, c) + if err != nil { + return nil, err + } + var pc singN.NetPacketConn + pc, err = s.v6.DialPacketConn(c) + if err != nil { + return nil, err + } + return NewPacketConn(N.NewThreadSafePacketConn(pc), s), nil + } + c, err = s.StreamConnContext(ctx, c, metadata) if err != nil { return nil, err @@ -164,6 +230,38 @@ func (s *Snell) ProxyInfo() C.ProxyInfo { return info } +// Close implements C.ProxyAdapter +func (s *Snell) Close() error { + if s.v6 != nil { + _ = s.v6.Close() + } + return s.Base.Close() +} + +// v6Dialer lets the snellv6 session pool dial the server through the +// adapter's own dialer and obfuscation settings. +type v6Dialer struct { + s *Snell +} + +func (d v6Dialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + c, err := d.s.dialer.DialContext(ctx, "tcp", d.s.addr) + if err != nil { + return nil, err + } + c, err = d.s.obfsConnContext(ctx, c) + if err != nil { + return nil, err + } + return c, nil +} + +func (d v6Dialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return nil, errSnellV6ListenPacket +} + +var _ singN.Dialer = v6Dialer{} + func NewSnell(option SnellOption) (*Snell, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) psk := []byte(option.Psk) @@ -249,16 +347,36 @@ func NewSnell(option SnellOption) (*Snell, error) { // Snell v5 servers are backward-compatible with v4 clients. option.Version = snell.Version4 } - reuse := option.Version == snell.Version2 || (option.Version == snell.Version4 && option.Reuse) + reuse := option.Version == snell.Version2 || + ((option.Version == snell.Version4 || option.Version == snell.Version6) && option.Reuse) + var v6Mode snellv6.Mode switch option.Version { case snell.Version1, snell.Version2: if option.UDP { return nil, fmt.Errorf("snell version %d not support UDP", option.Version) } case snell.Version3, snell.Version4: + case snell.Version6: + // snell-server v6 rejects PSKs outside the 12..255-byte range + if len(psk) < 12 || len(psk) > 255 { + return nil, fmt.Errorf("snell %s version 6 psk length must be between 12 and 255 bytes", addr) + } + switch obfsOption.Mode { + case "tls", "http": + // v6 replaced the simple-obfs layer with PSK-derived traffic shaping + return nil, fmt.Errorf("snell %s version 6 does not support obfs mode %s", addr, obfsOption.Mode) + } + var err error + v6Mode, err = snellv6.ParseMode(option.Mode) + if err != nil { + return nil, fmt.Errorf("snell %s %w", addr, err) + } default: return nil, fmt.Errorf("snell version error: %d", option.Version) } + if option.Mode != "" && option.Version != snell.Version6 { + return nil, fmt.Errorf("snell %s mode is only supported on version 6", addr) + } s := &Snell{ Base: NewBase(BaseOption{ @@ -284,6 +402,21 @@ func NewSnell(option SnellOption) (*Snell, error) { } s.dialer = option.NewDialer(s.DialOptions()) + if s.version == snell.Version6 { + client, err := snellv6.NewClient(snellv6.ClientOptions{ + PSK: psk, + Mode: v6Mode, + Reuse: reuse, + Dialer: v6Dialer{s: s}, + Server: M.ParseSocksaddr(addr), + }) + if err != nil { + return nil, fmt.Errorf("snell %s %w", addr, err) + } + s.v6 = client + return s, nil + } + if s.reuse { s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) { c, err := s.dialer.DialContext(ctx, "tcp", addr) diff --git a/adapter/outbound/snell_v6_test.go b/adapter/outbound/snell_v6_test.go new file mode 100644 index 0000000000..2afe6df13d --- /dev/null +++ b/adapter/outbound/snell_v6_test.go @@ -0,0 +1,218 @@ +package outbound + +import ( + "bytes" + "context" + "crypto/rand" + "io" + "net" + "net/netip" + "strconv" + "testing" + "time" + + C "github.com/metacubex/mihomo/constant" + snell "github.com/metacubex/mihomo/transport/singsnell" + "github.com/metacubex/mihomo/transport/singsnell/snellv6" + + M "github.com/metacubex/sing/common/metadata" + N "github.com/metacubex/sing/common/network" + "github.com/stretchr/testify/require" +) + +const snellV6TestPSK = "mihomo-snell-v6-outbound-test-psk" + +type snellV6EchoHandler struct{} + +func (snellV6EchoHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose snell.CloseHandlerFunc) { + go func() { + var err error + defer func() { + _ = N.CloseWrite(conn) + _ = conn.Close() + onClose(err) + }() + var payload []byte + payload, err = io.ReadAll(conn) + if err != nil { + return + } + _, err = conn.Write(payload) + }() +} + +func (snellV6EchoHandler) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose snell.CloseHandlerFunc) { + go func() { + defer conn.Close() + readWaitOptions := N.NewReadWaitOptions(conn, conn) + for { + buffer := readWaitOptions.NewPacketBuffer() + dest, err := conn.ReadPacket(buffer) + if err != nil { + buffer.Release() + return + } + if err = conn.WritePacket(buffer, dest); err != nil { + return + } + } + }() +} + +func startSnellV6Server(t *testing.T, mode snellv6.Mode) (host string, port int) { + t.Helper() + service, err := snellv6.NewService(snellv6.ServerOptions{ + PSK: []byte(snellV6TestPSK), + Mode: mode, + Handler: snellV6EchoHandler{}, + }) + require.NoError(t, err) + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { listener.Close() }) + go func() { + for { + conn, err := listener.Accept() + if err != nil { + return + } + go func() { + if err := service.NewConnection(context.Background(), conn, M.SocksaddrFromNet(conn.RemoteAddr()), func(error) {}); err != nil { + conn.Close() + } + }() + } + }() + host, portStr, err := net.SplitHostPort(listener.Addr().String()) + require.NoError(t, err) + port, err = strconv.Atoi(portStr) + require.NoError(t, err) + return host, port +} + +func newSnellV6Outbound(t *testing.T, mode string, reuse bool) *Snell { + t.Helper() + host, port := startSnellV6Server(t, mustParseV6Mode(t, mode)) + proxy, err := NewSnell(SnellOption{ + Name: "snell-v6-" + mode, + Server: host, + Port: port, + Psk: snellV6TestPSK, + Version: 6, + UDP: true, + Reuse: reuse, + Mode: mode, + }) + require.NoError(t, err) + t.Cleanup(func() { proxy.Close() }) + return proxy +} + +func mustParseV6Mode(t *testing.T, mode string) snellv6.Mode { + t.Helper() + parsed, err := snellv6.ParseMode(mode) + require.NoError(t, err) + return parsed +} + +func snellV6EchoTCP(t *testing.T, proxy *Snell) { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + conn, err := proxy.DialContext(ctx, &C.Metadata{ + NetWork: C.TCP, + Host: "example.com", + DstPort: 443, + }) + require.NoError(t, err) + defer conn.Close() + + payload := make([]byte, 128*1024) + _, err = rand.Read(payload) + 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, received), "payload mismatch") +} + +func TestSnellV6OutboundTCP(t *testing.T) { + for _, mode := range []string{"default", "unshaped", "unsafe-raw"} { + t.Run(mode, func(t *testing.T) { + snellV6EchoTCP(t, newSnellV6Outbound(t, mode, false)) + }) + } +} + +func TestSnellV6OutboundTCPReuse(t *testing.T) { + proxy := newSnellV6Outbound(t, "default", true) + for round := 0; round < 3; round++ { + snellV6EchoTCP(t, proxy) + time.Sleep(50 * time.Millisecond) + } +} + +func TestSnellV6OutboundUDP(t *testing.T) { + proxy := newSnellV6Outbound(t, "default", false) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + target := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 53} + pc, err := proxy.ListenPacketContext(ctx, &C.Metadata{ + NetWork: C.UDP, + DstIP: netip.MustParseAddr("127.0.0.1"), + DstPort: 53, + }) + require.NoError(t, err) + defer pc.Close() + + for round := 0; round < 5; round++ { + message := make([]byte, 1200) + _, err = rand.Read(message) + require.NoError(t, err) + _, err = pc.WriteTo(message, target) + require.NoError(t, err) + + received := make([]byte, 2048) + require.NoError(t, pc.SetReadDeadline(time.Now().Add(10*time.Second))) + n, from, err := pc.ReadFrom(received) + require.NoError(t, err) + require.Equal(t, target.String(), from.String()) + require.True(t, bytes.Equal(message, received[:n]), "round %d", round) + } +} + +func TestSnellV6OutboundValidation(t *testing.T) { + base := func() SnellOption { + return SnellOption{Name: "v6", Server: "127.0.0.1", Port: 1, Psk: snellV6TestPSK, Version: 6} + } + + opt := base() + opt.Psk = "short" + _, err := NewSnell(opt) + require.Error(t, err, "psk shorter than 12 bytes must be rejected") + + opt = base() + opt.ObfsOpts = map[string]any{"mode": "http"} + _, err = NewSnell(opt) + require.Error(t, err, "v6 has no simple-obfs layer") + + opt = base() + opt.Mode = "bogus" + _, err = NewSnell(opt) + require.Error(t, err, "unknown mode must be rejected") + + opt = base() + opt.Version = 4 + opt.Mode = "unshaped" + _, err = NewSnell(opt) + require.Error(t, err, "mode is v6 only") + + opt = base() + opt.Mode = "unshaped" + proxy, err := NewSnell(opt) + require.NoError(t, err) + require.NoError(t, proxy.Close()) +} diff --git a/docs/config.yaml b/docs/config.yaml index 626755f35c..2e6f9b262f 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -414,20 +414,21 @@ proxies: # socks5 server: server port: 44046 psk: yourpsk - # version: 4 # support 1/2/3/4/5 - # udp: true # support UDP on version 3/4/5 - # reuse: false # v4/5 only + # version: 4 # support 1/2/3/4/5/6 + # udp: true # support UDP on version 3/4/5/6 + # reuse: false # v4/5/6 only + # mode: default # v6 only, default / unshaped / unsafe-raw, must match the server # obfs-opts: - # mode: http # or tls/shadow-tls/restls/jls + # mode: http # or tls/shadow-tls/restls/jls (http/tls are not available on v6) - name: "snell-shadow-tls" type: snell server: server port: 44046 psk: yourpsk - # version: 4 # support 1/2/3/4/5 - # udp: true # support UDP on version 3/4/5 - # reuse: false # v4/5 only + # version: 4 # support 1/2/3/4/5/6 + # udp: true # support UDP on version 3/4/5/6 + # reuse: false # v4/5/6 only # client-fingerprint: chrome obfs-opts: mode: shadow-tls @@ -441,7 +442,7 @@ proxies: # socks5 server: server port: 44046 psk: yourpsk - # version: 4 # support 1/2/3/4/5 + # version: 4 # support 1/2/3/4/5/6 # client-fingerprint: chrome obfs-opts: mode: restls @@ -454,7 +455,7 @@ proxies: # socks5 server: server port: 44046 psk: yourpsk - # version: 4 # support 1/2/3/4/5 + # version: 4 # support 1/2/3/4/5/6 # client-fingerprint: chrome obfs-opts: mode: jls diff --git a/transport/singsnell/LICENSE b/transport/singsnell/LICENSE new file mode 100644 index 0000000000..3e3e29e359 --- /dev/null +++ b/transport/singsnell/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/transport/singsnell/README.md b/transport/singsnell/README.md new file mode 100644 index 0000000000..588af27544 --- /dev/null +++ b/transport/singsnell/README.md @@ -0,0 +1,13 @@ +# singsnell + +Snell v6 client/server implementation ported from +[SagerNet/sing-snell](https://github.com/SagerNet/sing-snell) +(commit `bc5a12ac736f235b2de2926ecd2791cc925e6b8c`), licensed under GPL-3.0-or-later (see LICENSE). + +Changes relative to upstream: + +- imports rewritten to `github.com/metacubex/sing` and this module path +- root package renamed to `singsnell` +- language features newer than the Go version declared in go.mod replaced + with equivalent helpers +- only the v6 protocol is ported; v4/v5 live in `transport/snell` diff --git a/transport/singsnell/address.go b/transport/singsnell/address.go new file mode 100644 index 0000000000..c56856264a --- /dev/null +++ b/transport/singsnell/address.go @@ -0,0 +1,97 @@ +package singsnell + +import ( + "encoding/binary" + "io" + + "github.com/metacubex/sing/common/buf" + E "github.com/metacubex/sing/common/exceptions" + M "github.com/metacubex/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/transport/singsnell/compat.go b/transport/singsnell/compat.go new file mode 100644 index 0000000000..0ff6bcead1 --- /dev/null +++ b/transport/singsnell/compat.go @@ -0,0 +1,56 @@ +package singsnell + +import ( + "context" + "net" + + "github.com/metacubex/sing/common/buf" + M "github.com/metacubex/sing/common/metadata" + N "github.com/metacubex/sing/common/network" +) + +// Interfaces below exist in newer github.com/sagernet/sing releases but not in +// the github.com/metacubex/sing version mihomo depends on. They are declared +// here so the ported code keeps the same shape as upstream. + +type CloseHandlerFunc = func(it error) + +type TCPConnectionHandlerEx interface { + NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose CloseHandlerFunc) +} + +type UDPConnectionHandlerEx interface { + NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose CloseHandlerFunc) +} + +type VectorisedWriteCreator interface { + CreateVectorisedWriter() (N.VectorisedWriter, bool) +} + +type PacketBatchWriter interface { + WritePacketBatch(buffers []*buf.Buffer, destinations []M.Socksaddr) error +} + +type PacketBatchWriteCreator interface { + CreatePacketBatchWriter() (PacketBatchWriter, bool) +} + +type EarlyReader interface { + NeedHandshakeForRead() bool +} + +type EarlyWriter interface { + NeedHandshakeForWrite() bool +} + +// NewBufferSize mirrors ReadWaitOptions.NewBufferSize from newer sing releases. +func NewBufferSize(options N.ReadWaitOptions, bufferSize int) *buf.Buffer { + buffer := buf.NewSize(bufferSize + options.FrontHeadroom + options.RearHeadroom) + if options.FrontHeadroom > 0 { + buffer.Resize(options.FrontHeadroom, 0) + } + if options.RearHeadroom > 0 { + buffer.Reserve(options.RearHeadroom) + } + return buffer +} diff --git a/transport/singsnell/crypto.go b/transport/singsnell/crypto.go new file mode 100644 index 0000000000..c4cd23a236 --- /dev/null +++ b/transport/singsnell/crypto.go @@ -0,0 +1,31 @@ +package singsnell + +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/transport/singsnell/internal/reuse/reuse.go b/transport/singsnell/internal/reuse/reuse.go new file mode 100644 index 0000000000..954d34251d --- /dev/null +++ b/transport/singsnell/internal/reuse/reuse.go @@ -0,0 +1,291 @@ +package reuse + +import ( + "io" + "sync" + "sync/atomic" + "time" + + snell "github.com/metacubex/mihomo/transport/singsnell" + "github.com/metacubex/sing/common/buf" + E "github.com/metacubex/sing/common/exceptions" + N "github.com/metacubex/sing/common/network" + "github.com/metacubex/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]) Reset() { + p.access.Lock() + if p.closed { + p.access.Unlock() + return + } + var sessions []S + for element := p.entries.Front(); element != nil; element = element.Next() { + sessions = append(sessions, element.Value.session) + } + p.entries.Init() + p.access.Unlock() + for _, session := range sessions { + session.Close() + } +} + +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 + snell.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/transport/singsnell/request.go b/transport/singsnell/request.go new file mode 100644 index 0000000000..c5272f26ac --- /dev/null +++ b/transport/singsnell/request.go @@ -0,0 +1,97 @@ +package singsnell + +import ( + "io" + + "github.com/metacubex/sing/common/buf" + E "github.com/metacubex/sing/common/exceptions" + M "github.com/metacubex/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/transport/singsnell/snell.go b/transport/singsnell/snell.go new file mode 100644 index 0000000000..0d28b49e6c --- /dev/null +++ b/transport/singsnell/snell.go @@ -0,0 +1,94 @@ +package singsnell + +import ( + "context" + "net" + + E "github.com/metacubex/sing/common/exceptions" + M "github.com/metacubex/sing/common/metadata" + N "github.com/metacubex/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 { + TCPConnectionHandlerEx + UDPConnectionHandlerEx +} + +type Service interface { + NewConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose 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() +} + +type keepSessionKey struct{} + +func ContextWithKeepSession(ctx context.Context) context.Context { + return context.WithValue(ctx, (*keepSessionKey)(nil), true) +} + +func KeepSessionFromContext(ctx context.Context) bool { + keep, _ := ctx.Value((*keepSessionKey)(nil)).(bool) + return keep +} diff --git a/transport/singsnell/snellv6/client.go b/transport/singsnell/snellv6/client.go new file mode 100644 index 0000000000..566b89a06e --- /dev/null +++ b/transport/singsnell/snellv6/client.go @@ -0,0 +1,369 @@ +package snellv6 + +import ( + "net" + "sync" + "sync/atomic" + + snell "github.com/metacubex/mihomo/transport/singsnell" + "github.com/metacubex/mihomo/transport/singsnell/internal/reuse" + "github.com/metacubex/sing/common" + "github.com/metacubex/sing/common/buf" + "github.com/metacubex/sing/common/bufio" + E "github.com/metacubex/sing/common/exceptions" + M "github.com/metacubex/sing/common/metadata" + N "github.com/metacubex/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] + closeIdle atomic.Bool +} + +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) + _ snell.VectorisedWriteCreator = (*clientConn)(nil) + _ N.VectorisedWriter = (*clientVectorisedWriter)(nil) + _ snell.EarlyReader = (*clientConn)(nil) + _ snell.EarlyWriter = (*clientConn)(nil) + _ N.WriteCloser = (*clientConn)(nil) +) + +// NeedHandshake implements network.EarlyConn so callers can defer the +// handshake until the first payload write. +func (c *clientConn) NeedHandshake() bool { + return c.writer == nil +} + +var _ N.EarlyConn = (*clientConn)(nil) diff --git a/transport/singsnell/snellv6/handshake.go b/transport/singsnell/snellv6/handshake.go new file mode 100644 index 0000000000..45dceb7792 --- /dev/null +++ b/transport/singsnell/snellv6/handshake.go @@ -0,0 +1,216 @@ +package snellv6 + +import ( + "crypto/rand" + "io" + "time" + + snell "github.com/metacubex/mihomo/transport/singsnell" + "github.com/metacubex/mihomo/transport/singsnell/internal/reuse" + "github.com/metacubex/sing/common" + "github.com/metacubex/sing/common/buf" + N "github.com/metacubex/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 := common.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/transport/singsnell/snellv6/loopback_test.go b/transport/singsnell/snellv6/loopback_test.go new file mode 100644 index 0000000000..5b82c8e480 --- /dev/null +++ b/transport/singsnell/snellv6/loopback_test.go @@ -0,0 +1,238 @@ +package snellv6_test + +import ( + "bytes" + "context" + "crypto/rand" + "io" + "net" + "sync/atomic" + "testing" + "time" + + snell "github.com/metacubex/mihomo/transport/singsnell" + "github.com/metacubex/mihomo/transport/singsnell/snellv6" + + M "github.com/metacubex/sing/common/metadata" + N "github.com/metacubex/sing/common/network" + "github.com/stretchr/testify/require" +) + +const testPSK = "snell-v6-loopback-psk-0123456789" + +var testModes = map[string]snellv6.Mode{ + "default": snellv6.ModeDefault, + "unshaped": snellv6.ModeUnshaped, + "unsafe-raw": snellv6.ModeUnsafeRaw, +} + +// echoHandler echoes TCP streams and UDP datagrams straight back to the client. +type echoHandler struct{} + +func (echoHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose snell.CloseHandlerFunc) { + go func() { + var err error + defer func() { + _ = N.CloseWrite(conn) + _ = conn.Close() + onClose(err) + }() + // mirror a real upstream: consume the request until the client half-closes, + // then send the response back + var payload []byte + payload, err = io.ReadAll(conn) + if err != nil { + return + } + _, err = conn.Write(payload) + }() +} + +func (echoHandler) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose snell.CloseHandlerFunc) { + go func() { + defer conn.Close() + readWaitOptions := N.NewReadWaitOptions(conn, conn) + for { + buffer := readWaitOptions.NewPacketBuffer() + dest, err := conn.ReadPacket(buffer) + if err != nil { + buffer.Release() + return + } + if err = conn.WritePacket(buffer, dest); err != nil { + return + } + } + }() +} + +type testServer struct { + address string + accepts atomic.Int32 +} + +func startServer(t *testing.T, mode snellv6.Mode) *testServer { + t.Helper() + service, err := snellv6.NewService(snellv6.ServerOptions{ + PSK: []byte(testPSK), + Mode: mode, + Handler: echoHandler{}, + }) + require.NoError(t, err) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { listener.Close() }) + + server := &testServer{address: listener.Addr().String()} + go func() { + for { + conn, err := listener.Accept() + if err != nil { + return + } + server.accepts.Add(1) + go func() { + err := service.NewConnection(context.Background(), conn, M.SocksaddrFromNet(conn.RemoteAddr()), func(error) {}) + if err != nil { + conn.Close() + } + }() + } + }() + return server +} + +func randomPayload(t *testing.T, size int) []byte { + t.Helper() + payload := make([]byte, size) + _, err := rand.Read(payload) + require.NoError(t, err) + return payload +} + +func echoRoundTrip(t *testing.T, conn net.Conn, payload []byte) { + t.Helper() + _, 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, received), "payload mismatch") +} + +func TestLoopbackTCP(t *testing.T) { + for name, mode := range testModes { + t.Run(name, func(t *testing.T) { + server := startServer(t, mode) + client, err := snellv6.NewClient(snellv6.ClientOptions{PSK: []byte(testPSK), Mode: mode}) + require.NoError(t, err) + + serverConn, err := net.Dial("tcp", server.address) + require.NoError(t, err) + defer serverConn.Close() + + proxyConn, err := client.DialConn(serverConn, M.ParseSocksaddrHostPort("example.com", 443)) + require.NoError(t, err) + echoRoundTrip(t, proxyConn, randomPayload(t, 256*1024)) + }) + } +} + +func TestLoopbackTCPEarlyData(t *testing.T) { + for name, mode := range testModes { + t.Run(name, func(t *testing.T) { + server := startServer(t, mode) + client, err := snellv6.NewClient(snellv6.ClientOptions{PSK: []byte(testPSK), Mode: mode}) + require.NoError(t, err) + + serverConn, err := net.Dial("tcp", server.address) + require.NoError(t, err) + defer serverConn.Close() + + proxyConn := client.DialEarlyConn(serverConn, M.ParseSocksaddrHostPort("127.0.0.1", 80)) + require.True(t, proxyConn.(N.EarlyConn).NeedHandshake()) + // larger than one record so the request and payload span several frames + echoRoundTrip(t, proxyConn, randomPayload(t, 80*1024)) + require.False(t, proxyConn.(N.EarlyConn).NeedHandshake()) + }) + } +} + +func TestLoopbackUDP(t *testing.T) { + for name, mode := range testModes { + t.Run(name, func(t *testing.T) { + server := startServer(t, mode) + client, err := snellv6.NewClient(snellv6.ClientOptions{PSK: []byte(testPSK), Mode: mode}) + require.NoError(t, err) + + serverConn, err := net.Dial("tcp", server.address) + require.NoError(t, err) + defer serverConn.Close() + + packetConn, err := client.DialPacketConn(serverConn) + require.NoError(t, err) + target := M.ParseSocksaddrHostPort("127.0.0.1", 53).UDPAddr() + + for round := 0; round < 10; round++ { + message := randomPayload(t, 1400) + _, err = packetConn.WriteTo(message, target) + require.NoError(t, err) + + received := make([]byte, 2048) + require.NoError(t, packetConn.SetReadDeadline(time.Now().Add(10*time.Second))) + n, from, err := packetConn.ReadFrom(received) + require.NoError(t, err) + require.Equal(t, target.String(), from.String()) + require.True(t, bytes.Equal(message, received[:n]), "round %d", round) + } + }) + } +} + +func TestLoopbackReuse(t *testing.T) { + for name, mode := range testModes { + t.Run(name, func(t *testing.T) { + server := startServer(t, mode) + client, err := snellv6.NewClient(snellv6.ClientOptions{ + PSK: []byte(testPSK), + Mode: mode, + Reuse: true, + Dialer: N.SystemDialer, + Server: M.ParseSocksaddr(server.address), + }) + require.NoError(t, err) + defer client.Close() + + destination := M.ParseSocksaddrHostPort("example.com", 443) + for round := 0; round < 3; round++ { + conn, err := client.DialContext(context.Background(), destination) + require.NoError(t, err) + echoRoundTrip(t, conn, randomPayload(t, 16*1024)) + require.NoError(t, conn.Close()) + // give the session time to return to the pool before the next dial + time.Sleep(50 * time.Millisecond) + } + require.Equal(t, int32(1), server.accepts.Load(), "sessions should be reused over a single tcp connection") + }) + } +} + +func TestClientOptionsValidation(t *testing.T) { + _, err := snellv6.NewClient(snellv6.ClientOptions{}) + require.ErrorIs(t, err, snell.ErrMissingPSK) + + _, err = snellv6.NewClient(snellv6.ClientOptions{PSK: []byte(testPSK), Mode: snellv6.Mode(99)}) + require.Error(t, err) + + _, err = snellv6.NewService(snellv6.ServerOptions{PSK: []byte("short")}) + require.Error(t, err) + + for _, name := range []string{"", "default", "unshaped", "unsafe-raw"} { + _, err = snellv6.ParseMode(name) + require.NoError(t, err, name) + } + _, err = snellv6.ParseMode("bogus") + require.Error(t, err) +} diff --git a/transport/singsnell/snellv6/mixing.go b/transport/singsnell/snellv6/mixing.go new file mode 100644 index 0000000000..dfeb78977f --- /dev/null +++ b/transport/singsnell/snellv6/mixing.go @@ -0,0 +1,56 @@ +package snellv6 + +import "github.com/metacubex/sing/common" + +func (p *Profile) mixPaddingPayload(seq uint32, padding []byte, payloadCipher []byte) { + n := common.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 := common.Max(p.mixStride+int(round%3), 1) + if stride == 1 { + for i := 0; i < n; i++ { + 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 := common.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 := 0; i < n; i++ { + 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/transport/singsnell/snellv6/mode.go b/transport/singsnell/snellv6/mode.go new file mode 100644 index 0000000000..f80232e833 --- /dev/null +++ b/transport/singsnell/snellv6/mode.go @@ -0,0 +1,41 @@ +package snellv6 + +import ( + E "github.com/metacubex/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/transport/singsnell/snellv6/packet.go b/transport/singsnell/snellv6/packet.go new file mode 100644 index 0000000000..23b2521fa9 --- /dev/null +++ b/transport/singsnell/snellv6/packet.go @@ -0,0 +1,447 @@ +package snellv6 + +import ( + "io" + "net" + "os" + "sync" + + snell "github.com/metacubex/mihomo/transport/singsnell" + "github.com/metacubex/mihomo/transport/singsnell/internal/reuse" + "github.com/metacubex/sing/common" + "github.com/metacubex/sing/common/buf" + "github.com/metacubex/sing/common/bufio" + E "github.com/metacubex/sing/common/exceptions" + M "github.com/metacubex/sing/common/metadata" + N "github.com/metacubex/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() (snell.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 { + return maxPayload - 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() (snell.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 { + return maxPayload - 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) + _ snell.PacketBatchWriteCreator = (*clientPacketConn)(nil) + _ N.WriterWithMTU = (*clientPacketConn)(nil) + _ snell.PacketBatchWriter = (*clientPacketBatchWriter)(nil) + _ N.PacketConn = (*serverPacketConn)(nil) + _ N.PacketReadWaiter = (*serverPacketConn)(nil) + _ snell.PacketBatchWriteCreator = (*serverPacketConn)(nil) + _ N.WriterWithMTU = (*serverPacketConn)(nil) + _ snell.PacketBatchWriter = (*serverPacketBatchWriter)(nil) +) diff --git a/transport/singsnell/snellv6/profile.go b/transport/singsnell/snellv6/profile.go new file mode 100644 index 0000000000..b146017a78 --- /dev/null +++ b/transport/singsnell/snellv6/profile.go @@ -0,0 +1,523 @@ +package snellv6 + +import ( + "encoding/binary" + "github.com/metacubex/sing/common" + "math/bits" + + snell "github.com/metacubex/mihomo/transport/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 := common.Min(padMin+pick(ns.prfStatic(labelPadMax, 0), 0xa0, 0x3c0), maxExtraPadding) + + prefixMinHS := pick(ns.prfStatic(labelPrefixMin, handshakeDomain), 0x10, 0x60) + prefixMaxHS := common.Min(prefixMinHS+pick(ns.prfStatic(labelPrefixMax, handshakeDomain), 0x10, 0xa0), 0x80) + prefixMinHS = common.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 := common.Min(prefixMinRecord+pick(ns.prfStatic(labelPrefixMax, 0), 0x10, 0xa0), 0x80) + prefixMinRecord = common.Min(prefixMinRecord, prefixMaxRecord) + + chunkInitial := common.Max(0x60, common.Min(pick(ns.prfStatic(labelChunkInitial, 0), 0x200, 0x05b4), 0x05b4)) + firstRecordCap := common.Max(0x100, common.Min(pick(ns.prfStatic(labelChunkFirstCap, chunkInitialDomain), 0x100, 0x300), common.Min(chunkInitial, 0x300))) + chunkMax := common.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: common.Min(pick(ns.prfStatic(labelChunkStep, 0), 0x400, 0x1000), 0x0b68), + chunkJitter: common.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 := 0; index < 8; index++ { + 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 common.Max(0x40, common.Min(chunkSize, p.chunkMax)) +} + +func (p *Profile) nextChunkSize(chunkSize int) int { + if chunkSize == 0 { + return p.chunkInitial + } + return common.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 += common.Min(targetLen-frameLen, maxExtraPadding) + } + if totalLen > 0 { + paddingLen = p.firstRecordPaddingLen(paddingLen, prefixLen, saltPrefixLen, payloadLen) + } + return common.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 = common.Max(targetLen+jitter, 1) + } + spread := common.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 := common.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 := common.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 := common.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/transport/singsnell/snellv6/record.go b/transport/singsnell/snellv6/record.go new file mode 100644 index 0000000000..00c1dbaad7 --- /dev/null +++ b/transport/singsnell/snellv6/record.go @@ -0,0 +1,654 @@ +package snellv6 + +import ( + "crypto/cipher" + "encoding/binary" + "io" + "sync" + + snell "github.com/metacubex/mihomo/transport/singsnell" + "github.com/metacubex/sing/common" + "github.com/metacubex/sing/common/buf" + "github.com/metacubex/sing/common/bufio" + E "github.com/metacubex/sing/common/exceptions" + N "github.com/metacubex/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 := snell.NewBufferSize(r.readWaitOptions, 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) { + for len(p) > 0 { + var data []byte + if len(p) > maxPayload { + data = p[:maxPayload] + p = p[maxPayload:] + } else { + data = p + p = nil + } + buffer := buf.NewSize(snell.HeaderCipherLen + len(data) + snell.AEADTagLen) + w.access.Lock() + w.sealHeader(buffer.Extend(snell.HeaderCipherLen), len(data)) + buffer.Write(data) + w.cipher.Seal(buffer.From(snell.HeaderCipherLen)[:0], w.nonce, buffer.From(snell.HeaderCipherLen), nil) + buffer.Extend(snell.AEADTagLen) + snell.IncreaseNonce(w.nonce) + _, err = w.upstream.Write(buffer.Bytes()) + w.access.Unlock() + buffer.Release() + if err != nil { + return + } + n += len(data) + } + return +} + +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 := common.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 validatePacketVectorBuffers(buffers []*buf.Buffer) error { + for _, buffer := range buffers { + if buffer.Len() > maxPayload { + buf.ReleaseMulti(buffers) + return snell.ErrPayloadTooLarge + } + } + return nil +} + +func (w *packetVectorisedUnshapedWriter) WriteVectorised(buffers []*buf.Buffer) error { + err := validatePacketVectorBuffers(buffers) + if err != nil { + return err + } + 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() { + buffer.Release() + continue + } + 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 := snell.NewBufferSize(r.readWaitOptions, 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) { + for len(p) > 0 { + var data []byte + if len(p) > maxPayload { + data = p[:maxPayload] + p = p[maxPayload:] + } else { + data = p + p = nil + } + buffer := buf.NewSize(snell.HeaderPlainLen + len(data)) + putHeader(buffer.Extend(snell.HeaderPlainLen), 0, len(data)) + buffer.Write(data) + w.access.Lock() + _, err = w.upstream.Write(buffer.Bytes()) + w.access.Unlock() + buffer.Release() + if err != nil { + return + } + n += len(data) + } + return +} + +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 := common.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 { + err := validatePacketVectorBuffers(buffers) + if err != nil { + return err + } + 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() { + buffer.Release() + continue + } + 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) + _ snell.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) + _ snell.VectorisedWriteCreator = (*rawWriter)(nil) + _ N.VectorisedWriter = (*vectorisedRawWriter)(nil) + _ N.VectorisedWriter = (*packetVectorisedRawWriter)(nil) + _ snell.VectorisedWriteCreator = (*shapedWriter)(nil) + _ N.WriterWithMTU = (*unshapedWriter)(nil) + _ N.WriterWithMTU = (*rawWriter)(nil) + _ N.WriterWithMTU = (*shapedWriter)(nil) +) diff --git a/transport/singsnell/snellv6/reuse.go b/transport/singsnell/snellv6/reuse.go new file mode 100644 index 0000000000..2e9c22da18 --- /dev/null +++ b/transport/singsnell/snellv6/reuse.go @@ -0,0 +1,1031 @@ +package snellv6 + +import ( + "context" + "errors" + "io" + "net" + "sync" + "sync/atomic" + "time" + + snell "github.com/metacubex/mihomo/transport/singsnell" + "github.com/metacubex/mihomo/transport/singsnell/internal/reuse" + "github.com/metacubex/sing/common" + "github.com/metacubex/sing/common/buf" + "github.com/metacubex/sing/common/bufio" + E "github.com/metacubex/sing/common/exceptions" + M "github.com/metacubex/sing/common/metadata" + N "github.com/metacubex/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 + } + if snell.KeepSessionFromContext(ctx) { + session.keepOnce.Store(true) + } + 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 + } + // Surge 6.4.4 (10661): -[SNConnectorV4 targetHandshakeData] appends the connector early data to + // the handshake buffer, so the first record carries both. + return c.DialEarlyConn(conn, destination), 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) Reset() { + if c.reuse { + c.pool.Reset() + } +} + +func (c *Client) SetKeepIdleConnections(keep bool) { + c.closeIdle.Store(!keep) + if !keep { + c.CloseIdleConnections() + } +} + +func (c *Client) CloseIdleConnections() { + if c.reuse { + c.pool.Reset() + } +} + +func (c *Client) Close() error { + return c.pool.Close() +} + +type reuseSession struct { + net.Conn + client *Client + + state atomic.Uint32 + keepOnce atomic.Bool + 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") + } + + return &reuseConn{Conn: s.Conn, session: s, destination: destination}, nil +} + +func (s *reuseSession) closeIdle() bool { + return s.client.closeIdle.Load() && !s.keepOnce.Swap(false) +} + +func (s *reuseSession) Release(reusable bool) { + if !reusable || s.closeIdle() { + 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.closeIdle() { + s.Close() + return + } + 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 + + access sync.Mutex + writer reuse.RecordWriter + 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) writeRequest(payload []byte) error { + requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: c.session.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() + + data := request.Bytes() + if c.session.writer == nil { + first := data + if len(first) > maxPayload { + first = data[:maxPayload] + } + c.session.writer, err = writeFirstRecord(c.session.Conn, c.session.client.mode, c.session.client.psk, c.session.client.profile, first) + if err != nil { + c.session.Release(false) + return E.Cause(err, "write request") + } + data = data[len(first):] + } + if len(data) > 0 { + _, err = c.session.writer.Write(data) + if err != nil { + c.session.Release(false) + return E.Cause(err, "write request") + } + } + c.writer = c.session.writer + return nil +} + +func (c *reuseConn) writeRequestBuffer(buffer *buf.Buffer) error { + requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: c.session.client.userKey, Destination: c.destination} + request := buf.With(buffer.ExtendHeader(requestPayload.Len())) + err := requestPayload.Write(request) + if err != nil { + buffer.Release() + return err + } + if c.session.writer == nil { + c.session.writer, err = writeFirstRecordBuffer(c.session.Conn, c.session.client.mode, c.session.client.psk, c.session.client.profile, buffer) + } else { + err = c.session.writer.WriteBuffer(buffer) + } + if err != nil { + c.session.Release(false) + return E.Cause(err, "write request") + } + c.writer = c.session.writer + return nil +} + +func (c *reuseConn) 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 *reuseConn) 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 *reuseConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { + upstreamWriter, created := bufio.CreateVectorisedWriter(c.session.Conn) + if !created { + return nil, false + } + return &reuseVectorisedWriter{conn: c, upstream: upstreamWriter}, true +} + +type reuseVectorisedWriter struct { + conn *reuseConn + upstream N.VectorisedWriter +} + +func (w *reuseVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { + conn := w.conn + if conn.writer != nil { + return conn.writer.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) + } + conn.access.Lock() + defer conn.access.Unlock() + if conn.writer != nil { + return conn.writer.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) + } + for index, buffer := range buffers { + if buffer.IsEmpty() { + buffer.Release() + continue + } + err := conn.writeRequestBuffer(buffer) + if err != nil { + buf.ReleaseMulti(buffers[index+1:]) + return err + } + if index+1 < len(buffers) { + return conn.writer.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers[index+1:]) + } + return nil + } + return nil +} + +func (c *reuseConn) 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 *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 + } + if c.session.closeIdle() { + c.session.Close() + 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 { + if c.writer != nil { + return c.writer.FrontHeadroom() + } + requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: c.session.client.userKey, Destination: c.destination} + switch c.session.client.mode { + case ModeUnsafeRaw: + return requestPayload.Len() + snell.HeaderPlainLen + case ModeUnshaped: + return requestPayload.Len() + snell.SaltLen + snell.HeaderCipherLen + default: + return requestPayload.Len() + c.session.client.profile.saltBlockLen + c.session.client.profile.recordPrefixMax + snell.HeaderCipherLen + c.session.client.profile.padMaxHeadroom + } +} + +func (c *reuseConn) RearHeadroom() int { + if c.writer != nil { + return c.writer.RearHeadroom() + } + if c.session.client.mode == ModeUnsafeRaw { + return 0 + } + return snell.AEADTagLen +} + +func (c *reuseConn) WriterMTU() int { + if c.writer != nil { + return c.writer.WriterMTU() + } + if c.session.client.mode == ModeDefault { + return c.session.client.profile.chunkMax + } + return maxPayload +} + +func (c *reuseConn) NeedHandshakeForRead() bool { + return !c.replyRead.Load() +} + +func (c *reuseConn) NeedHandshakeForWrite() bool { + return c.writer == nil +} + +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 snell.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 = s.service.newPacketConnection(requestCtx, packetConn, source, onClose) + if err != nil { + s.Conn.Close() + return err + } + s.writer = packetConn.writer + callOnClose = false + 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 + writeClosed 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.writeClosed { + return 0, net.ErrClosed + } + 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.writeClosed { + buffer.Release() + return net.ErrClosed + } + 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() + defer conn.access.Unlock() + if conn.writeClosed { + buf.ReleaseMulti(buffers) + return net.ErrClosed + } + if conn.replyWritten { + return conn.session.writer.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) + } + for index, buffer := range buffers { + if buffer.IsEmpty() { + buffer.Release() + continue + } + err := conn.writeResponseBuffer(buffer) + if err != nil { + buf.ReleaseMulti(buffers[index+1:]) + return err + } + if index+1 < len(buffers) { + return conn.session.writer.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers[index+1:]) + } + return nil + } + return nil +} + +func (c *serverReuseConn[U]) CloseWrite() error { + c.closeWriteOnce.Do(func() { + c.access.Lock() + defer c.access.Unlock() + c.writeClosed = true + // 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() { + // The handler may close this side while its peer copy is still reading. + // onClose owns completion and runs only after both copy directions exit. + c.closeErr = c.CloseWrite() + }) + 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) + _ snell.VectorisedWriteCreator = (*reuseConn)(nil) + _ snell.EarlyReader = (*reuseConn)(nil) + _ snell.EarlyWriter = (*reuseConn)(nil) + _ N.WriteCloser = (*reuseConn)(nil) + _ N.VectorisedWriter = (*reuseVectorisedWriter)(nil) + _ N.ReadWaiter = (*reuseReadWaiter)(nil) + _ N.ExtendedConn = (*serverReuseConn[struct{}])(nil) + _ N.ReadWaiter = (*serverReuseConn[struct{}])(nil) + _ snell.VectorisedWriteCreator = (*serverReuseConn[struct{}])(nil) + _ N.VectorisedWriter = (*serverReuseVectorisedWriter[struct{}])(nil) + _ N.WriteCloser = (*serverReuseConn[struct{}])(nil) +) diff --git a/transport/singsnell/snellv6/salt.go b/transport/singsnell/snellv6/salt.go new file mode 100644 index 0000000000..fa0878afd4 --- /dev/null +++ b/transport/singsnell/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 := 0; i < length; i++ { + 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 := 0; i < saltLen; i++ { + 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 := 0; i < saltLen; i++ { + block[perm[i]] = saltMask(p.namespaces.salt, byte(p.mixStrideHandshake), uint32(i)) ^ salt[i] + } +} diff --git a/transport/singsnell/snellv6/server.go b/transport/singsnell/snellv6/server.go new file mode 100644 index 0000000000..9789be6c7d --- /dev/null +++ b/transport/singsnell/snellv6/server.go @@ -0,0 +1,463 @@ +package snellv6 + +import ( + "context" + "io" + "net" + "sync" + + snell "github.com/metacubex/mihomo/transport/singsnell" + "github.com/metacubex/mihomo/transport/singsnell/internal/reuse" + "github.com/metacubex/sing/common/auth" + "github.com/metacubex/sing/common/buf" + "github.com/metacubex/sing/common/bufio" + E "github.com/metacubex/sing/common/exceptions" + M "github.com/metacubex/sing/common/metadata" + N "github.com/metacubex/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 snell.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 snell.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 snell.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() + return s.newPacketConnection(ctx, &serverPacketConn{Conn: conn, service: s, reader: reader}, source, onClose) + 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 *Service) newPacketConnection(ctx context.Context, packetConn *serverPacketConn, source M.Socksaddr, onClose snell.CloseHandlerFunc) error { + err := packetConn.writeTunnelReply() + if err != nil { + return err + } + firstPacket := buf.NewPacket() + destination, err := packetConn.ReadPacket(firstPacket) + if err != nil { + firstPacket.Release() + return E.Cause(err, "read first packet") + } + s.handler.NewPacketConnectionEx(ctx, bufio.NewCachedPacketConn(packetConn, firstPacket, destination), source, destination, onClose) + return nil +} + +func (s *MultiService[U]) newConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose snell.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() + return s.newPacketConnection(requestCtx, &serverPacketConn{Conn: conn, service: s.Service, reader: reader}, source, onClose) + 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) + _ snell.VectorisedWriteCreator = (*serverConn)(nil) + _ N.VectorisedWriter = (*serverVectorisedWriter)(nil) + _ N.WriteCloser = (*serverConn)(nil) +) diff --git a/transport/singsnell/snellv6/shaped.go b/transport/singsnell/snellv6/shaped.go new file mode 100644 index 0000000000..61eb799873 --- /dev/null +++ b/transport/singsnell/snellv6/shaped.go @@ -0,0 +1,463 @@ +package snellv6 + +import ( + "crypto/cipher" + "encoding/binary" + "github.com/metacubex/sing/common" + "io" + "sync" + "time" + + snell "github.com/metacubex/mihomo/transport/singsnell" + "github.com/metacubex/sing/common/buf" + "github.com/metacubex/sing/common/bufio" + E "github.com/metacubex/sing/common/exceptions" + N "github.com/metacubex/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 = common.Min(payloadLimit, w.profile.firstRecordCap) + } + w.chunkSize = w.profile.nextChunkSize(w.chunkSize) + w.lastWriteUnix = nowUnix + return common.Max(1, common.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 := common.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 := common.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 + } + if dataLen > maxPayload { + buffer.Release() + return snell.ErrPayloadTooLarge + } + w.access.Lock() + defer w.access.Unlock() + // snell-server v6.0.0rc: UDP packet mode still advances the dynamic chunk + // state, but emits each complete datagram as one record regardless of the + // current stream chunk limit. + w.payloadLimitFor(time.Now()) + 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 := common.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 { + err := validatePacketVectorBuffers(buffers) + if err != nil { + return err + } + 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() { + buffer.Release() + continue + } + recordWriter.payloadLimitFor(time.Now()) + 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 := snell.NewBufferSize(r.readWaitOptions, 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/transport/singsnell/snellv6/streaming_test.go b/transport/singsnell/snellv6/streaming_test.go new file mode 100644 index 0000000000..760409c319 --- /dev/null +++ b/transport/singsnell/snellv6/streaming_test.go @@ -0,0 +1,105 @@ +package snellv6_test + +import ( + "bytes" + "context" + "io" + "net" + "testing" + "time" + + snell "github.com/metacubex/mihomo/transport/singsnell" + "github.com/metacubex/mihomo/transport/singsnell/snellv6" + + M "github.com/metacubex/sing/common/metadata" + N "github.com/metacubex/sing/common/network" + "github.com/stretchr/testify/require" +) + +// streamingEchoHandler echoes bytes as they arrive, like a real relay would. +type streamingEchoHandler struct{} + +func (streamingEchoHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose snell.CloseHandlerFunc) { + go func() { + _, err := io.Copy(conn, conn) + _ = N.CloseWrite(conn) + _ = conn.Close() + onClose(err) + }() +} + +func (streamingEchoHandler) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose snell.CloseHandlerFunc) { + echoHandler{}.NewPacketConnectionEx(ctx, conn, source, destination, onClose) +} + +func startStreamingServer(t *testing.T, mode snellv6.Mode) string { + t.Helper() + service, err := snellv6.NewService(snellv6.ServerOptions{PSK: []byte(testPSK), Mode: mode, Handler: streamingEchoHandler{}}) + require.NoError(t, err) + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { listener.Close() }) + go func() { + for { + conn, err := listener.Accept() + if err != nil { + return + } + go func() { + if err := service.NewConnection(context.Background(), conn, M.SocksaddrFromNet(conn.RemoteAddr()), func(error) {}); err != nil { + conn.Close() + } + }() + } + }() + return listener.Addr().String() +} + +// streamingRoundTrip writes while concurrently reading, without half-closing +// first, which is how a proxy relay drives the connection. +func streamingRoundTrip(t *testing.T, conn net.Conn, payload []byte) { + t.Helper() + go func() { _, _ = conn.Write(payload) }() + received := make([]byte, len(payload)) + require.NoError(t, conn.SetReadDeadline(time.Now().Add(10*time.Second))) + _, err := io.ReadFull(conn, received) + require.NoError(t, err) + require.True(t, bytes.Equal(payload, received), "payload mismatch") +} + +func TestStreamingReuse(t *testing.T) { + for name, mode := range testModes { + t.Run(name, func(t *testing.T) { + address := startStreamingServer(t, mode) + client, err := snellv6.NewClient(snellv6.ClientOptions{ + PSK: []byte(testPSK), Mode: mode, Reuse: true, + Dialer: N.SystemDialer, Server: M.ParseSocksaddr(address), + }) + require.NoError(t, err) + defer client.Close() + for round := 0; round < 2; round++ { + conn, err := client.DialContext(context.Background(), M.ParseSocksaddrHostPort("example.com", 443)) + require.NoError(t, err) + streamingRoundTrip(t, conn, randomPayload(t, 16*1024)) + require.NoError(t, conn.Close()) + time.Sleep(50 * time.Millisecond) + } + }) + } +} + +func TestStreamingNoReuse(t *testing.T) { + for name, mode := range testModes { + t.Run(name, func(t *testing.T) { + address := startStreamingServer(t, mode) + client, err := snellv6.NewClient(snellv6.ClientOptions{PSK: []byte(testPSK), Mode: mode}) + require.NoError(t, err) + serverConn, err := net.Dial("tcp", address) + require.NoError(t, err) + defer serverConn.Close() + conn, err := client.DialConn(serverConn, M.ParseSocksaddrHostPort("example.com", 443)) + require.NoError(t, err) + streamingRoundTrip(t, conn, randomPayload(t, 16*1024)) + }) + } +} diff --git a/transport/snell/snell.go b/transport/snell/snell.go index 12548dd89e..c9b2d92528 100644 --- a/transport/snell/snell.go +++ b/transport/snell/snell.go @@ -20,6 +20,7 @@ const ( Version3 = 3 Version4 = 4 Version5 = 5 + Version6 = 6 DefaultSnellVersion = Version1 // max packet length From 7a7c0903b629759ac65463611ffb5d1d677dd910 Mon Sep 17 00:00:00 2001 From: hungrybirder Date: Thu, 5 Nov 2026 09:47:52 +0800 Subject: [PATCH 2/4] feat: add snell version 6 inbound Accept `version: 6` on the snell listener, with a `mode` option (default / unshaped / unsafe-raw) that must match the client. The listener builds a snellv6 service from transport/singsnell and forwards its TCP and UDP connections into the mihomo tunnel. v6 rejects PSKs outside 12..255 bytes and the http/tls simple-obfs layer, while the shadow-tls/restls/jls wrappers keep working. Co-Authored-By: Claude Fable 5.1 --- docs/config.yaml | 5 +- listener/config/snell.go | 1 + listener/inbound/snell.go | 4 +- listener/inbound/snell_test.go | 173 +++++++++++++++++++++++++++++++++ listener/snell/server.go | 20 +++- listener/snell/v6.go | 133 +++++++++++++++++++++++++ 6 files changed, 332 insertions(+), 4 deletions(-) create mode 100644 listener/snell/v6.go diff --git a/docs/config.yaml b/docs/config.yaml index 2e6f9b262f..b176041382 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -2196,10 +2196,11 @@ listeners: listen: 0.0.0.0 # routing-mark: 0 # 为监听socket设置routing-mark(仅支持linux) psk: your-password - version: 4 # 支持 1/2/3/4/5 + version: 4 # 支持 1/2/3/4/5/6 udp: true # UDP over TCP tunnel,默认 true + # mode: default # 仅 v6:default / unshaped / unsafe-raw,需与客户端一致;v6 的 psk 长度须为 12 到 255 字节 # obfs-opts: - # mode: http # 可选:http / tls + # mode: http # 可选:http / tls(v6 不支持,配置时会报错) # host: bing.com # shadow-tls: # enable: false # 设置为true时开启 diff --git a/listener/config/snell.go b/listener/config/snell.go index d4f9aef7f0..99f4a7725b 100644 --- a/listener/config/snell.go +++ b/listener/config/snell.go @@ -6,6 +6,7 @@ type SnellServer struct { Listen string Psk string Version int + Mode string // v6 only: default / unshaped / unsafe-raw UDP bool ObfsMode string ObfsHost string diff --git a/listener/inbound/snell.go b/listener/inbound/snell.go index 93c52d3cac..36d3bc08cb 100644 --- a/listener/inbound/snell.go +++ b/listener/inbound/snell.go @@ -14,6 +14,7 @@ type SnellOption struct { BaseOption Psk string `inbound:"psk"` Version int `inbound:"version,omitempty"` + Mode string `inbound:"mode,omitempty"` // v6 only: default / unshaped / unsafe-raw UDP bool `inbound:"udp,omitempty"` ObfsOpts SnellObfsOption `inbound:"obfs-opts,omitempty"` ShadowTLS ShadowTLS `inbound:"shadow-tls,omitempty"` @@ -45,7 +46,7 @@ func NewSnell(options *SnellOption) (*Snell, error) { if options.Version == 0 { options.Version = 4 } - if options.Version < 1 || options.Version > 5 { + if options.Version < 1 || options.Version > 6 { return nil, fmt.Errorf("snell inbound version %d is not supported", options.Version) } @@ -56,6 +57,7 @@ func NewSnell(options *SnellOption) (*Snell, error) { Listen: base.RawAddress(), Psk: options.Psk, Version: options.Version, + Mode: options.Mode, UDP: options.UDP, ObfsMode: options.ObfsOpts.Mode, ObfsHost: options.ObfsOpts.Host, diff --git a/listener/inbound/snell_test.go b/listener/inbound/snell_test.go index ff53a2025b..929dab57e7 100644 --- a/listener/inbound/snell_test.go +++ b/listener/inbound/snell_test.go @@ -1,11 +1,16 @@ package inbound_test import ( + "bytes" + "context" + "crypto/rand" "net" "net/netip" "testing" + "time" "github.com/metacubex/mihomo/adapter/outbound" + C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/listener/inbound" "github.com/metacubex/mihomo/transport/jls" "github.com/metacubex/mihomo/transport/restls" @@ -157,3 +162,171 @@ func TestInboundSnell_JLS(t *testing.T) { } testInboundSnellUTLS(t, inboundOptions, outboundOptions) } + +func TestInboundSnell_V6(t *testing.T) { + for _, mode := range []string{"default", "unshaped", "unsafe-raw"} { + mode := mode + t.Run(mode, func(t *testing.T) { + t.Parallel() + t.Run("TCP", func(t *testing.T) { + inboundOptions := inbound.SnellOption{Version: 6, Mode: mode} + outboundOptions := outbound.SnellOption{Version: 6, Mode: mode} + testInboundSnell(t, inboundOptions, outboundOptions) + }) + t.Run("TCPReuse", func(t *testing.T) { + inboundOptions := inbound.SnellOption{Version: 6, Mode: mode} + outboundOptions := outbound.SnellOption{Version: 6, Mode: mode, Reuse: true} + testInboundSnell(t, inboundOptions, outboundOptions) + }) + t.Run("UDP", func(t *testing.T) { + testInboundSnellV6UDP(t, mode) + }) + }) + } +} + +func TestInboundSnell_V6_ShadowTlsv3(t *testing.T) { + inboundOptions := inbound.SnellOption{ + Version: 6, + ShadowTLS: inbound.ShadowTLS{ + Enable: true, + Version: 3, + Users: []inbound.ShadowTLSUser{{Name: "test", Password: shadowsocksPassword16}}, + Handshake: inbound.ShadowTLSHandshakeOptions{Dest: net.JoinHostPort(realityDest, "443")}, + }, + } + outboundOptions := outbound.SnellOption{ + Version: 6, + ObfsOpts: map[string]any{"mode": shadowtls.Mode, "host": realityDest, "password": shadowsocksPassword16, "fingerprint": tlsFingerprint, "version": 3}, + } + testInboundSnellUTLS(t, inboundOptions, outboundOptions) +} + +// testInboundSnellV6UDP runs a UDP-over-TCP echo through the v6 inbound: the +// test tunnel reflects every datagram back to the source it came from. +func testInboundSnellV6UDP(t *testing.T, mode string) { + t.Parallel() + inboundOptions := inbound.SnellOption{ + BaseOption: inbound.BaseOption{ + NameStr: "snell_inbound_udp", + Listen: "127.0.0.1", + Port: "0", + }, + Psk: userUUID, + Version: 6, + Mode: mode, + UDP: true, + } + in, err := inbound.NewSnell(&inboundOptions) + if !assert.NoError(t, err) { + return + } + + tunnel := NewHttpTestTunnel() + defer tunnel.Close() + tunnel.HandleUDPPacketFn = func(packet C.UDPPacket, metadata *C.Metadata) { + defer packet.Drop() + if !assert.Equal(t, C.SNELL, metadata.Type) { + return + } + _, err := packet.WriteBack(packet.Data(), metadata.UDPAddr()) + assert.NoError(t, err) + } + + err = in.Listen(tunnel) + if !assert.NoError(t, err) { + return + } + defer in.Close() + + addrPort, err := netip.ParseAddrPort(in.Address()) + if !assert.NoError(t, err) { + return + } + + outboundOptions := outbound.SnellOption{ + Name: "snell_outbound_udp", + Server: addrPort.Addr().String(), + Port: int(addrPort.Port()), + Psk: userUUID, + Version: 6, + Mode: mode, + UDP: true, + } + outboundOptions.DialerForAPI = tunnel.NewDialer() + outboundOptions.TunnelForAPI = tunnel + out, err := outbound.NewSnell(outboundOptions) + if !assert.NoError(t, err) { + return + } + defer out.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + target := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 53} + pc, err := out.ListenPacketContext(ctx, &C.Metadata{ + NetWork: C.UDP, + DstIP: netip.MustParseAddr("127.0.0.1"), + DstPort: 53, + }) + if !assert.NoError(t, err) { + return + } + defer pc.Close() + + for round := 0; round < 5; round++ { + message := make([]byte, 1200) + _, err = rand.Read(message) + if !assert.NoError(t, err) { + return + } + _, err = pc.WriteTo(message, target) + if !assert.NoError(t, err) { + return + } + + received := make([]byte, 2048) + if !assert.NoError(t, pc.SetReadDeadline(time.Now().Add(10*time.Second))) { + return + } + n, from, err := pc.ReadFrom(received) + if !assert.NoError(t, err, "round %d", round) { + return + } + assert.Equal(t, target.String(), from.String()) + assert.True(t, bytes.Equal(message, received[:n]), "round %d payload mismatch", round) + } +} + +func TestInboundSnell_V6_Validation(t *testing.T) { + t.Parallel() + newInbound := func(options inbound.SnellOption) error { + options.BaseOption = inbound.BaseOption{ + NameStr: "snell_inbound_validation", + Listen: "127.0.0.1", + Port: "0", + } + if options.Psk == "" { + options.Psk = userUUID + } + in, err := inbound.NewSnell(&options) + if err != nil { + return err + } + tunnel := NewHttpTestTunnel() + defer tunnel.Close() + err = in.Listen(tunnel) + if err == nil { + _ = in.Close() + } + return err + } + + assert.Error(t, newInbound(inbound.SnellOption{Version: 6, Psk: "short"}), "psk shorter than 12 bytes must be rejected") + assert.Error(t, newInbound(inbound.SnellOption{Version: 6, ObfsOpts: inbound.SnellObfsOption{Mode: "http"}}), "v6 has no simple-obfs layer") + assert.Error(t, newInbound(inbound.SnellOption{Version: 6, ObfsOpts: inbound.SnellObfsOption{Mode: "tls"}}), "v6 has no simple-obfs layer") + assert.Error(t, newInbound(inbound.SnellOption{Version: 6, Mode: "bogus"}), "unknown mode must be rejected") + assert.Error(t, newInbound(inbound.SnellOption{Version: 4, Mode: "unshaped"}), "mode is v6 only") + assert.Error(t, newInbound(inbound.SnellOption{Version: 7}), "unknown version must be rejected") + assert.NoError(t, newInbound(inbound.SnellOption{Version: 6, Mode: "unshaped"})) +} diff --git a/listener/snell/server.go b/listener/snell/server.go index e8c0a87a8f..c3b09f3ab8 100644 --- a/listener/snell/server.go +++ b/listener/snell/server.go @@ -22,6 +22,7 @@ import ( "github.com/metacubex/mihomo/listener/shadowtls" "github.com/metacubex/mihomo/transport/shadowsocks/shadowaead" obfs "github.com/metacubex/mihomo/transport/simple-obfs" + "github.com/metacubex/mihomo/transport/singsnell/snellv6" "github.com/metacubex/mihomo/transport/snell" ) @@ -31,6 +32,7 @@ type Listener struct { closed bool config LC.SnellServer listeners []net.Listener + v6 *snellv6.Service // only set for version 6 } func New(config LC.SnellServer, lc C.InboundListenConfig, tunnel C.Tunnel, additions ...inbound.Addition) (C.MultiAddrListener, error) { @@ -39,6 +41,10 @@ func New(config LC.SnellServer, lc C.InboundListenConfig, tunnel C.Tunnel, addit } switch config.Version { case snell.Version1, snell.Version2, snell.Version3, snell.Version4, snell.Version5: + if config.Mode != "" { + return nil, errors.New("snell inbound mode is only supported on version 6") + } + case snell.Version6: default: return nil, fmt.Errorf("snell inbound version %d is not supported", config.Version) } @@ -52,6 +58,13 @@ func New(config LC.SnellServer, lc C.InboundListenConfig, tunnel C.Tunnel, addit } l := &Listener{config: config} + if config.Version == snell.Version6 { + var err error + l.v6, err = newV6Service(config, tunnel) + if err != nil { + return nil, err + } + } securityModes := make([]string, 0, 3) if config.ShadowTLS.Enable { @@ -144,7 +157,6 @@ func (l *Listener) AddrList() (addrList []net.Addr) { } func (l *Listener) HandleConn(rawConn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { - defer rawConn.Close() user, loaded := shadowtls.UserFromConn(rawConn) if jlsUser, jlsLoaded := jls.UserFromConn(rawConn); jlsLoaded { user, loaded = jlsUser, true @@ -152,6 +164,12 @@ func (l *Listener) HandleConn(rawConn net.Conn, tunnel C.Tunnel, additions ...in if loaded { additions = append(append([]inbound.Addition(nil), additions...), inbound.WithInUser(user)) } + if l.v6 != nil { + // the v6 service and its handler own rawConn from here on + l.handleV6Conn(rawConn, additions...) + return + } + defer rawConn.Close() conn := rawConn switch l.config.ObfsMode { case "http": diff --git a/listener/snell/v6.go b/listener/snell/v6.go new file mode 100644 index 0000000000..ec253286fb --- /dev/null +++ b/listener/snell/v6.go @@ -0,0 +1,133 @@ +package snell + +import ( + "context" + "errors" + "fmt" + "net" + + "github.com/metacubex/mihomo/adapter/inbound" + N "github.com/metacubex/mihomo/common/net" + C "github.com/metacubex/mihomo/constant" + LC "github.com/metacubex/mihomo/listener/config" + "github.com/metacubex/mihomo/listener/sing" + "github.com/metacubex/mihomo/log" + singsnell "github.com/metacubex/mihomo/transport/singsnell" + "github.com/metacubex/mihomo/transport/singsnell/snellv6" + + "github.com/metacubex/sing/common/bufio/deadline" + M "github.com/metacubex/sing/common/metadata" + singN "github.com/metacubex/sing/common/network" +) + +// newV6Service validates the v6 specific parts of the config and builds the +// snellv6 server bound to a handler that forwards into the mihomo tunnel. +func newV6Service(config LC.SnellServer, tunnel C.Tunnel) (*snellv6.Service, error) { + // snell-server v6 rejects PSKs outside the 12..255-byte range + if len(config.Psk) < 12 || len(config.Psk) > 255 { + return nil, errors.New("snell inbound version 6 psk length must be between 12 and 255 bytes") + } + if config.ObfsMode != "" { + // v6 replaced the simple-obfs layer with PSK-derived traffic shaping + return nil, fmt.Errorf("snell inbound version 6 does not support obfs mode %s", config.ObfsMode) + } + mode, err := snellv6.ParseMode(config.Mode) + if err != nil { + return nil, fmt.Errorf("snell inbound %w", err) + } + handler, err := newV6Handler(tunnel, config.UDP) + if err != nil { + return nil, err + } + return snellv6.NewService(snellv6.ServerOptions{ + PSK: []byte(config.Psk), + Mode: mode, + Handler: handler, + }) +} + +// handleV6Conn hands a freshly accepted (and already shadow-tls/restls/jls +// unwrapped) connection to the snellv6 server. Per-connection additions (such +// as the outer layer's user) travel through the context so a single handler can +// serve every connection. Ownership of rawConn moves to the service/handler on +// success; it is only closed here when the handshake fails. +func (l *Listener) handleV6Conn(rawConn net.Conn, additions ...inbound.Addition) { + ctx := sing.WithAdditions(context.Background(), additions...) + ctx = sing.WithInAddr(ctx, rawConn.LocalAddr()) + source := M.SocksaddrFromNet(rawConn.RemoteAddr()) + err := l.v6.NewConnection(ctx, rawConn, source, func(error) {}) + if err != nil { + _ = rawConn.Close() + log.Debugln("snell v6 inbound from %s: %v", rawConn.RemoteAddr(), err) + } +} + +// v6Handler bridges snellv6's TCPConnectionHandlerEx / UDPConnectionHandlerEx +// callbacks into mihomo tunnel calls. TCP is handled directly, UDP reuses the +// packet loop of the sing listener bridge. +type v6Handler struct { + tunnel C.Tunnel + udp bool + packet *sing.ListenerHandler +} + +func newV6Handler(tunnel C.Tunnel, udp bool) (*v6Handler, error) { + packetHandler, err := sing.NewListenerHandler(sing.ListenerConfig{ + Tunnel: tunnel, + Type: C.SNELL, + UDPTimeout: sing.UDPTimeout, + }) + if err != nil { + return nil, err + } + return &v6Handler{tunnel: tunnel, udp: udp, packet: packetHandler}, nil +} + +func (h *v6Handler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose singsnell.CloseHandlerFunc) { + if deadline.NeedAdditionalReadDeadline(conn) { + conn = N.NewDeadlineConn(conn) // conn from sing should check NeedAdditionalReadDeadline + } + + metadata := &C.Metadata{ + NetWork: C.TCP, + Type: C.SNELL, + } + if source.IsIP() && source.Fqdn == "" { + metadata.RawSrcAddr = source.Unwrap().TCPAddr() + } + if destination.IsIP() && destination.Fqdn == "" { + metadata.RawDstAddr = destination.Unwrap().TCPAddr() + } + inbound.ApplyAdditions(metadata, inbound.WithDstAddr(destination), inbound.WithSrcAddr(source), inbound.WithInAddr(conn.LocalAddr())) + inbound.ApplyAdditions(metadata, sing.AdditionsFromContext(ctx)...) + + // The reuse session blocks until onClose is called, so the tunnel must run + // in its own goroutine. HandleTCPConn closes conn once both directions end. + go func() { + h.tunnel.HandleTCPConn(conn, metadata) + if onClose != nil { + onClose(nil) + } + }() +} + +func (h *v6Handler) NewPacketConnectionEx(ctx context.Context, conn singN.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose singsnell.CloseHandlerFunc) { + if !h.udp { + _ = conn.Close() + if onClose != nil { + onClose(errors.New("snell UDP is disabled")) + } + return + } + go func() { + err := h.packet.NewPacketConnection(ctx, conn, M.Metadata{ + Source: source, + Destination: destination, + }) + if onClose != nil { + onClose(err) + } + }() +} + +var _ singsnell.Handler = (*v6Handler)(nil) From 8c0ef64a37e7391e205ab5bf0e24ac61163296b1 Mon Sep 17 00:00:00 2001 From: hungrybirder Date: Thu, 5 Nov 2026 11:06:42 +0800 Subject: [PATCH 3/4] refactor: implement snell v6 natively on transport/snell Replace the ported sing-snell package with a native implementation that builds on the existing transport/snell infrastructure, the same way v4/v5 are structured. A v6Conn wraps the raw connection with the v6 record layer and the existing request/reply, UDP packet, pool and listener code is reused unchanged, since v6 kept the inner request format. v6 keeps the v4 AES-128-GCM record framing and adds a PSK-derived profile (transport/snell/v6_profile.go): the salt is scattered inside a padded block, each record carries an unencrypted prefix used as the header AAD, padding is interleaved with the payload ciphertext and record sizes follow a shaped schedule. The derivation follows the protocol notes published by SagerNet/sing-snell. The `unshaped` mode drops the profile layer and `unsafe-raw` drops the crypto as well. The sing-based listener bridge and the transport/singsnell package are removed; the inbound now uses ServerStreamConnV6 in its normal request loop. Before removal, the native layer was cross-tested against the ported package in all three modes (TCP and UDP, both directions). Co-Authored-By: Claude Fable 5.1 --- adapter/outbound/snell.go | 126 +- adapter/outbound/snell_v6_test.go | 218 ---- listener/snell/server.go | 28 +- listener/snell/v6.go | 133 --- transport/singsnell/LICENSE | 14 - transport/singsnell/README.md | 13 - transport/singsnell/address.go | 97 -- transport/singsnell/compat.go | 56 - transport/singsnell/crypto.go | 31 - transport/singsnell/internal/reuse/reuse.go | 291 ----- transport/singsnell/request.go | 97 -- transport/singsnell/snell.go | 94 -- transport/singsnell/snellv6/client.go | 369 ------ transport/singsnell/snellv6/handshake.go | 216 ---- transport/singsnell/snellv6/loopback_test.go | 238 ---- transport/singsnell/snellv6/mixing.go | 56 - transport/singsnell/snellv6/mode.go | 41 - transport/singsnell/snellv6/packet.go | 447 ------- transport/singsnell/snellv6/profile.go | 523 --------- transport/singsnell/snellv6/record.go | 654 ----------- transport/singsnell/snellv6/reuse.go | 1031 ----------------- transport/singsnell/snellv6/salt.go | 56 - transport/singsnell/snellv6/server.go | 463 -------- transport/singsnell/snellv6/shaped.go | 463 -------- transport/singsnell/snellv6/streaming_test.go | 105 -- transport/snell/v6.go | 663 +++++++++++ transport/snell/v6_profile.go | 650 +++++++++++ transport/snell/v6_test.go | 204 ++++ 28 files changed, 1543 insertions(+), 5834 deletions(-) delete mode 100644 adapter/outbound/snell_v6_test.go delete mode 100644 listener/snell/v6.go delete mode 100644 transport/singsnell/LICENSE delete mode 100644 transport/singsnell/README.md delete mode 100644 transport/singsnell/address.go delete mode 100644 transport/singsnell/compat.go delete mode 100644 transport/singsnell/crypto.go delete mode 100644 transport/singsnell/internal/reuse/reuse.go delete mode 100644 transport/singsnell/request.go delete mode 100644 transport/singsnell/snell.go delete mode 100644 transport/singsnell/snellv6/client.go delete mode 100644 transport/singsnell/snellv6/handshake.go delete mode 100644 transport/singsnell/snellv6/loopback_test.go delete mode 100644 transport/singsnell/snellv6/mixing.go delete mode 100644 transport/singsnell/snellv6/mode.go delete mode 100644 transport/singsnell/snellv6/packet.go delete mode 100644 transport/singsnell/snellv6/profile.go delete mode 100644 transport/singsnell/snellv6/record.go delete mode 100644 transport/singsnell/snellv6/reuse.go delete mode 100644 transport/singsnell/snellv6/salt.go delete mode 100644 transport/singsnell/snellv6/server.go delete mode 100644 transport/singsnell/snellv6/shaped.go delete mode 100644 transport/singsnell/snellv6/streaming_test.go create mode 100644 transport/snell/v6.go create mode 100644 transport/snell/v6_profile.go create mode 100644 transport/snell/v6_test.go diff --git a/adapter/outbound/snell.go b/adapter/outbound/snell.go index be20274d9c..ce5c58c0f2 100644 --- a/adapter/outbound/snell.go +++ b/adapter/outbound/snell.go @@ -2,7 +2,6 @@ package outbound import ( "context" - "errors" "fmt" "net" "strconv" @@ -14,11 +13,7 @@ import ( "github.com/metacubex/mihomo/transport/restls" "github.com/metacubex/mihomo/transport/shadowtls" obfs "github.com/metacubex/mihomo/transport/simple-obfs" - "github.com/metacubex/mihomo/transport/singsnell/snellv6" "github.com/metacubex/mihomo/transport/snell" - - M "github.com/metacubex/sing/common/metadata" - singN "github.com/metacubex/sing/common/network" ) type Snell struct { @@ -26,12 +21,12 @@ type Snell struct { option *SnellOption psk []byte pool *snell.Pool - v6 *snellv6.Client obfsOption *simpleObfsOption shadowTLSOption *shadowtls.ShadowTLSOption restlsConfig *restls.Config jlsConfig *jls.ClientConfig version int + v6Mode snell.V6Mode reuse bool } @@ -49,14 +44,7 @@ type SnellOption struct { ClientFingerprint string `proxy:"client-fingerprint,omitempty"` } -var ( - errSnellV6UDPStream = errors.New("snell v6 udp must be created with ListenPacketContext") - errSnellV6ListenPacket = errors.New("snell v6 dialer does not support ListenPacket") -) - -// obfsConnContext wraps the raw server connection with the configured -// obfuscation layer, which is shared by every Snell version. -func (s *Snell) obfsConnContext(ctx context.Context, c net.Conn) (net.Conn, error) { +func (s *Snell) streamConnContext(ctx context.Context, c net.Conn) (*snell.Snell, error) { var err error switch s.obfsOption.Mode { case "tls": @@ -80,22 +68,14 @@ func (s *Snell) obfsConnContext(ctx context.Context, c net.Conn) (net.Conn, erro return nil, err } } - return c, nil -} - -func (s *Snell) streamConnContext(ctx context.Context, c net.Conn) (*snell.Snell, error) { - c, err := s.obfsConnContext(ctx, c) - if err != nil { - return nil, err + if s.version == snell.Version6 { + return snell.StreamConnV6(c, s.psk, s.v6Mode), nil } return snell.StreamConn(c, s.psk, s.version), nil } // StreamConnContext implements C.ProxyAdapter func (s *Snell) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { - if s.version == snell.Version6 { - return s.streamV6ConnContext(ctx, c, metadata) - } c, err := s.streamConnContext(ctx, c) if err != nil { return nil, err @@ -104,25 +84,6 @@ func (s *Snell) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M return c, err } -func (s *Snell) streamV6ConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) { - if metadata.NetWork == C.UDP { - return nil, errSnellV6UDPStream - } - c, err = s.obfsConnContext(ctx, c) - if err != nil { - return nil, err - } - destination := M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort) - if N.NeedHandshake(c) { - return s.v6.DialEarlyConn(c, destination), nil - } - if ctx.Done() != nil { - done := N.SetupContextForConn(ctx, c) - defer done(&err) - } - return s.v6.DialConn(c, destination) -} - func (s *Snell) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (err error) { if ctx.Done() != nil { done := N.SetupContextForConn(ctx, c) @@ -145,15 +106,6 @@ func (s *Snell) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C. // DialContext implements C.ProxyAdapter func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { if s.reuse { - if s.version == snell.Version6 { - // the v6 client owns its own session pool and dials through v6Dialer - c, err := s.v6.DialContext(ctx, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", s.addr, err) - } - return NewConn(c, s), nil - } - c, err := s.pool.Get() if err != nil { return nil, err @@ -196,19 +148,6 @@ func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata) ( safeConnClose(c, err) }(c) - if s.version == snell.Version6 { - c, err = s.obfsConnContext(ctx, c) - if err != nil { - return nil, err - } - var pc singN.NetPacketConn - pc, err = s.v6.DialPacketConn(c) - if err != nil { - return nil, err - } - return NewPacketConn(N.NewThreadSafePacketConn(pc), s), nil - } - c, err = s.StreamConnContext(ctx, c, metadata) if err != nil { return nil, err @@ -230,38 +169,6 @@ func (s *Snell) ProxyInfo() C.ProxyInfo { return info } -// Close implements C.ProxyAdapter -func (s *Snell) Close() error { - if s.v6 != nil { - _ = s.v6.Close() - } - return s.Base.Close() -} - -// v6Dialer lets the snellv6 session pool dial the server through the -// adapter's own dialer and obfuscation settings. -type v6Dialer struct { - s *Snell -} - -func (d v6Dialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - c, err := d.s.dialer.DialContext(ctx, "tcp", d.s.addr) - if err != nil { - return nil, err - } - c, err = d.s.obfsConnContext(ctx, c) - if err != nil { - return nil, err - } - return c, nil -} - -func (d v6Dialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - return nil, errSnellV6ListenPacket -} - -var _ singN.Dialer = v6Dialer{} - func NewSnell(option SnellOption) (*Snell, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) psk := []byte(option.Psk) @@ -349,7 +256,7 @@ func NewSnell(option SnellOption) (*Snell, error) { } reuse := option.Version == snell.Version2 || ((option.Version == snell.Version4 || option.Version == snell.Version6) && option.Reuse) - var v6Mode snellv6.Mode + var v6Mode snell.V6Mode switch option.Version { case snell.Version1, snell.Version2: if option.UDP { @@ -357,9 +264,8 @@ func NewSnell(option SnellOption) (*Snell, error) { } case snell.Version3, snell.Version4: case snell.Version6: - // snell-server v6 rejects PSKs outside the 12..255-byte range - if len(psk) < 12 || len(psk) > 255 { - return nil, fmt.Errorf("snell %s version 6 psk length must be between 12 and 255 bytes", addr) + if err := snell.ValidateV6PSK(psk); err != nil { + return nil, fmt.Errorf("snell %s %w", addr, err) } switch obfsOption.Mode { case "tls", "http": @@ -367,7 +273,7 @@ func NewSnell(option SnellOption) (*Snell, error) { return nil, fmt.Errorf("snell %s version 6 does not support obfs mode %s", addr, obfsOption.Mode) } var err error - v6Mode, err = snellv6.ParseMode(option.Mode) + v6Mode, err = snell.ParseV6Mode(option.Mode) if err != nil { return nil, fmt.Errorf("snell %s %w", addr, err) } @@ -398,25 +304,11 @@ func NewSnell(option SnellOption) (*Snell, error) { restlsConfig: restlsConfig, jlsConfig: jlsConfig, version: option.Version, + v6Mode: v6Mode, reuse: reuse, } s.dialer = option.NewDialer(s.DialOptions()) - if s.version == snell.Version6 { - client, err := snellv6.NewClient(snellv6.ClientOptions{ - PSK: psk, - Mode: v6Mode, - Reuse: reuse, - Dialer: v6Dialer{s: s}, - Server: M.ParseSocksaddr(addr), - }) - if err != nil { - return nil, fmt.Errorf("snell %s %w", addr, err) - } - s.v6 = client - return s, nil - } - if s.reuse { s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) { c, err := s.dialer.DialContext(ctx, "tcp", addr) diff --git a/adapter/outbound/snell_v6_test.go b/adapter/outbound/snell_v6_test.go deleted file mode 100644 index 2afe6df13d..0000000000 --- a/adapter/outbound/snell_v6_test.go +++ /dev/null @@ -1,218 +0,0 @@ -package outbound - -import ( - "bytes" - "context" - "crypto/rand" - "io" - "net" - "net/netip" - "strconv" - "testing" - "time" - - C "github.com/metacubex/mihomo/constant" - snell "github.com/metacubex/mihomo/transport/singsnell" - "github.com/metacubex/mihomo/transport/singsnell/snellv6" - - M "github.com/metacubex/sing/common/metadata" - N "github.com/metacubex/sing/common/network" - "github.com/stretchr/testify/require" -) - -const snellV6TestPSK = "mihomo-snell-v6-outbound-test-psk" - -type snellV6EchoHandler struct{} - -func (snellV6EchoHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose snell.CloseHandlerFunc) { - go func() { - var err error - defer func() { - _ = N.CloseWrite(conn) - _ = conn.Close() - onClose(err) - }() - var payload []byte - payload, err = io.ReadAll(conn) - if err != nil { - return - } - _, err = conn.Write(payload) - }() -} - -func (snellV6EchoHandler) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose snell.CloseHandlerFunc) { - go func() { - defer conn.Close() - readWaitOptions := N.NewReadWaitOptions(conn, conn) - for { - buffer := readWaitOptions.NewPacketBuffer() - dest, err := conn.ReadPacket(buffer) - if err != nil { - buffer.Release() - return - } - if err = conn.WritePacket(buffer, dest); err != nil { - return - } - } - }() -} - -func startSnellV6Server(t *testing.T, mode snellv6.Mode) (host string, port int) { - t.Helper() - service, err := snellv6.NewService(snellv6.ServerOptions{ - PSK: []byte(snellV6TestPSK), - Mode: mode, - Handler: snellV6EchoHandler{}, - }) - require.NoError(t, err) - listener, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - t.Cleanup(func() { listener.Close() }) - go func() { - for { - conn, err := listener.Accept() - if err != nil { - return - } - go func() { - if err := service.NewConnection(context.Background(), conn, M.SocksaddrFromNet(conn.RemoteAddr()), func(error) {}); err != nil { - conn.Close() - } - }() - } - }() - host, portStr, err := net.SplitHostPort(listener.Addr().String()) - require.NoError(t, err) - port, err = strconv.Atoi(portStr) - require.NoError(t, err) - return host, port -} - -func newSnellV6Outbound(t *testing.T, mode string, reuse bool) *Snell { - t.Helper() - host, port := startSnellV6Server(t, mustParseV6Mode(t, mode)) - proxy, err := NewSnell(SnellOption{ - Name: "snell-v6-" + mode, - Server: host, - Port: port, - Psk: snellV6TestPSK, - Version: 6, - UDP: true, - Reuse: reuse, - Mode: mode, - }) - require.NoError(t, err) - t.Cleanup(func() { proxy.Close() }) - return proxy -} - -func mustParseV6Mode(t *testing.T, mode string) snellv6.Mode { - t.Helper() - parsed, err := snellv6.ParseMode(mode) - require.NoError(t, err) - return parsed -} - -func snellV6EchoTCP(t *testing.T, proxy *Snell) { - t.Helper() - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - conn, err := proxy.DialContext(ctx, &C.Metadata{ - NetWork: C.TCP, - Host: "example.com", - DstPort: 443, - }) - require.NoError(t, err) - defer conn.Close() - - payload := make([]byte, 128*1024) - _, err = rand.Read(payload) - 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, received), "payload mismatch") -} - -func TestSnellV6OutboundTCP(t *testing.T) { - for _, mode := range []string{"default", "unshaped", "unsafe-raw"} { - t.Run(mode, func(t *testing.T) { - snellV6EchoTCP(t, newSnellV6Outbound(t, mode, false)) - }) - } -} - -func TestSnellV6OutboundTCPReuse(t *testing.T) { - proxy := newSnellV6Outbound(t, "default", true) - for round := 0; round < 3; round++ { - snellV6EchoTCP(t, proxy) - time.Sleep(50 * time.Millisecond) - } -} - -func TestSnellV6OutboundUDP(t *testing.T) { - proxy := newSnellV6Outbound(t, "default", false) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - target := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 53} - pc, err := proxy.ListenPacketContext(ctx, &C.Metadata{ - NetWork: C.UDP, - DstIP: netip.MustParseAddr("127.0.0.1"), - DstPort: 53, - }) - require.NoError(t, err) - defer pc.Close() - - for round := 0; round < 5; round++ { - message := make([]byte, 1200) - _, err = rand.Read(message) - require.NoError(t, err) - _, err = pc.WriteTo(message, target) - require.NoError(t, err) - - received := make([]byte, 2048) - require.NoError(t, pc.SetReadDeadline(time.Now().Add(10*time.Second))) - n, from, err := pc.ReadFrom(received) - require.NoError(t, err) - require.Equal(t, target.String(), from.String()) - require.True(t, bytes.Equal(message, received[:n]), "round %d", round) - } -} - -func TestSnellV6OutboundValidation(t *testing.T) { - base := func() SnellOption { - return SnellOption{Name: "v6", Server: "127.0.0.1", Port: 1, Psk: snellV6TestPSK, Version: 6} - } - - opt := base() - opt.Psk = "short" - _, err := NewSnell(opt) - require.Error(t, err, "psk shorter than 12 bytes must be rejected") - - opt = base() - opt.ObfsOpts = map[string]any{"mode": "http"} - _, err = NewSnell(opt) - require.Error(t, err, "v6 has no simple-obfs layer") - - opt = base() - opt.Mode = "bogus" - _, err = NewSnell(opt) - require.Error(t, err, "unknown mode must be rejected") - - opt = base() - opt.Version = 4 - opt.Mode = "unshaped" - _, err = NewSnell(opt) - require.Error(t, err, "mode is v6 only") - - opt = base() - opt.Mode = "unshaped" - proxy, err := NewSnell(opt) - require.NoError(t, err) - require.NoError(t, proxy.Close()) -} diff --git a/listener/snell/server.go b/listener/snell/server.go index c3b09f3ab8..0c68e14f56 100644 --- a/listener/snell/server.go +++ b/listener/snell/server.go @@ -22,7 +22,6 @@ import ( "github.com/metacubex/mihomo/listener/shadowtls" "github.com/metacubex/mihomo/transport/shadowsocks/shadowaead" obfs "github.com/metacubex/mihomo/transport/simple-obfs" - "github.com/metacubex/mihomo/transport/singsnell/snellv6" "github.com/metacubex/mihomo/transport/snell" ) @@ -32,7 +31,7 @@ type Listener struct { closed bool config LC.SnellServer listeners []net.Listener - v6 *snellv6.Service // only set for version 6 + v6Mode snell.V6Mode // only used for version 6 } func New(config LC.SnellServer, lc C.InboundListenConfig, tunnel C.Tunnel, additions ...inbound.Addition) (C.MultiAddrListener, error) { @@ -59,10 +58,17 @@ func New(config LC.SnellServer, lc C.InboundListenConfig, tunnel C.Tunnel, addit l := &Listener{config: config} if config.Version == snell.Version6 { + if err := snell.ValidateV6PSK([]byte(config.Psk)); err != nil { + return nil, fmt.Errorf("snell inbound %w", err) + } + if config.ObfsMode != "" { + // v6 replaced the simple-obfs layer with PSK-derived traffic shaping + return nil, fmt.Errorf("snell inbound version 6 does not support obfs mode %s", config.ObfsMode) + } var err error - l.v6, err = newV6Service(config, tunnel) + l.v6Mode, err = snell.ParseV6Mode(config.Mode) if err != nil { - return nil, err + return nil, fmt.Errorf("snell inbound %w", err) } } @@ -157,6 +163,7 @@ func (l *Listener) AddrList() (addrList []net.Addr) { } func (l *Listener) HandleConn(rawConn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { + defer rawConn.Close() user, loaded := shadowtls.UserFromConn(rawConn) if jlsUser, jlsLoaded := jls.UserFromConn(rawConn); jlsLoaded { user, loaded = jlsUser, true @@ -164,12 +171,6 @@ func (l *Listener) HandleConn(rawConn net.Conn, tunnel C.Tunnel, additions ...in if loaded { additions = append(append([]inbound.Addition(nil), additions...), inbound.WithInUser(user)) } - if l.v6 != nil { - // the v6 service and its handler own rawConn from here on - l.handleV6Conn(rawConn, additions...) - return - } - defer rawConn.Close() conn := rawConn switch l.config.ObfsMode { case "http": @@ -180,7 +181,12 @@ func (l *Listener) HandleConn(rawConn net.Conn, tunnel C.Tunnel, additions ...in default: return } - stream := snell.ServerStreamConn(conn, []byte(l.config.Psk), l.config.Version) + var stream *snell.Snell + if l.config.Version == snell.Version6 { + stream = snell.ServerStreamConnV6(conn, []byte(l.config.Psk), l.v6Mode) + } else { + stream = snell.ServerStreamConn(conn, []byte(l.config.Psk), l.config.Version) + } for { reuse, err := l.handleRequest(stream, tunnel, additions...) if err != nil || !reuse { diff --git a/listener/snell/v6.go b/listener/snell/v6.go deleted file mode 100644 index ec253286fb..0000000000 --- a/listener/snell/v6.go +++ /dev/null @@ -1,133 +0,0 @@ -package snell - -import ( - "context" - "errors" - "fmt" - "net" - - "github.com/metacubex/mihomo/adapter/inbound" - N "github.com/metacubex/mihomo/common/net" - C "github.com/metacubex/mihomo/constant" - LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/sing" - "github.com/metacubex/mihomo/log" - singsnell "github.com/metacubex/mihomo/transport/singsnell" - "github.com/metacubex/mihomo/transport/singsnell/snellv6" - - "github.com/metacubex/sing/common/bufio/deadline" - M "github.com/metacubex/sing/common/metadata" - singN "github.com/metacubex/sing/common/network" -) - -// newV6Service validates the v6 specific parts of the config and builds the -// snellv6 server bound to a handler that forwards into the mihomo tunnel. -func newV6Service(config LC.SnellServer, tunnel C.Tunnel) (*snellv6.Service, error) { - // snell-server v6 rejects PSKs outside the 12..255-byte range - if len(config.Psk) < 12 || len(config.Psk) > 255 { - return nil, errors.New("snell inbound version 6 psk length must be between 12 and 255 bytes") - } - if config.ObfsMode != "" { - // v6 replaced the simple-obfs layer with PSK-derived traffic shaping - return nil, fmt.Errorf("snell inbound version 6 does not support obfs mode %s", config.ObfsMode) - } - mode, err := snellv6.ParseMode(config.Mode) - if err != nil { - return nil, fmt.Errorf("snell inbound %w", err) - } - handler, err := newV6Handler(tunnel, config.UDP) - if err != nil { - return nil, err - } - return snellv6.NewService(snellv6.ServerOptions{ - PSK: []byte(config.Psk), - Mode: mode, - Handler: handler, - }) -} - -// handleV6Conn hands a freshly accepted (and already shadow-tls/restls/jls -// unwrapped) connection to the snellv6 server. Per-connection additions (such -// as the outer layer's user) travel through the context so a single handler can -// serve every connection. Ownership of rawConn moves to the service/handler on -// success; it is only closed here when the handshake fails. -func (l *Listener) handleV6Conn(rawConn net.Conn, additions ...inbound.Addition) { - ctx := sing.WithAdditions(context.Background(), additions...) - ctx = sing.WithInAddr(ctx, rawConn.LocalAddr()) - source := M.SocksaddrFromNet(rawConn.RemoteAddr()) - err := l.v6.NewConnection(ctx, rawConn, source, func(error) {}) - if err != nil { - _ = rawConn.Close() - log.Debugln("snell v6 inbound from %s: %v", rawConn.RemoteAddr(), err) - } -} - -// v6Handler bridges snellv6's TCPConnectionHandlerEx / UDPConnectionHandlerEx -// callbacks into mihomo tunnel calls. TCP is handled directly, UDP reuses the -// packet loop of the sing listener bridge. -type v6Handler struct { - tunnel C.Tunnel - udp bool - packet *sing.ListenerHandler -} - -func newV6Handler(tunnel C.Tunnel, udp bool) (*v6Handler, error) { - packetHandler, err := sing.NewListenerHandler(sing.ListenerConfig{ - Tunnel: tunnel, - Type: C.SNELL, - UDPTimeout: sing.UDPTimeout, - }) - if err != nil { - return nil, err - } - return &v6Handler{tunnel: tunnel, udp: udp, packet: packetHandler}, nil -} - -func (h *v6Handler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose singsnell.CloseHandlerFunc) { - if deadline.NeedAdditionalReadDeadline(conn) { - conn = N.NewDeadlineConn(conn) // conn from sing should check NeedAdditionalReadDeadline - } - - metadata := &C.Metadata{ - NetWork: C.TCP, - Type: C.SNELL, - } - if source.IsIP() && source.Fqdn == "" { - metadata.RawSrcAddr = source.Unwrap().TCPAddr() - } - if destination.IsIP() && destination.Fqdn == "" { - metadata.RawDstAddr = destination.Unwrap().TCPAddr() - } - inbound.ApplyAdditions(metadata, inbound.WithDstAddr(destination), inbound.WithSrcAddr(source), inbound.WithInAddr(conn.LocalAddr())) - inbound.ApplyAdditions(metadata, sing.AdditionsFromContext(ctx)...) - - // The reuse session blocks until onClose is called, so the tunnel must run - // in its own goroutine. HandleTCPConn closes conn once both directions end. - go func() { - h.tunnel.HandleTCPConn(conn, metadata) - if onClose != nil { - onClose(nil) - } - }() -} - -func (h *v6Handler) NewPacketConnectionEx(ctx context.Context, conn singN.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose singsnell.CloseHandlerFunc) { - if !h.udp { - _ = conn.Close() - if onClose != nil { - onClose(errors.New("snell UDP is disabled")) - } - return - } - go func() { - err := h.packet.NewPacketConnection(ctx, conn, M.Metadata{ - Source: source, - Destination: destination, - }) - if onClose != nil { - onClose(err) - } - }() -} - -var _ singsnell.Handler = (*v6Handler)(nil) diff --git a/transport/singsnell/LICENSE b/transport/singsnell/LICENSE deleted file mode 100644 index 3e3e29e359..0000000000 --- a/transport/singsnell/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/transport/singsnell/README.md b/transport/singsnell/README.md deleted file mode 100644 index 588af27544..0000000000 --- a/transport/singsnell/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# singsnell - -Snell v6 client/server implementation ported from -[SagerNet/sing-snell](https://github.com/SagerNet/sing-snell) -(commit `bc5a12ac736f235b2de2926ecd2791cc925e6b8c`), licensed under GPL-3.0-or-later (see LICENSE). - -Changes relative to upstream: - -- imports rewritten to `github.com/metacubex/sing` and this module path -- root package renamed to `singsnell` -- language features newer than the Go version declared in go.mod replaced - with equivalent helpers -- only the v6 protocol is ported; v4/v5 live in `transport/snell` diff --git a/transport/singsnell/address.go b/transport/singsnell/address.go deleted file mode 100644 index c56856264a..0000000000 --- a/transport/singsnell/address.go +++ /dev/null @@ -1,97 +0,0 @@ -package singsnell - -import ( - "encoding/binary" - "io" - - "github.com/metacubex/sing/common/buf" - E "github.com/metacubex/sing/common/exceptions" - M "github.com/metacubex/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/transport/singsnell/compat.go b/transport/singsnell/compat.go deleted file mode 100644 index 0ff6bcead1..0000000000 --- a/transport/singsnell/compat.go +++ /dev/null @@ -1,56 +0,0 @@ -package singsnell - -import ( - "context" - "net" - - "github.com/metacubex/sing/common/buf" - M "github.com/metacubex/sing/common/metadata" - N "github.com/metacubex/sing/common/network" -) - -// Interfaces below exist in newer github.com/sagernet/sing releases but not in -// the github.com/metacubex/sing version mihomo depends on. They are declared -// here so the ported code keeps the same shape as upstream. - -type CloseHandlerFunc = func(it error) - -type TCPConnectionHandlerEx interface { - NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose CloseHandlerFunc) -} - -type UDPConnectionHandlerEx interface { - NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose CloseHandlerFunc) -} - -type VectorisedWriteCreator interface { - CreateVectorisedWriter() (N.VectorisedWriter, bool) -} - -type PacketBatchWriter interface { - WritePacketBatch(buffers []*buf.Buffer, destinations []M.Socksaddr) error -} - -type PacketBatchWriteCreator interface { - CreatePacketBatchWriter() (PacketBatchWriter, bool) -} - -type EarlyReader interface { - NeedHandshakeForRead() bool -} - -type EarlyWriter interface { - NeedHandshakeForWrite() bool -} - -// NewBufferSize mirrors ReadWaitOptions.NewBufferSize from newer sing releases. -func NewBufferSize(options N.ReadWaitOptions, bufferSize int) *buf.Buffer { - buffer := buf.NewSize(bufferSize + options.FrontHeadroom + options.RearHeadroom) - if options.FrontHeadroom > 0 { - buffer.Resize(options.FrontHeadroom, 0) - } - if options.RearHeadroom > 0 { - buffer.Reserve(options.RearHeadroom) - } - return buffer -} diff --git a/transport/singsnell/crypto.go b/transport/singsnell/crypto.go deleted file mode 100644 index c4cd23a236..0000000000 --- a/transport/singsnell/crypto.go +++ /dev/null @@ -1,31 +0,0 @@ -package singsnell - -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/transport/singsnell/internal/reuse/reuse.go b/transport/singsnell/internal/reuse/reuse.go deleted file mode 100644 index 954d34251d..0000000000 --- a/transport/singsnell/internal/reuse/reuse.go +++ /dev/null @@ -1,291 +0,0 @@ -package reuse - -import ( - "io" - "sync" - "sync/atomic" - "time" - - snell "github.com/metacubex/mihomo/transport/singsnell" - "github.com/metacubex/sing/common/buf" - E "github.com/metacubex/sing/common/exceptions" - N "github.com/metacubex/sing/common/network" - "github.com/metacubex/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]) Reset() { - p.access.Lock() - if p.closed { - p.access.Unlock() - return - } - var sessions []S - for element := p.entries.Front(); element != nil; element = element.Next() { - sessions = append(sessions, element.Value.session) - } - p.entries.Init() - p.access.Unlock() - for _, session := range sessions { - session.Close() - } -} - -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 - snell.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/transport/singsnell/request.go b/transport/singsnell/request.go deleted file mode 100644 index c5272f26ac..0000000000 --- a/transport/singsnell/request.go +++ /dev/null @@ -1,97 +0,0 @@ -package singsnell - -import ( - "io" - - "github.com/metacubex/sing/common/buf" - E "github.com/metacubex/sing/common/exceptions" - M "github.com/metacubex/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/transport/singsnell/snell.go b/transport/singsnell/snell.go deleted file mode 100644 index 0d28b49e6c..0000000000 --- a/transport/singsnell/snell.go +++ /dev/null @@ -1,94 +0,0 @@ -package singsnell - -import ( - "context" - "net" - - E "github.com/metacubex/sing/common/exceptions" - M "github.com/metacubex/sing/common/metadata" - N "github.com/metacubex/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 { - TCPConnectionHandlerEx - UDPConnectionHandlerEx -} - -type Service interface { - NewConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose 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() -} - -type keepSessionKey struct{} - -func ContextWithKeepSession(ctx context.Context) context.Context { - return context.WithValue(ctx, (*keepSessionKey)(nil), true) -} - -func KeepSessionFromContext(ctx context.Context) bool { - keep, _ := ctx.Value((*keepSessionKey)(nil)).(bool) - return keep -} diff --git a/transport/singsnell/snellv6/client.go b/transport/singsnell/snellv6/client.go deleted file mode 100644 index 566b89a06e..0000000000 --- a/transport/singsnell/snellv6/client.go +++ /dev/null @@ -1,369 +0,0 @@ -package snellv6 - -import ( - "net" - "sync" - "sync/atomic" - - snell "github.com/metacubex/mihomo/transport/singsnell" - "github.com/metacubex/mihomo/transport/singsnell/internal/reuse" - "github.com/metacubex/sing/common" - "github.com/metacubex/sing/common/buf" - "github.com/metacubex/sing/common/bufio" - E "github.com/metacubex/sing/common/exceptions" - M "github.com/metacubex/sing/common/metadata" - N "github.com/metacubex/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] - closeIdle atomic.Bool -} - -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) - _ snell.VectorisedWriteCreator = (*clientConn)(nil) - _ N.VectorisedWriter = (*clientVectorisedWriter)(nil) - _ snell.EarlyReader = (*clientConn)(nil) - _ snell.EarlyWriter = (*clientConn)(nil) - _ N.WriteCloser = (*clientConn)(nil) -) - -// NeedHandshake implements network.EarlyConn so callers can defer the -// handshake until the first payload write. -func (c *clientConn) NeedHandshake() bool { - return c.writer == nil -} - -var _ N.EarlyConn = (*clientConn)(nil) diff --git a/transport/singsnell/snellv6/handshake.go b/transport/singsnell/snellv6/handshake.go deleted file mode 100644 index 45dceb7792..0000000000 --- a/transport/singsnell/snellv6/handshake.go +++ /dev/null @@ -1,216 +0,0 @@ -package snellv6 - -import ( - "crypto/rand" - "io" - "time" - - snell "github.com/metacubex/mihomo/transport/singsnell" - "github.com/metacubex/mihomo/transport/singsnell/internal/reuse" - "github.com/metacubex/sing/common" - "github.com/metacubex/sing/common/buf" - N "github.com/metacubex/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 := common.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/transport/singsnell/snellv6/loopback_test.go b/transport/singsnell/snellv6/loopback_test.go deleted file mode 100644 index 5b82c8e480..0000000000 --- a/transport/singsnell/snellv6/loopback_test.go +++ /dev/null @@ -1,238 +0,0 @@ -package snellv6_test - -import ( - "bytes" - "context" - "crypto/rand" - "io" - "net" - "sync/atomic" - "testing" - "time" - - snell "github.com/metacubex/mihomo/transport/singsnell" - "github.com/metacubex/mihomo/transport/singsnell/snellv6" - - M "github.com/metacubex/sing/common/metadata" - N "github.com/metacubex/sing/common/network" - "github.com/stretchr/testify/require" -) - -const testPSK = "snell-v6-loopback-psk-0123456789" - -var testModes = map[string]snellv6.Mode{ - "default": snellv6.ModeDefault, - "unshaped": snellv6.ModeUnshaped, - "unsafe-raw": snellv6.ModeUnsafeRaw, -} - -// echoHandler echoes TCP streams and UDP datagrams straight back to the client. -type echoHandler struct{} - -func (echoHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose snell.CloseHandlerFunc) { - go func() { - var err error - defer func() { - _ = N.CloseWrite(conn) - _ = conn.Close() - onClose(err) - }() - // mirror a real upstream: consume the request until the client half-closes, - // then send the response back - var payload []byte - payload, err = io.ReadAll(conn) - if err != nil { - return - } - _, err = conn.Write(payload) - }() -} - -func (echoHandler) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose snell.CloseHandlerFunc) { - go func() { - defer conn.Close() - readWaitOptions := N.NewReadWaitOptions(conn, conn) - for { - buffer := readWaitOptions.NewPacketBuffer() - dest, err := conn.ReadPacket(buffer) - if err != nil { - buffer.Release() - return - } - if err = conn.WritePacket(buffer, dest); err != nil { - return - } - } - }() -} - -type testServer struct { - address string - accepts atomic.Int32 -} - -func startServer(t *testing.T, mode snellv6.Mode) *testServer { - t.Helper() - service, err := snellv6.NewService(snellv6.ServerOptions{ - PSK: []byte(testPSK), - Mode: mode, - Handler: echoHandler{}, - }) - require.NoError(t, err) - - listener, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - t.Cleanup(func() { listener.Close() }) - - server := &testServer{address: listener.Addr().String()} - go func() { - for { - conn, err := listener.Accept() - if err != nil { - return - } - server.accepts.Add(1) - go func() { - err := service.NewConnection(context.Background(), conn, M.SocksaddrFromNet(conn.RemoteAddr()), func(error) {}) - if err != nil { - conn.Close() - } - }() - } - }() - return server -} - -func randomPayload(t *testing.T, size int) []byte { - t.Helper() - payload := make([]byte, size) - _, err := rand.Read(payload) - require.NoError(t, err) - return payload -} - -func echoRoundTrip(t *testing.T, conn net.Conn, payload []byte) { - t.Helper() - _, 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, received), "payload mismatch") -} - -func TestLoopbackTCP(t *testing.T) { - for name, mode := range testModes { - t.Run(name, func(t *testing.T) { - server := startServer(t, mode) - client, err := snellv6.NewClient(snellv6.ClientOptions{PSK: []byte(testPSK), Mode: mode}) - require.NoError(t, err) - - serverConn, err := net.Dial("tcp", server.address) - require.NoError(t, err) - defer serverConn.Close() - - proxyConn, err := client.DialConn(serverConn, M.ParseSocksaddrHostPort("example.com", 443)) - require.NoError(t, err) - echoRoundTrip(t, proxyConn, randomPayload(t, 256*1024)) - }) - } -} - -func TestLoopbackTCPEarlyData(t *testing.T) { - for name, mode := range testModes { - t.Run(name, func(t *testing.T) { - server := startServer(t, mode) - client, err := snellv6.NewClient(snellv6.ClientOptions{PSK: []byte(testPSK), Mode: mode}) - require.NoError(t, err) - - serverConn, err := net.Dial("tcp", server.address) - require.NoError(t, err) - defer serverConn.Close() - - proxyConn := client.DialEarlyConn(serverConn, M.ParseSocksaddrHostPort("127.0.0.1", 80)) - require.True(t, proxyConn.(N.EarlyConn).NeedHandshake()) - // larger than one record so the request and payload span several frames - echoRoundTrip(t, proxyConn, randomPayload(t, 80*1024)) - require.False(t, proxyConn.(N.EarlyConn).NeedHandshake()) - }) - } -} - -func TestLoopbackUDP(t *testing.T) { - for name, mode := range testModes { - t.Run(name, func(t *testing.T) { - server := startServer(t, mode) - client, err := snellv6.NewClient(snellv6.ClientOptions{PSK: []byte(testPSK), Mode: mode}) - require.NoError(t, err) - - serverConn, err := net.Dial("tcp", server.address) - require.NoError(t, err) - defer serverConn.Close() - - packetConn, err := client.DialPacketConn(serverConn) - require.NoError(t, err) - target := M.ParseSocksaddrHostPort("127.0.0.1", 53).UDPAddr() - - for round := 0; round < 10; round++ { - message := randomPayload(t, 1400) - _, err = packetConn.WriteTo(message, target) - require.NoError(t, err) - - received := make([]byte, 2048) - require.NoError(t, packetConn.SetReadDeadline(time.Now().Add(10*time.Second))) - n, from, err := packetConn.ReadFrom(received) - require.NoError(t, err) - require.Equal(t, target.String(), from.String()) - require.True(t, bytes.Equal(message, received[:n]), "round %d", round) - } - }) - } -} - -func TestLoopbackReuse(t *testing.T) { - for name, mode := range testModes { - t.Run(name, func(t *testing.T) { - server := startServer(t, mode) - client, err := snellv6.NewClient(snellv6.ClientOptions{ - PSK: []byte(testPSK), - Mode: mode, - Reuse: true, - Dialer: N.SystemDialer, - Server: M.ParseSocksaddr(server.address), - }) - require.NoError(t, err) - defer client.Close() - - destination := M.ParseSocksaddrHostPort("example.com", 443) - for round := 0; round < 3; round++ { - conn, err := client.DialContext(context.Background(), destination) - require.NoError(t, err) - echoRoundTrip(t, conn, randomPayload(t, 16*1024)) - require.NoError(t, conn.Close()) - // give the session time to return to the pool before the next dial - time.Sleep(50 * time.Millisecond) - } - require.Equal(t, int32(1), server.accepts.Load(), "sessions should be reused over a single tcp connection") - }) - } -} - -func TestClientOptionsValidation(t *testing.T) { - _, err := snellv6.NewClient(snellv6.ClientOptions{}) - require.ErrorIs(t, err, snell.ErrMissingPSK) - - _, err = snellv6.NewClient(snellv6.ClientOptions{PSK: []byte(testPSK), Mode: snellv6.Mode(99)}) - require.Error(t, err) - - _, err = snellv6.NewService(snellv6.ServerOptions{PSK: []byte("short")}) - require.Error(t, err) - - for _, name := range []string{"", "default", "unshaped", "unsafe-raw"} { - _, err = snellv6.ParseMode(name) - require.NoError(t, err, name) - } - _, err = snellv6.ParseMode("bogus") - require.Error(t, err) -} diff --git a/transport/singsnell/snellv6/mixing.go b/transport/singsnell/snellv6/mixing.go deleted file mode 100644 index dfeb78977f..0000000000 --- a/transport/singsnell/snellv6/mixing.go +++ /dev/null @@ -1,56 +0,0 @@ -package snellv6 - -import "github.com/metacubex/sing/common" - -func (p *Profile) mixPaddingPayload(seq uint32, padding []byte, payloadCipher []byte) { - n := common.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 := common.Max(p.mixStride+int(round%3), 1) - if stride == 1 { - for i := 0; i < n; i++ { - 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 := common.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 := 0; i < n; i++ { - 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/transport/singsnell/snellv6/mode.go b/transport/singsnell/snellv6/mode.go deleted file mode 100644 index f80232e833..0000000000 --- a/transport/singsnell/snellv6/mode.go +++ /dev/null @@ -1,41 +0,0 @@ -package snellv6 - -import ( - E "github.com/metacubex/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/transport/singsnell/snellv6/packet.go b/transport/singsnell/snellv6/packet.go deleted file mode 100644 index 23b2521fa9..0000000000 --- a/transport/singsnell/snellv6/packet.go +++ /dev/null @@ -1,447 +0,0 @@ -package snellv6 - -import ( - "io" - "net" - "os" - "sync" - - snell "github.com/metacubex/mihomo/transport/singsnell" - "github.com/metacubex/mihomo/transport/singsnell/internal/reuse" - "github.com/metacubex/sing/common" - "github.com/metacubex/sing/common/buf" - "github.com/metacubex/sing/common/bufio" - E "github.com/metacubex/sing/common/exceptions" - M "github.com/metacubex/sing/common/metadata" - N "github.com/metacubex/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() (snell.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 { - return maxPayload - 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() (snell.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 { - return maxPayload - 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) - _ snell.PacketBatchWriteCreator = (*clientPacketConn)(nil) - _ N.WriterWithMTU = (*clientPacketConn)(nil) - _ snell.PacketBatchWriter = (*clientPacketBatchWriter)(nil) - _ N.PacketConn = (*serverPacketConn)(nil) - _ N.PacketReadWaiter = (*serverPacketConn)(nil) - _ snell.PacketBatchWriteCreator = (*serverPacketConn)(nil) - _ N.WriterWithMTU = (*serverPacketConn)(nil) - _ snell.PacketBatchWriter = (*serverPacketBatchWriter)(nil) -) diff --git a/transport/singsnell/snellv6/profile.go b/transport/singsnell/snellv6/profile.go deleted file mode 100644 index b146017a78..0000000000 --- a/transport/singsnell/snellv6/profile.go +++ /dev/null @@ -1,523 +0,0 @@ -package snellv6 - -import ( - "encoding/binary" - "github.com/metacubex/sing/common" - "math/bits" - - snell "github.com/metacubex/mihomo/transport/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 := common.Min(padMin+pick(ns.prfStatic(labelPadMax, 0), 0xa0, 0x3c0), maxExtraPadding) - - prefixMinHS := pick(ns.prfStatic(labelPrefixMin, handshakeDomain), 0x10, 0x60) - prefixMaxHS := common.Min(prefixMinHS+pick(ns.prfStatic(labelPrefixMax, handshakeDomain), 0x10, 0xa0), 0x80) - prefixMinHS = common.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 := common.Min(prefixMinRecord+pick(ns.prfStatic(labelPrefixMax, 0), 0x10, 0xa0), 0x80) - prefixMinRecord = common.Min(prefixMinRecord, prefixMaxRecord) - - chunkInitial := common.Max(0x60, common.Min(pick(ns.prfStatic(labelChunkInitial, 0), 0x200, 0x05b4), 0x05b4)) - firstRecordCap := common.Max(0x100, common.Min(pick(ns.prfStatic(labelChunkFirstCap, chunkInitialDomain), 0x100, 0x300), common.Min(chunkInitial, 0x300))) - chunkMax := common.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: common.Min(pick(ns.prfStatic(labelChunkStep, 0), 0x400, 0x1000), 0x0b68), - chunkJitter: common.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 := 0; index < 8; index++ { - 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 common.Max(0x40, common.Min(chunkSize, p.chunkMax)) -} - -func (p *Profile) nextChunkSize(chunkSize int) int { - if chunkSize == 0 { - return p.chunkInitial - } - return common.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 += common.Min(targetLen-frameLen, maxExtraPadding) - } - if totalLen > 0 { - paddingLen = p.firstRecordPaddingLen(paddingLen, prefixLen, saltPrefixLen, payloadLen) - } - return common.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 = common.Max(targetLen+jitter, 1) - } - spread := common.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 := common.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 := common.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 := common.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/transport/singsnell/snellv6/record.go b/transport/singsnell/snellv6/record.go deleted file mode 100644 index 00c1dbaad7..0000000000 --- a/transport/singsnell/snellv6/record.go +++ /dev/null @@ -1,654 +0,0 @@ -package snellv6 - -import ( - "crypto/cipher" - "encoding/binary" - "io" - "sync" - - snell "github.com/metacubex/mihomo/transport/singsnell" - "github.com/metacubex/sing/common" - "github.com/metacubex/sing/common/buf" - "github.com/metacubex/sing/common/bufio" - E "github.com/metacubex/sing/common/exceptions" - N "github.com/metacubex/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 := snell.NewBufferSize(r.readWaitOptions, 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) { - for len(p) > 0 { - var data []byte - if len(p) > maxPayload { - data = p[:maxPayload] - p = p[maxPayload:] - } else { - data = p - p = nil - } - buffer := buf.NewSize(snell.HeaderCipherLen + len(data) + snell.AEADTagLen) - w.access.Lock() - w.sealHeader(buffer.Extend(snell.HeaderCipherLen), len(data)) - buffer.Write(data) - w.cipher.Seal(buffer.From(snell.HeaderCipherLen)[:0], w.nonce, buffer.From(snell.HeaderCipherLen), nil) - buffer.Extend(snell.AEADTagLen) - snell.IncreaseNonce(w.nonce) - _, err = w.upstream.Write(buffer.Bytes()) - w.access.Unlock() - buffer.Release() - if err != nil { - return - } - n += len(data) - } - return -} - -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 := common.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 validatePacketVectorBuffers(buffers []*buf.Buffer) error { - for _, buffer := range buffers { - if buffer.Len() > maxPayload { - buf.ReleaseMulti(buffers) - return snell.ErrPayloadTooLarge - } - } - return nil -} - -func (w *packetVectorisedUnshapedWriter) WriteVectorised(buffers []*buf.Buffer) error { - err := validatePacketVectorBuffers(buffers) - if err != nil { - return err - } - 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() { - buffer.Release() - continue - } - 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 := snell.NewBufferSize(r.readWaitOptions, 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) { - for len(p) > 0 { - var data []byte - if len(p) > maxPayload { - data = p[:maxPayload] - p = p[maxPayload:] - } else { - data = p - p = nil - } - buffer := buf.NewSize(snell.HeaderPlainLen + len(data)) - putHeader(buffer.Extend(snell.HeaderPlainLen), 0, len(data)) - buffer.Write(data) - w.access.Lock() - _, err = w.upstream.Write(buffer.Bytes()) - w.access.Unlock() - buffer.Release() - if err != nil { - return - } - n += len(data) - } - return -} - -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 := common.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 { - err := validatePacketVectorBuffers(buffers) - if err != nil { - return err - } - 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() { - buffer.Release() - continue - } - 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) - _ snell.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) - _ snell.VectorisedWriteCreator = (*rawWriter)(nil) - _ N.VectorisedWriter = (*vectorisedRawWriter)(nil) - _ N.VectorisedWriter = (*packetVectorisedRawWriter)(nil) - _ snell.VectorisedWriteCreator = (*shapedWriter)(nil) - _ N.WriterWithMTU = (*unshapedWriter)(nil) - _ N.WriterWithMTU = (*rawWriter)(nil) - _ N.WriterWithMTU = (*shapedWriter)(nil) -) diff --git a/transport/singsnell/snellv6/reuse.go b/transport/singsnell/snellv6/reuse.go deleted file mode 100644 index 2e9c22da18..0000000000 --- a/transport/singsnell/snellv6/reuse.go +++ /dev/null @@ -1,1031 +0,0 @@ -package snellv6 - -import ( - "context" - "errors" - "io" - "net" - "sync" - "sync/atomic" - "time" - - snell "github.com/metacubex/mihomo/transport/singsnell" - "github.com/metacubex/mihomo/transport/singsnell/internal/reuse" - "github.com/metacubex/sing/common" - "github.com/metacubex/sing/common/buf" - "github.com/metacubex/sing/common/bufio" - E "github.com/metacubex/sing/common/exceptions" - M "github.com/metacubex/sing/common/metadata" - N "github.com/metacubex/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 - } - if snell.KeepSessionFromContext(ctx) { - session.keepOnce.Store(true) - } - 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 - } - // Surge 6.4.4 (10661): -[SNConnectorV4 targetHandshakeData] appends the connector early data to - // the handshake buffer, so the first record carries both. - return c.DialEarlyConn(conn, destination), 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) Reset() { - if c.reuse { - c.pool.Reset() - } -} - -func (c *Client) SetKeepIdleConnections(keep bool) { - c.closeIdle.Store(!keep) - if !keep { - c.CloseIdleConnections() - } -} - -func (c *Client) CloseIdleConnections() { - if c.reuse { - c.pool.Reset() - } -} - -func (c *Client) Close() error { - return c.pool.Close() -} - -type reuseSession struct { - net.Conn - client *Client - - state atomic.Uint32 - keepOnce atomic.Bool - 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") - } - - return &reuseConn{Conn: s.Conn, session: s, destination: destination}, nil -} - -func (s *reuseSession) closeIdle() bool { - return s.client.closeIdle.Load() && !s.keepOnce.Swap(false) -} - -func (s *reuseSession) Release(reusable bool) { - if !reusable || s.closeIdle() { - 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.closeIdle() { - s.Close() - return - } - 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 - - access sync.Mutex - writer reuse.RecordWriter - 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) writeRequest(payload []byte) error { - requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: c.session.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() - - data := request.Bytes() - if c.session.writer == nil { - first := data - if len(first) > maxPayload { - first = data[:maxPayload] - } - c.session.writer, err = writeFirstRecord(c.session.Conn, c.session.client.mode, c.session.client.psk, c.session.client.profile, first) - if err != nil { - c.session.Release(false) - return E.Cause(err, "write request") - } - data = data[len(first):] - } - if len(data) > 0 { - _, err = c.session.writer.Write(data) - if err != nil { - c.session.Release(false) - return E.Cause(err, "write request") - } - } - c.writer = c.session.writer - return nil -} - -func (c *reuseConn) writeRequestBuffer(buffer *buf.Buffer) error { - requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: c.session.client.userKey, Destination: c.destination} - request := buf.With(buffer.ExtendHeader(requestPayload.Len())) - err := requestPayload.Write(request) - if err != nil { - buffer.Release() - return err - } - if c.session.writer == nil { - c.session.writer, err = writeFirstRecordBuffer(c.session.Conn, c.session.client.mode, c.session.client.psk, c.session.client.profile, buffer) - } else { - err = c.session.writer.WriteBuffer(buffer) - } - if err != nil { - c.session.Release(false) - return E.Cause(err, "write request") - } - c.writer = c.session.writer - return nil -} - -func (c *reuseConn) 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 *reuseConn) 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 *reuseConn) CreateVectorisedWriter() (N.VectorisedWriter, bool) { - upstreamWriter, created := bufio.CreateVectorisedWriter(c.session.Conn) - if !created { - return nil, false - } - return &reuseVectorisedWriter{conn: c, upstream: upstreamWriter}, true -} - -type reuseVectorisedWriter struct { - conn *reuseConn - upstream N.VectorisedWriter -} - -func (w *reuseVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { - conn := w.conn - if conn.writer != nil { - return conn.writer.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) - } - conn.access.Lock() - defer conn.access.Unlock() - if conn.writer != nil { - return conn.writer.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) - } - for index, buffer := range buffers { - if buffer.IsEmpty() { - buffer.Release() - continue - } - err := conn.writeRequestBuffer(buffer) - if err != nil { - buf.ReleaseMulti(buffers[index+1:]) - return err - } - if index+1 < len(buffers) { - return conn.writer.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers[index+1:]) - } - return nil - } - return nil -} - -func (c *reuseConn) 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 *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 - } - if c.session.closeIdle() { - c.session.Close() - 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 { - if c.writer != nil { - return c.writer.FrontHeadroom() - } - requestPayload := snell.Request{Command: snell.CommandConnectV2, ClientID: c.session.client.userKey, Destination: c.destination} - switch c.session.client.mode { - case ModeUnsafeRaw: - return requestPayload.Len() + snell.HeaderPlainLen - case ModeUnshaped: - return requestPayload.Len() + snell.SaltLen + snell.HeaderCipherLen - default: - return requestPayload.Len() + c.session.client.profile.saltBlockLen + c.session.client.profile.recordPrefixMax + snell.HeaderCipherLen + c.session.client.profile.padMaxHeadroom - } -} - -func (c *reuseConn) RearHeadroom() int { - if c.writer != nil { - return c.writer.RearHeadroom() - } - if c.session.client.mode == ModeUnsafeRaw { - return 0 - } - return snell.AEADTagLen -} - -func (c *reuseConn) WriterMTU() int { - if c.writer != nil { - return c.writer.WriterMTU() - } - if c.session.client.mode == ModeDefault { - return c.session.client.profile.chunkMax - } - return maxPayload -} - -func (c *reuseConn) NeedHandshakeForRead() bool { - return !c.replyRead.Load() -} - -func (c *reuseConn) NeedHandshakeForWrite() bool { - return c.writer == nil -} - -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 snell.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 = s.service.newPacketConnection(requestCtx, packetConn, source, onClose) - if err != nil { - s.Conn.Close() - return err - } - s.writer = packetConn.writer - callOnClose = false - 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 - writeClosed 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.writeClosed { - return 0, net.ErrClosed - } - 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.writeClosed { - buffer.Release() - return net.ErrClosed - } - 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() - defer conn.access.Unlock() - if conn.writeClosed { - buf.ReleaseMulti(buffers) - return net.ErrClosed - } - if conn.replyWritten { - return conn.session.writer.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers) - } - for index, buffer := range buffers { - if buffer.IsEmpty() { - buffer.Release() - continue - } - err := conn.writeResponseBuffer(buffer) - if err != nil { - buf.ReleaseMulti(buffers[index+1:]) - return err - } - if index+1 < len(buffers) { - return conn.session.writer.CreateVectorisedWriterFor(w.upstream).WriteVectorised(buffers[index+1:]) - } - return nil - } - return nil -} - -func (c *serverReuseConn[U]) CloseWrite() error { - c.closeWriteOnce.Do(func() { - c.access.Lock() - defer c.access.Unlock() - c.writeClosed = true - // 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() { - // The handler may close this side while its peer copy is still reading. - // onClose owns completion and runs only after both copy directions exit. - c.closeErr = c.CloseWrite() - }) - 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) - _ snell.VectorisedWriteCreator = (*reuseConn)(nil) - _ snell.EarlyReader = (*reuseConn)(nil) - _ snell.EarlyWriter = (*reuseConn)(nil) - _ N.WriteCloser = (*reuseConn)(nil) - _ N.VectorisedWriter = (*reuseVectorisedWriter)(nil) - _ N.ReadWaiter = (*reuseReadWaiter)(nil) - _ N.ExtendedConn = (*serverReuseConn[struct{}])(nil) - _ N.ReadWaiter = (*serverReuseConn[struct{}])(nil) - _ snell.VectorisedWriteCreator = (*serverReuseConn[struct{}])(nil) - _ N.VectorisedWriter = (*serverReuseVectorisedWriter[struct{}])(nil) - _ N.WriteCloser = (*serverReuseConn[struct{}])(nil) -) diff --git a/transport/singsnell/snellv6/salt.go b/transport/singsnell/snellv6/salt.go deleted file mode 100644 index fa0878afd4..0000000000 --- a/transport/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 := 0; i < length; i++ { - 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 := 0; i < saltLen; i++ { - 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 := 0; i < saltLen; i++ { - block[perm[i]] = saltMask(p.namespaces.salt, byte(p.mixStrideHandshake), uint32(i)) ^ salt[i] - } -} diff --git a/transport/singsnell/snellv6/server.go b/transport/singsnell/snellv6/server.go deleted file mode 100644 index 9789be6c7d..0000000000 --- a/transport/singsnell/snellv6/server.go +++ /dev/null @@ -1,463 +0,0 @@ -package snellv6 - -import ( - "context" - "io" - "net" - "sync" - - snell "github.com/metacubex/mihomo/transport/singsnell" - "github.com/metacubex/mihomo/transport/singsnell/internal/reuse" - "github.com/metacubex/sing/common/auth" - "github.com/metacubex/sing/common/buf" - "github.com/metacubex/sing/common/bufio" - E "github.com/metacubex/sing/common/exceptions" - M "github.com/metacubex/sing/common/metadata" - N "github.com/metacubex/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 snell.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 snell.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 snell.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() - return s.newPacketConnection(ctx, &serverPacketConn{Conn: conn, service: s, reader: reader}, source, onClose) - 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 *Service) newPacketConnection(ctx context.Context, packetConn *serverPacketConn, source M.Socksaddr, onClose snell.CloseHandlerFunc) error { - err := packetConn.writeTunnelReply() - if err != nil { - return err - } - firstPacket := buf.NewPacket() - destination, err := packetConn.ReadPacket(firstPacket) - if err != nil { - firstPacket.Release() - return E.Cause(err, "read first packet") - } - s.handler.NewPacketConnectionEx(ctx, bufio.NewCachedPacketConn(packetConn, firstPacket, destination), source, destination, onClose) - return nil -} - -func (s *MultiService[U]) newConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose snell.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() - return s.newPacketConnection(requestCtx, &serverPacketConn{Conn: conn, service: s.Service, reader: reader}, source, onClose) - 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) - _ snell.VectorisedWriteCreator = (*serverConn)(nil) - _ N.VectorisedWriter = (*serverVectorisedWriter)(nil) - _ N.WriteCloser = (*serverConn)(nil) -) diff --git a/transport/singsnell/snellv6/shaped.go b/transport/singsnell/snellv6/shaped.go deleted file mode 100644 index 61eb799873..0000000000 --- a/transport/singsnell/snellv6/shaped.go +++ /dev/null @@ -1,463 +0,0 @@ -package snellv6 - -import ( - "crypto/cipher" - "encoding/binary" - "github.com/metacubex/sing/common" - "io" - "sync" - "time" - - snell "github.com/metacubex/mihomo/transport/singsnell" - "github.com/metacubex/sing/common/buf" - "github.com/metacubex/sing/common/bufio" - E "github.com/metacubex/sing/common/exceptions" - N "github.com/metacubex/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 = common.Min(payloadLimit, w.profile.firstRecordCap) - } - w.chunkSize = w.profile.nextChunkSize(w.chunkSize) - w.lastWriteUnix = nowUnix - return common.Max(1, common.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 := common.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 := common.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 - } - if dataLen > maxPayload { - buffer.Release() - return snell.ErrPayloadTooLarge - } - w.access.Lock() - defer w.access.Unlock() - // snell-server v6.0.0rc: UDP packet mode still advances the dynamic chunk - // state, but emits each complete datagram as one record regardless of the - // current stream chunk limit. - w.payloadLimitFor(time.Now()) - 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 := common.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 { - err := validatePacketVectorBuffers(buffers) - if err != nil { - return err - } - 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() { - buffer.Release() - continue - } - recordWriter.payloadLimitFor(time.Now()) - 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 := snell.NewBufferSize(r.readWaitOptions, 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/transport/singsnell/snellv6/streaming_test.go b/transport/singsnell/snellv6/streaming_test.go deleted file mode 100644 index 760409c319..0000000000 --- a/transport/singsnell/snellv6/streaming_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package snellv6_test - -import ( - "bytes" - "context" - "io" - "net" - "testing" - "time" - - snell "github.com/metacubex/mihomo/transport/singsnell" - "github.com/metacubex/mihomo/transport/singsnell/snellv6" - - M "github.com/metacubex/sing/common/metadata" - N "github.com/metacubex/sing/common/network" - "github.com/stretchr/testify/require" -) - -// streamingEchoHandler echoes bytes as they arrive, like a real relay would. -type streamingEchoHandler struct{} - -func (streamingEchoHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose snell.CloseHandlerFunc) { - go func() { - _, err := io.Copy(conn, conn) - _ = N.CloseWrite(conn) - _ = conn.Close() - onClose(err) - }() -} - -func (streamingEchoHandler) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose snell.CloseHandlerFunc) { - echoHandler{}.NewPacketConnectionEx(ctx, conn, source, destination, onClose) -} - -func startStreamingServer(t *testing.T, mode snellv6.Mode) string { - t.Helper() - service, err := snellv6.NewService(snellv6.ServerOptions{PSK: []byte(testPSK), Mode: mode, Handler: streamingEchoHandler{}}) - require.NoError(t, err) - listener, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - t.Cleanup(func() { listener.Close() }) - go func() { - for { - conn, err := listener.Accept() - if err != nil { - return - } - go func() { - if err := service.NewConnection(context.Background(), conn, M.SocksaddrFromNet(conn.RemoteAddr()), func(error) {}); err != nil { - conn.Close() - } - }() - } - }() - return listener.Addr().String() -} - -// streamingRoundTrip writes while concurrently reading, without half-closing -// first, which is how a proxy relay drives the connection. -func streamingRoundTrip(t *testing.T, conn net.Conn, payload []byte) { - t.Helper() - go func() { _, _ = conn.Write(payload) }() - received := make([]byte, len(payload)) - require.NoError(t, conn.SetReadDeadline(time.Now().Add(10*time.Second))) - _, err := io.ReadFull(conn, received) - require.NoError(t, err) - require.True(t, bytes.Equal(payload, received), "payload mismatch") -} - -func TestStreamingReuse(t *testing.T) { - for name, mode := range testModes { - t.Run(name, func(t *testing.T) { - address := startStreamingServer(t, mode) - client, err := snellv6.NewClient(snellv6.ClientOptions{ - PSK: []byte(testPSK), Mode: mode, Reuse: true, - Dialer: N.SystemDialer, Server: M.ParseSocksaddr(address), - }) - require.NoError(t, err) - defer client.Close() - for round := 0; round < 2; round++ { - conn, err := client.DialContext(context.Background(), M.ParseSocksaddrHostPort("example.com", 443)) - require.NoError(t, err) - streamingRoundTrip(t, conn, randomPayload(t, 16*1024)) - require.NoError(t, conn.Close()) - time.Sleep(50 * time.Millisecond) - } - }) - } -} - -func TestStreamingNoReuse(t *testing.T) { - for name, mode := range testModes { - t.Run(name, func(t *testing.T) { - address := startStreamingServer(t, mode) - client, err := snellv6.NewClient(snellv6.ClientOptions{PSK: []byte(testPSK), Mode: mode}) - require.NoError(t, err) - serverConn, err := net.Dial("tcp", address) - require.NoError(t, err) - defer serverConn.Close() - conn, err := client.DialConn(serverConn, M.ParseSocksaddrHostPort("example.com", 443)) - require.NoError(t, err) - streamingRoundTrip(t, conn, randomPayload(t, 16*1024)) - }) - } -} diff --git a/transport/snell/v6.go b/transport/snell/v6.go new file mode 100644 index 0000000000..0b382d7ccf --- /dev/null +++ b/transport/snell/v6.go @@ -0,0 +1,663 @@ +package snell + +import ( + "crypto/cipher" + cryptorand "crypto/rand" + "encoding/binary" + "errors" + "fmt" + "io" + "net" + "sync" + "time" + + "github.com/metacubex/mihomo/transport/shadowsocks/shadowaead" +) + +// Snell v6 keeps the v4 record layout (7-byte header sealed with AES-128-GCM, +// followed by the sealed payload) but wraps it in a PSK-derived "profile": the +// salt is scattered inside a padded block, every record carries an unencrypted +// prefix that doubles as the header's AAD, padding is interleaved with the +// payload ciphertext and record sizes follow a shaped schedule. The `unshaped` +// mode drops the profile layer and the `unsafe-raw` mode drops the crypto as +// well; both must match the server's configured mode. + +type V6Mode int + +const ( + V6ModeDefault V6Mode = iota + V6ModeUnshaped + V6ModeUnsafeRaw +) + +func ParseV6Mode(name string) (V6Mode, error) { + switch name { + case "", "default": + return V6ModeDefault, nil + case "unshaped": + return V6ModeUnshaped, nil + case "unsafe-raw": + return V6ModeUnsafeRaw, nil + default: + return 0, fmt.Errorf("snell v6 unknown mode: %s", name) + } +} + +func (m V6Mode) String() string { + switch m { + case V6ModeDefault: + return "default" + case V6ModeUnshaped: + return "unshaped" + case V6ModeUnsafeRaw: + return "unsafe-raw" + default: + return fmt.Sprintf("mode(%d)", int(m)) + } +} + +const ( + v6SaltSize = 16 + v6NonceSize = 12 + v6TagSize = 16 + v6HeaderVersion = 4 + v6HeaderPlainSize = 7 + v6HeaderCipherSize = v6HeaderPlainSize + v6TagSize + v6MaxPayload = 0xffff + // the official server rejects PSKs outside this range + V6MinPSKLength = 12 + V6MaxPSKLength = 255 +) + +var ( + errV6BadHeader = errors.New("snell v6 invalid record header") + errV6RecordPadding = errors.New("snell v6 unexpected padding in record") + errV6PayloadTooLong = errors.New("snell v6 payload too large") +) + +func ValidateV6PSK(psk []byte) error { + if len(psk) < V6MinPSKLength || len(psk) > V6MaxPSKLength { + return fmt.Errorf("snell v6 psk length must be between %d and %d bytes", V6MinPSKLength, V6MaxPSKLength) + } + return nil +} + +type v6RecordReader interface { + // readRecord returns the payload of the next record, or + // shadowaead.ErrZeroChunk for an end-of-stream record. + readRecord() ([]byte, error) +} + +type v6RecordWriter interface { + // write splits p into as many records as the mode requires + write(p []byte) error + // writePacket emits p as exactly one record + writePacket(p []byte) error + writeZeroChunk() error +} + +type v6Conn struct { + net.Conn + psk []byte + mode V6Mode + profile *v6Profile + + r v6RecordReader + rBuf []byte + rMux sync.Mutex + + w v6RecordWriter + wMux sync.Mutex +} + +func newV6Conn(conn net.Conn, psk []byte, mode V6Mode) *v6Conn { + c := &v6Conn{Conn: conn, psk: psk, mode: mode} + if mode == V6ModeDefault { + c.profile = newV6Profile(psk) + } + return c +} + +func (c *v6Conn) initReader() { + switch c.mode { + case V6ModeUnsafeRaw: + c.r = &v6RawReader{Reader: c.Conn} + case V6ModeUnshaped: + c.r = &v6UnshapedReader{Reader: c.Conn, psk: c.psk} + default: + c.r = &v6ShapedReader{Reader: c.Conn, psk: c.psk, profile: c.profile} + } +} + +func (c *v6Conn) initWriter() error { + switch c.mode { + case V6ModeUnsafeRaw: + c.w = &v6RawWriter{Writer: c.Conn} + return nil + case V6ModeUnshaped: + w, err := newV6UnshapedWriter(c.Conn, c.psk) + if err != nil { + return err + } + c.w = w + return nil + default: + w, err := newV6ShapedWriter(c.Conn, c.psk, c.profile) + if err != nil { + return err + } + c.w = w + return nil + } +} + +func (c *v6Conn) Read(b []byte) (int, error) { + c.rMux.Lock() + defer c.rMux.Unlock() + + if c.r == nil { + c.initReader() + } + if len(c.rBuf) == 0 { + payload, err := c.r.readRecord() + if err != nil { + return 0, err + } + c.rBuf = payload + } + n := copy(b, c.rBuf) + c.rBuf = c.rBuf[n:] + return n, nil +} + +func (c *v6Conn) Write(b []byte) (int, error) { + c.wMux.Lock() + defer c.wMux.Unlock() + + if c.w == nil { + if err := c.initWriter(); err != nil { + return 0, err + } + } + if len(b) == 0 { + return 0, c.w.writeZeroChunk() + } + if err := c.w.write(b); err != nil { + return 0, err + } + return len(b), nil +} + +func (c *v6Conn) WritePacketFrame(b []byte) (int, error) { + if len(b) > v6MaxPayload { + return 0, errV6PayloadTooLong + } + c.wMux.Lock() + defer c.wMux.Unlock() + + if c.w == nil { + if err := c.initWriter(); err != nil { + return 0, err + } + } + if err := c.w.writePacket(b); err != nil { + return 0, err + } + return len(b), nil +} + +func (c *v6Conn) WriteTo(w io.Writer) (int64, error) { + var written int64 + buf := make([]byte, v6MaxPayload) + for { + n, err := c.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 *v6Conn) ReadFrom(r io.Reader) (int64, error) { + var read int64 + buf := make([]byte, v6MaxPayload) + for { + n, err := r.Read(buf) + if n > 0 { + read += int64(n) + if _, ew := c.Write(buf[:n]); ew != nil { + return read, ew + } + } + if err != nil { + if err == io.EOF { + err = nil + } + return read, err + } + } +} + +func v6AEAD(psk, salt []byte) (cipher.AEAD, error) { + return aesGCM(snellKDF(psk, salt, 16)) +} + +func v6RandomSalt() ([]byte, error) { + salt := make([]byte, v6SaltSize) + if _, err := io.ReadFull(cryptorand.Reader, salt); err != nil { + return nil, err + } + return salt, nil +} + +func v6PutHeader(header []byte, paddingLength, payloadLength int) { + header[0] = v6HeaderVersion + header[1] = 0 + header[2] = 0 + binary.BigEndian.PutUint16(header[3:5], uint16(paddingLength)) + binary.BigEndian.PutUint16(header[5:7], uint16(payloadLength)) +} + +// v6ParseHeader validates a plain header. strict additionally requires the two +// reserved bytes to be zero, which the official server only checks outside of +// the default mode. +func v6ParseHeader(header []byte, strict bool) (paddingLength, payloadLength int, err error) { + if header[0] != v6HeaderVersion { + return 0, 0, errV6BadHeader + } + if strict && (header[1] != 0 || header[2] != 0) { + return 0, 0, errV6BadHeader + } + return int(binary.BigEndian.Uint16(header[3:5])), int(binary.BigEndian.Uint16(header[5:7])), nil +} + +// --- unsafe-raw --------------------------------------------------------------- + +type v6RawReader struct { + io.Reader +} + +func (r *v6RawReader) readRecord() ([]byte, error) { + var header [v6HeaderPlainSize]byte + if _, err := io.ReadFull(r.Reader, header[:]); err != nil { + return nil, err + } + paddingLength, payloadLength, err := v6ParseHeader(header[:], true) + if err != nil { + return nil, err + } + if paddingLength != 0 { + return nil, errV6RecordPadding + } + if payloadLength == 0 { + return nil, shadowaead.ErrZeroChunk + } + payload := make([]byte, payloadLength) + if _, err := io.ReadFull(r.Reader, payload); err != nil { + return nil, err + } + return payload, nil +} + +type v6RawWriter struct { + io.Writer +} + +func (w *v6RawWriter) writeRecord(payload []byte) error { + record := make([]byte, v6HeaderPlainSize+len(payload)) + v6PutHeader(record, 0, len(payload)) + copy(record[v6HeaderPlainSize:], payload) + _, err := w.Writer.Write(record) + return err +} + +func (w *v6RawWriter) write(p []byte) error { + for len(p) > 0 { + n := minInt(len(p), v6MaxPayload) + if err := w.writeRecord(p[:n]); err != nil { + return err + } + p = p[n:] + } + return nil +} + +func (w *v6RawWriter) writePacket(p []byte) error { + return w.writeRecord(p) +} + +func (w *v6RawWriter) writeZeroChunk() error { + return w.writeRecord(nil) +} + +// --- unshaped ------------------------------------------------------------------- + +type v6UnshapedReader struct { + io.Reader + psk []byte + aead cipher.AEAD + nonce [v6NonceSize]byte +} + +func (r *v6UnshapedReader) readRecord() ([]byte, error) { + if r.aead == nil { + salt := make([]byte, v6SaltSize) + if _, err := io.ReadFull(r.Reader, salt); err != nil { + return nil, err + } + aead, err := v6AEAD(r.psk, salt) + if err != nil { + return nil, err + } + r.aead = aead + } + + headerCipher := make([]byte, v6HeaderCipherSize) + 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 + } + paddingLength, payloadLength, err := v6ParseHeader(header, true) + if err != nil { + return nil, err + } + if paddingLength != 0 { + return nil, errV6RecordPadding + } + if payloadLength == 0 { + return nil, shadowaead.ErrZeroChunk + } + + payloadCipher := make([]byte, payloadLength+v6TagSize) + if _, err := io.ReadFull(r.Reader, payloadCipher); err != nil { + return nil, err + } + payload, err := r.aead.Open(payloadCipher[:0], r.nonce[:], payloadCipher, nil) + incrementV4Nonce(r.nonce[:]) + if err != nil { + return nil, err + } + return payload, nil +} + +type v6UnshapedWriter struct { + io.Writer + aead cipher.AEAD + nonce [v6NonceSize]byte + salt []byte + saltSent bool +} + +func newV6UnshapedWriter(w io.Writer, psk []byte) (*v6UnshapedWriter, error) { + salt, err := v6RandomSalt() + if err != nil { + return nil, err + } + aead, err := v6AEAD(psk, salt) + if err != nil { + return nil, err + } + return &v6UnshapedWriter{Writer: w, aead: aead, salt: salt}, nil +} + +// appendRecord seals payload as one record and appends it to out. +func (w *v6UnshapedWriter) appendRecord(out []byte, payload []byte) []byte { + start := len(out) + out = append(out, make([]byte, v6HeaderPlainSize)...) + v6PutHeader(out[start:], 0, len(payload)) + out = w.aead.Seal(out[:start], w.nonce[:], out[start:start+v6HeaderPlainSize], nil) + incrementV4Nonce(w.nonce[:]) + if len(payload) > 0 { + out = w.aead.Seal(out, w.nonce[:], payload, nil) + incrementV4Nonce(w.nonce[:]) + } + return out +} + +func (w *v6UnshapedWriter) writeRecords(payloads [][]byte) error { + size := 0 + if !w.saltSent { + size += v6SaltSize + } + for _, payload := range payloads { + size += v6HeaderCipherSize + len(payload) + if len(payload) > 0 { + size += v6TagSize + } + } + out := make([]byte, 0, size) + if !w.saltSent { + out = append(out, w.salt...) + w.saltSent = true + } + for _, payload := range payloads { + out = w.appendRecord(out, payload) + } + _, err := w.Writer.Write(out) + return err +} + +func (w *v6UnshapedWriter) write(p []byte) error { + var payloads [][]byte + for len(p) > 0 { + n := minInt(len(p), v6MaxPayload) + payloads = append(payloads, p[:n]) + p = p[n:] + } + return w.writeRecords(payloads) +} + +func (w *v6UnshapedWriter) writePacket(p []byte) error { + return w.writeRecords([][]byte{p}) +} + +func (w *v6UnshapedWriter) writeZeroChunk() error { + return w.writeRecords([][]byte{nil}) +} + +// --- default (shaped) ------------------------------------------------------------ + +type v6ShapedReader struct { + io.Reader + psk []byte + profile *v6Profile + aead cipher.AEAD + nonce [v6NonceSize]byte + seq uint32 +} + +func (r *v6ShapedReader) readRecord() ([]byte, error) { + if r.aead == nil { + block := make([]byte, r.profile.saltBlockLen) + if _, err := io.ReadFull(r.Reader, block); err != nil { + return nil, err + } + salt := r.profile.extractSalt(block) + aead, err := v6AEAD(r.psk, salt[:]) + if err != nil { + return nil, err + } + r.aead = aead + } + + prefixLength := r.profile.recordPrefixLen(r.seq) + head := make([]byte, prefixLength+v6HeaderCipherSize) + if _, err := io.ReadFull(r.Reader, head); err != nil { + return nil, err + } + prefix := head[:prefixLength] + headerCipher := head[prefixLength:] + header, err := r.aead.Open(headerCipher[:0], r.nonce[:], headerCipher, prefix) + incrementV4Nonce(r.nonce[:]) + if err != nil { + return nil, err + } + // the reserved header bytes are ignored in default mode + paddingLength, payloadLength, err := v6ParseHeader(header, false) + if err != nil { + return nil, err + } + seq := r.seq + r.seq++ + + if payloadLength == 0 { + if paddingLength > 0 { + if _, err := io.CopyN(io.Discard, r.Reader, int64(paddingLength)); err != nil { + return nil, err + } + } + return nil, shadowaead.ErrZeroChunk + } + + body := make([]byte, paddingLength+payloadLength+v6TagSize) + if _, err := io.ReadFull(r.Reader, body); err != nil { + return nil, err + } + padding := body[:paddingLength] + payloadCipher := body[paddingLength:] + r.profile.mixPaddingPayload(seq, padding, payloadCipher) + payload, err := r.aead.Open(payloadCipher[:0], r.nonce[:], payloadCipher, padding) + incrementV4Nonce(r.nonce[:]) + if err != nil { + return nil, err + } + return payload, nil +} + +type v6ShapedWriter struct { + io.Writer + profile *v6Profile + aead cipher.AEAD + nonce [v6NonceSize]byte + salt []byte + saltSent bool + seq uint32 + chunkSize int + lastWriteUnix int64 +} + +func newV6ShapedWriter(w io.Writer, psk []byte, profile *v6Profile) (*v6ShapedWriter, error) { + salt, err := v6RandomSalt() + if err != nil { + return nil, err + } + aead, err := v6AEAD(psk, salt) + if err != nil { + return nil, err + } + return &v6ShapedWriter{Writer: w, profile: profile, aead: aead, salt: salt}, nil +} + +// payloadLimitFor advances the shaped chunk schedule and returns how much +// payload the next record may carry. The idle reset is second-granular. +func (w *v6ShapedWriter) payloadLimitFor(now time.Time) int { + nowUnix := now.Unix() + 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 = minInt(payloadLimit, w.profile.firstRecordCap) + } + w.chunkSize = w.profile.nextChunkSize(w.chunkSize) + w.lastWriteUnix = nowUnix + return maxInt(1, minInt(payloadLimit, v6MaxPayload)) +} + +// appendRecord builds one shaped record for payload and appends it to out. +func (w *v6ShapedWriter) appendRecord(out []byte, payload []byte) []byte { + prefixLength := w.profile.recordPrefixLen(w.seq) + saltBlockLength := 0 + saltPrefixLength := 0 + if !w.saltSent { + saltBlockLength = w.profile.saltBlockLen + saltPrefixLength = saltBlockLength - v6SaltSize + } + paddingLength := w.profile.paddingLen(w.seq, len(payload), prefixLength, saltPrefixLength, saltBlockLength) + payloadCipherLength := 0 + if len(payload) > 0 { + payloadCipherLength = len(payload) + v6TagSize + } + + start := len(out) + out = append(out, make([]byte, saltBlockLength+prefixLength+v6HeaderCipherSize+paddingLength+payloadCipherLength)...) + record := out[start:] + + if saltBlockLength > 0 { + block := record[:saltBlockLength] + // the salt block is shaped padding generated for the sentinel sequence 0xffffffff + // with the salt bytes scattered over it + w.profile.fillPadding(0xffffffff, block) + w.profile.writeSaltBlock(w.salt, block) + w.saltSent = true + } + offset := saltBlockLength + prefix := record[offset : offset+prefixLength] + w.profile.fillPadding(w.seq, prefix) + offset += prefixLength + + header := record[offset : offset+v6HeaderCipherSize] + v6PutHeader(header, paddingLength, len(payload)) + w.aead.Seal(header[:0], w.nonce[:], header[:v6HeaderPlainSize], prefix) + incrementV4Nonce(w.nonce[:]) + offset += v6HeaderCipherSize + + padding := record[offset : offset+paddingLength] + w.profile.fillPadding(w.seq, padding) + offset += paddingLength + + if len(payload) > 0 { + region := record[offset : offset+payloadCipherLength] + copy(region, payload) + w.aead.Seal(region[:0], w.nonce[:], region[:len(payload)], padding) + incrementV4Nonce(w.nonce[:]) + w.profile.mixPaddingPayload(w.seq, padding, region) + } + w.seq++ + return out +} + +func (w *v6ShapedWriter) write(p []byte) error { + now := time.Now() + var out []byte + for len(p) > 0 { + n := minInt(len(p), w.payloadLimitFor(now)) + out = w.appendRecord(out, p[:n]) + p = p[n:] + } + _, err := w.Writer.Write(out) + return err +} + +func (w *v6ShapedWriter) writePacket(p []byte) error { + // packet mode still advances the chunk schedule but emits the whole + // datagram as a single record regardless of the current limit + w.payloadLimitFor(time.Now()) + _, err := w.Writer.Write(w.appendRecord(nil, p)) + return err +} + +func (w *v6ShapedWriter) writeZeroChunk() error { + w.payloadLimitFor(time.Now()) + _, err := w.Writer.Write(w.appendRecord(nil, nil)) + return err +} diff --git a/transport/snell/v6_profile.go b/transport/snell/v6_profile.go new file mode 100644 index 0000000000..58a36501ca --- /dev/null +++ b/transport/snell/v6_profile.go @@ -0,0 +1,650 @@ +package snell + +import ( + "encoding/binary" + "math/bits" + + "golang.org/x/crypto/blake2b" +) + +// Snell v6 derives a deployment specific "profile" from the PSK. The profile +// decides how the salt is scattered inside the first block, how long the +// per-record prefix and padding are, how padding bytes look and how padding is +// interleaved with the ciphertext. Both peers derive the same profile, so every +// value below must be computed exactly the same way as the official +// implementation. The constants and the derivation order follow the protocol +// notes published by SagerNet/sing-snell (GPL-3.0-or-later), which reference +// the corresponding routines in Surge 6.7.0 (11520). + +const ( + v6GoldenGamma = 0x9e3779b97f4a7c15 + + v6LabelPadding = 0 + v6LabelBitPercent = 1 + v6LabelMotif = 2 + v6LabelMixOffset = 3 + v6LabelSalt = 3 + v6LabelProfileID = 5 + v6LabelGenerator = 6 + v6LabelPadMin = 7 + v6LabelPadMax = 8 + v6LabelPadCount = 9 + v6LabelPadInterval = 10 + v6LabelSmallLimit = 11 + v6LabelBitMin = 12 + v6LabelBitMax = 13 + v6LabelPrefixMin = 14 + v6LabelPrefixMax = 15 + v6LabelMixMode = 16 + v6LabelMixRounds = 17 + v6LabelMixStride = 18 + v6LabelMixOffsetBase = 19 + v6LabelMixBlock = 20 + v6LabelChunkPolicy = 21 + v6LabelChunkInitial = 22 + v6LabelChunkFirstCap = 22 + v6LabelChunkMax = 23 + v6LabelChunkStep = 24 + v6LabelChunkJitter = 25 + v6LabelChunkBucket = 26 + v6LabelIdleReset = 27 + v6LabelWritePolicy = 28 + v6LabelWriteFirst = 29 + v6LabelWriteBucket = 30 + v6LabelWriteSeq = 31 + v6LabelWriteJitter = 32 + v6LabelRecordPrefix = 33 + v6LabelPayloadPad = 34 + v6LabelWriteTarget = 35 + v6LabelWriteJitterV = 36 + v6LabelWriteNext = 37 + v6LabelChunkSize = 38 + v6LabelChunkJitterV = 39 + + // domain separators for handshake and chunk profile values + v6HandshakeDomain = 0x7053 + v6MixHandshakeDomain = 0x51a7 + v6ChunkInitialDomain = 0xf17c + + // namespace seeds + v6NSSeedProfile = 0xb46c2e7d9a1538f1 + v6NSSeedPrefix = 0x5d9217c083e64ab9 + v6NSSeedMotif = 0xa71f0c54d8396e2b + v6NSSeedSalt = 0x3e8a91b52740f6cd + v6NSSeedMix = 0xc9f4260b7d1e835a + v6NSSeedChunk = 0x62d0b5e19c4a783f + v6NSSeedWrite = 0x917b3c48e6a205d4 + + // arithmetic constants of the profile PRFs + v6DomainMul = 0xd6e8feb86659fd93 + v6NamespaceAdd = 0xa0761d6478bd642f + v6CoefB = 0x589965cc75374cc3 + v6AddB = 0x33a213ec50ffe2e9 + v6CoefA = 0xe7037ed1a0b428db + v6AddA = 0x8f3907f7b2b80c35 + + // salt shuffle/mask namespace xor + v6SaltNSXor = 0xdaa66d2c7ddf743f + + v6MaxExtraPadding = 0x02da +) + +// v6ProfileSeed is the BLAKE2b prefix mixed with the PSK to derive the profile secret. +var v6ProfileSeed = [...]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, +} + +// v6BitRotateTable maps low nibbles through bit rotations for padding generator 0. +var v6BitRotateTable = [...]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 minInt(a, b int) int { + if a < b { + return a + } + return b +} + +func maxInt(a, b int) int { + if a > b { + return a + } + return b +} + +func v6SplitMix64(x uint64) uint64 { + x ^= x >> 30 + x *= 0xbf58476d1ce4e5b9 + x ^= x >> 27 + x *= 0x94d049bb133111eb + x ^= x >> 31 + return x +} + +func v6PRF32Fold(namespace uint64, label uint32, a uint64, b uint64) uint32 { + x := namespace ^ (b*v6CoefB + v6AddB) ^ (uint64(label) * v6GoldenGamma) ^ (a*v6CoefA + v6AddA) + y := v6SplitMix64(x) + return uint32(y ^ (y >> 32)) +} + +func v6Pick(raw uint32, lo int, hi int) int { + if hi < lo { + panic("snell v6: invalid profile range") + } + return lo + int(uint64(raw)%uint64(hi-lo+1)) +} + +func v6PickU32(raw uint32, lo uint32, hi uint32) uint32 { + if hi < lo { + panic("snell v6: invalid profile range") + } + return lo + raw%(hi-lo+1) +} + +type v6Namespaces struct { + profile uint64 + prefix uint64 + motif uint64 + salt uint64 + mix uint64 + chunk uint64 + write uint64 +} + +func v6DeriveNamespace(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)*v6DomainMul ^ + (seedConst + v6NamespaceAdd) ^ + s0 ^ + (s1 + v6GoldenGamma) ^ + bits.RotateLeft64(s2, 17) ^ + bits.RotateLeft64(s3, -11) + return v6SplitMix64(mixed) +} + +func newV6Namespaces(secret []byte) v6Namespaces { + return v6Namespaces{ + profile: v6DeriveNamespace(secret, v6LabelProfileID, v6NSSeedProfile), + prefix: v6DeriveNamespace(secret, v6LabelPadding, v6NSSeedPrefix), + motif: v6DeriveNamespace(secret, v6LabelMotif, v6NSSeedMotif), + salt: v6DeriveNamespace(secret, v6LabelSalt, v6NSSeedSalt), + mix: v6DeriveNamespace(secret, v6LabelMixMode, v6NSSeedMix), + chunk: v6DeriveNamespace(secret, v6LabelChunkPolicy, v6NSSeedChunk), + write: v6DeriveNamespace(secret, v6LabelWritePolicy, v6NSSeedWrite), + } +} + +func (n v6Namespaces) forLabel(label uint32) uint64 { + switch label { + case v6LabelPadding, v6LabelBitPercent, v6LabelPrefixMin, v6LabelPrefixMax, v6LabelRecordPrefix, v6LabelPayloadPad: + return n.prefix + case v6LabelMotif: + return n.motif + case v6LabelMixOffset, v6LabelMixMode, v6LabelMixRounds, v6LabelMixStride, v6LabelMixOffsetBase, v6LabelMixBlock: + return n.mix + case v6LabelChunkPolicy, v6LabelChunkInitial, v6LabelChunkMax, v6LabelChunkStep, v6LabelChunkJitter, v6LabelChunkBucket, v6LabelChunkSize, v6LabelChunkJitterV: + return n.chunk + case v6LabelWritePolicy, v6LabelWriteFirst, v6LabelWriteBucket, v6LabelWriteSeq, v6LabelWriteJitter, v6LabelWriteTarget, v6LabelWriteJitterV, v6LabelWriteNext: + return n.write + default: + return n.profile + } +} + +func (n v6Namespaces) prf32(label uint32, a uint32, b uint32) uint32 { + return v6PRF32Fold(n.forLabel(label), label, uint64(a), uint64(b)) +} + +func (n v6Namespaces) prfBytes(label uint32, a uint32, output []byte) { + seed := n.forLabel(label) ^ + (uint64(a)*v6DomainMul + 0xb57de1f3f82cb33f) ^ + (uint64(label) * 0xa24baed4963ee407) ^ + (uint64(len(output))*0x165667b19e3779f9 + 0x0d4cd3e7b14a36d7) + offset := 0 + for offset < len(output) { + seed += v6GoldenGamma + block := v6SplitMix64(seed) + for shift := 0; shift < 64 && offset < len(output); shift += 8 { + output[offset] = byte(block >> shift) + offset++ + } + } +} + +func (n v6Namespaces) prfStatic(label uint32, domain uint32) uint32 { + return v6PRF32Fold(n.forLabel(label), label, 0, uint64(domain)) +} + +type v6Profile struct { + namespaces v6Namespaces + + 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 +} + +func v6ProfileSecret(psk []byte) [32]byte { + input := make([]byte, len(v6ProfileSeed)+len(psk)) + copy(input, v6ProfileSeed[:]) + copy(input[len(v6ProfileSeed):], psk) + return blake2b.Sum256(input) +} + +func newV6Profile(psk []byte) *v6Profile { + secret := v6ProfileSecret(psk) + ns := newV6Namespaces(secret[:]) + + padMin := v6Pick(ns.prfStatic(v6LabelPadMin, 0), 0x18, 0xa0) + padMax := minInt(padMin+v6Pick(ns.prfStatic(v6LabelPadMax, 0), 0xa0, 0x3c0), v6MaxExtraPadding) + + prefixMinHS := v6Pick(ns.prfStatic(v6LabelPrefixMin, v6HandshakeDomain), 0x10, 0x60) + prefixMaxHS := minInt(prefixMinHS+v6Pick(ns.prfStatic(v6LabelPrefixMax, v6HandshakeDomain), 0x10, 0xa0), 0x80) + prefixMinHS = minInt(prefixMinHS, prefixMaxHS) + saltPrefixLen := v6Pick(ns.prfStatic(v6LabelRecordPrefix, v6HandshakeDomain), prefixMinHS, prefixMaxHS) + saltBlockLen := v6SaltSize + saltPrefixLen + + mixRoundsHS := v6PickU32(ns.prfStatic(v6LabelMixRounds, v6MixHandshakeDomain), 1, 4) + mixStrideHS := v6Pick(ns.prfStatic(v6LabelMixStride, v6MixHandshakeDomain), 0x11, 0xfb) + + prefixMinRecord := v6Pick(ns.prfStatic(v6LabelPrefixMin, 0), 0x08, 0x50) + prefixMaxRecord := minInt(prefixMinRecord+v6Pick(ns.prfStatic(v6LabelPrefixMax, 0), 0x10, 0xa0), 0x80) + prefixMinRecord = minInt(prefixMinRecord, prefixMaxRecord) + + chunkInitial := maxInt(0x60, minInt(v6Pick(ns.prfStatic(v6LabelChunkInitial, 0), 0x200, 0x05b4), 0x05b4)) + firstRecordCap := maxInt(0x100, minInt(v6Pick(ns.prfStatic(v6LabelChunkFirstCap, v6ChunkInitialDomain), 0x100, 0x300), minInt(chunkInitial, 0x300))) + chunkMax := maxInt(v6Pick(ns.prfStatic(v6LabelChunkMax, 0), 0x2000, 0x3fff), chunkInitial) + + profile := &v6Profile{ + namespaces: ns, + generator: ns.prfStatic(v6LabelGenerator, 0) & 3, + padMin: padMin, + padMax: padMax, + padCount: v6PickU32(ns.prfStatic(v6LabelPadCount, 0), 2, 8), + padInterval: v6PickU32(ns.prfStatic(v6LabelPadInterval, 0), 2, 0x0b), + smallLimit: v6Pick(ns.prfStatic(v6LabelSmallLimit, 0), 0x60, 0x300), + bitMin: v6PickU32(ns.prfStatic(v6LabelBitMin, 0), 0x18, 0x29), + bitMax: v6PickU32(ns.prfStatic(v6LabelBitMax, 0), 0x3a, 0x4c), + prefixMinRecord: prefixMinRecord, + prefixMaxRecord: prefixMaxRecord, + mixMode: ns.prfStatic(v6LabelMixMode, 0) % 3, + mixRounds: v6PickU32(ns.prfStatic(v6LabelMixRounds, 0), 1, 3), + mixStride: v6Pick(ns.prfStatic(v6LabelMixStride, 0), 2, 13), + mixOffsetBase: v6Pick(ns.prfStatic(v6LabelMixOffsetBase, 0), 0, 15), + mixBlock: v6Pick(ns.prfStatic(v6LabelMixBlock, 0), 8, 0x40), + chunkPolicy: ns.prfStatic(v6LabelChunkPolicy, 0) % 3, + chunkInitial: chunkInitial, + firstRecordCap: firstRecordCap, + chunkMax: chunkMax, + chunkStep: minInt(v6Pick(ns.prfStatic(v6LabelChunkStep, 0), 0x400, 0x1000), 0x0b68), + chunkJitter: minInt(v6Pick(ns.prfStatic(v6LabelChunkJitter, 0), 0x10, 0xc0), 0x0b6), + idleResetSec: v6Pick(ns.prfStatic(v6LabelIdleReset, 0), 0x0c, 0x5a), + writePolicy: ns.prfStatic(v6LabelWritePolicy, 0) % 3, + writeFirst: v6PickU32(ns.prfStatic(v6LabelWriteFirst, 0), 4, 8), + writeJitter: v6Pick(ns.prfStatic(v6LabelWriteJitter, 0), 0x08, 0x60), + writeJitterPct: v6Pick(ns.prfStatic(v6LabelWritePolicy, 0x504c), 8, 0x30), + g1: v6Pick(ns.prfStatic(v6LabelGenerator, 1), 0x18, 0x80), + g2: v6Pick(ns.prfStatic(v6LabelGenerator, 2), 0x10, 0x60), + g3: v6Pick(ns.prfStatic(v6LabelGenerator, 3), 0x10, 0x60), + g4: v6Pick(ns.prfStatic(v6LabelGenerator, 4), 0x00, 0x09), + g5: v6Pick(ns.prfStatic(v6LabelGenerator, 5), 0x01, 0x08), + g6: v6Pick(ns.prfStatic(v6LabelGenerator, 6), 0x07, 0x17), + saltBlockLen: saltBlockLen, + mixStrideHandshake: mixStrideHS, + mixRoundsHandshake: mixRoundsHS, + } + + for index := 0; index < 8; index++ { + profile.chunkBuckets[index] = v6Pick(ns.prfStatic(v6LabelChunkBucket, uint32(index)), 0x1000, chunkMax) + profile.writeBuckets[index] = v6Pick(ns.prfStatic(v6LabelWriteBucket, uint32(index)), 0x140, 0x05b4) + profile.writeSeq[index] = v6Pick(ns.prfStatic(v6LabelWriteSeq, uint32(index)), 0x168, 0x05b4) + } + + return profile +} + +// recordPrefixLen returns the length of the unencrypted prefix in front of the +// header of the record with the given sequence number. +func (p *v6Profile) recordPrefixLen(seq uint32) int { + return v6Pick(p.namespaces.prf32(v6LabelRecordPrefix, seq, 0), p.prefixMinRecord, p.prefixMaxRecord) +} + +func (p *v6Profile) chunkPayloadLimit(seq uint32, chunkSize int) int { + if chunkSize == 0 { + chunkSize = p.chunkInitial + } + switch p.chunkPolicy { + case 1: + chunkSize = p.chunkBuckets[p.namespaces.prf32(v6LabelChunkSize, seq, uint32(chunkSize))%uint32(len(p.chunkBuckets))] + case 2: + rawJitter := p.namespaces.prf32(v6LabelChunkJitterV, seq, uint32(chunkSize)) + chunkSize += int(rawJitter%uint32(p.chunkJitter*2+1)) - p.chunkJitter + } + return maxInt(0x40, minInt(chunkSize, p.chunkMax)) +} + +func (p *v6Profile) nextChunkSize(chunkSize int) int { + if chunkSize == 0 { + return p.chunkInitial + } + return minInt(chunkSize+p.chunkStep, p.chunkMax) +} + +// paddingLen decides how much padding the record with sequence number seq +// carries. totalLen is the salt block length when this is the first record. +func (p *v6Profile) 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 = v6Pick(p.namespaces.prf32(v6LabelPayloadPad, seq, uint32(payloadLen)), p.padMin, p.padMax) + } + payloadTagLen := 0 + if payloadLen > 0 { + payloadTagLen = v6TagSize + } + frameLen := totalLen + prefixLen + v6HeaderCipherSize + paddingLen + payloadLen + payloadTagLen + targetLen := p.writeTarget(seq, frameLen) + if targetLen > frameLen { + paddingLen += minInt(targetLen-frameLen, v6MaxExtraPadding) + } + if totalLen > 0 { + paddingLen = p.firstRecordPaddingLen(paddingLen, prefixLen, saltPrefixLen, payloadLen) + } + return minInt(paddingLen, 0xffff) +} + +func (p *v6Profile) 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(v6LabelWriteTarget, seq, uint32(frameLen))%uint32(len(p.writeBuckets))] + } + if p.writePolicy == 2 { + rawJitter := p.namespaces.prf32(v6LabelWriteJitterV, seq, 0) + jitter := int(rawJitter%uint32(p.writeJitter*2+1)) - p.writeJitter + targetLen = maxInt(targetLen+jitter, 1) + } + spread := minInt(v6MaxExtraPadding, int(uint64(frameLen)*uint64(p.writeJitterPct)/100)) + if p.namespaces.prf32(v6LabelWriteTarget, 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(v6LabelWriteNext, 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 *v6Profile) firstRecordPaddingLen(paddingLen int, prefixLen int, saltPrefixLen int, payloadLen int) int { + overheadLen := saltPrefixLen + prefixLen + paddingLen + thresholdInput := payloadLen + 0x37 + if payloadLen == 0 { + thresholdInput = payloadLen + 0x27 + } + threshold := maxInt((thresholdInput*25+0x4a)/75, 0xc0) + if overheadLen >= threshold { + return paddingLen + } + targetPaddingLen := threshold - saltPrefixLen - prefixLen + maxPaddingLen := p.padMax + v6MaxExtraPadding + if maxPaddingLen <= targetPaddingLen { + targetPaddingLen = maxPaddingLen + } + if targetPaddingLen > 0xfffe { + return paddingLen + } + return targetPaddingLen +} + +// fillPadding generates the deterministic padding bytes of a record. +func (p *v6Profile) fillPadding(seq uint32, output []byte) { + if len(output) == 0 { + return + } + p.namespaces.prfBytes(v6LabelPadding, seq, output) + switch p.generator { + case 0: + bitCount := v6PickU32(p.namespaces.prf32(v6LabelBitPercent, 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 := maxInt(rotate, 1) + tableValue := v6BitRotateTable[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: + // the byte/index mix is truncated to a byte before being reduced into the printable range + output[index] = byte(v6Pick(uint32(byte(int(value)+index)), 0x20, 0x7e)) + case bucket < p.g1+p.g2: + output[index] = byte(v6Pick(uint32(byte(int(value)^index)), 0x80, 0xbf)) + default: + output[index] = byte(v6Pick(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(v6LabelMotif, seq, motif) + motifLen := p.g5 * 4 + if motifLen == 0 { + motifLen = 4 + } + period := maxInt(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 + } + } + } +} + +// --- salt block ------------------------------------------------------------- + +func v6SaltShufflePRF(nsSalt uint64, domain uint32, index uint32) uint32 { + rdx := uint64(index)*v6CoefB + v6AddB + rdi := nsSalt ^ v6SaltNSXor + rsi := uint64(domain)*v6CoefA + v6AddA + y := v6SplitMix64((rdx ^ rdi) ^ rsi) + return uint32(y ^ (y >> 32)) +} + +func v6ShufflePerm(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(v6MixHandshakeDomain) + round + for i := 0; i < length; i++ { + span := uint64(length - i) + raw := uint64(v6SaltShufflePRF(nsSalt, domain, uint32(i))) + j := i + int(raw%span) + out[i], out[j] = out[j], out[i] + } + } + return out +} + +func v6SaltMask(nsSalt uint64, mixStride uint8, index uint32) byte { + prf := v6PRF32Fold(nsSalt, v6LabelMotif, uint64(v6MixHandshakeDomain), uint64(index)) + return byte(index)*mixStride ^ byte(prf) +} + +// extractSalt recovers the 16-byte salt scattered inside the first block. +func (p *v6Profile) extractSalt(block []byte) [v6SaltSize]byte { + perm := v6ShufflePerm(p.namespaces.salt, byte(p.mixRoundsHandshake), len(block)) + var out [v6SaltSize]byte + for i := 0; i < v6SaltSize; i++ { + out[i] = v6SaltMask(p.namespaces.salt, byte(p.mixStrideHandshake), uint32(i)) ^ block[perm[i]] + } + return out +} + +// writeSaltBlock scatters the salt into block, which must already hold the +// shaped padding generated for the sentinel sequence 0xffffffff. +func (p *v6Profile) writeSaltBlock(salt []byte, block []byte) { + perm := v6ShufflePerm(p.namespaces.salt, byte(p.mixRoundsHandshake), len(block)) + for i := 0; i < v6SaltSize; i++ { + block[perm[i]] = v6SaltMask(p.namespaces.salt, byte(p.mixStrideHandshake), uint32(i)) ^ salt[i] + } +} + +// --- padding / ciphertext interleave ----------------------------------------- + +// mixPaddingPayload swaps bytes between the padding and the payload ciphertext. +// The operation is its own inverse, so the reader applies it before decrypting. +func (p *v6Profile) mixPaddingPayload(seq uint32, padding []byte, payloadCipher []byte) { + n := minInt(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 *v6Profile) mixFixedStride(round uint32, padding []byte, payloadCipher []byte, n int) { + stride := maxInt(p.mixStride+int(round%3), 1) + if stride == 1 { + for i := 0; i < n; i++ { + 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 *v6Profile) 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 *v6Profile) mixPRFStride(seq uint32, round uint32, padding []byte, payloadCipher []byte, n int) { + stride := maxInt(p.mixStride+int(round%3), 1) + off := int((uint64(p.namespaces.prf32(v6LabelMixOffset, seq, round)) + uint64(p.mixOffsetBase)) % uint64(stride)) + if stride == 1 { + for i := 0; i < n; i++ { + 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/transport/snell/v6_test.go b/transport/snell/v6_test.go new file mode 100644 index 0000000000..513168b023 --- /dev/null +++ b/transport/snell/v6_test.go @@ -0,0 +1,204 @@ +package snell + +import ( + "bytes" + "crypto/rand" + "errors" + "io" + "net" + "reflect" + "testing" + "time" + + "github.com/metacubex/mihomo/transport/shadowsocks/shadowaead" + + "github.com/stretchr/testify/require" +) + +var v6TestModes = map[string]V6Mode{ + "default": V6ModeDefault, + "unshaped": V6ModeUnshaped, + "unsafe-raw": V6ModeUnsafeRaw, +} + +const v6TestPSK = "snell-v6-test-psk-0123456789" + +func TestV6ParseMode(t *testing.T) { + for _, name := range []string{"", "default", "unshaped", "unsafe-raw"} { + _, err := ParseV6Mode(name) + require.NoError(t, err, name) + } + _, err := ParseV6Mode("bogus") + require.Error(t, err) + require.Equal(t, "unshaped", V6ModeUnshaped.String()) +} + +func TestV6ProfileDeterministic(t *testing.T) { + a := newV6Profile([]byte(v6TestPSK)) + b := newV6Profile([]byte(v6TestPSK)) + require.True(t, reflect.DeepEqual(a, b), "same psk must derive the same profile") + + c := newV6Profile([]byte(v6TestPSK + "x")) + require.False(t, reflect.DeepEqual(a.namespaces, c.namespaces), "different psk must derive a different profile") +} + +func TestV6SaltBlockRoundTrip(t *testing.T) { + for i := 0; i < 32; i++ { + psk := make([]byte, 24) + _, err := rand.Read(psk) + require.NoError(t, err) + profile := newV6Profile(psk) + + salt, err := v6RandomSalt() + require.NoError(t, err) + block := make([]byte, profile.saltBlockLen) + profile.fillPadding(0xffffffff, block) + profile.writeSaltBlock(salt, block) + extracted := profile.extractSalt(block) + require.Equal(t, salt, extracted[:]) + } +} + +func TestV6MixRoundTrip(t *testing.T) { + for i := 0; i < 32; i++ { + psk := make([]byte, 16) + _, err := rand.Read(psk) + require.NoError(t, err) + profile := newV6Profile(psk) + + padding := make([]byte, 300) + payload := make([]byte, 700) + _, _ = rand.Read(padding) + _, _ = rand.Read(payload) + origPadding := append([]byte(nil), padding...) + origPayload := append([]byte(nil), payload...) + + profile.mixPaddingPayload(uint32(i), padding, payload) + profile.mixPaddingPayload(uint32(i), padding, payload) + require.Equal(t, origPadding, padding) + require.Equal(t, origPayload, payload) + } +} + +// v6Pair returns a client and a server side wrapped with the v6 record layer +// over a real loopback TCP connection. +func v6Pair(t *testing.T, mode V6Mode) (client, server *Snell) { + t.Helper() + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer listener.Close() + + accepted := make(chan net.Conn, 1) + go func() { + conn, err := listener.Accept() + if err != nil { + close(accepted) + return + } + accepted <- conn + }() + clientConn, err := net.Dial("tcp", listener.Addr().String()) + require.NoError(t, err) + serverConn, ok := <-accepted + require.True(t, ok) + t.Cleanup(func() { + clientConn.Close() + serverConn.Close() + }) + return StreamConnV6(clientConn, []byte(v6TestPSK), mode), ServerStreamConnV6(serverConn, []byte(v6TestPSK), mode) +} + +func TestV6ConnStream(t *testing.T) { + for name, mode := range v6TestModes { + t.Run(name, func(t *testing.T) { + client, server := v6Pair(t, mode) + payload := make([]byte, 300*1024) + _, err := rand.Read(payload) + require.NoError(t, err) + + errCh := make(chan error, 1) + go func() { + _, err := client.Write(payload) + if err == nil { + err = HalfClose(client) + } + errCh <- err + }() + + received := make([]byte, len(payload)) + require.NoError(t, server.SetReadDeadline(time.Now().Add(10*time.Second))) + _, err = io.ReadFull(server.Conn, received) + require.NoError(t, err) + require.True(t, bytes.Equal(payload, received), "payload mismatch") + require.NoError(t, <-errCh) + + // the zero chunk ends the stream + _, err = server.Conn.Read(received[:1]) + require.True(t, errors.Is(err, shadowaead.ErrZeroChunk), "expected zero chunk, got %v", err) + + // and the other direction works independently + reply := []byte("hello from server") + _, err = server.Write(reply) + require.NoError(t, err) + got := make([]byte, len(reply)) + require.NoError(t, client.SetReadDeadline(time.Now().Add(10*time.Second))) + _, err = io.ReadFull(client.Conn, got) + require.NoError(t, err) + require.Equal(t, reply, got) + }) + } +} + +func TestV6ConnPacketFrames(t *testing.T) { + for name, mode := range v6TestModes { + t.Run(name, func(t *testing.T) { + client, server := v6Pair(t, mode) + for round := 0; round < 20; round++ { + // sizes above the shaped chunk limit must still arrive as one record + packet := make([]byte, 1400+round*97) + _, err := rand.Read(packet) + require.NoError(t, err) + _, err = client.WritePacketFrame(packet) + require.NoError(t, err) + + buf := make([]byte, 64*1024) + require.NoError(t, server.SetReadDeadline(time.Now().Add(10*time.Second))) + n, err := server.Conn.Read(buf) + require.NoError(t, err) + require.Equal(t, len(packet), n, "a packet frame must be read back in one piece") + require.True(t, bytes.Equal(packet, buf[:n])) + } + }) + } +} + +func TestV6ConnWrongPSK(t *testing.T) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer listener.Close() + go func() { + conn, err := listener.Accept() + if err != nil { + return + } + defer conn.Close() + server := ServerStreamConnV6(conn, []byte(v6TestPSK+"-other"), V6ModeDefault) + buf := make([]byte, 16) + _, _ = server.Conn.Read(buf) + }() + conn, err := net.Dial("tcp", listener.Addr().String()) + require.NoError(t, err) + defer conn.Close() + client := StreamConnV6(conn, []byte(v6TestPSK), V6ModeDefault) + _, err = client.Write([]byte("hello")) + require.NoError(t, err) + require.NoError(t, client.SetReadDeadline(time.Now().Add(5*time.Second))) + _, err = client.Conn.Read(make([]byte, 16)) + require.Error(t, err, "the server must not produce a readable reply with a different psk") +} + +func TestValidateV6PSK(t *testing.T) { + require.Error(t, ValidateV6PSK([]byte("short"))) + require.Error(t, ValidateV6PSK(make([]byte, 256))) + require.NoError(t, ValidateV6PSK([]byte(v6TestPSK))) +} From 0a1464c3b1cf01102eda68be6f93a75645f2fa7b Mon Sep 17 00:00:00 2001 From: hungrybirder Date: Thu, 5 Nov 2026 11:07:18 +0800 Subject: [PATCH 4/4] feat: add StreamConnV6 and ServerStreamConnV6 helpers Missed from the previous commit; wires the native v6 record layer into the shared Snell stream constructors. Co-Authored-By: Claude Fable 5.1 --- transport/snell/snell.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/transport/snell/snell.go b/transport/snell/snell.go index c9b2d92528..a05a3d9f7f 100644 --- a/transport/snell/snell.go +++ b/transport/snell/snell.go @@ -155,6 +155,9 @@ func HalfClose(conn net.Conn) error { } func StreamConn(conn net.Conn, psk []byte, version int) *Snell { + if version >= Version6 { + return StreamConnV6(conn, psk, V6ModeDefault) + } if version >= Version4 { return &Snell{Conn: newV4Conn(conn, psk)} } @@ -174,6 +177,18 @@ func ServerStreamConn(conn net.Conn, psk []byte, version int) *Snell { return stream } +// StreamConnV6 wraps conn with the Snell v6 record layer in the given mode. +// The request/reply layer above it is shared with the other versions. +func StreamConnV6(conn net.Conn, psk []byte, mode V6Mode) *Snell { + return &Snell{Conn: newV6Conn(conn, psk, mode)} +} + +func ServerStreamConnV6(conn net.Conn, psk []byte, mode V6Mode) *Snell { + stream := StreamConnV6(conn, psk, mode) + stream.reply = true + return stream +} + func PacketConn(conn net.Conn) net.PacketConn { return &packetConn{ Conn: conn,