A fast, interactive OpenVPN config scanner, tester and connector for
the command line. It finds .ovpn files on disk, tests them concurrently,
remembers which ones work, and lets you connect, switch and copy configs — all
from a keyboard-driven terminal UI backed by SQLite.
⚠️ Note: by default vmate-cli cleans up only the OpenVPN processes it spawned: each process group is sent SIGTERM, given a grace period, then SIGKILLed if needed. Pass--killallto also run thekillall -9 openvpnsweep (the behavior of the original Go tool) during connection switching and shutdown.
- 🔍 Scan — recursively discover
.ovpnfiles and test them concurrently. - ⚡ Connect — intelligent retry, manual skip, deferred reshuffling.
- 🗂️ Recent — browse previously successful configs in a clickable TUI.
- 🎬 All — scan, store, then connect using only the filtered matches.
- 📦 Export — copy successful configs with sanitized, country-prefixed names.
- 🌍 Filter — filter by country code, case-insensitive, across every command.
- 🖱️ Click-to-copy — copy config paths from the recent TUI.
- 💾 SQLite (WAL mode) — persistent history with automatic migrations.
- 🩺 Doctor — environment and dependency checks.
- ⚙️ Completions — shell completions for bash/zsh/fish.
- 🔍 Country detection — filename heuristics, an IP cache, and a geo IP API.
- Rust 1.85+ (edition 2024)
- OpenVPN —
openvpnonPATH, or pass--openvpn-bin - Root/sudo for
scan,connectandall(vmate-cli re-executes undersudoautomatically on an interactive terminal; setVMATE_NO_ELEVATE=1to run without elevation — OpenVPN will likely fail) killall(optional) only if you pass--killallfor the global OpenVPN sweep
cargo build --release # optimized, stripped binary → target/release/vmate-cli
cargo test # unit + integration tests
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --checkReleases are built automatically when a v* tag is pushed. Each release
attaches a zip per platform (vmate-cli-<version>-<target>.zip):
| Target | Binary | Notes |
|---|---|---|
aarch64-apple-darwin |
Apple Silicon Macs | arm64 |
x86_64-apple-darwin |
Intel Macs | x86_64 |
x86_64-unknown-linux-gnu |
Linux x86_64 | most servers / desktops |
aarch64-unknown-linux-gnu |
Linux arm64 | Pi, AWS Graviton, etc. |
Windows is not shipped — vmate-cli is Unix-only (process groups, root checks,
killall when --killall is used).
Download and extract a zip, then install the binary to your PATH:
tar -xzf vmate-cli-1.0.1-aarch64-apple-darwin.zip
cd vmate-cli-1.0.1-aarch64-apple-darwin
sudo ./install.sh # copies vmate-cli → /usr/local/bin
vmate-cli --helpinstall.sh can also replace an existing install, install to a different
directory by editing the DEST/OPERATION variables at the top, and remove
itself with sudo ./install.sh --uninstall.
vmate-cli <COMMAND> [OPTIONS]
Run vmate-cli --help for all options and vmate-cli <COMMAND> --help for
per-command help.
These apply to every subcommand:
| Option | Description |
|---|---|
-f, --filter <COUNTRY> |
Filter by country code, e.g. jp,kr. Repeatable. |
--db <PATH> |
Path to the SQLite database (default ~/.config/vmate-cli/vmate.db). |
--openvpn-bin <BIN> |
OpenVPN binary to use (default openvpn). |
--killall |
Also run killall -9 openvpn on shutdown/switch. Default is per-process cleanup of only the openvpn processes vmate spawned. |
--no-elevate |
Don't re-execute under sudo; run with current privileges (OpenVPN will likely fail). |
--ipinfo-token <TOKEN> |
ipinfo.io API token (defaults to a bundled free token). |
--save-defaults |
Persist explicitly-passed default flags (e.g. --max, --timeout, --retry-count) to the config file for future sessions. |
-v, -vv, -q |
Verbosity / quiet logging. |
-h, --help |
Print help. |
The scan/connect tunables (--max, --limit, --timeout, --connect-timeout,
--cooldown, --retry-count, --stability-grace) have built-in defaults. Pass
--save-defaults alongside the value flags to persist them for future sessions:
vmate-cli scan --save-defaults --max 500 --timeout 20
vmate-cli connect --save-defaults --retry-count 5 --connect-timeout 10 --cooldown 60 --stability-grace 8--save-defaults only writes the new defaults and exits — it does not scan
or connect (no root/OpenVPN needed). Run the command again without
--save-defaults to actually scan/connect, resolving each value as
explicit flag → persisted → built-in default. scan/all persist their
workers (--max), --limit, and --timeout defaults; connect/all persist
--connect-timeout, --cooldown, --retry-count, and --stability-grace.
--retry-count controls how many times a failing config is retried before it is
dropped from history; --connect-timeout is the handshake threshold,
--cooldown the delay before retrying a recently-failed config, and
--stability-grace how long a connected session must last before its crash
resets the retry budget.
Only the flags you explicitly pass are saved; unmentioned tunables keep
their existing persisted or built-in default. Persisted settings live in
vmate-cli/settings.json inside your config directory (printed when you save)
and can be edited by hand. Each tunable is resolved as:
explicit CLI flag → persisted setting → built-in default
A missing or corrupt settings.json is ignored and falls back to the built-in
defaults.
# Scan ~/configs, keep testing until 20 Japan/Korea configs succeed.
# --max/-m controls concurrency, --timeout/-t is per-test seconds.
vmate-cli scan ~/configs --filter jp,kr --limit 20 --max 64 --timeout 15 -v
# Scan the built-in vpn-gate remotes over UDP (no directory argument)
vmate-cli scan --filter jp
# Try the same built-in remotes over TCP instead
vmate-cli scan --proto tcp
# Do not write results to the database
vmate-cli scan ~/configs --no-save
# Also copy this scan's filtered matches into ./out
vmate-cli scan ~/configs --filter jp --export ./outscan tests every .ovpn file it finds, stores the successful ones in the
database (so they show up in vmate-cli recent later), and reports the configs
that match the current --filter.
With no directory argument, scan materializes the built-in configs for the
chosen provider and protocol into ~/.config/vmate-cli/builtin/<provider>/<proto>/
and scans those. --provider selects the built-in provider (default vpn-gate)
and --proto selects the transport protocol (udp or tcp, default udp);
re-scan with --proto tcp to try the other protocol.
# Connect using stored JP candidates only
vmate-cli connect --filter jp
# Connect to an explicit config (fallbacks still respect the filter)
vmate-cli connect ./some.ovpn --filter jp
# Reject an explicit config that does not match the filter
vmate-cli connect ./us.ovpn --filter jp --strict-filterconnect picks candidates from the stored history, tries each one, retries a
failed handshake once, and drops a config from history after repeated failures.
Use the interactive keys below while connected.
# Show the last 50 successful configs in a TUI
vmate-cli recent
# Plain table output (no TUI)
vmate-cli recent --no-tui
# Show everything
vmate-cli recent --all
# Copy the newest config path immediately
vmate-cli recent --copy-first
# Also copy the listed configs into ./out
vmate-cli recent --filter jp --export ./outIn the TUI: press Enter or c to copy a config path, / to filter the list,
arrow keys / j k to move, and q / Ctrl+C to quit. Clicking a row copies
its path too.
# Scan, then connect using only the filtered matches
vmate-cli all ~/configs --filter jp,kr
# Scan the built-in vpn-gate remotes and connect (no directory argument)
vmate-cli all --filter jp,kr
# Scan and report only (do not connect)
vmate-cli all ~/configs --no-connect
# Scan, connect, and also export this scan's matches
vmate-cli all ~/configs --filter jp --export ./outall runs a full scan (storing successes as usual), reports the matches, then
hands them to the connect flow. --no-connect stops after the scan report. As
with scan, omitting the directory scans the built-in remotes for
--provider/--proto.
# Export JP configs to ./exported
vmate-cli export --filter jp --out ./exportedExports configs from the database, naming each file COUNTRY_<original name>
and avoiding collisions with _1, _2, ... suffixes. For exporting from a
fresh scan or from the recent list, use scan --export or recent --export
instead.
vmate-cli doctorChecks for OpenVPN, root access, and database health.
vmate-cli installs completion scripts for bash, zsh and fish into the
standard location for each shell, then prints how to activate them:
vmate-cli completions bash
vmate-cli completions zsh
vmate-cli completions fishFor zsh it prefers a Homebrew zsh-completions dir already on $fpath
(falling back to ~/.zfunc). After installing, restart your shell — or just
run compinit in zsh to pick it up immediately.
If you need the raw script (for example to capture it into a dotfiles repo),
use --print:
vmate-cli completions zsh --print > _vmate-cliManual one-liners, if you prefer to place the script yourself:
# bash
echo 'source <(vmate-cli completions bash --print)' >> ~/.bashrc
# zsh (with fpath + compinit)
mkdir -p ~/.zfunc && vmate-cli completions zsh --print > ~/.zfunc/_vmate-cli
echo 'fpath=(~/.zfunc $fpath); autoload -Uz compinit && compinit' >> ~/.zshrc
# fish
mkdir -p ~/.config/fish/completions
vmate-cli completions fish --print > ~/.config/fish/completions/vmate-cli.fish--filter is a global flag, case-insensitive, and can be repeated or comma
separated:
vmate-cli scan ./configs --filter JP,KR
vmate-cli scan ./configs --filter jp,kr
vmate-cli scan ./configs -f jp -f krUNKNOWN is an allowed value. An empty filter matches everything.
During a scan, the filter limits what is reported and exported, not what is
tested — unfiltered successes are still stored so they show up in
vmate-cli recent later.
Each config is tagged with a country using, in order:
- Filename heuristic — a two-letter code embedded in the file name, e.g.
vpngate_20260801_jp_vpn-gate.ovpn→JP. Fast, no network. - IP cache — the remote host is resolved and looked up in the SQLite cache.
- Geo IP API — a lookup against ipinfo.io, persisted to the cache.
By default vmate sends client IPs to ipinfo.io under a shared free-tier token,
so country detection works with no configuration. That shared token is
rate-limited and shared across all vmate-cli users — for privacy-sensitive
setups, provide your own token with --ipinfo-token or the IPINFO_TOKEN
environment variable (a warning is emitted when the shared token is in use).
Failures degrade to UNKNOWN — geo lookup never aborts a scan.
While connected:
n Next config (kill current, skip, defer)
r Reconnect to the same config
c Copy current config path
v Toggle live OpenVPN output log
? Show help
q Quit
Ctrl+C Quit and cleanup
v toggles a panel showing the OpenVPN process's output — the connection
handshake as well as live lines. c shows a Copied: ... confirmation that
fades after a few seconds. Keys respond immediately, even while a connection
is being established or switched. When a config is removed after repeated
failures, a removed <file> from recent list notice is shown briefly, and the
Config line shows the .ovpn file name (not the full path).
Pressing n gracefully kills the current OpenVPN process group (SIGTERM, then
SIGKILL after the grace period), runs killall -9 openvpn too when --killall
is enabled, marks the config as skipped (it is not deleted from history),
and moves it to the end of a shuffled deferred queue.
Two ways to copy configs out of vmate-cli:
vmate-cli scan <dir> --filter jp --export ./out— copy this scan's fresh matches. The scan still stores its successes, sovmate-cli recentis updated as usual.vmate-cli recent --filter jp --export ./out— copy previously scanned stored configs matching the filter.vmate-cli export --filter jp --out ./out— copy stored configs to an output directory.
Exported files are named COUNTRY_<sanitized-name>.ovpn; collisions get _1,
_2, ... suffixes.
The database lives at ~/.config/vmate-cli/vmate.db by default (override with
--db or the VMATE_DB environment variable). WAL mode is enabled and
migrations run automatically on startup:
sqlite3 ~/.config/vmate-cli/vmate.db "PRAGMA journal_mode;" # → wal.
├── Cargo.toml # workspace
├── migrations/ # SQLite schema
├── crates/
│ ├── vmate-core/ # domain logic (UI-agnostic)
│ │ ├── country.rs / filter.rs # --filter parsing & matching
│ │ ├── db/ # SQLite pool, models, repository (WAL)
│ │ ├── ovpn/ # parser, cipher repair, process runner, monitor
│ │ ├── geo/ # country detection (filename/IP cache/geo API)
│ │ ├── scan/ # concurrent test orchestration
│ │ ├── connect/ # candidate queue + connect session
│ │ ├── export/ # sanitized config export
│ │ └── system/ # process killer, root, signals
│ └── vmate-cli/ # clap CLI, commands, TUIs, progress, clipboard
│ └── tests/ # integration tests (assert_cmd)
Design principles:
- No global mutable state. Everything is constructed per run and passed in.
- Traits for external effects.
VpnTester,OpenVpnRunner,ProcessKiller,GeoLocatorandConnectHostkeep the core testable. - RAII cleanup.
CleanupGuardandTuiGuardrestore the terminal and kill stale OpenVPN processes even on panic/error paths. - Structured concurrency.
JoinSet+Semaphore+CancellationTokenfor scans;tokio::select!for the interactive connect loop.