# 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). - **Human-readable text lives in string resources** — English base + full `values-ru` parity (see architecture.md). Language-neutral tokens (IPs, ports, `MTU`, `IPv6`, `example.com`, protocol names — `ProxyType.label`/`badge` included) stay code literals. Russian developer *comments* are fine as they are; only user-visible text gets translated. ## 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. 10. **Domain routing recovers domains via hev mapped-DNS (fake-IP); the DIRECT path must bypass it.** When `routedSites` is non-empty, `buildEngineConfig()` enables a `mapdns:` block and the tun DNS is pointed at the synthetic resolver `198.18.0.2`. The fake-IP pool is `198.19.0.0/16` — it clears the `198.18.0.1/32` tun address and is deliberately NOT in the `isUnroutableRelay()` CGNAT/private set (those fake IPs become domains before reaching any upstream, so that relay path is never involved). `DirectDialer` resolves on the **underlying non-VPN network** and `protect()`s the socket: resolving via the default resolver would return a fake IP and loop the "direct" flow back into the tun. The direct dialer also requires the underlying network to carry `NET_CAPABILITY_INTERNET` — a device can expose an IMS/MMTEL cellular network (NOT_VPN, but no INTERNET) that can't resolve, which silently broke every direct dial until filtered out (device-confirmed). Mapped-DNS is IPv4-only, so v6 is force-suppressed while sites are active — don't "restore" it. Keep the fake-IP cache sized to the whole pool (`MAPDNS_CACHE_SIZE` == `~netmask`): the LRU evicts by the *cache* limit, not the pool, and an evicted-but-still-referenced fake IP reverse-maps to nothing and dead-ends. ## 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.