Raspberry Pi 4 configured as a split-tunnel VPN gateway — non-RU traffic exits via AmneziaWG VPN, Russian IP ranges exit direct via ISP, transparent to all LAN devices.
Документация на русском | Technical Reference
The RPi acts as the default gateway for all LAN devices. Traffic is split into two paths:
- Non-RU traffic exits through the AmneziaWG VPN tunnel (
awg0) - Russian IP ranges (updated daily from
russia.iplist.opencck.org) exit direct via ISP - LAN devices require no individual configuration — the split is fully transparent
Internet
↓
Router (192.168.1.1) — ISP uplink
↓ eth0
RPi4 (192.168.1.254) — VPN gateway
↓
LAN devices (default gateway = 192.168.1.254 via router DHCP)
Non-RU → awg0 → AmneziaWG VPN (endpoint: <VPN_SERVER_IP>:<port>)
RU CIDRs → eth0 → ISP direct (via 192.168.1.1)
Add to ~/.ssh/config on your Mac:
Host pi4
HostName 192.168.1.254
User ar
IdentityFile ~/.ssh/id_ed25519
Verify: ssh pi4 "echo ok" — must succeed without a password prompt.
VPN keys (.env.secrets):
cp .env.secrets.example .env.secrets
# Fill in AWG_PRIVATE_KEY, AWG_PUBLIC_KEY, AWG_PRESHARED_KEY (44-char base64 each)AmneziaWG config template (src/configs/amnezia.key.template.txt): holds server-specific obfuscation parameters (Jc, Jmin, Jmax, S1, S2, H1–H4) and the endpoint. Copy the [Interface] and [Peer] blocks from your AmneziaWG server's client config, then replace the key values with {{PrivateKey}}, {{PublicKey}}, {{PresharedKey}} placeholders — deploy.sh substitutes them at deploy time.
Network config (.env): committed to the repo, safe to edit. Update if your network differs from defaults:
SSH_HOST="pi4" # SSH alias for the RPi (from ~/.ssh/config)
RPI_LAN_IP=192.168.1.254 # RPi LAN IP
KEENETIC_GW=192.168.1.1 # ISP gateway (your router)
VPN_SERVER_IP=<your-server-ip> # AmneziaWG server IP — set in .env.secrets
CRON_UPDATE_HOUR=5 # Hour (0–23) for daily RU list refresh
Optional — ISP-bypass custom routes (src/configs/isp-routes-custom.txt): CIDRs that bypass VPN and exit via ISP, added on top of the auto-downloaded RU list. Create from example when needed:
cp src/configs/isp-routes-custom.txt.example src/configs/isp-routes-custom.txtOptional — VPN-force custom routes (src/configs/vpn-routes-custom.txt): CIDRs forced through VPN even if present in the auto-downloaded RU list. Highest-priority override. Create from example when needed:
cp src/configs/vpn-routes-custom.txt.example src/configs/vpn-routes-custom.txtAfter editing either custom-route file, use bash src/deploy-routes.sh instead of a full bash src/deploy.sh. It SCPs only the two custom-route files and runs routing.sh --no-update — skipping AmneziaWG install, key validation, and systemd setup. Takes seconds instead of minutes.
Optional — RU list exclusions (src/configs/ru-list-exclude.txt): CIDRs to strip from the downloaded RU list server-side (use when the RU list incorrectly includes a range you want tunneled). Create from example when needed:
cp src/configs/ru-list-exclude.txt.example src/configs/ru-list-exclude.txtAll three files are gitignored. Full workflow: docs/REFERENCE.md.
Gateway — set RPi as the LAN default gateway:
http://192.168.1.1→ Home network → Segments → Default → IP parameters- Set Gateway address to
192.168.1.254→ Save - LAN devices apply on next DHCP renewal (or disconnect/reconnect Wi-Fi)
DNS — required for domain names in splitgate status:
- Home network → Segments → Default → DNS server → set to
192.168.1.254→ Save
Rollback: clear Gateway address in router (set back to 192.168.1.1).
bash src/deploy.sh # deploy all files + activate routing
bash src/deploy.sh --no-run # deploy files only (use before tunnel is up)
bash src/deploy-routes.sh # fast push of custom-route files only (after editing isp-routes-custom.txt or vpn-routes-custom.txt)Non-destructive redeploy: deploy.sh no longer overwrites an already-present /etc/splitgate/vpn-gateway.env or /etc/amnezia/amneziawg/awg0.conf on the RPi — it skips the overwrite with a warning if the remote file already exists. Edit these files going forward via the admin Settings page, or delete the remote file first to force a fresh deploy from the repo template.
deploy.sh also installs the traceroute apt package (required by the admin Diagnostics page), mirroring the existing dnsmasq install pattern.
ssh pi4 "sudo awg-quick up awg0" # bring up VPN tunnel (manual — not idempotent)
ssh pi4 "sudo awg show" # verify peer handshakeRun on RPi via SSH. splitgate is the dispatcher CLI at /usr/local/bin/splitgate.
| Command | What it does |
|---|---|
splitgate status |
Recent connections with VPN/ISP routing and org info |
splitgate watch |
Real-time traffic stream |
splitgate rollback |
Undo VPN gateway entirely |
splitgate routing |
Rebuild split-tunnel routes |
splitgate update |
Refresh RU IP list now |
# Show recent connections
ssh pi4 "splitgate status"
ssh pi4 "splitgate status --via=vpn --last=100"
ssh pi4 "splitgate status --summary" # top-20 orgs by connection count
# Watch real-time traffic
ssh pi4 "splitgate watch"
ssh pi4 "splitgate watch --src 192.168.1.x --tag VPN"
# Rollback
ssh pi4 "splitgate rollback"Full CLI reference, verify routing, troubleshooting →
splitgate-watch.service runs watch-routes.py --daemon as a persistent systemd service, writing
connection logs to /etc/splitgate/logs/watch-YYYY-MM-DD.log (a new file each day, 14-day rotation).
Deployed by deploy.sh Stage 28.
# Service control
ssh pi4 "systemctl status splitgate-watch"
ssh pi4 "sudo systemctl restart splitgate-watch"
# Check today's log
ssh pi4 "tail -f /etc/splitgate/logs/watch-$(date +%F).log"
# Find ISP routes that failed to connect (✗ = not found in conntrack)
ssh pi4 "grep '[ISP] ✗' /etc/splitgate/logs/watch-$(date +%F).log"Each log line shows routing tag, connection status, source/destination, protocol:port, and org:
2026-05-29T10:14:00 [ISP] ✓ 192.168.1.237 → yandex.ru TCP:443 | TELETECH, RU
2026-05-29T10:14:05 [ISP] ✗ 192.168.1.237 → github.com TCP:443 | FASTLY, US
✓ = connection found in conntrack (ESTABLISHED/TIME_WAIT); ✗ = not found (UDP connections always show ✗).
Tuning workflow — if [ISP] ✗ lines point to RU CIDRs going via ISP that actually should go via VPN, uncomment the candidate block in src/configs/vpn-routes-custom.txt and re-run routing.sh.
| File | Location on RPi | Purpose |
|---|---|---|
install.log |
/etc/splitgate/logs/install.log |
Output from routing.sh and update-vpn-routes — download source, excluded CIDRs, route counts |
watch-YYYY-MM-DD.log |
/etc/splitgate/logs/watch-2026-05-29.log |
Daily connection log written by splitgate-watch.service in daemon mode |
watch-error.log |
/etc/splitgate/logs/watch-error.log |
stderr from watch-routes.py --daemon (startup errors, Python exceptions) |
# View install/routing log
ssh pi4 "sudo tail -20 /etc/splitgate/logs/install.log"
ssh pi4 "sudo grep vpn-routes /etc/splitgate/logs/install.log | tail -10"
# View today's watch log
ssh pi4 "sudo tail -f /etc/splitgate/logs/watch-$(date +%F).log"A browser-based admin UI runs on the RPi at http://192.168.1.254:8080.
Deploy:
cd src/admin && npm run build && cd ../..
bash src/deploy.shAccess: Open http://192.168.1.254:8080 from any LAN device. Enter the admin password when prompted.
Default password: admin — change it on the Settings page or:
echo 'NEWPASSWORD' | ssh pi4 "sudo tee /etc/splitgate/admin.secret"Pages:
- Dashboard — VPN tunnel status, daemon state, RU list age, route counts (auto-refreshes 10s)
- Services — start/stop/restart awg0, splitgate-watch, networking, dnsmasq
- Routes — add/remove CIDRs in vpn-routes-custom.txt and isp-routes-custom.txt; Apply button reloads routes
- Logs — Watch Live (real-time SSE stream), Install Log, Watch Errors, System Journal
- Config — edit ru-list-exclude.txt; trigger manual RU list refresh
- Settings — edit /etc/splitgate/vpn-gateway.env and AWG config; change admin password; Rollback
CLI management:
splitgate admin status # check if admin service is running
splitgate admin start
splitgate admin stop
splitgate admin restart