Skip to content

Policy routing (gfwlist / chinadns)

Client-side; Linux + macOS + Windows.

By default the client is a full tunnel: every packet that reaches the TUN is encrypted to the server, and what you route into the TUN is your business (see Routing & IP forwarding). For the common case of "send only some destinations through the tunnel", the client has a built-in policy-routing mode — no external daemon, and no ipset/iptables/nft required.

How it works

A small split-DNS proxy runs inside the client. For each query it decides whether the name should be tunneled and, for those that should be, programs a per-destination host route (<ip>/32) into the tun device using the OS's native routing socket — rtnetlink on Linux, PF_ROUTE on macOS, the IP Helper API on Windows — so the work is done entirely in user mode. The route's source is the tun address, so the server's masquerade matches with no client-side NAT. Direct (non-tunneled) traffic stays on the normal kernel path untouched, and every route added is removed again on exit.

ShadowVPN client policy routing — control and data plane

The proxy is built for low latency:

  • Cache — answers are cached (TTL-respecting, like dnsmasq) so repeat lookups skip the upstream round-trip.
  • chinadns fast-path — the local and clean resolvers are queried concurrently, but a domestic answer returns immediately instead of waiting for the slower tunneled upstream, so China sites resolve at local-DNS speed.
  • Pre-warm — on startup a built-in list of common domains is resolved in the background, so their first real lookup (and their tunnel routes) are already hot. Customize with the prewarm config list or disable with --no-prewarm.
  • Persistence — the cache is saved on exit and reloaded on startup (--cache-file, default dns-cache.json next to the binary; --no-cache-persist to disable), so a restart doesn't start cold.

Modes

ModeDecisionNeeds
gfwlisttunnel names listed in a gfwlist file; everything else is direct--gfwlist
chinadnsquery a domestic + a clean resolver; tunnel anything not resolving to an in-China address. An optional --gfwlist is a force-tunnel override--chnroute or --geoip (+ optional --gfwlist)
fullno policy routing (the default)
sh
# gfwlist mode: tunnel only the domains in gfwlist.txt
sudo ./target/release/shadowvpn-client -c client.json \
  --mode gfwlist --gfwlist /etc/shadowvpn/gfwlist.txt

# chinadns mode: tunnel everything that isn't a China IP (CIDR file)
sudo ./target/release/shadowvpn-client -c client.json \
  --mode chinadns --chnroute /etc/shadowvpn/chnroute.txt

# chinadns mode: derive the China set from a GeoLite2 database instead
sudo ./target/release/shadowvpn-client -c client.json \
  --mode chinadns --geoip /etc/shadowvpn/GeoLite2-Country.mmdb

# chinadns mode + a gfwlist force list: domains on the list always tunnel,
# even if the domestic resolver returns an in-China (poisoned) address
sudo ./target/release/shadowvpn-client -c client.json \
  --mode chinadns --geoip /etc/shadowvpn/GeoLite2-Country.mmdb \
  --gfwlist /etc/shadowvpn/gfwlist.txt

System DNS handling

Policy routing only takes effect for names resolved through the proxy (that's what installs the routes), so the system resolver must point at it. By default the client does this for you: on startup it points the OS resolver at the proxy (networksetup on macOS, /etc/resolv.conf on Linux, netsh on Windows) and restores the previous setting on exit — including on Ctrl-C / SIGTERM, which it handles for a clean shutdown. Pass --no-set-dns to manage DNS yourself instead.

Automatic setup only applies when dns_listen uses port 53 (the OS resolver can't target a custom port) — which is the default, so it works out of the box; if you move the proxy to another port, point your resolver at it manually.

Configuration

All client-only; CLI overrides JSON:

JSON fieldCLI flagMeaningDefault
mode--modefull | gfwlist | chinadnsfull
dns_listen--dns-listenaddress the split-DNS proxy listens on127.0.0.1:53
dns_local--dns-localdomestic / direct DNS upstream114.114.114.114:53
dns_remote--dns-remoteclean DNS upstream (reached through the tunnel)8.8.8.8:53
gfwlist--gfwlistdomain-suffix file (gfwlist mode; optional force-tunnel list in chinadns mode)
chnroute--chnrouteChina CIDR file (chinadns mode)
geoip--geoipGeoLite2/GeoIP2 .mmdb; builds the China set from it
geoip_country--geoip-countryISO country code to select from the GeoIP databaseCN
set_dns--set-dns / --no-set-dnspoint the system resolver at the proxy (auto-restored on exit)true (needs dns_listen port 53)
prewarm--no-prewarmpre-resolve common domains into the cache on startupbuilt-in list
cache_file--cache-file / --no-cache-persistpersist the DNS cache across restartsdns-cache.json (next to the binary)

Data files

  • gfwlist file — one domain per line; #/! comments and a leading *./. are accepted (the plain list produced by gfwlist2dnsmasq, not the base64 blob). A name matches if it equals or is a subdomain of a listed suffix.
  • chnroute file — one a.b.c.d/len per line (the classic APNIC-derived chnroute.txt).
  • geoip database — a MaxMind GeoLite2-Country.mmdb (or paid GeoIP2). On startup every IPv4 network whose country is --geoip-country (default CN) is enumerated and merged into the China set, so you don't have to maintain a CIDR file. Takes precedence over --chnroute when both are given.

Bundled data files

If a gfwlist.txt or GeoLite2-Country.mmdb sits next to the shadowvpn-client binary, it is auto-discovered when the relevant mode needs it but no path is configured:

  • gfwlist mode falls back to a bundled gfwlist.txt (the routing list).
  • chinadns mode with no --chnroute/--geoip falls back to a bundled GeoLite2-Country.mmdb, and — with no --gfwlist — also auto-applies a bundled gfwlist.txt as its force-tunnel override (names on the list always take the clean tunneled path). This matches the iOS client, whose network extension always injects its bundled gfwlist.txt in chinadns mode.

So the packaged clients run these modes out of the box; an explicit --gfwlist/--geoip/--chnroute path is only needed to override a bundled copy.

The gfwlist is vendored in the repo at assets/gfwlist.txt (regenerate from the upstream AutoProxy list with scripts/gen-gfwlist.sh); the release packages bundle it next to the client — the Unix tarballs and the Windows zip — and the macOS desktop .app ships it inside Contents/MacOS/. The GeoLite2 database is not vendored (it is large and separately licensed): the Windows zip downloads it at package time, and the desktop .app ships a copy.

Requirements & scope

This needs root / Administrator (to create the tun and edit the routing table) and runs on Linux, macOS, and Windows; routes are programmed directly via the OS routing interface (rtnetlink, PF_ROUTE, or the Windows IP Helper API), so no ipset/iptables/route/netsh binaries are involved for routing. The docker/run-e2e-policy.sh test exercises both modes end to end.

The server still needs forwarding + NAT so tunneled traffic can egress — see Routing & IP forwarding.

Released under the MIT License.