Manage VPN exit nodes on a Tailscale network using DigitalOcean droplets — from a terminal or a browser.
yVPN simplifies the creation and management of distributed VPN exit nodes. It provisions DigitalOcean droplets preconfigured as Tailscale exit nodes, giving you on-demand VPN endpoints in various geographic locations.
Two front ends share one workflow:
| App | Path | What it is |
|---|---|---|
| CLI / TUI | apps/cli |
Go binary — interactive TUI, scriptable CLI, SSH server mode |
| Web | apps/web |
Single static HTML file — no build, no npm, no framework |
The web app is documented in apps/web/README.md. It is deployed as one Cloudflare Worker (or run locally as one Go binary) that serves the page and relays its Tailscale API calls from the same origin, so users only enter their two API credentials. DigitalOcean is called straight from the page.
The rest of this document covers the CLI.
- Interactive TUI - Clean terminal interface with keyboard navigation
- Scriptable CLI - JSON output for automation and scripting
- One-click provisioning - Create exit nodes in any DigitalOcean datacenter
- Automated setup - Droplets are fully configured via cloud-init (Tailscale installation, IP forwarding) and reach your tailnet in about a minute
- SSH access - Run as an SSH server for remote management without local installation
- Exit node management - View, create, and delete exit nodes from a unified dashboard
- DigitalOcean API token (with read/write access)
- Tailscale API key
Download the latest binary for your platform from the Releases page.
# Linux
curl -LO https://github.com/bnsmcx/yvpn/releases/latest/download/yvpn-linux-amd64
chmod +x yvpn-linux-amd64
sudo mv yvpn-linux-amd64 /usr/local/bin/yvpn
# macOS (Apple Silicon)
curl -LO https://github.com/bnsmcx/yvpn/releases/latest/download/yvpn-darwin-arm64
chmod +x yvpn-darwin-arm64
sudo mv yvpn-darwin-arm64 /usr/local/bin/yvpn
# macOS (Intel)
curl -LO https://github.com/bnsmcx/yvpn/releases/latest/download/yvpn-darwin-amd64
chmod +x yvpn-darwin-amd64
sudo mv yvpn-darwin-amd64 /usr/local/bin/yvpngit clone https://github.com/bnsmcx/yvpn.git
cd yvpn/apps/cli
go build -o yvpn ./cmd/tuiRequires Go 1.21+.
Set your credentials as environment variables:
export DIGITAL_OCEAN_TOKEN=<your_digitalocean_token>
export TAILSCALE_API=<your_tailscale_api_key>yvpn tuiIf credentials aren't set, you'll be prompted to enter them on startup.
# List existing exit nodes
yvpn list
# List available datacenters
yvpn datacenters
# Create a new exit node
yvpn create nyc1
# Delete an exit node by ID
yvpn delete 12345
# JSON output for scripting
yvpn list --json
yvpn datacenters --json
yvpn create nyc1 --json
# Show version
yvpn --versionRun yVPN as an SSH server for remote access:
yvpn sshThis starts an SSH server on port 1337. Connect with:
ssh -p 1337 user@hostnamePass credentials via SSH environment variables.
| Key | Action |
|---|---|
n |
Create new exit node |
d |
Delete selected exit node |
↑/↓ |
Navigate list |
Enter |
Confirm selection |
Esc |
Go back / Cancel |
q |
Quit |
-
Create: Select a DigitalOcean datacenter. yVPN generates a temporary Tailscale auth key, provisions a droplet with cloud-init configuration, and waits for the node to appear on your tailnet.
-
Configure: The droplet installs Tailscale from the static tarball, enables IP forwarding, and advertises itself as an exit node. It is ready in about a minute; see Boot time for why.
-
Enable: Once the droplet joins your tailnet, yVPN enables it as an exit node via the Tailscale API.
-
Use: Connect to your new exit node from any device on your tailnet.
yvpn/
├── apps/
│ ├── cli/ # Go TUI + CLI
│ │ ├── cmd/tui/
│ │ │ ├── main.go # Entry point, SSH server
│ │ │ ├── cli.go # CLI commands
│ │ │ ├── dash.go # Dashboard view
│ │ │ ├── add.go # Create exit node screen
│ │ │ ├── delete.go # Delete exit node screen
│ │ │ ├── onboard.go # Credential input
│ │ │ └── style.go # UI styling
│ │ ├── pkg/
│ │ │ ├── digital_ocean/
│ │ │ └── tailscale/
│ │ ├── Dockerfile
│ │ ├── go.mod
│ │ └── shell.nix # Nix development environment
│ └── web/ # Single-page web app
│ ├── index.html # The entire application
│ ├── proxy/ # Serves the page + Tailscale relay (Worker + Go)
│ └── test/ # Playwright end-to-end test
└── README.md
cd apps/cli && nix-shellThis provides Go, Git, gopls, and development tools.
Exit nodes are created with:
- OS: Ubuntu 24.04 x64
- Size: s-1vcpu-1gb (1 vCPU, 1GB RAM)
- Tag:
yVPN
A node used to take about 4.5 minutes to reach the tailnet. It now takes roughly a minute. Measured on identical nyc1 droplets, seconds from kernel boot to the node being ready:
| cloud-init | ready at |
|---|---|
package_update + package_upgrade + install.sh (old) |
266 s |
| without the apt upgrade | 99 s |
plus the static Tailscale tarball instead of install.sh |
89 s |
| plus skipping DigitalOcean's vendor data (current) | 31 s |
Three changes, in order of what they saved:
- No
package_upgradeon first boot (~145 s). Upgrading every package on a machine that lives for hours buys nothing. vendor_data: enabled: false(~57 s). DigitalOcean's vendor script installs its monitoring agent before user scripts run. Two consequences worth knowing: the image's default login isubunturather thanroot, and no DO agent is installed, so the droplet does not report metrics.- The static tarball instead of
install.sh(~22 s). No apt repository to add, noapt-get update, and no waiting on unattended-upgrades for the dpkg lock. The URL pinsamd64, which matches the droplet size above.
Tailscale installs its own netfilter rules for an exit node, so the manual iptables rules the cloud-init used to write are gone.
- Bubble Tea - TUI framework
- Lip Gloss - Styling
- Wish - SSH server
- godo - DigitalOcean API client
This project is licensed under the MIT License - see the LICENSE file for details.