Skip to content

feat: add Snell version 6 support (outbound + inbound) - #3410

Merged
wwqgtxx merged 4 commits into
MetaCubeX:Alphafrom
hungrybirder:feat/snell-v6
Nov 5, 2026
Merged

wwqgtxx merged 4 commits into
MetaCubeX:Alphafrom
hungrybirder:feat/snell-v6

Conversation

@hungrybirder

@hungrybirder hungrybirder commented Nov 5, 2026 •

Copy link
Copy Markdown

What

Adds Snell v6 to the snell outbound and inbound, implemented natively on top of the existing transport/snell infrastructure in the same way v4/v5 are structured.

  • transport/snell/v6.go: a v6Conn that wraps the raw connection with the v6 record layer (default / unshaped / unsafe-raw), exposed through StreamConnV6 / ServerStreamConnV6. The request/reply layer, UDP packet framing, the reuse pool and the listener request loop are reused unchanged, since v6 kept the inner request format.
  • transport/snell/v6_profile.go: the PSK-derived profile of the default mode (salt block scatter/mask, per-record prefix used as header AAD, padding generation and padding/ciphertext interleave, shaped record sizes). The derivation follows the protocol notes published by SagerNet/sing-snell; v6 otherwise keeps the v4 AES-128-GCM framing and KDF.
  • Outbound: version: 6 with a mode option that must match the server. TCP, UDP over TCP and reuse work through the existing code paths. http/tls simple-obfs is rejected on v6; the shadow-tls/restls/jls wrappers still apply. PSKs must be 12..255 bytes.
  • Inbound: the listener accepts version: 6 and mode and serves it through its normal request loop.
  • docs/config.yaml updated for both sides.

Why

Snell v6 (snell-server 6.0.0) is now the default in Surge. Closes #3083.

Testing

  • transport/snell/v6_test.go: profile determinism, salt block and interleave round trips, loopback stream / zero chunk / packet frame tests for all three modes, wrong-PSK rejection.
  • listener/inbound/snell_test.go: outbound ↔ inbound end-to-end for all three modes covering TCP, reuse and UDP, plus shadow-tls v3 on top of v6 and config validation.
  • Before the ported package was removed, the native layer was cross-tested against it in all three modes (TCP and UDP, both directions) and passed.
  • Verified against the official snell-server 6.0.0 binary in all three modes (default, unshaped, unsafe-raw): HTTP over TCP (including an ~86 KB response spanning many records), three sequential requests over a reused session, and UDP DNS queries all succeed. Not yet tested with a Surge client as the peer of the inbound.

History

The first two commits were a straight port of sing-snell; the later commits replace it with the native implementation as requested in review. Happy to squash if preferred.

🤖 Generated with Claude Code

hungrybirder and others added 2 commits November 5, 2026 09:44
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@wwqgtxx

wwqgtxx commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator

Since you are already using AI for programming, you shouldn't just port the sing implementation as-is. We already have a substantial amount of infrastructure in transport/snell; the most sensible approach would be to build upon that—much like how the current v4/v5 implementations are structured.

hungrybirder and others added 2 commits November 5, 2026 11:06
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 <noreply@anthropic.com>
Missed from the previous commit; wires the native v6 record layer into
the shared Snell stream constructors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@hungrybirder

Copy link
Copy Markdown
Author

Thanks for the review. I have reworked it as suggested: the ported package is gone and v6 is now implemented natively on top of transport/snell, structured like v4/v5. A v6Conn provides the record layer (StreamConnV6 / ServerStreamConnV6), and the existing request/reply, UDP packet, pool and listener request-loop code is reused unchanged since v6 kept the inner request format. The PSK-derived profile lives in transport/snell/v6_profile.go.

Before removing the ported package I cross-tested the native layer against it in all three modes (TCP and UDP, both directions). The PR description is updated; I can squash the history if you prefer.

@hungrybirder

Copy link
Copy Markdown
Author

Interop update: the native implementation is now verified against the official snell-server 6.0.0 binary in all three modes (default, unshaped, unsafe-raw). For each mode: HTTP over TCP including an ~86 KB response that spans many records, three sequential requests over a reused session (reuse: true), and UDP DNS queries through the tunnel all succeed.

@wwqgtxx
wwqgtxx merged commit f1f640b into MetaCubeX:Alpha Nov 5, 2026
135 checks passed
@hungrybirder
hungrybirder deleted the feat/snell-v6 branch November 5, 2026 10:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants