3.8 KiB
3.8 KiB
Conventions & load-bearing invariants
Code style
- Match the surrounding code: dense, explanatory comments that say why (the data path is
full of non-obvious protocol/platform reasoning — keep that). Kotlin idioms, coroutines with
explicit dispatchers,
runCatchingat boundaries. - Keep platform glue thin and honest. Comments must not over-claim (a probe detects a bad Shadowsocks key but usually can't name it — say exactly that).
- No new dependencies without a strong reason — a deliberate property of this app is a tiny dependency surface (no crypto lib; AEAD is on platform JCA).
Commits
- No
Co-Authored-Bytrailer. Keep each commit message ≤ 30 words. - Split work into logical, per-subsystem commits.
Load-bearing invariants — DO NOT "fix" these without reading
Each of these looks like a bug or an easy cleanup and has already cost a regression. Verify against the code and the harness before touching.
protect()returningfalseis benign. Our own sockets are already excluded by the app filter, soprotect()may return false with no harm. Log, don't throw. (Throwing here once broke every upstream connection.)- IPv6 is routed into the tun only when
settings.ipv6. hev does not drop unconfigured- family packets — it forwards a v6 CONNECT to a v6-incapable proxy (rep=5), stalling Happy-Eyeballs apps. With v6 absent, Android adds::/0 unreachable, so v6 is blackholed (no leak).dnsServerFor()coerces a v6-literal DNS to1.1.1.1while v6 is off — keep it. - SOCKS5 UDP socket is
connect()-ed to the relay. RFC 1928 §7 anti-spoof = the kernel(IP, port)filter; that's also the ephemeral-port DNS-spoof defence. Do not replace it with a host-only userspace filter (weaker — drops the port check). Matches sing-box/canon. isUnroutableRelay()rewrite is intentional and broader than canon. A SOCKS5 UDP ASSOCIATE BND.ADDR that is wildcard/loopback/private/CGNAT/ULA is rewritten to the proxy host. This fixes real servers (e.g. 3proxy returning10.x) that would otherwise black-hole all UDP/DNS. Narrowing it to wildcard-only would reintroduce a device-confirmed bug.- Shadowsocks reader EOF semantics. EOF on a frame boundary = clean end of stream
(return -1). EOF inside a frame = truncation = error. And a zero-length chunk is valid
(canon emits/accepts empty keep-alive frames) — reject only
> MAX_PAYLOAD. - The local SOCKS5 bridge requires random per-session auth. It's the boundary that stops a non-whitelisted local app from using the loopback proxy. Don't make it no-auth.
- ChaCha20 cipher name is
ChaCha20/Poly1305/NoPadding(Conscrypt registers the slash name since API 28; the hyphenated alias only exists on newer Mainline). Not a typo. ConfigStorenever wipes on a decode failure. It snapshots last-known-good before each write and recovers from it; only a genuineSerializationException/IllegalArgumentExceptioncounts as corruption (letError/OOM propagate so a transient failure doesn't wipe servers).- MTU 8500 is the baseline (not a tuned value); the engine config and tun must agree.
The "no universal core" decision
Replacing the Kotlin backend (SS crypto + local SOCKS5 + upstream clients) with sing-box /
sing-tun / mihomo / libbox was evaluated and rejected: those are GPLv3 (live
enforcement) and would force open-sourcing this closed app, and any Go/Rust runtime breaks the
small-APK goal. hev-socks5-tunnel is MIT and stays. The only defensible strategic swap is
shadowsocks-rust (MIT) for the SS crypto surface only, and only if Kotlin-SS upkeep ever
dominates — soak-test behind hev before deleting anything. Our SS crypto is verified correct
against shadowsocks-rust and sing-shadowsocks.