whoisthat: A Rust TUI VPN Client Backed by a Go/Xray Engine
Most VPN clients are either a desktop GUI you can barely script around, or a raw config file with no operational feedback at all. whoisthat sits between those: a Rust TUI that drives a Go engine backed by Xray-core. The split is intentional — the frontend handles display, keybindings, and state; the backend handles the protocol work. This guide covers building both halves, connecting them, and running a working Xray tunnel through the terminal interface.
Step 1 – Understand the architecture before building
whoisthat has two processes that talk over a local socket. The Go engine starts Xray-core, manages inbound/outbound config, and exposes a control socket. The Rust TUI connects to that socket, reads stats, and sends commands (start, stop, switch config, show logs). Neither process depends on the other being up first — the TUI will wait and reconnect.
This matters for deployment. You can run the engine as a system service and the TUI as an on-demand terminal session. Kill the TUI, the tunnel stays up. Restart the engine, the TUI reconnects when it comes back. That is more operationally sensible than a monolithic client that takes the tunnel down every time you close the window.
Prerequisites: Rust 1.78+ (needed for the Ratatui/BubbleTea equivalent, Ratatui 0.27), Go 1.22+, and a working Xray-core binary in your PATH. Install Xray-core separately from the official releases — whoisthat expects it at /usr/local/bin/xray by default.
Step 2 – Build the Go engine
Clone the repo and build the engine first:
git clone https://github.com/kvunoff/whoisthat.git
cd whoisthat
# Build the Go engine
cd engine
go build -o whoisthat-engine ./cmd/engine
sudo mv whoisthat-engine /usr/local/bin/
# Verify
whoisthat-engine --version
The engine takes a single flag on start: the path to its config directory. By default it looks at ~/.whoisthat/. Create that directory and drop your Xray JSON config there:
mkdir -p ~/.whoisthat/configs
# Place your Xray config at ~/.whoisthat/configs/default.json
whoisthat-engine --config-dir ~/.whoisthat
The engine does not daemonize by default. For a persistent background process, create a systemd unit:
[Unit]
Description=whoisthat VPN engine
After=network.target
[Service]
ExecStart=/usr/local/bin/whoisthat-engine --config-dir /home/YOUR_USER/.whoisthat
Restart=on-failure
User=YOUR_USER
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now whoisthat-engine
Step 3 – Build the Rust TUI frontend
From the repo root, build the TUI:
cd ../tui
cargo build --release
sudo cp target/release/whoisthat /usr/local/bin/whoisthat-tui
The TUI binary is whoisthat-tui. Launch it:
whoisthat-tui
If the engine is already running, the TUI connects immediately and shows the current tunnel state, active config, inbound/outbound byte counts, and a real-time log panel at the bottom. If the engine is not up, the TUI shows a “waiting for engine” status and retries every two seconds. No crash, no error exit — it just waits.
The Rust/Go split is the right call for this use case. Ratatui in Rust handles terminal rendering without fighting the event loop that the Go engine needs for network I/O. Mixing them in one process would mean either blocking the render loop on network events or spawning goroutines that the Rust runtime does not know about.
Step 4 – Configure and switch Xray configs from the TUI
whoisthat’s TUI key layout is modal. Press ? for the keybinding overlay. The ones you will use most:
c open config switcher
s start/stop tunnel
r reload current config (picks up edits without restarting)
l toggle log panel
Tab cycle between panels
q quit TUI (engine keeps running)
The config switcher reads all JSON files from ~/.whoisthat/configs/ and lists them. Arrow keys to select, Enter to switch. The engine reloads Xray-core with the new config in place — active connections on the old config are drained, not dropped mid-packet, because the engine waits for in-flight requests to complete before tearing down the inbound listener.
To add a new Xray config — say a VLESS+Reality outbound — write the JSON to the configs directory and press c in the TUI without restarting anything:
cat > ~/.whoisthat/configs/reality-outbound.json << 'EOF'
{
"inbounds": [
{
"port": 10809,
"protocol": "socks",
"settings": { "auth": "noauth", "udp": true }
}
],
"outbounds": [
{
"protocol": "vless",
"settings": {
"vnext": [{
"address": "YOUR_SERVER",
"port": 443,
"users": [{ "id": "YOUR_UUID", "flow": "xtls-rprx-vision" }]
}]
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"serverName": "YOUR_SNI",
"fingerprint": "chrome",
"publicKey": "YOUR_PUBLIC_KEY",
"shortId": "YOUR_SHORT_ID"
}
}
}
]
}
EOF
Step 5 - Monitor traffic and debug from the terminal
The stats panel in the TUI shows per-connection byte counts and latency pulled from Xray-core's stats API. If you need the raw numbers outside the TUI — for a script or a cron check — the engine exposes them over its local socket as JSON:
# Query engine stats directly (socket path configurable, default shown)
curl --unix-socket /tmp/whoisthat.sock http://local/stats | jq .
For debugging a broken outbound, the log panel (l in TUI) tails Xray-core's stderr in real time. Increase log verbosity in your Xray config without touching the engine:
# Add to your Xray JSON config under the root object:
"log": {
"loglevel": "debug"
}
Reload with r in the TUI. The log panel starts showing connection attempts, TLS negotiation, and routing decisions. Set it back to warning before you leave it running overnight.
One practical limit to know: whoisthat does not ship a subscription parser. If you are working from a subscription URL that gives you a base64-encoded list of Xray/V2Ray configs, you need to decode and split that list into individual JSON files yourself before the config switcher sees them. Tools like xrat handle subscription parsing and can write the individual config files to a directory — point that directory at ~/.whoisthat/configs/ and whoisthat picks them up automatically.
Next steps
whoisthat is a young project. The issue tracker has open items for latency testing per config (think built-in real-delay checks before switching), IPv6 inbound support, and a headless mode that drops the TUI entirely for server deployments. The headless mode would make it usable as a pure CLI tool driven by the socket API, which is the missing piece for automation.
If you run into the TUI not rendering correctly over SSH, set TERM=xterm-256color before launching. Ratatui relies on terminal capability detection and some SSH clients report a stripped TERM value that kills color and box-drawing characters. That is a terminal emulator problem, not whoisthat's, but it is the first thing to check.