English · فارسی · 中文 · Русский · اردو · Türkmen
Tunforge is a single-source-of-truth VPN control plane for Ubuntu 22.04. It manages four mutually exclusive connection profile types and prevents DNS, IP, and IPv6 leaks — including across profile switches.
- Direct — no VPN; router-supplied DNS
- WireGuard —
wg-quick-based, multi-server - OpenVPN —
.ovpnconfigs, multi-server - sing-box TUN — system-wide tunnel for VMess / VLESS / Trojan / Shadowsocks
A whiptail TUI is the primary interface. Operations are serialized under flock, the previous profile is fully torn down before the next comes up, DNS is locked to the tunnel via systemd-resolved per-link routing, an nftables kill switch is installed, and IPv6 is disabled system-wide while a non-direct profile is active.
Status: tested and working on Ubuntu 22.04 (amd64 / arm64).
Tunforge is a small control plane: one CLI/TUI process owns connect/disconnect under a global lock, delegates tunnel bring-up to a connector, then hardens the host (DNS, nftables, IPv6) and verifies before declaring success.
flowchart TB
subgraph ui [User interface]
TUI["whiptail TUI"]
CLI["CLI: connect status doctor …"]
end
subgraph core [Control plane]
Core["core.sh\nflock + state machine"]
Active["/var/lib/tunforge/active"]
end
subgraph connectors [Connectors]
Direct["direct"]
WG["wireguard"]
OVPN["openvpn"]
SB["sing-box TUN"]
end
subgraph harden [Leak hardening]
DNS["dns.sh\nresolvectl per-link lock"]
FW["firewall.sh\nnftables kill switch"]
Leak["leak-prevent.sh\nIPv6 + avahi"]
Verify["verify.sh\npost-connect checks"]
end
TUI --> Core
CLI --> Core
Core --> Active
Core --> Direct
Core --> WG
Core --> OVPN
Core --> SB
Core --> DNS
Core --> FW
Core --> Leak
Core --> Verify
Verify -->|failure| Direct
Connect path (happy path):
- Acquire
flockon/var/lib/tunforge/lock - Tear down the previous profile completely (connector down + DNS revert + firewall clear + IPv6 restore as needed)
- Bring up the selected connector (WireGuard / OpenVPN / sing-box / direct)
- Lock DNS to the tunnel iface (
resolvectl+~.route-only domain) - Install the nftables kill switch (
inet tunforge, output drop by default) - Disable IPv6 (non-direct) and stop LAN discovery noise
- Run
verify.sh; on failure, auto-rollback to direct
Design principles:
| Principle | How |
|---|---|
| Single source of truth | /var/lib/tunforge/active updated atomically; only one profile at a time |
| Serialized mutations | All connect/disconnect paths take the same flock |
| Fail closed | Kill switch + verify rollback; boot unit re-applies firewall before NetworkManager |
| Pluggable tunnels | Connectors share a thin contract; core owns policy, not protocol details |
Repo layout mirrors the runtime layout under /usr/local and /etc/tunforge (see Layout).
git clone <this-repo> tunforge
cd tunforge
sudo ./install.shWhat the installer does:
- Preflight: Ubuntu 22.04 /
--forcefor others, root, arch (amd64 / arm64),systemd-resolvedpresent. - Apt: installs only what's missing —
wireguard-tools openvpn nftables whiptail iproute2 curl jq gawk dnsutils ca-certificates moreutils. - sing-box: fetches the latest
.debfrom GitHub Releases for your arch, verifies SHA256 if a.sha256sidecar exists, thendpkg -i. Skip with--no-singboxor supply an offline.debwith--singbox-deb /path/to/file.deb. - Deploys binaries into
/usr/local/bin/, libraries into/usr/local/lib/tunforge/, configs into/etc/tunforge/. - Installs
NetworkManagerandsystemd-resolveddrop-ins so they stop fighting over DNS / VPN interfaces, and points/etc/resolv.confat the systemd stub (with backup). - Enables
tunforge-killswitch.serviceso a reboot mid-VPN cannot leak.
Re-runs are safe (idempotent). Pass --check for a dry run.
sudo ./install.sh --check
sudo ./install.sh --no-singbox
sudo ./install.sh --singbox-deb ./sing-box_1.10.3_linux_amd64.debsudo ./uninstall.sh # removes Tunforge but keeps your profiles + configs
sudo ./uninstall.sh --purge # also wipes /etc/tunforge and /var/lib/tunforgeApt packages are left in place (they may be used elsewhere). A removal hint is printed at the end.
-
Drop your config files:
- WireGuard:
/etc/tunforge/configs/wireguard/de1.conf(chmod 600 — contains a private key) - OpenVPN:
/etc/tunforge/configs/openvpn/nl1.ovpn - sing-box:
/etc/tunforge/configs/singbox/jp1.json
For V2Ray-style URIs (
vmess://,vless://,trojan://,ss://) skip step 1 and use the importer:sudo tunforge import 'vless://uuid@server:443?security=tls&type=ws&path=/api#JP-Tokyo'The importer writes both a sing-box JSON config and a profile descriptor (named after the URI's
#label, e.g.jp-tokyo). You can also pass an explicit name as the second argument, paste from stdin (pbpaste | sudo tunforge import -), or batch-import a file (sudo tunforge import -f subscriptions.txt). See V2Ray URI import below. - WireGuard:
-
Create a profile descriptor in
/etc/tunforge/profiles/<name>.profile. Two shortcuts:sudo tunforge scaffold # creates profiles for any configs that don't have oneor use the TUI action Manage profiles → Scaffold from existing configs. Existing profiles are never overwritten. The importer also skips this step (it produces both files).
-
Launch:
sudo tunforge
- Connect — switch profiles (radio list;
*marks the active one) - Disconnect — return to direct mode
- Status — active profile, iface, DNS in use, public IP via tunnel
- Live logs — colorized journald tail
- Doctor — health check + kill-switch ruleset dump
- Connect — switch profiles (radio list;
CLI is also available for scripting:
sudo tunforge list
sudo tunforge connect wg-de1
sudo tunforge status
sudo tunforge doctor
sudo tunforge disconnect
sudo tunforge import '<uri>'
sudo tunforge bypass add backend.local
sudo tunforge bypass add-cidr 172.17.0.0/16
tunforge logsProviders usually hand you a one-line URI. Tunforge converts it into a sing-box config + profile in one shot:
sudo tunforge import 'vless://...#JP-Tokyo' # name = JP-Tokyo (slugified)
sudo tunforge import 'vmess://eyJ2I...' jp-tokyo # explicit profile name
echo "$URI" | sudo tunforge import - # from stdin (pbpaste / xclip)
sudo tunforge import -f ~/subs.txt # batch (one URI per line)Supported schemes:
| Scheme | Notes |
|---|---|
vmess:// |
Base64-encoded V2RayN JSON dialect, with tcp / ws / grpc / http transports and TLS. |
vless:// |
URL-style. Supports TLS + uTLS fingerprint + REALITY (security=reality&pbk=...&sid=...). |
trojan:// |
URL-style with TLS + ws / grpc transports. |
ss:// |
Shadowsocks, both legacy base64(method:pass@host:port) and SIP002 base64(method:pass)@host:port. |
After import, edit /etc/tunforge/profiles/<name>.profile if you want to tweak DNS servers, then:
sudo tunforge connect <name>Unusual transports (xtls-rprx-vision + REALITY, custom obfs, plugin-based shadowsocks, etc.) may need a hand edit — sing-box check -c /etc/tunforge/configs/singbox/<name>.json will tell you what to fix.
A subscription is a remote URL that returns a list of V2Ray URIs (usually base64-encoded, one per line). Store the URL once and refresh it to pull the provider's current server list — old servers are removed and new ones added automatically.
sudo tunforge subscription add https://provider.example/sub?token=abc myprovider
sudo tunforge subscription update myprovider
sudo tunforge subscription update all
sudo tunforge subscription list
sudo tunforge subscription remove myproviderEach server becomes its own profile named <sub>-NN (e.g. myprovider-01) tagged with SOURCE=<sub>, so it appears in the normal connect picker. The server's human label is kept in DESC. Connect as usual:
sudo tunforge connect myprovider-01update fully rebuilds that subscription's server set: it deletes previously generated profiles (and sing-box configs) for that subscription, then re-imports the fresh list. Hand-imported profiles and other subscriptions are never touched. If the subscription host's DNS is poisoned, the fetch retries with the host pinned to an IP resolved via direct public DNS. Also available from the TUI under Manage V2Ray subscriptions (soft sub).
TYPE=wireguard # direct | wireguard | openvpn | singbox
DESC="Germany #1"
COUNTRY=Germany # optional display label; if omitted, Tunforge caches geo country after a verified connect
CONFIG=/etc/tunforge/configs/wireguard/de1.conf
DNS_SERVERS="1.1.1.1 9.9.9.9" # required for VPN types; NEVER 8.8.8.8 — see below
DNS_OVER_TLS=opportunistic # off | opportunistic | yes
KILL_SWITCH=yes # default-drop firewall while active
IPV6=disable # disable (recommended) | allow
ENDPOINT_IP=203.0.113.45 # optional: pin so we don't need DNS at connect
MTU=1280 # optional: useful on hostile networks| Layer | Mechanism |
|---|---|
| DNS | resolvectl dns <iface> <servers> + resolvectl domain <iface> '~.' makes the VPN iface the only resolver. Every other link is resolvectl revert-ed. Successful resolutions are cached in /var/lib/tunforge/dns-cache (default 24h TTL). tunforge dns-cache clear resets it. |
| Routes | WireGuard / OpenVPN / sing-box install a default route via the tunnel (Table=auto, redirect-gateway def1, auto_route+strict_route). |
| Kill switch | nftables table inet tunforge with output policy drop. Only lo, the tunnel iface, resolved VPN endpoint IPs on the WAN, configured local bypass CIDRs, and DHCP renewal are allowed. |
| IPv6 | sysctl net.ipv6.conf.{all,default}.disable_ipv6=1 while a non-direct profile is active. Kill-switch v6 chains drop everything as belt-and-braces. |
| mDNS / LLMNR | MulticastDNS=no + LLMNR=no in resolved.conf.d/tunforge.conf. avahi-daemon is stopped while VPN is active. |
| Boot | tunforge-killswitch.service re-applies the firewall before NetworkManager.service if /var/lib/tunforge/active is non-none, so a reboot mid-VPN cannot leak. |
| Single source of truth | Connect/disconnect operations are serialized by flock /var/lib/tunforge/lock. State file /var/lib/tunforge/active is updated atomically. The TUI runs a health check on launch and offers to reset on drift. |
| Post-connect verification | verify.sh checks: iface up, default v4 route via tunnel, ~. domain on tunnel only, public IP via tunnel, DNS via stub, IPv6 default absent or also on tunnel. Failure auto-rolls back to direct mode. |
Local development networks are bypassed by default while the VPN is on:
127.0.0.0/8
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
169.254.0.0/16
This keeps localhost, LAN services, and Docker-style private networks reachable. Add your own:
sudo tunforge bypass add-cidr 172.17.0.0/16
sudo tunforge bypass add-domain backend.local
sudo tunforge bypass add mongodb://mongo.dev:27017
tunforge bypass listCIDR entries allow that traffic outside the VPN. Domain entries route matching DNS names to the WAN link's DNS while the default ~. resolver stays pinned to the tunnel. Changes apply on the next reconnect. Set TUNFORGE_BYPASS_DEFAULTS=0 to disable the built-in local CIDRs.
8.8.8.8 and 8.8.4.4 are operated by the same vendor as Gmail / Workspace / Firebase. Using them lets that resolver correlate DNS patterns against account activity, even via a VPN. The profile loader hard-rejects them. Prefer Cloudflare (1.1.1.1), Quad9 (9.9.9.9), or another non-FAANG resolver — ideally one in your VPN exit country.
System-level routing cannot fix:
- WebRTC — browsers can probe your real IP via STUN. Firefox:
about:config→media.peerconnection.enabled=false. Chromium: WebRTC Network Limiter extension. - Browser fingerprinting — canvas, fonts, time zone, language. Match system time zone to the VPN exit (
timedatectl set-timezone Europe/Berlin) or use a per-exit browser profile. - Logged-in account correlation — signing in from your real IP and later from a VPN exit correlates the sessions. Use separate browser profiles per exit.
- HTTP/3 (QUIC) — with the kill switch this is fine (UDP/443 not via tunnel is dropped); with the kill switch off you may see unexpected traffic.
/usr/local/bin/tunforge whiptail TUI + CLI
/usr/local/bin/tunforge-logs colorized journald tail
/usr/local/bin/tunforge-import V2Ray URI → sing-box JSON + profile
/usr/local/bin/tunforge-subscription V2Ray soft-sub URL → refreshable server list
/usr/local/bin/tunforge-scaffold create .profile files for existing configs
/usr/local/lib/tunforge/
core.sh flock, state machine, dispatch
dns.sh resolvectl-based DNS lock
firewall.sh nftables kill switch
leak-prevent.sh IPv6 sysctl, avahi
verify.sh post-connect verification
nm-tame.sh NM + resolved configurator
connectors/{direct,wireguard,openvpn,singbox}.sh
/etc/tunforge/
config.yaml global config
profiles/<name>.profile one per profile
configs/{wireguard,openvpn,singbox}/...
templates/singbox-tun.json.tmpl
/var/lib/tunforge/
active authoritative current profile
dns-cache successful DNS resolutions (host → ip)
test-cache ping/TCP probe results (used by TUI)
active.iface current tun iface name
lock flock target
leak.snapshot sysctl baseline for restore
firewall.params persisted killswitch params (boot reapply)
/etc/systemd/system/tunforge-killswitch.service
/etc/systemd/system/tunforge@.service (parameterized; boot-time auto-up)
/etc/NetworkManager/conf.d/99-tunforge.conf
/etc/systemd/resolved.conf.d/tunforge.conf
Contributions are welcome — whether you are fixing a bug on Ubuntu 22.04, hardening leak paths, adding a connector, or improving docs.
Good first areas:
- Connector edge cases (WireGuard / OpenVPN / sing-box bring-up and teardown)
- DNS / nftables / IPv6 leak scenarios and verification coverage
- Installer portability and clearer operator messaging
- TUI/CLI UX and documentation
How to contribute:
- Fork the repo and create a focused branch (
feat/…,fix/…, ordocs/…) - Keep changes scoped; prefer one concern per PR
- Test on Ubuntu 22.04 when touching networking, DNS, or firewall code
- Open a pull request that explains the problem, the approach, and how you verified it
If you are unsure where to start, open an issue describing your environment and the failure (or the feature you want). Serious reviews of architecture and security assumptions are especially appreciated.
sudo tunforge doctor # health check + ruleset dump
sudo tunforge logs # live tail
sudo tunforge disconnect # nuke everything, return to direct
sudo nft list table inet tunforge
resolvectl status
ip -4 route get 1.1.1.1If verify.sh rolled you back, the failure reason is in journalctl -t tunforge -n 50.