Skip to content

Troubleshooting ​

First checks ​

  • Turn on verbose logging. Both binaries log to stderr; set RUST_LOG=debug for detailed tracing of the relay loops, DNS proxy, and route programming.
  • Ping across the tunnel. From the client, ping 10.9.0.1 (the server's tun_ip). A lossless ping proves the whole path — TUN → encrypt → UDP → server → decrypt → TUN — in both directions.
  • Read the startup hints. Both binaries print the routing / forwarding commands they expect you to have run (IP forwarding, NAT, host route to the server).

Tunnel comes up but nothing flows ​

  • Password / cipher mismatch. There is no handshake, so a wrong password or cipher doesn't produce a connection error — packets simply fail to decrypt and are dropped. Check both configs use the same password and cipher.
  • obfs mismatch. The obfs setting must be identical on both ends; a mismatched peer just sees its traffic dropped (packets fail to decode before decryption). It is config-file-only — check both JSON files.
  • UDP port blocked. The tunnel is pure UDP; make sure the server's port (e.g. 8388/udp) is open in every firewall/security-group on the path.

Tunnel pings, but the internet doesn't work ​

The server must forward and masquerade tunneled traffic:

sh
sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -t nat -A POSTROUTING -s 10.9.0.0/24 -o <wan-if> -j MASQUERADE

See Routing & IP forwarding.

On the client side (full tunnel), remember the host route to the server via the real gateway — without it the encrypted UDP loops back into the tunnel and everything stalls. See the client routing steps.

Windows: error 193 / Wintun won't load ​

wintun.dll must match the binary's CPU architecture, not the OS marketing name. An x64 DLL next to an ARM64 shadowvpn-client.exe (or vice versa) fails to load — typically Windows error 193 (%1 is not a valid Win32 application). Download the matching build from wintun.net and place it next to the exe (the release zip already ships the right one).

DNS is broken after the client exited uncleanly ​

In policy-routing mode the client points the system resolver at its local split-DNS proxy and restores the previous setting on exit — but only on a graceful exit (Ctrl-C / SIGTERM / service stop). Current clients no longer rewrite the OS resolver (DNS is intercepted on the TUN). A leftover 127.0.0.1 nameserver is from an older build that did take over DNS.

Fix: open the desktop app (it runs --restore-dns on launch whenever an elevated helper is already live and no client is running, which restores the journal or resets a leftover 127.0.0.1 to automatic/DHCP DNS), or restore DNS by hand (networksetup -setdnsservers <service> empty on macOS, edit /etc/resolv.conf on Linux, adapter DNS settings / netsh on Windows).

Policy routing doesn't split anything ​

  • Routes are only installed for names resolved through TUN intercept (IPv4 UDP/53). If host DNS never reaches the TUN (loopback stub with no hijacked upstream, or a resolver the client did not attract), the policy never sees the queries. Check that lookups go to a public resolver IP that has a /32 on the tun (for example 8.8.8.8).
  • Long-lived connections opened before a route existed keep using their old path until they reconnect.
  • In chinadns mode, a domain the domestic resolver answers with an in-China (possibly poisoned) address goes direct; add it to a --gfwlist force list to always tunnel it.

Assignment failed / "server did not assign an IP" ​

Auto-assign clients (omit tun_ip / peer_ip) wait up to 10 seconds for an Assign Ok. Common causes:

  • Old server — it drops unknown 0x03 control types. Upgrade the server, or set a static tun_ip / peer_ip.
  • Server --nat — assignment is exclusive with NAT. A new NAT server replies NatMode (fatal); an old one times out. Drop "nat": true from server.json if you want client↔client, or put static IPs back on the client. install.sh --setup writes learning mode (no "nat").
  • Pool exhausted — status Exhausted. Widen tun_netmask / assign_pool, or wait for idle leases (default 7 days) to expire.
  • Only one of tun_ip / peer_ip set — both must be present (static) or both omitted (auto).

The node_id is in <config>.state, not the JSON. Do not copy that file onto a second live machine — both clients will flap on the same IP. Copying it is the supported way to move a node.

A static client that uses an address the server has already leased is not learned (warn on the server). Add that address to reserved_ips. .2 (peer_ip) is reserved by default.

Keepalives, NAT timeouts, and idle drops ​

The client sends an encrypted keepalive every 15 seconds by default so stateful NAT/firewall mappings stay open and the server keeps an up-to-date view of the client's UDP address. Auto-assign clients send AssignRequest on that same tick instead of the 5-byte keepalive. If your NAT/router expires UDP mappings faster than the keepalive interval, the server sees the client's address "churn" — each keepalive arrives from a fresh mapping. Either raise the router's UDP session timeout or lower --keepalive-secs so it stays below the timeout. In server NAT mode, idle client leases are reclaimed after lease_ttl_secs (default 120) — an idle-but-alive client keeps its lease via the keepalive alone. Assignment leases last 7 days by default (assign_ttl_secs).

MTU problems (some sites hang, large transfers stall) ​

The default TUN MTU is 1400. With base64 obfuscation the wire payload grows ~33%, so lower mtu accordingly (see carrier obfuscation). Note that in server NAT mode, ICMP error payloads aren't rewritten, so tunneled path-MTU discovery may degrade — if a specific host hangs on large packets, try a lower client-side mtu.

AES-GCM is slow on ARM ​

A plain cargo build for aarch64 runs AES-GCM in constant-time software. Either use chacha20-poly1305 (default; fast everywhere) or rebuild with RUSTFLAGS="-C target-feature=+aes,+neon". See ciphers.

Still stuck? ​

Open an issue at github.com/madeye/shadowvpn/issues with RUST_LOG=debug output from both ends and your (password-redacted) configs.

Released under the MIT License.