Skip to content
deivi98Public

About

Mirror of https://git.deivii.es/deivi/tsvpn

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

tsvpn

Runs a Tailscale exit node that tunnels all client internet traffic through one or more WireGuard VPN tunnels. Your entire Tailscale mesh gets a single private internet exit point — no VPN client needed on individual devices, and the exit node is never exposed to the internet.

How it works

[Any Tailscale device]
        │
        ▼ tailscale0
  [Exit node / tsvpn]  ──► policy routing table 51820
        │
        ▼ wg-<profile>
  [WireGuard VPN tunnel]
        │
        ▼
   [Internet]

tsvpn is a daemon that runs on the exit node. It:

  1. Brings up all configured WireGuard tunnel interfaces on start
  2. Installs policy routing rules that intercept traffic arriving from tailscale0 and redirect internet-bound packets into a custom routing table
  3. Points that table at the active tunnel — only one tunnel routes at a time, but all are connected
  4. Runs health checks on every tunnel and automatically fails over to the next-priority tunnel if the active one degrades
  5. Serves a web dashboard and CLI for monitoring and manual switching

Traffic between Tailscale peers (e.g. 100.x.x.x ranges) is never sent into the VPN — only internet-bound traffic is redirected. Docker networks are also excluded.

Rule priority Match Action
700 iif tailscale0 → 172.16.0.0/12 (Docker) main table
800 iif tailscale0 → 100.64.0.0/10 (Tailscale peers) main table
800 iif tailscale0 → fd7a:115c:a1e0::/48 (Tailscale IPv6) main table
1000 iif tailscale0 → everything else table 51820 → active tunnel

What you get

  • Single internet exit for every device in your Tailscale mesh
  • No public IP exposure — the exit node has no internet-facing listener
  • VPN privacy for all devices with zero per-device configuration
  • Automatic failover — multiple tunnels stay connected; if one drops the next takes over instantly
  • Live monitoring — web UI accessible from any device in the mesh, plus a tsvpn status CLI command

Dependencies

NixOS

Nothing to install manually. The flake declares everything and Nix resolves it:

Dependency Where declared Who needs it
Go toolchain buildGoModule Build only
pkg-config, gtk3, libayatana-appindicator buildInputs in flake Build + runtime of tsvpn-client (tray)
wireguard-tools (wg, wg-quick) NixOS module Server (enable = true)
iproute2 (ip) NixOS module Server (enable = true)
tailscale Your existing NixOS config Both

GTK and appindicator are linked into the binary at build time via Nix store paths — the system does not need them installed separately.

Non-NixOS (Make)

Must be installed on the system before building or running:

Dependency Purpose
go ≥ 1.21 Build
pkg-config Build (tsvpn-client only)
gtk3-dev, libayatana-appindicator3-dev Build (tsvpn-client only)
wireguard-tools Runtime — wg and wg-quick
iproute2 Runtime — ip rule / ip route
tailscale Runtime — exit node and IP detection

make build-server (used by make install) skips CGO entirely, so the GTK/appindicator dev packages are only needed if you build the tray variant with make build.


Installation

Option A — NixOS (recommended)

The repo ships a Nix flake with two packages and a shared NixOS module.

Package Tray Systemd service
tsvpn-server No Managed by NixOS (enable = true)
tsvpn-client Yes (GTK) Optional — can also run daemon manually

The only real differences between the two packages are the system tray and whether the systemd service is enabled. Both packages contain the full daemon — a client machine can run sudo tsvpn daemon and it works identically to the server. The split exists so headless servers don't pull in GTK.

Add the flake input to your flake.nix:

inputs.tsvpn = {
  url = "gitlab:deivi98/tsvpn";
  inputs.nixpkgs.follows = "nixpkgs";
};

Server (exit node) — import the module and enable the service:

{ inputs, pkgs, ... }:
{
  imports = [ inputs.tsvpn.nixosModules.tsvpn ];

  services.tsvpn = {
    enable = true;   # installs and starts the systemd daemon

    # All options below are optional — defaults shown.
    web.port = 7171;
    web.bind = "auto";  # "auto" resolves to the node's Tailscale IPv4

    routing.tableId       = 51820;
    routing.tailscaleIPv4 = "100.64.0.0/10";
    routing.tailscaleIPv6 = "fd7a:115c:a1e0::/48";
    routing.dockerCIDR    = "172.16.0.0/12";
  };
}

Client (desktop in the mesh) — same module, different package, no service:

{ inputs, pkgs, ... }:
{
  imports = [ inputs.tsvpn.nixosModules.tsvpn ];

  services.tsvpn = {
    # enable = false  (default — no systemd service)
    package = inputs.tsvpn.packages.${pkgs.stdenv.hostPlatform.system}.tsvpn-client;
  };
}

In both cases the module:

  • Writes /etc/tsvpn/config.yaml from the NixOS options
  • Creates /etc/tsvpn/profiles/ (writable, for tsvpn import)
  • Puts tsvpn in PATH

On a client you can point CLI commands at the remote daemon with the -addr flag:

