burrow/evolution/proposals/BEP-0012-mihomo-compatible-shadowsocks-runtime.md
2026-06-05 10:33:41 -07:00

356 lines
22 KiB
Markdown

# `BEP-0012` - Mihomo-Compatible Shadowsocks Runtime
```text
Status: Draft
Proposal: BEP-0012
Authors: Codex
Coordinator: Jett Chen
Reviewers: Pending
Constitution Sections: 2, 4, 5
Implementation PRs: Pending
Decision Date: Pending
```
## Summary
Burrow's proxy subscription runtime should move from a narrow hand-written
Shadowsocks AEAD implementation toward Mihomo-compatible Shadowsocks behavior
for imported nodes. Mihomo remains the reference for accepted ciphers, URI
conversion, plugins, UDP-over-TCP, and packet tunnel outbound semantics, while
Burrow keeps the daemon-owned packet runtime and Apple gRPC boundary described
by `BEP-0005` and `BEP-0010`.
This proposal covers the compatibility path for Shadowsocks outbounds. It starts
by delegating protocol crypto and stream/datagram framing to a maintained Rust
Shadowsocks implementation, adds Burrow-owned adapters for Mihomo cipher gaps
where Rust crates are available, and implements Mihomo-compatible UDP-over-TCP.
Plugin transports remain staged behind explicit lifecycle and security controls.
## Motivation
- Subscriptions that work in Mihomo commonly use more than `aes-128-gcm`,
`aes-256-gcm`, or `chacha20-ietf-poly1305`.
- Mihomo supports legacy stream ciphers, AEAD extra ciphers, AEAD 2022 methods,
SIP003-style plugins, ShadowTLS/restls/kcptun transports, and UDP-over-TCP.
- Burrow's previous hand-written Shadowsocks runtime made unsupported ciphers
appear like permanent product choices rather than implementation gaps.
- Protocol crypto and framing are security-sensitive. Burrow should use a
maintained Rust crate where it can do so without shelling out to Mihomo or
weakening the daemon packet-runtime boundary.
## Detailed Design
- Use the Rust `shadowsocks` crate as Burrow's default Shadowsocks protocol
adapter for supported cipher families, TCP stream framing, and UDP datagram
framing.
- Add Burrow-owned AEAD adapters backed by Rust crypto crates for Mihomo methods
missing from the default adapter when their wire behavior is straightforward.
The first such methods are `aes-192-gcm`, `aes-192-ccm`,
`chacha8-ietf-poly1305`, `xchacha8-ietf-poly1305`, `rabbit128-poly1305`,
`aegis-128l`, `aegis-256`, `aez-384`, `deoxys-ii-256-128`, `ascon128`,
`ascon128a`, `lea-*-gcm`, `2022-blake3-aes-128-ccm`, and
`2022-blake3-aes-256-ccm`.
- Add Burrow-owned legacy stream adapters for Mihomo's `chacha20` and
`xchacha20` methods. They use the same Shadowsocks v1 IV-plus-stream shape as
Mihomo's `sing-shadowsocks2` implementation, backed by Rust `chacha20`
primitives.
- Keep Burrow-owned socket creation, route exclusion, and Apple physical
interface binding behavior. The Shadowsocks adapter wraps already-connected
Burrow sockets where needed instead of taking over tunnel ownership.
- Runtime-supported ciphers include the compiled Rust crate feature set:
legacy stream methods, AEAD methods, AEAD extra methods, and AEAD 2022
methods, plus an explicit Mihomo-compatible `none` transport, custom
ChaCha20/XChaCha20 stream, AES-192 GCM/CCM, ChaCha8-Poly1305,
Rabbit128-Poly1305, AEGIS, AEZ-384, Deoxys-II-256-128, Ascon128, Ascon128a,
LEA-GCM, and AEAD 2022 AES-CCM adapters. `none` stays Burrow-owned because
the upstream Rust crate accepts the method name during validation but cannot
build a client runtime for it.
AEAD 2022 password handling follows Mihomo's base64 key parsing rule:
every decoded password segment must be exactly the method key length, with
no hashing or truncation fallback for oversized material. Burrow pre-validates
the delegated GCM/ChaCha 2022 methods so the upstream Rust adapter cannot
accept unpadded key material that Mihomo's `base64.StdEncoding` would reject.
Burrow keeps legacy aliases such as `aead_aes_128_gcm` and
`chacha20-poly1305` for previously imported payloads.
Burrow also preserves Mihomo's top-level Shadowsocks `smux` option during
import. The packet runtime supports TCP and UDP `protocol: h2mux`, `protocol:
smux`, and `protocol: yamux` paths for delegated standard ciphers, `none`, Burrow's custom ciphers,
and AEAD 2022 CCM by dialing Mihomo's `sp.mux.sing-box.arpa:444` control
destination through Shadowsocks, writing the sing-mux protocol preface, and
opening mux streams with Mihomo's per-stream SOCKS destination request. Burrow
also supports Mihomo's top-level sing-mux padding shape: the version 1 session
preface carries a padding flag and 256-767 bytes of skipped padding, then the
first 16 mux reads and writes are length-prefixed padded frames before the
stream falls back to raw mux bytes. UDP-over-sing-mux uses Mihomo's packet
stream shape: UDP stream flags, one SOCKS destination, one status byte, and
length-prefixed datagrams. Brutal mode performs Mihomo's `_BrutalBwExchange`
stream handshake and negotiates the same send-rate cap; OS-level TCP Brutal
congestion control remains a runtime best-effort because it is
Linux-kernel-module dependent in Mihomo and unavailable through every Burrow
plugin wrapper.
The remaining Mihomo compatibility gaps for Shadowsocks are advanced plugin
transports and live interop coverage for additional external permutations,
not base Shadowsocks cipher, UDP-over-TCP framing, or top-level sing-mux
negotiation.
- Honor the Shadowsocks `udp` flag at runtime. Clash/Mihomo YAML imports default
omitted `udp` to false like Mihomo's `ShadowSocksOption` zero value, while
`ss://` URI conversion keeps Mihomo's converter behavior of setting `udp` to
true. UDP-disabled nodes must reject UDP sessions instead of silently
attempting native UDP relay.
- Honor `udp-over-tcp` for Shadowsocks nodes by opening a Shadowsocks TCP stream
to Mihomo's UOT magic destination. Burrow supports both Mihomo's legacy
version 1 destination, `sp.udp-over-tcp.arpa`, and version 2 destination,
`sp.v2.udp-over-tcp.arpa`; absent or zero versions normalize to legacy v1 to
match Mihomo config defaults. Runtime UDP sessions are allowed when
`udp-over-tcp` is active even if native UDP is disabled, because packets are
carried over the selected TCP plugin path instead of a native Shadowsocks UDP
relay.
- Support Mihomo's `obfs` plugin for HTTP and TLS simple-obfs by wrapping the
already-connected TCP socket before the Shadowsocks stream handshake. Native
UDP remains direct Shadowsocks UDP, while `udp-over-tcp` uses the same wrapped
TCP path. The plugin mode must be explicit, matching Mihomo's `obfs` option
decoder: simple-obfs accepts only `http` and `tls`. Burrow normalizes SIP002
URI plugin names that contain `obfs` into Mihomo's YAML/runtime plugin name
`obfs` during import only when the plugin string has semicolon-delimited
SIP002 options; bare `ss://` plugin names and non-Mihomo-converted URI plugin
names are ignored like Mihomo's converter. Daemon runtime and Apple usability
checks intentionally require Mihomo's exact, case-sensitive plugin names. YAML
`plugin: obfs-local`, `plugin: Obfs`, `plugin: shadowtls`, and other
unrecognized plugin names therefore fall through to plain Shadowsocks like
Mihomo's outbound constructor instead of being treated as supported plugin
transports.
- Support Mihomo's `v2ray-plugin` websocket mode, including the lightweight
v2ray-plugin mux preface enabled by Mihomo's default `mux: true`, optional
TLS, custom path, host, and `v2ray-http-upgrade` raw-upgrade mode. Burrow also supports Mihomo's
`v2ray-http-upgrade-fast-open` behavior by returning the upgraded raw stream
after sending the HTTP request and validating the 101 response on first read.
Imported `ss://` v2ray-plugin strings preserve Mihomo's SIP002 aliases such
as `obfs=websocket`, `obfs-host`, and bare `tls` flags, while also
materializing Mihomo's converted `mode`, `host`, and boolean `tls` option
shape.
For websocket paths with Mihomo's `ed=` query option, Burrow removes `ed`
from the request path and moves the first payload bytes into the
`Sec-WebSocket-Protocol` header with unpadded URL-safe base64 encoding.
When TLS is enabled, Burrow accepts Mihomo's `certificate` and `private-key`
plugin options as a PEM client
certificate/key pair for mTLS, and enforces Mihomo's `fingerprint`
certificate pin option on both Rustls and Apple native TLS paths. Burrow also
supports Mihomo's `ech-opts` for websocket TLS by passing inline ECH config
lists to Rustls, or by resolving HTTPS records through the physical DNS
resolver when `ech-opts.enable` is set without an inline config.
- Support Mihomo's `gost-plugin` websocket mode, including the Go-smux client
stream enabled by Mihomo's default `mux: true` and `mux=false` raw websocket
mode. `v2ray-plugin` and `gost-plugin` require explicit `mode: websocket` or
the imported `obfs=websocket` alias, because Mihomo rejects those plugin nodes
when the mode field is omitted. TLS-enabled gost websocket nodes use the same
Mihomo-compatible client certificate/key and certificate pin handling,
including websocket ECH support.
- Support Mihomo's `shadow-tls` plugin for versions 1, 2, and 3 by performing the
cover TLS handshake in-process with Rustls, then exposing the post-handshake
stream to the Shadowsocks adapter. Version 2 wraps application data in
ShadowTLS application-data records and prepends the first client payload with
the handshake HMAC, matching Mihomo's `sing-shadowtls` client behavior.
Version 3 uses a ShadowTLS-capable Rustls fork for the ClientHello session ID
HMAC, then verifies and emits the four-byte HMAC application-data frames used
by Mihomo's `sing-shadowtls` v3 client. Version 1 follows Mihomo's option
shape and does not require a plugin password.
Burrow also parses and enforces Mihomo's ShadowTLS `fingerprint` certificate
pin option and supports Mihomo's `certificate` and `private-key` mTLS client
certificate options. `client-fingerprint` is preserved from subscriptions.
ShadowTLS v1 ignores `client-fingerprint` like Mihomo because it always uses a
normal TLS 1.2 handshake. For ShadowTLS v2/v3, unknown fingerprint names fall
back to the normal TLS ClientHello, while names that Mihomo maps to uTLS
profiles are rejected by the default runtime and Apple usability filter until
Burrow has uTLS-style ClientHello impersonation. Burrow carries an optional
`boring-browser-fingerprints` feature that wires ShadowTLS v2 active browser
fingerprints through Rama's BoringSSL-backed embedded browser TLS profiles.
ShadowTLS ALPN defaults to `h2,http/1.1` only when the option is absent;
explicitly empty `alpn` remains empty like Mihomo's decoded option struct.
The same feature also provides the ShadowTLS v3 browser-profile path by
signing the first ClientHello record's 32-byte session ID before it reaches
the wire, matching Mihomo's uTLS `SessionIDGenerator` shape. The BoringSSL
browser-profile paths also load Mihomo-compatible `certificate` and
`private-key` client-auth material, so ShadowTLS v2/v3 browser fingerprints
and mTLS can be combined the same way Mihomo passes those options into
`sing-shadowtls`. For ShadowTLS `client-fingerprint: random`, Burrow follows
Mihomo's initial random selection shape by choosing one process-wide browser
profile with the same chrome/safari/ios/firefox weights instead of re-rolling
each connection. The ShadowTLS v2 browser-profile path also removes the
`X25519MLKEM768` supported group before building the BoringSSL connector,
matching Mihomo's v2-only workaround for servers that fail with that hybrid
key-share. These paths are intentionally not default yet because they add
a CMake/BoringSSL toolchain requirement, still need CI coverage, and still need
live interop coverage. The optional browser-profile path covers Mihomo's
common `chrome`, `firefox`, `safari`, `ios`, `android`, `edge`, `random`, and
`safari16` names where Rama has matching embedded profiles; older fixed
aliases such as `chrome120` and `firefox120` remain gated until Burrow has
exact matching ClientHello profiles instead of approximate fallbacks.
- Support Mihomo's `kcptun` plugin in-process with Rust KCP, snappy, and smux
crates. Burrow parses Mihomo's kcptun option names and defaults, forces
UDP-over-TCP for kcptun nodes like Mihomo, and opens Shadowsocks TCP streams
over KCP plus optional snappy plus smux. Burrow maintains a Mihomo-style
round-robin smux session pool for `conn` and rotates expired sessions using
`autoexpire` plus delayed `scavengettl` cleanup. Burrow mirrors Mihomo's smux
keepalive timeout shape: the default timeout remains 30 seconds unless the
keepalive interval is at least that value, in which case timeout becomes three
times the interval. Burrow also applies Mihomo-style best-effort `dscp` and
`sockbuf` settings to the underlying UDP socket. For `ratelimit`, Burrow
honors Mihomo's bytes-per-second value with
the same `64 * 1500` byte burst shape at the KCP stream boundary; this is the
closest in-process equivalent available through the selected Rust KCP crate,
which does not expose kcp-go's exact queued UDP packet transmit hook.
- Plugin support landed in stages and must stay covered:
- `obfs`, matching Mihomo URI conversion and option shapes for
`mode`/`obfs`, `host`/`obfs-host`, and the default `bing.com` host.
- `v2ray-plugin` websocket and its lightweight mux.
- `gost-plugin` websocket and smux.
- TLS client certificate/key support for websocket plugins and `shadow-tls`
v1/v2.
- `kcptun` with Rust KCP, snappy, and smux transport code.
- ShadowTLS v3 with a Rustls fork that exposes the session ID generator
required by the protocol.
- Websocket `ech-opts` on Rustls-backed TLS paths.
- `v2ray-http-upgrade-fast-open` for v2ray-plugin raw upgrade mode.
- Websocket early data via the `ed=` path query option.
- `restls` after its TLS ClientHello, authentication, and traffic-shaping
behavior are mapped to audited Rust code. Burrow now parses Mihomo's
`restls` option shape, including `host`, `password`, `version-hint`,
`restls-script`, top-level `client-fingerprint` preservation, Mihomo's
case-sensitive Restls client ID lookup with Chrome fallback, Mihomo's TLS
1.3 session ticket disablement rule, and the BLAKE3-derived traffic secret.
The default runtime still rejects non-`none` uTLS fingerprints, while the
optional `boring-browser-fingerprints` feature can run TLS 1.3 Restls over a
BoringSSL-backed browser profile and signs the first ClientHello record's
session ID using Mihomo's Restls TLS 1.3 auth material. It also
ports and tests Mihomo's BLAKE3-authenticated ClientHello session-ID
material for TLS 1.2 and TLS 1.3, extraction of TLS 1.3 key-share and PSK
identity labels from the serialized ClientHello shape exposed by Burrow's
Rustls session-ID hook, the server-auth record unmasking offsets, a
handshake stream shim that captures ServerHello random, unmasks the first
server-auth record, and captures the encrypted ClientFinished record for
later application-data authentication, the application-data record header,
masked length/command bytes, one-shot ClientFinished binding on the first
client application record,
script-driven padding target, TLS 1.2 GCM explicit counter shape, Mihomo's
signed one-byte response-interrupt count behavior, and the post-auth stream
state machine for buffering blocked writes, resuming them after server data,
and emitting response-interrupt records. The Restls
post-handshake state machine is also covered by an async stream wrapper that
turns user writes into authenticated TLS application-data records, decodes
server records back into plaintext reads, and flushes resumed uploads plus
response-interrupt records before surfacing server plaintext. Burrow now
wires TLS 1.3 Restls nodes into the Shadowsocks outbound path through the
Rustls session-ID hook, the server-auth handshake shim, and the async Restls
stream wrapper. Burrow vendors the ShadowTLS Rustls fork and its Tokio
adapter so TLS 1.2 Restls can pre-generate the ECDHE keys used in Mihomo's
session-ID HMAC material and then reuse the matching private key when
emitting ClientKeyExchange. TLS 1.2 Restls nodes are now accepted by runtime
support checks, surfaced as selectable Apple subscription nodes, and use the
TLS 1.2 GCM Restls application-record codec.
Restls still needs end-to-end interop coverage before it can be claimed as
full Mihomo parity.
- `Scripts/burrow-proxy-selftest` exercises daemon packet streaming against a
controlled local Trojan/Shadowsocks harness and asserts observed TCP, native
UDP, legacy UDP-over-TCP, version 2 UDP-over-TCP, simple-obfs HTTP/TLS,
v2ray HTTP-upgrade/websocket/mux/early-data, and gost raw websocket shapes.
- `Scripts/burrow-shadowsocks-singmux-interop` drives Burrow's daemon against a
temporary Go peer built from Mihomo's `github.com/metacubex/sing-mux` module
and asserts top-level Shadowsocks `smux`, `yamux`, and `h2mux` TCP and UDP
behavior through the daemon with both unpadded and padded sing-mux sessions,
including the `sp.mux.sing-box.arpa:444` control destination, per-stream TCP
destination metadata, UDP packet destination metadata, and UDP packet echo
delivery. The interop matrix intentionally leaves the underlying Shadowsocks
`udp` flag false so Burrow matches Mihomo's behavior: top-level sing-mux
carries UDP unless `only-tcp` is set.
Remaining compatibility evidence still needs live interop against full
Mihomo/sing-box deployments for plugin combinations that the local harness
does not cover, especially Restls post-handshake behavior and
kernel-dependent TCP Brutal socket behavior.
- Keep UDP-over-TCP packet framing compatible with Mihomo's
`udp-over-tcp-version` handling, including the version 2 request prefix and
per-packet address framing.
- UI runtime-support checks must not hard-code stale cipher lists that disagree
with the daemon. When local decoding is unavoidable, the local list must be
updated with the daemon-supported cipher families or replaced with daemon
preview/list-node data.
## Security and Operational Considerations
- Shadowsocks passwords and subscription URLs remain secrets. Error messages and
logs must not include decoded credentials or bearer tokens.
- Plugin support must stay within Burrow's daemon-owned socket lifecycle unless
a later BEP explicitly introduces subprocess management. Shelling out to
SIP003 plugins would add lifecycle and supply-chain risk and must include
teardown and selected-network scoping if introduced.
- `none` and legacy stream methods exist for compatibility, not as a security
recommendation. UI warnings may be added later for weak ciphers without
rejecting Mihomo-compatible imports.
- AEAD 2022 passwords may be encoded keys. Validation must report malformed keys
without logging the raw password.
- Rollback is a code rollback plus re-importing affected subscriptions if stored
payload shape changes.
## Contributor Playbook
1. Compare behavior against `/Users/jettchen/dev/vpn-ref/mihomo` on the `Alpha`
branch before changing Shadowsocks protocol semantics.
2. Keep Apple UI subscription operations behind daemon gRPC.
3. Prefer maintained Rust crates for Shadowsocks crypto/framing rather than
hand-rolled protocol code.
4. Preserve Burrow socket binding and proxy-server route exclusion behavior.
5. Add tests for each parity expansion:
- cipher acceptance and legacy aliases
- `udp` gating
- plugin parsing and runtime gating
- UDP-over-TCP version handling and packet framing
- AEAD 2022 TCP/UDP packet framing for custom adapters
- end-to-end local Shadowsocks TCP, native UDP, UDP-over-TCP, and plugin
selftests; the proxy selftest must switch the daemon from a Trojan node to
Shadowsocks nodes, prove both selected outbounds can carry a TCP probe,
prove the selected Shadowsocks outbounds can carry native UDP, legacy
UDP-over-TCP, and version 2 UDP-over-TCP echo probes, and prove the
simple-obfs HTTP/TLS plugins, v2ray-plugin websocket/default-mux/
HTTP-upgrade/early-data modes, and gost-plugin raw websocket mode wrap
selected Shadowsocks TCP probes
- top-level sing-mux Shadowsocks TCP and UDP interop against Mihomo's Go
`sing-mux` module
6. Run:
```bash
uv run python Scripts/check-bep-metadata.py
cargo fmt -p burrow
cargo test -p burrow proxy_subscription::tests::
cargo test -p burrow proxy_runtime::tests::
cargo check -p burrow
Scripts/burrow-proxy-selftest
Scripts/burrow-shadowsocks-singmux-interop
```
## Alternatives Considered
- Keep the hand-written AEAD-only implementation. Rejected because it locks
Burrow into a narrow subset and makes Mihomo parity much slower.
- Shell out to Mihomo. Rejected for the same reasons recorded in `BEP-0010`:
Burrow would inherit a separate runtime, control plane, and lifecycle surface.
- Implement plugins before cipher parity. Rejected because cipher parity is a
lower-risk foundation and reduces custom crypto code first.
## Impact on Other Work
- Refines the Shadowsocks runtime portion of `BEP-0010`.
- Complements `BEP-0011`, which covers Trojan-specific Mihomo compatibility.
- Future plugin work may require a dedicated subprocess lifecycle BEP if the
runtime cannot stay within the existing daemon teardown model.
## Decision
Pending review.
## References
- `evolution/proposals/BEP-0005-daemon-ipc-and-apple-boundary.md`
- `evolution/proposals/BEP-0010-proxy-subscriptions-as-packet-tunnel-networks.md`
- `evolution/proposals/BEP-0011-mihomo-compatible-trojan-runtime.md`
- `/Users/jettchen/dev/vpn-ref/mihomo/adapter/outbound/shadowsocks.go`
- `/Users/jettchen/dev/vpn-ref/mihomo/common/convert/converter.go`
- Rust `shadowsocks` crate: https://crates.io/crates/shadowsocks