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

22 KiB

BEP-0012 - Mihomo-Compatible Shadowsocks Runtime

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:
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