| name | shadowvpn-deploy-server |
| description | Deploy, install, and run the ShadowVPN server in a target environment (Linux/systemd). Use when the user wants to set up, install, deploy, cross-build, or run the shadowvpn-server on a host (VPS, droplet, Raspberry Pi), enable internet egress for tunneled clients (IP forwarding + NAT/MASQUERADE), wire up the systemd unit, open the UDP port, or troubleshoot a server that clients cannot reach. |
Deploy the ShadowVPN server
The server terminates the encrypted UDP tunnel onto a TUN device and (optionally)
NATs tunneled clients to the internet. It needs root (TUN creation) and runs
on Linux in practice (the systemd unit ships in dist/).
Repo references — read these for the canonical artifacts:
dist/README.md + dist/systemd/shadowvpn-server.service — the install recipe and unit.
README.md §Configuration, §Building, §"Server: enable IP forwarding + NAT".
docker/server.json, docker/run-server-nat.sh — working config + NAT example.
1. Build the binary for the target
Native (build on the server itself, needs a stable Rust toolchain):
cargo build --release --bin shadowvpn-server
cargo test --lib
Cross-build from a dev box (preferred for remote Linux targets — uses Zig as the
linker, no Docker, works from any path). Pin the glibc version to the target's:
cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.39 --bin shadowvpn-server
cargo zigbuild --release --target aarch64-unknown-linux-gnu.2.31 --bin shadowvpn-server
cargo install cargo-zigbuild if the subcommand is missing. Confirm the built
binary's max GLIBC requirement is ≤ the target's before shipping.
2. Write server.json
{
"server": "0.0.0.0:8388",
"password": "correct horse battery staple",
"cipher": "chacha20-poly1305",
"tun_ip": "10.9.0.1",
"tun_netmask": "255.255.255.0",
"peer_ip": "10.9.0.2",
"mtu": 1400,
"obfs": "quic",
"nat": true
}
Key points:
server is the UDP bind address (0.0.0.0:PORT). The client's server is
this host's public host:port.
password, cipher, and obfs must match the client exactly (obfs defaults
to none; both ends must agree). Wrong cipher/obfs = silent decrypt failure.
tun_ip/peer_ip are mirror images of the client's (server.tun_ip ==
client.peer_ip, and vice versa).
"nat": true lets many clients share one identical static config (the
server keys each by UDP endpoint and maps it onto a distinct internal IP). Without
NAT, every client needs a distinct tun_ip. With NAT, idle mappings are reclaimed
after lease_ttl_secs (default 120). See README §"Multiple clients with --nat".
3. Install + enable (systemd)
sudo install -Dm755 target/release/shadowvpn-server /usr/local/bin/shadowvpn-server
sudo install -Dm600 server.json /etc/shadowvpn/server.json
sudo cp dist/systemd/shadowvpn-server.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now shadowvpn-server
The shipped unit's ExecStartPre lines enable IP forwarding + MASQUERADE so
tunneled clients reach the internet. They are idempotent and re-applied on boot.
You MUST edit the unit if your setup differs from the defaults:
- WAN egress interface — the unit assumes
eth0. Find yours with
ip route get 1.1.1.1 (the dev …), and replace eth0 in the three iptables lines.
- Tunnel subnet — the unit assumes
10.9.0.0/24; match your tun_ip/netmask.
After editing: sudo systemctl daemon-reload && sudo systemctl restart shadowvpn-server.
4. Open the UDP port
The data plane is UDP on the server port. Allow it on the host firewall AND
the cloud security group / provider firewall:
sudo ufw allow 8388/udp
On DigitalOcean/AWS/GCP also open the port in the cloud firewall — a host that
forwards fine but drops inbound UDP looks exactly like a wrong password to a client.
5. Verify
journalctl -u shadowvpn-server -f
ss -lunp | grep 8388
From a connected client: ping 10.9.0.1 (the server's in-tunnel IP) should answer,
and the client's public egress IP (e.g. curl ifconfig.me) should become this host.
Manual forwarding + NAT (if not using the unit's ExecStartPre)
sudo sysctl -w net.ipv4.ip_forward=1
WAN=$(ip route get 1.1.1.1 | grep -oP 'dev \K\S+')
sudo iptables -t nat -A POSTROUTING -s 10.9.0.0/24 -o "$WAN" -j MASQUERADE
sudo iptables -A FORWARD -s 10.9.0.0/24 -j ACCEPT
sudo iptables -A FORWARD -d 10.9.0.0/24 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
Troubleshooting
- Client connects but no internet → forwarding/NAT not applied, or wrong WAN
interface / subnet in the iptables rules. Check
sysctl net.ipv4.ip_forward and
iptables -t nat -L POSTROUTING -n -v.
- Client times out entirely → UDP port not open in the cloud firewall, or wrong
public
host:port in the client config.
- Garbage / decrypt errors in the log →
password, cipher, or obfs mismatch
between server and client.
- Updating the binary → back up the old one,
systemctl stop, swap, systemctl start. Live clients reconnect within ~1 s (UDP, stateless handshake). Keep a
timestamped .bak so you can roll back.
When deploying to a known live host, record the host, port, obfs, cipher, and the
NAT subnet so the matching client config can be produced without guessing.