tsvpn status  -addr vps.example.com:7171
tsvpn switch  -addr vps.example.com:7171 proton-es75
tsvpn tray    -addr vps.example.com:7171

Or run the daemon locally on any machine that has WireGuard profiles:

sudo tsvpn daemon

Updating the Go vendor directory after changing dependencies:

go mod vendor
git add vendor/

No Go toolchain is needed on machines that just build from the flake — vendor/ is committed.


Option B — Make (non-NixOS)

Requirements

  • Linux with wireguard-tools (wg, wg-quick) and iproute2
  • Tailscale running and configured as an exit node:
    tailscale up --advertise-exit-node
    
  • IP forwarding enabled:
    # /etc/sysctl.conf
    net.ipv4.ip_forward = 1
    net.ipv6.conf.all.forwarding = 1
    
  • Root access (the daemon runs as root via systemd)

Install

From the project directory (inside the dev shell if using devenv):

make install     # build binary, install to /usr/local/bin, copy config and profiles, install systemd unit
make enable      # enable at boot
make start       # start now

make install is safe to re-run — it never overwrites files that already exist in /etc/tsvpn/.


First-time setup (both install methods)

1. Add a VPN profile

Download a WireGuard .conf file from your VPN provider and import it:

tsvpn import ~/Downloads/protonvpn-es75.conf

The import command:

  • Enforces Table = off and DNS = none (required — the daemon owns routing)
  • Removes any PostUp / PreDown hooks
  • Prompts for a name (≤ 15 chars), failover priority, and metadata
  • Writes sanitized files to /etc/tsvpn/profiles/

To add more profiles for failover, import additional configs and give each a different priority (1 = highest).

2. Verify

tsvpn status
Active tunnel: proton-es75

NAME               STATUS     HANDSHAKE    PRIORITY  RX/TX       PROVIDER
────────────────────────────────────────────────────────────────────────────
proton-es75        healthy    12s ago      1         142M/18M    protonvpn (ES) ◀
mullvad-se         healthy    8s ago       2         0B/0B       mullvad (SE)

Or open the web dashboard from any device in your Tailscale network:

http://<exit-node-tailscale-ip>:7171

Configuration reference

The daemon reads /etc/tsvpn/config.yaml. With NixOS this file is generated from module options; with Make you edit it directly.

profiles_dir: /etc/tsvpn/profiles   # where WireGuard profiles are stored

web:
  port: 7171        # web UI and API port
  bind: auto        # "auto" = Tailscale IPv4; or a specific IP / "0.0.0.0"

routing:
  table_id: 51820               # policy routing table ID
  tailscale_ipv4: "100.64.0.0/10"
  tailscale_ipv6: "fd7a:115c:a1e0::/48"
  docker_cidr: "172.16.0.0/12"  # excluded from VPN routing

Profile format

Profiles live in /etc/tsvpn/profiles/. Each profile is two files:

<name>.conf — standard WireGuard config. Must have Table = off and DNS = none. The interface name is the filename stem (max 15 chars).

[Interface]
PrivateKey = ...
Address = 10.2.0.2/32
Table = off
DNS = none

[Peer]
PublicKey = ...
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = 1.2.3.4:51820
PersistentKeepalive = 25

<name>.meta.yaml — metadata and health check config:

provider: protonvpn
country: ES
server: "ES#75"
priority: 1          # 1 = highest; 0 = not eligible for auto-failover

health:
  method: handshake  # handshake | ping | all
  interval: 15s
  max_handshake_age: 180s
  ping_target: "1.1.1.1"
  failures_before_failover: 3

Priority determines failover order. Profiles without a priority (or priority: 0) are available for manual switching but won't be used in automatic failover.


CLI reference

tsvpn daemon              Start the daemon (used by systemd)
tsvpn status              Show all tunnel states
tsvpn list                List configured profiles
tsvpn switch <name>       Manually promote a tunnel to active
tsvpn import <file.conf>  Import and sanitize a WireGuard config

All commands except daemon and import talk to the running daemon over HTTP on localhost:7171. Use -addr host:port to override if you changed the port.


Makefile targets

Target Description
make build Compile ./bin/tsvpn (with tray, requires CGO + GTK)
make build-server Compile ./bin/tsvpn (no tray, CGO-free)
make install Full install (binary + config + service)
make install-bin Binary only
make install-config Config and profiles only (skips existing files)
make install-service Systemd unit only
make uninstall Remove binary and service (config untouched)
make start / stop / restart / status Control the systemd service
make enable / disable Boot behaviour
make fmt / vet Format and lint

Project structure

cmd/tsvpn/          CLI entry point and subcommands
internal/config/    Config and profile loading
internal/routing/   ip rule / ip route management
internal/wg/        wg-quick wrapper, handshake and stats queries
internal/health/    Per-tunnel health check goroutine
internal/daemon/    Orchestrator: tunnel lifecycle, failover, stats
internal/api/       HTTP API and embedded web dashboard
nix/                NixOS module (nix/module.nix)
systemd/            tsvpn.service unit file
vendor/             Vendored Go dependencies (no Go toolchain needed to build)

About

Mirror of https://git.deivii.es/deivi/tsvpn

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages