proxy/docs/architecture.md

6.5 KiB

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 LocalSocks5ServerbuildEngineConfig() → 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/VpnStateStateFlow 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.
  • nativetun2socks.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 StateFlows) 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.