UDP · PSK · user-mode VPN · Rust

ShadowVPN

A UDP-based, pre-shared-key, user-mode VPN written in Rust on Tokio.

A fixed point-to-point / multi-client tunnel whose on-wire crypto matches the shadowsocks AEAD UDP scheme byte-for-byte. Runs on Linux, macOS, and Windows, with optional user-mode policy routing (gfwlist / chinadns), QUIC/HTTP3 carrier obfuscation, and no kernel ipset or nft rules.

Wire: shadowsocks AEAD (UDP) Ciphers: AES-128/256-GCM · ChaCha20-Poly1305 Platforms: Linux · macOS · Windows Obfuscation: QUIC/HTTP3 · base64 License: MIT

01Why ShadowVPN

A TUN-based client reads IP packets from a virtual interface, encrypts each as a single UDP datagram, and sends it to the server; the server decrypts, routes, and tunnels return traffic back. Small, spec-correct, and entirely in user space.

🔒

Spec-correct crypto

Per-datagram random salt, HKDF-SHA1 ss-subkey, zero nonce, AEAD tag — the shadowsocks UDP scheme exactly, interoperable by construction.

📦

One packet, one datagram

No length prefix, no SOCKS header, no multiplexing. The plaintext is the raw IP packet — UDP boundaries are the frame boundaries.

🖧

User-mode TUN

Async TUN on Tokio via tun-rs — Linux tun0 and macOS utun. Two relay loops plus a keepalive.

🧭

Policy routing

Optional split-tunnel: send only selected destinations through the tunnel, decided in user space — no ipset, no nft.

🌏

gfwlist · chinadns

Tunnel a domain list, or everything that isn't a China IP. Build the China set from a plain CIDR file or a GeoLite2 database.

🎭

Carrier obfuscation

Optionally shape the UDP payload to look like QUIC/HTTP3, or encode it as printable base64 — cosmetic framing to dodge naive classification.

🪟

Cross-platform client

The client runs on Linux, macOS, and Windows (TUN via Wintun), policy routing included — a self-elevating launcher ships in scripts/.

🦀

Lean Rust

Tokio + RustCrypto, a tiny dependency set, and a Docker end-to-end test suite covering the tunnel, HTTP/3, and policy routing.

02Architecture

The client encrypts every TUN packet to the server over UDP; the server decrypts, writes to its own TUN, and (with forwarding + NAT) lets traffic egress. Return traffic is matched back to the client by its inner tunnel IP and re-encrypted.

ShadowVPN end-to-end data flow: apps to TUN to client encrypt, encrypted UDP to server decrypt, server TUN, NAT, Internet, and the return path.
End-to-end data flow between a client host and a server host.

03Wire protocol

Each datagram is salt ++ AEAD(ciphertext ++ tag). A fresh random salt per datagram yields a unique subkey, so the all-zero nonce is never reused.

On-wire datagram layout: salt, AEAD ciphertext of the raw IP packet, and a 16-byte tag, with subkey and key derivation notes.
One datagram = one encrypted IP packet.

Deviation from ss-proxy: standard shadowsocks UDP relays prepend a SOCKS target address; ShadowVPN does not. This is a fixed tunnel, so the plaintext is exactly the raw IP packet — everything else matches the AEAD scheme byte-for-byte.

Optional obfuscation: set obfs (both ends must match) to wrap each datagram as a QUIC/HTTP3 short-header packet, or encode it as printable base64. This is cosmetic framing to dodge naive UDP classification — it adds no security; the AEAD envelope underneath is unchanged.

04Policy routing

Instead of pushing all traffic through the tunnel, the client can route only selected destinations. A built-in split-DNS proxy decides per query, then programs a per-destination route into the tun via the OS routing socket — rtnetlink on Linux, PF_ROUTE on macOS. Direct traffic stays on the normal kernel path.

Client policy routing: control plane where the split-DNS proxy decides and installs a per-destination route; data plane where the routing table sends tunneled IPs through the tunnel and everything else direct.
Control plane installs routes; the routing table picks the path. No ipset / nft.
ModeDecisionNeeds
gfwlisttunnel names listed in a gfwlist file; everything else is direct--gfwlist
chinadnsquery a domestic + a clean resolver; tunnel anything that does not resolve to an in-China address--chnroute or --geoip
fullno policy routing — the whole TUN is the tunnel (default)

05Quick start

Build the two binaries, then run the server and client. Creating a TUN device needs root / sudo.

Build

cargo build --release
# produces target/release/shadowvpn-server and shadowvpn-client

Server

sudo ./shadowvpn-server \
  --listen 0.0.0.0:8388 \
  --password "correct horse battery staple" \
  --cipher chacha20-poly1305 \
  --tun-ip 10.9.0.1 --peer-ip 10.9.0.2

Client

sudo ./shadowvpn-client \
  --server vpn.example.com:8388 \
  --password "correct horse battery staple" \
  --cipher chacha20-poly1305 \
  --tun-ip 10.9.0.2 --peer-ip 10.9.0.1

Split tunnel (policy routing)

# tunnel only the domains in a gfwlist
sudo ./shadowvpn-client -c client.json --mode gfwlist --gfwlist /etc/shadowvpn/gfwlist.txt

# or: tunnel everything that isn't a China IP, from a GeoLite2 database
sudo ./shadowvpn-client -c client.json --mode chinadns --geoip /etc/shadowvpn/GeoLite2-Country.mmdb

# the client points the system resolver at its split-DNS proxy automatically
# (restored on exit); pass --no-set-dns to manage DNS yourself.

Configuration can come from a JSON file, CLI flags, or both (CLI wins). See the full configuration reference and the policy-routing guide in the README.