111 lines
6.5 KiB
Markdown
111 lines
6.5 KiB
Markdown
# Architecture
|
|
|
|
## The data path
|
|
|
|
```
|
|
routed app
|
|
│ (only apps allowed by the per-app filter; see below)
|
|
▼
|
|
VpnService tun ProxyVpnService.establish()
|
|
│ IPv4 198.18.0.1/32, route 0.0.0.0/0, DNS = dnsServerFor(settings)
|
|
│ (IPv6 fc00::1/128 ULA + ::/0 ONLY when settings.ipv6 = true)
|
|
▼
|
|
hev-socks5-tunnel native engine, reads the tun fd over JNI (Tun2Socks / tun2socks.c)
|
|
│ config built by ProxyVpnService.buildEngineConfig() (a small YAML blob)
|
|
│ speaks SOCKS5 upstream only → points at our loopback server
|
|
▼
|
|
LocalSocks5Server 127.0.0.1:<random port>, random per-session user/pass
|
|
│ RFC 1928/1929; handles CONNECT (TCP) and UDP ASSOCIATE
|
|
▼
|
|
Upstream client Socks5Upstream | HttpUpstream | ShadowsocksUpstream
|
|
│ socket is VpnService.protect()-ed → it exits the tunnel, no loop
|
|
▼
|
|
real proxy server → internet
|
|
```
|
|
|
|
**Why a local SOCKS5 server in the middle?** `hev-socks5-tunnel` only speaks SOCKS5 to its
|
|
upstream. To support HTTP(S) and Shadowsocks upstreams we run our own loopback SOCKS5 server
|
|
that hev forwards into, and *it* translates to whatever the configured upstream speaks. This
|
|
loopback-bridge pattern is standard for this engine. The bridge is gated with a random
|
|
per-session username/password so a non-whitelisted local app can't hijack it.
|
|
|
|
**Why the upstream socket is `protect()`-ed.** Without `protect()`, the upstream connection
|
|
would itself be captured by the tun and loop. `protect()` excludes that socket from the VPN.
|
|
(Note: `protect()` returning `false` here is benign — our own app is already excluded by the
|
|
app filter — so we log and continue, never throw. See conventions.md.)
|
|
|
|
## Connect / restart / stop sequence
|
|
|
|
The `VpnService` is driven by three intent actions (`ProxyVpnService.ACTION_START` / `ACTION_STOP`
|
|
/ `ACTION_RESTART`; static helpers `ProxyVpnService.start/stop/restart(context)`):
|
|
|
|
1. UI tap → `MainActivity` calls `VpnService.prepare()` (consent dialog, or null when the
|
|
`ACTIVATE_VPN` appop is granted) → `ProxyVpnService.start()`.
|
|
2. `onStartCommand(ACTION_START)` promotes to foreground immediately (the 5 s FGS window starts
|
|
here), then runs `startTunnel()` on a dedicated lifecycle thread.
|
|
3. `startTunnel()` → `establish()` builds the tun → starts `LocalSocks5Server` →
|
|
`buildEngineConfig()` → starts the native engine (generation counter + stuck-engine reaper) →
|
|
fires the one-shot upstream **probe**.
|
|
4. `MainViewModel.reconnectIfRunning()` sends `ACTION_RESTART` when the active server or the app
|
|
filter changes while connected. `ACTION_STOP` tears down and `stopSelf()`s.
|
|
|
|
To touch lifecycle, start at `onStartCommand` / `startTunnel` / `establish`, not the 700-line
|
|
file top.
|
|
|
|
## Components
|
|
|
|
- **`service/ProxyVpnService`** — the heart. Builds the tun (`establish()`), starts/stops the
|
|
native engine with a generation counter + a stuck-engine reaper thread, runs a one-shot
|
|
upstream connectivity **probe** after connect (journal flips to «Прокси подтверждён» or a
|
|
classified error), watches network changes, owns the foreground notification.
|
|
- **`service/VpnState`** — `StateFlow` holder for status, stats, and the journal (`TunnelEvent`
|
|
list). The UI observes this; the service writes it.
|
|
- **`core/socks/LocalSocks5Server`** — the loopback bridge. Accept loop on a control dispatcher,
|
|
relay on a separate dispatcher, a both-directions idle watchdog, per-session auth.
|
|
- **`core/upstream/*`** — `Upstream` is the interface (`connectTcp`, `openUdpAssociation`,
|
|
`protect`). Three implementations. `Socks5Upstream` also does UDP ASSOCIATE with a BND.ADDR
|
|
rewrite for broken servers (see conventions.md).
|
|
- **`core/crypto/*`** — Shadowsocks AEAD (SIP004): `ShadowsocksCrypto` (EVP_BytesToKey master
|
|
key, HKDF-SHA1 subkey, AES-GCM / ChaCha20-Poly1305 `Aead`), `ShadowsocksStream` (the TCP
|
|
chunk framing + UDP packet framing). Implemented on platform JCA — no extra crypto dep.
|
|
- **`core/ConfigImport`** — parses `ss://` (SIP002 + legacy), `socks5://`, `http(s)://` share
|
|
links into `ProxyConfig`.
|
|
- **`data/ConfigStore`** — the only persistence. One DataStore Preferences entry `state` holding
|
|
`ProxyState` JSON (`configs` + `settings`). Keeps a last-known-good backup and recovers from
|
|
it rather than wiping on a decode failure.
|
|
- **`ui/`** — `MainScreen` hosts a 4-tab `HorizontalPager`: `ConnectionScreen`, `ServersScreen`,
|
|
`AppsScreen`, `SettingsScreen`. `MainViewModel` bridges UI ↔ `ConfigStore`/`VpnState`.
|
|
- **native** — `tun2socks.c` is the JNI bridge (start/stop, hands the tun fd + YAML config to
|
|
hev). `hev-socks5-tunnel/` is the vendored engine (includes `hev-mapped-dns.c`, a fake-DNS
|
|
facility worth knowing about for any domain-aware routing work).
|
|
|
|
## Per-app routing (model for any future per-target filtering)
|
|
|
|
`AppSettings.allowedApps: Set<String>` drives `ProxyVpnService.applyAppFilter()`:
|
|
|
|
- **non-empty** → whitelist: `builder.addAllowedApplication(pkg)` for each (skipping our own
|
|
package). If *every* selected package is uninstalled it throws rather than silently routing
|
|
everything (that would violate the whitelist contract).
|
|
- **empty** → route everything except our own app (`addDisallowedApplication(self)`).
|
|
|
|
This is an **OS-level filter keyed by app UID**. It is the model the "Apps" tab exposes. Note
|
|
that anything keyed by *destination* (domain/IP) is NOT an OS feature and cannot reuse this —
|
|
it has to be decided in the data path (the local SOCKS5 server sees the destination host).
|
|
|
|
## State & status
|
|
|
|
- `ProxyState` = `{ configs: List<ProxyConfig>, settings: AppSettings }`. `activeConfig` is
|
|
`settings.activeConfigId` (or the first config).
|
|
- The tunnel is built from a *snapshot* of the active config; live/green UI styling keys off the
|
|
applied config id, not the merely-selected server.
|
|
- Settings take effect only at `establish()` / `buildEngineConfig()` time. `MainViewModel`
|
|
force-restarts a running tunnel (`reconnectIfRunning()` → `ACTION_RESTART`) when the active
|
|
server or the app filter changes.
|
|
|
|
### Status & journal
|
|
|
|
`service/VpnState` (a set of `StateFlow`s) is the service↔UI contract. To surface a step in the
|
|
UI, call `VpnState.event(label, detail?, tone)` — `tone ∈ {NEUTRAL, OK, WARN, ERROR}`. Events are
|
|
a ring buffer (`MAX_EVENTS = 30`); `ConnectionScreen` renders them newest-first with auto-scroll.
|
|
This is the *only* channel the service uses to report progress (connect steps, the probe verdict,
|
|
errors) — use it rather than inventing a parallel one.
|