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: vmate-cli intentionally runskillall -9 openvpnduring connection switching and shutdown. This is a deliberate cleanup strategy, not a bug. Use--no-killallto disable the global sweep (per-process kills still happen).
- π 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) killallfor the intentional global OpenVPN cleanup
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 --checkvmate-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). |
--no-killall |
Disable the global killall -9 openvpn cleanup. |
--ipinfo-token <TOKEN> |
ipinfo.io API token (defaults to a bundled free token). |
-v, -vv, -q |
Verbosity / quiet logging. |
-h, --help |
Print help. |
# 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
# 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.
# 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 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.
# 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 can generate completion scripts for bash, zsh and fish β either
printed to stdout so you can capture them, or installed automatically:
# Print the script (capture it yourself)
vmate-cli completions bash
vmate-cli completions zsh
vmate-cli completions fish
# Install it to the standard location and print activation steps
vmate-cli completions bash --install
vmate-cli completions zsh --install
vmate-cli completions fish --install--install writes the script where your shell already looks (for zsh it
prefers a Homebrew zsh-completions dir already on $fpath, falling back to
~/.zfunc), then tells you what to do to activate it. If you installed the
script with a specific shell manually, the equivalent one-liners are:
# bash
echo 'source <(vmate-cli completions bash)' >> ~/.bashrc
# zsh (with fpath + compinit)
mkdir -p ~/.zfunc && vmate-cli completions zsh > ~/.zfunc/_vmate-cli
echo 'fpath=(~/.zfunc $fpath); autoload -Uz compinit && compinit' >> ~/.zshrc
# fish
mkdir -p ~/.config/fish/completions
vmate-cli completions fish > ~/.config/fish/completions/vmate-cli.fishRestart your shell (or run compinit in zsh) for the completions to load.
--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.
A free token is bundled, so country detection works with no configuration.
Override it with --ipinfo-token or the IPINFO_TOKEN environment variable.
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.
Pressing n kills the current OpenVPN process group, runs killall -9 openvpn
(when 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.