docs: document domain routing, fake-IP data path, and its invariants/limits
This commit is contained in:
parent
ef165b6b9b
commit
4e419146ca
2 changed files with 66 additions and 4 deletions
|
|
@ -76,8 +76,9 @@ file top.
|
|||
- **`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).
|
||||
hev). `hev-socks5-tunnel/` is the vendored engine; its `hev-mapped-dns.c` fake-DNS facility is
|
||||
what makes domain routing possible and is enabled via a `mapdns:` config block (see Domain
|
||||
routing below).
|
||||
|
||||
## Per-app routing (model for any future per-target filtering)
|
||||
|
||||
|
|
@ -90,7 +91,54 @@ file top.
|
|||
|
||||
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).
|
||||
it has to be decided in the data path (the local SOCKS5 server sees the destination host). That
|
||||
is exactly how the "Сайты" tab works, below.
|
||||
|
||||
## Domain routing ("Сайты" tab)
|
||||
|
||||
`AppSettings.routedSites: Set<String>` + `routingMode: RoutingMode` add a **destination-keyed**
|
||||
proxy-vs-direct split *on top of* the per-app filter — orthogonal layers: per-app decides which
|
||||
apps enter the tun; domain routing decides, within that traffic, where each destination goes.
|
||||
|
||||
The data path is IP-only by the time it reaches hev (the OS already resolved the domain), so
|
||||
domains are recovered with hev's **mapped-DNS / fake-IP** facility, enabled only when
|
||||
`routedSites` is non-empty:
|
||||
|
||||
1. `buildEngineConfig()` emits a `mapdns:` block (synthetic resolver `198.18.0.2:53`, fake-IP pool
|
||||
`198.19.0.0/16`, `cache-size 10000`); `establish()` points the tun's DNS at `198.18.0.2`.
|
||||
2. hev intercepts A-queries to that resolver, answers with a fake IP from the pool, and on the
|
||||
resulting TCP connection **reverse-maps the fake IP back to the original domain**, sending it
|
||||
upstream as a SOCKS5 *domain* CONNECT (`hev-mapped-dns.c` + `misc/hev-utils.c`).
|
||||
3. `LocalSocks5Server` now sees the real domain in `dest.host`; `core/route/SiteRouter` picks
|
||||
PROXY (the existing upstream path) or DIRECT (a `protect()`-ed socket dialed straight to the
|
||||
host on the underlying non-VPN network, via `ProxyVpnService` as `DirectDialer`) — both reuse
|
||||
the same half-close relay.
|
||||
|
||||
Entries are explicit suffix domains (`example.com` ⇒ it + all subdomains) or geosite categories
|
||||
(`geosite:netflix`). The full MIT v2fly `geosite.dat` (`assets/geosite/`, ~1500 categories,
|
||||
refresh via `tools/fetch-geosite.sh`) is parsed lazily by `core/route/GeositeIndex` (a small
|
||||
hand-rolled protobuf reader, no dependency) into a process-shared index used both by the Сайты
|
||||
search (live category/domain suggestions) and by `SiteRouter` at tunnel build. `RoutingMode` is
|
||||
the global default and the list
|
||||
is the exceptions: `PROXY_ALL` ⇒ listed go direct; `DIRECT_ALL` ⇒ listed go through the proxy.
|
||||
|
||||
**Known limitation — DNS that bypasses the fake resolver isn't split.** hev only diverts UDP whose
|
||||
destination is exactly the advertised resolver `198.18.0.2:53`. An app that hardcodes its own
|
||||
resolver (e.g. `8.8.8.8`), or uses DoH/DoT/QUIC, never hits the fake DNS, so its flows arrive as
|
||||
IP literals and take the **global default**. Under `DIRECT_ALL` that means a site the user added
|
||||
to go *through the proxy* can still exit direct if its app resolves on its own. Closing this would
|
||||
require hijacking all `:53` (and ideally DoH) in the engine — a possible follow-up. (Also: synthesized
|
||||
answers carry TTL=1 and only A records; AAAA/HTTPS-SVCB get an empty answer, which is fine while v6
|
||||
is suppressed but strips HTTP/3 hints.)
|
||||
|
||||
**Known limitation — UDP is not domain-routed.** Only `handleConnect` (TCP) consults the router;
|
||||
`handleUdpAssociate` always sends UDP through the proxy (and an HTTP(S) upstream, which has no UDP
|
||||
relay, drops it). So QUIC / HTTP-3 is proxied regardless of mode — under `DIRECT_ALL` a "direct"
|
||||
site's QUIC still traverses the proxy. TCP is the routed path; UDP direct-routing is a possible
|
||||
follow-up (it needs a protected per-destination UDP relay, mindful of the §3 anti-spoof invariant).
|
||||
|
||||
Mapped-DNS is **IPv4-only**, so while `routedSites` is non-empty the tun is forced IPv4-only (v6
|
||||
suppressed regardless of `settings.ipv6`) to stop dual-stack apps bypassing the split over v6.
|
||||
|
||||
## State & status
|
||||
|
||||
|
|
@ -100,7 +148,7 @@ it has to be decided in the data path (the local SOCKS5 server sees the destinat
|
|||
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.
|
||||
server, the app filter, or the site list / routing mode changes.
|
||||
|
||||
### Status & journal
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,20 @@ against the code and the harness before touching.
|
|||
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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue