# 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, `runCatching` at 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-By` trailer.** 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. 1. **`protect()` returning `false` is benign.** Our own sockets are already excluded by the app filter, so `protect()` may return false with no harm. Log, don't throw. (Throwing here once broke *every* upstream connection.) 2. **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 to `1.1.1.1` while v6 is off — keep it. 3. **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. 4. **`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 returning `10.x`) that would otherwise black-hole all UDP/DNS. Narrowing it to wildcard-only would reintroduce a device-confirmed bug. 5. **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`. 6. **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. 7. **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. 8. **`ConfigStore` never wipes on a decode failure.** It snapshots last-known-good before each write and recovers from it; only a genuine `SerializationException`/`IllegalArgumentException` counts as corruption (let `Error`/OOM propagate so a transient failure doesn't wipe servers). 9. **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.