xrat π
xrat is a command-line proxy configuration manager for Xray-core and sing-box. Import subscriptions, test latency, scan edge IPs, rotate proxies, and run a managed Xray/V2Ray proxy runtime β all from a single Rust binary.
What you can do
- Import proxy configs from subscription URLs, base64 files, or raw VLESS/VMESS/Trojan/Shadowsocks links
- Test proxies with real-delay, TCP ping, ICMP, download/upload speed measurements
- Scan Cloudflare edge IPs and persist working endpoints
- Run Xray-core or V2Ray as a managed runtime with automatic proxy rotation
- Control everything through CLI, terminal UI (TUI), HTTP API, or background daemon
sing-box support currently covers parsing and runtime-config preview, including
Hysteria2 diagnostics through xrat parse --engine sing-box. Managed runtime
process lifecycle is Xray/V2Ray-focused.
Sections
| Section | Description |
|---|---|
| Getting Started | Installation, quickstart, configuration |
| CLI Reference | Command reference for all subcommands |
| Features | Deep-dives into each major subsystem |
| Deployment | systemd services, database backends |
| Reference | Protocols, config file, database schema, errors |
| Architecture | Module map, config generation pipeline |
Getting Started
xrat is a Rust-based CLI tool and daemon for managing proxy configurations. It imports subscription links, parses and normalizes proxy URIs, tests connectivity and performance, previews runtime configs for Xray-core and sing-box, manages an Xray/V2Ray local proxy runtime process, and exposes an HTTP API.
Prerequisites
- Xray-core binary installed and available in
PATH - sing-box binary (optional, used for sing-box parse/preview support)
- Rust toolchain and just when building from source
Installation
Choose one install path:
- Installation Script β recommended Linux install from the latest verified release archive
- Docker Install β run the published container image with bundled Xray-core
- Manual Binary Install β download, verify, and place release files yourself
- Build From Source β Justfile-oriented workflow for local development builds and source installs
Configuration Directory
xrat uses a configuration directory with the following resolution order:
--config <path>CLI flagXRAT_PATHenvironment variable~/.config/xrat/
The directory layout:
~/.config/xrat/
βββ config.toml # Application configuration
βββ db.sqlite # SQLite database (default)
βββ runtime/ # Runtime session files (generated configs, logs)
βββ logs/ # Xray/V2Ray process logs
Next Steps
- Quickstart β import, test, and connect in 3 commands
- Installation Script β recommended install path
- Configuration β config.toml reference
Installation Script
Use the installer script for a normal Linux install. It downloads the matching
release archive, verifies the checksum, installs xrat, and can run first-time
setup for you.
For other install paths, see Docker Install, Manual Binary Install, or Build From Source.
Requirements
Runtime dependencies
| Tool | Required | Purpose | Install |
|---|---|---|---|
xray | Yes | Managed Xray runtime and real-delay tests | XTLS/Xray-install |
sing-box | No | sing-box parsing and runtime-config preview for diagnostics | sing-box.app |
Install xray:
bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install
Install sing-box (optional):
curl -fsSL https://sing-box.app/install.sh | sh
System requirements
| Requirement | Details |
|---|---|
| OS | Linux x86_64 or aarch64 |
| libc | None β release binaries are statically linked |
| SQLite | Bundled β no system SQLite needed |
| PostgreSQL | Optional β version 14+ if used instead of SQLite |
| Network | Outbound HTTPS for imports and release downloads |
Install
curl -fsSL https://raw.githubusercontent.com/mhyrzt/xrat/master/install.sh | bash
The installer will:
- Check for
xrayand warn if optionalsing-boxis missing. - Detect
x86_64oraarch64. - Download the latest GitHub release archive.
- Verify the archive against
SHASUMS256.txt. - Install
xratto~/.local/bin/xrat. - Offer to run
xrat init. - Offer to install and start the systemd user daemon.
To install to a different directory:
INSTALL_DIR=/usr/local/bin curl -fsSL https://raw.githubusercontent.com/mhyrzt/xrat/master/install.sh | bash
Make sure the install directory is in PATH:
export PATH="$HOME/.local/bin:$PATH"
Add that line to ~/.bashrc, ~/.zshrc, or your shellβs equivalent startup
file if needed.
Build and Install From Local Checkout
Set BUILD_FROM_SOURCE=1 to have the installer build the binary from the
repository instead of downloading a release archive. Run the script directly
from the repo root β piping from curl will not work because the script needs
Cargo.toml present alongside it.
Requirements: cargo must be in PATH. git, curl, tar, and sha256sum
are not needed.
git clone https://github.com/mhyrzt/xrat.git
cd xrat
BUILD_FROM_SOURCE=1 bash install.sh
To install to a different directory:
BUILD_FROM_SOURCE=1 INSTALL_DIR=/usr/local/bin bash install.sh
The script will:
- Run
cargo build --releaseinside the checkout. - Generate man pages and shell completions from the built binary.
- Install
xratand extras the same way as the release path. - Offer first-time setup prompts.
For a pure Cargo-managed install or a development workflow, see Build From Source.
First-Time Setup
If you skipped the installerβs setup prompts, initialize xrat manually:
xrat init
To install the daemon later:
xrat daemon install --start
Then follow the Quickstart to import configs and connect.
State Paths
| Path | Purpose | Override |
|---|---|---|
$HOME/.config/xrat/ | App root | XRAT_PATH env var |
$HOME/.config/xrat/config.toml | Configuration | --config flag |
$HOME/.config/xrat/db.sqlite | SQLite database | --database flag |
$HOME/.config/xrat/runtime/ | Daemon socket, session state | - |
$HOME/.config/xrat/logs/ | Runtime logs | [runtime.log].dir |
$HOME/.config/xrat/mmdb/ | GeoIP data | [mmdb].dir |
Docker Install
Use the Docker image when you want xrat and Xray-core in one container. The
image is published to GitHub Container Registry on each tagged release.
For host-level systemd daemon management, shell completions, or man pages, use the Installation Script or Manual Binary Install instead.
Pull
docker pull ghcr.io/mhyrzt/xrat:latest
For a specific release, use the version tag:
docker pull ghcr.io/mhyrzt/xrat:0.1.2
State
The container stores all xrat state under /data/xrat.
docker volume create xrat-data
Run commands with the volume mounted:
docker run --rm -it \
-v xrat-data:/data/xrat \
ghcr.io/mhyrzt/xrat:latest init
Import and List
docker run --rm -it \
-v xrat-data:/data/xrat \
ghcr.io/mhyrzt/xrat:latest import "https://example.com/sub.txt"
docker run --rm -it \
-v xrat-data:/data/xrat \
ghcr.io/mhyrzt/xrat:latest list
Serve the HTTP API
Bind the API to all container interfaces and publish the port on the host:
docker run --rm -it \
-v xrat-data:/data/xrat \
-p 8080:8080 \
ghcr.io/mhyrzt/xrat:latest serve --host 0.0.0.0 --port 8080
Run a Local Proxy
The image includes Xray-core. Publish the proxy ports you enable in
config.toml.
docker run --rm -it \
-v xrat-data:/data/xrat \
-p 1080:1080 \
ghcr.io/mhyrzt/xrat:latest connect <config-id>
The default generated config binds the SOCKS proxy to 127.0.0.1 inside the
container. To reach it from the host through -p, set the runtime host to
0.0.0.0 in /data/xrat/config.toml.
[runtime.socks]
enabled = true
host = "0.0.0.0"
port = 1080
Build Locally
docker build -t xrat .
docker run --rm -it -v xrat-data:/data/xrat xrat --help
Manual Binary Install
Use this path when you want to inspect or place release files yourself instead of running the installer script.
For the recommended path, see Installation Script. To compile from this repository, see Build From Source.
Download
Go to the latest GitHub release and download the archive for your architecture:
| File | Architecture |
|---|---|
xrat-vX.Y.Z-x86_64-unknown-linux-musl.tar.gz | x86_64 (most PCs) |
xrat-vX.Y.Z-aarch64-unknown-linux-musl.tar.gz | ARM64 (Pi 4/5, Graviton) |
Download SHASUMS256.txt from the same release.
Verify
Run the checksum verification from the directory containing the archive and
SHASUMS256.txt:
sha256sum -c SHASUMS256.txt --ignore-missing
The command should report OK for the archive you downloaded.
Install Binary
tar -xzf xrat-vX.Y.Z-x86_64-unknown-linux-musl.tar.gz
mkdir -p ~/.local/bin
mv xrat ~/.local/bin/xrat
chmod +x ~/.local/bin/xrat
Ensure ~/.local/bin is in PATH:
export PATH="$HOME/.local/bin:$PATH"
Add that line to your shell startup file if needed.
Install Shell Completions
The release archive includes generated completion files.
Bash
mkdir -p ~/.local/share/bash-completion/completions
cp completions/xrat.bash ~/.local/share/bash-completion/completions/xrat
Reload by opening a new shell or sourcing your Bash startup file.
Zsh
mkdir -p ~/.zfunc
cp completions/_xrat ~/.zfunc/_xrat
Add this to ~/.zshrc if ~/.zfunc is not already in fpath:
fpath=("$HOME/.zfunc" $fpath)
autoload -Uz compinit
compinit
Fish
mkdir -p ~/.config/fish/completions
cp completions/xrat.fish ~/.config/fish/completions/xrat.fish
Install Man Pages
mkdir -p ~/.local/share/man/man1
cp man/man1/*.1 ~/.local/share/man/man1/
mandb ~/.local/share/man
If mandb is unavailable, the man pages are still copied; your system may index
them later.
Install Desktop Entry
mkdir -p ~/.local/share/applications
mkdir -p ~/.local/share/icons/hicolor/48x48/apps
mkdir -p ~/.local/share/icons/hicolor/256x256/apps
cp desktop/xrat.desktop ~/.local/share/applications/
cp desktop/icons/xrat-48x48.png ~/.local/share/icons/hicolor/48x48/apps/xrat.png
cp desktop/icons/xrat-256x256.png ~/.local/share/icons/hicolor/256x256/apps/xrat.png
update-desktop-database ~/.local/share/applications/
First-Time Setup
xrat init
xrat daemon install --start
Then follow the Quickstart.
Build From Source
Use this path when you want to build from a checkout, test local changes, or install a development build. The source workflow is Justfile-oriented; direct Cargo commands are shown only where they help explain what each target does.
For release binaries, use Installation Script or Manual Binary Install.
Requirements
Install:
git- Rust via rustup
justxrayinPATHsing-boxif you need sing-box parsing or runtime-config preview support
Install just with Cargo if your distribution does not package it:
cargo install just
Check the local task list:
just --list
Clone
git clone https://github.com/mhyrzt/xrat.git
cd xrat
Build
For a development build:
just build
For a release build using the locked dependency graph:
just release
The release binary is written to:
target/release/xrat
Run a local command from the checkout:
just run status
Install From Checkout
Install the current checkout to ~/.cargo/bin/xrat:
just install
Replace an existing Cargo-installed binary:
just reinstall
Remove the Cargo-installed binary:
just uninstall
Ensure ~/.cargo/bin is in PATH:
export PATH="$HOME/.cargo/bin:$PATH"
Rustup usually adds this automatically. Add it to your shell startup file if
xrat --version cannot find the installed binary.
Install Man Pages From Source
Generate and install man pages from the local command definitions:
just install-manpages
This writes pages to ~/.local/share/man/man1 and refreshes that man database
when mandb is available.
Install Completions From Source
The completions target prints generated completions for the requested shell.
Redirect the output to the location your shell reads.
Bash
mkdir -p ~/.local/share/bash-completion/completions
just completions bash > ~/.local/share/bash-completion/completions/xrat
Open a new shell or source your Bash startup file.
Zsh
mkdir -p ~/.zfunc
just completions zsh > ~/.zfunc/_xrat
Add this to ~/.zshrc if needed:
fpath=("$HOME/.zfunc" $fpath)
autoload -Uz compinit
compinit
Fish
mkdir -p ~/.config/fish/completions
just completions fish > ~/.config/fish/completions/xrat.fish
First-Time Setup
Initialize the config directory and database:
xrat init
Install and start the systemd user daemon:
xrat daemon install --start
Then follow the Quickstart.
Source-Tree Checks
Run the same commands as .github/workflows/ci.yml:
just ci
That expands to:
just fmt-rust-check
just lint
just test
For broader local formatting checks across Rust, Markdown, and SQL:
just fmt-check
Useful supporting targets:
| Target | Purpose |
|---|---|
just check | Run cargo check --locked |
just fmt | Format Rust, Markdown, and SQL |
just fmt-check | Check Rust, Markdown, and SQL formatting |
just docs | Serve the mdBook locally |
just clean | Remove Cargo build artifacts |
just postgres-up | Start the local PostgreSQL verification database |
just test-postgres | Run the PostgreSQL real-backend verification test |
just postgres-down | Stop the local PostgreSQL verification database |
Quickstart
This guide walks through the core xrat workflow: import a subscription, test configs, and start a local proxy.
0. Initialize
Run once after installing to create the config directory, default config file, and database:
xrat init
See init for details on what gets created and how to use
a custom path via XRAT_PATH.
1. Import a Subscription
Import from a URL:
xrat import https://example.com/subscription
Import from a local file:
xrat import ./subscription.txt
Import raw subscription text directly:
xrat import "vless://uuid@example.com:443?type=ws&security=tls#MyNode"
xrat automatically detects the input format: subscription URL, local file, raw base64-encoded subscription, plain link list, SIP008 JSON, or Xray JSON.
2. List Imported Configs
xrat list configs
Filter by subscription source:
xrat list configs --subscription 1
You can mark a preferred config without starting a proxy:
xrat select 1
Use enable and disable to control whether a config appears in enabled-only
workflows:
xrat disable 7
xrat enable 7
3. Test Connectivity
Test a single config by ID:
xrat test 1
Bulk-test all enabled configs:
xrat test --enabled-only --concurrency 4
Skip specific stages:
xrat test 1 --skip-icmp --skip-download
4. Start a Proxy
Start the daemon first:
xrat daemon start
Connect using a tested config:
xrat connect 1
The command sends a daemon IPC request. The daemon starts the Xray (or V2Ray) process with a generated runtime config. By default, it exposes:
- SOCKS5 on
0.0.0.0:1080 - HTTP on
0.0.0.0:8080(if enabled in config.toml)
5. Check Status
xrat status
6. Disconnect
xrat disconnect
Interactive TUI
For an interactive view over configs, sources, tests, runtime status, and diagnostics:
xrat tui
Using the Daemon
For persistent operation with auto-rotation:
xrat daemon start
xrat proxy start
xrat proxy rotate
xrat proxy status
xrat daemon stop
xrat connect <id> starts one managed runtime session immediately through the
daemon. xrat proxy start enables daemon-driven auto-rotation.
See daemon and proxy for details.
Configuration
xrat reads a TOML configuration file from the config directory. The default
location is ~/.config/xrat/config.toml.
Override with --config <path> or the XRAT_PATH environment variable.
Sections
| Section | Purpose |
|---|---|
[paths] | Binary paths for xray, v2ray, sing-box |
[database] | Backend selection and connection settings |
[runtime] | Engine, rotation, logging, inbound settings |
[routing] | Domain strategy, direct/block rules |
[geo] | GeoIP auto-update settings |
[dns] | DNS query strategy, servers, hosts |
[parser] | Xray JSON schema validation mode |
[testing] | Concurrency, stage order, per-stage settings |
[server] | HTTP API host, port, API key |
Example
[paths]
database = "db.sqlite"
# xray = "/usr/local/bin/xray"
# v2ray = "/usr/local/bin/v2ray"
# sing_box = "/usr/local/bin/sing-box"
[database]
backend = "sqlite"
[database.sqlite]
path = "db.sqlite"
[database.postgres]
user = { env = "XRAT_POSTGRES_USER" }
password = { env = "XRAT_POSTGRES_PASSWORD" }
host = "localhost"
port = 5432
db_name = "xrat"
max_connections = 10
min_connections = 1
connect_timeout_secs = 10
[server]
enabled = false
host = "127.0.0.1"
port = 8080
key = { env = "XRAT_API_KEY" }
[runtime]
engine = "xray"
replace_active_session = true
[runtime.rotation]
enabled = false
interval_secs = 1800
health_trigger_enabled = true
cooldown_secs = 300
test_concurrency = 0
test_stages = ["real_delay", "download"]
[runtime.log]
enabled = true
mask = "none"
dir = "logs"
dns_log = false
level = "warning"
keep = true
[runtime.socks]
enabled = true
host = "0.0.0.0"
port = 1080
udp = true
auth = { enabled = true, username = "xrat", password = { env = "XRAT_SOCKS_PASSWORD" } }
[runtime.http]
enabled = false
host = "0.0.0.0"
port = 8080
[runtime.shadowsocks]
enabled = false
host = "0.0.0.0"
port = 1081
method = "aes-128-gcm"
password = { env = "XRAT_SHADOWSOCKS_PASSWORD" }
network = "tcp,udp"
[runtime.sniffing]
enabled = true
dest_override = ["http", "tls", "quic"]
route_only = true
metadata_only = false
domains_excluded = []
ips_excluded = []
[routing]
domain_strategy = "IPIfNonMatch"
[routing.direct]
domain = []
ip = []
geosite = []
geoip = []
[routing.block]
domain = []
ip = []
geosite = []
geoip = []
[geo]
auto_update = false
update_interval_hours = 168
[[geo.profiles]]
name = "default"
geosite = "https://example.com/geosite.dat"
geoip = "https://example.com/geoip.dat"
[parser]
parse_mode = "strict"
[dns]
query_strategy = "UseSystem"
servers = ["8.8.8.8", "https://1.1.1.1/dns-query"]
use_system_hosts = true
disable_cache = false
disable_fallback = false
enable_parallel_query = true
[dns.hosts]
"domain:example.test" = "127.0.0.1"
[testing]
concurrency = 0
order = ["icmp", "real_delay", "download"]
failure_policy = "continue"
[testing.real_delay]
enabled = true
url = "https://www.gstatic.com/generate_204"
timeout = 10_000
[testing.icmp]
enabled = true
timeout = 3000
attempts = 3
[testing.download]
enabled = false
url = "https://cachefly.cachefly.net/50mb.test"
timeout = 30_000
[testing.tcp]
enabled = true
timeout = 5000
Secret Values
Sensitive fields accept either a literal string or an environment variable reference:
password = "literal-value"
password = { env = "XRAT_SOCKS_PASSWORD" }
Full Reference
See config-file for the complete field reference with all defaults and accepted values.
CLI Reference
xrat is a command-first CLI tool. All operations are invoked as subcommands.
Global Flags
These flags apply to every command:
| Flag | Description |
|---|---|
-v, --verbose | Increase log verbosity. Repeat: -v=info, -vv=debug, -vvv=trace |
-q, --quiet | Suppress output except errors. Ignored if RUST_LOG is set |
--database <path> | SQLite database path override |
--config <path> | Config file path override |
--xray <path> | Xray binary path override |
--v2ray <path> | V2Ray binary path override |
--sing-box <path> | sing-box binary path override |
Commands
| Command | Description |
|---|---|
import | Import a subscription URL, file, or raw text into the database |
add | Add a single config URI directly to the database |
list | List stored configs or subscription sources |
show | Show details for a stored config |
select | Mark one config as the current selection |
enable | Include a config in normal operations |
disable | Exclude a config from normal operations |
delete | Soft-delete or permanently delete a config |
restore | Restore a soft-deleted config |
parse | Parse and validate config links without persisting |
test | Test connectivity and latency for stored configs |
scan | Scan candidate IPs for TCP reachability |
connect | Start a managed proxy runtime for a stored config |
disconnect | Stop the active managed proxy runtime |
status | Show the managed proxy runtime status |
daemon | Run or control the daemon supervisor process |
proxy | Control auto-rotating proxy scheduling via the daemon |
geoip | Manage GeoLite2 MMDB assets and inspect GeoIP backend config |
serve | Start the local HTTP API server |
tui | Start the interactive terminal UI |
Common State Terms
These words appear across the CLI, TUI, API, and database:
| Term | Meaning |
|---|---|
enabled | Included in bulk tests, selection workflows, and rotation candidate sets |
disabled | Stored but normally skipped by filtered workflows |
selected | User-selected config for workflows that need a preferred config |
active | Config attached to the current managed runtime session |
deleted | Soft-deleted row hidden from normal lists unless requested |
Use select to choose a preferred config. Use
connect to make a config active by starting a runtime
session.
Logging
xrat uses tracing for structured logging. Control verbosity with:
-v/--verbose: info level-vv: debug level-vvv: trace level-q/--quiet: error level onlyRUST_LOGenvironment variable: overrides all flags
Logs are written to stderr.
init
Initialize the xrat config directory, config file, and database.
xrat init [--dry-run]
Flags
| Flag | Description |
|---|---|
--dry-run | Print planned actions without creating anything |
Behavior
- Creates the app root directory (
$HOME/.config/xrat/or$XRAT_PATH) - Writes a default
config.tomlwith sensible defaults if not already present - Creates the SQLite database and runs all pending migrations
- Creates subdirectories:
runtime/,logs/,mmdb/ - Prints a summary of what was created and what was already present
Idempotent: safe to run multiple times. Existing files are never
overwritten. If config.toml exists and has been customized, it is left
untouched.
Example: first-time setup
xrat init
xrat initialized successfully.
Created:
/home/user/.config/xrat/
/home/user/.config/xrat/config.toml (written default template)
/home/user/.config/xrat/runtime/
/home/user/.config/xrat/logs/
/home/user/.config/xrat/mmdb/
Already present:
/home/user/.config/xrat/db.sqlite (database ready)
Next steps:
xrat import <subscription-url>
xrat list configs
Example: dry run
xrat init --dry-run
--- dry run: no files written ---
Would create (if absent):
/home/user/.config/xrat/
/home/user/.config/xrat/config.toml
/home/user/.config/xrat/db.sqlite
/home/user/.config/xrat/runtime/
/home/user/.config/xrat/logs/
/home/user/.config/xrat/mmdb/
State paths
| Path | Purpose | Override |
|---|---|---|
$HOME/.config/xrat/ | App root | XRAT_PATH env var |
$HOME/.config/xrat/config.toml | Configuration | --config flag |
$HOME/.config/xrat/db.sqlite | SQLite database | --database flag |
$HOME/.config/xrat/runtime/ | Daemon socket, session state | β |
$HOME/.config/xrat/logs/ | Runtime logs | [runtime.log].dir |
$HOME/.config/xrat/mmdb/ | GeoIP data | [mmdb].dir |
Default config.toml
The generated config enables SOCKS5 on port 1080, sets the Xray engine, and provides commented-out HTTP inbound and log settings:
[runtime]
engine = "xray"
[runtime.socks]
enabled = true
host = "127.0.0.1"
port = 1080
[runtime.http]
enabled = false
host = "127.0.0.1"
port = 8080
[runtime.log]
enabled = true
level = "warning"
[testing]
concurrency = 4
[geo]
auto_update = false
See Config File for full reference.
Related
- Quickstart
- Configuration
- daemon install β install as a systemd user service
import
Import a subscription URL, file, or raw text into the database.
xrat import <input>
Arguments
| Argument | Description |
|---|---|
input | Subscription source: a URL, local file path, or raw subscription text |
Input Formats
xrat automatically detects the input format:
| Format | Detection |
|---|---|
| Subscription URL | Starts with http:// or https:// |
| Local file | Path to an existing file on disk |
| Single share link | Single line starting with a supported protocol scheme |
| Base64 subscription | Multi-line or single-line base64-encoded text |
| Plain link list | Multiple lines, each a valid share link |
| SIP008 JSON | JSON with "servers" array |
| Xray JSON | JSON with "version" or "inbounds" fields |
Examples
Import from a subscription URL:
xrat import https://example.com/sub.txt
Import from a local file:
xrat import ./nodes.txt
Import raw base64 subscription text:
xrat import "dmxlc3M6Ly91dWlkQGV4YW1wbGUuY29tOjQ0Mw=="
Import a single share link:
xrat import "vless://uuid@example.com:443?type=ws&security=tls#MyNode"
Behavior
- Reads input from the specified source
- Detects format automatically
- Parses and normalizes each node
- Deduplicates against existing configs using a versioned key
- Persists new configs to the database
- Creates or updates the subscription source record
- Prints an import summary
Related
addβ add a single config URI without subscription trackinglist configsβ view imported configs
add
Add a single config URI directly to the database.
xrat add <input>
Arguments
| Argument | Description |
|---|---|
input | Config URI: vless://..., vmess://..., ss://..., trojan://..., hysteria2://..., etc. |
Examples
xrat add "vless://uuid-123@example.com:443?type=ws&security=tls&sni=cdn.example.com#Node"
xrat add "ss://YWVzLTI1Ni1nY206c2VjcmV0@example.com:8388#SS%20Node"
Behavior
Unlike import, add does not create a subscription source record. It parses
the single URI, normalizes it, deduplicates, and persists directly.
For the full config-management command set, see
config management.
Config Management Commands
Manage individual stored configs after import.
These commands operate on config IDs from xrat list configs.
When to Use These Commands
| Command | Use when you want to |
|---|---|
add | Store one share link without creating a subscription source record |
show | Inspect one stored config |
select | Mark one config as the preferred config for interactive workflows |
enable | Include a config in normal filtered workflows |
disable | Keep a config stored but skip it in normal filtered workflows |
delete | Hide a config from normal lists while preserving history |
restore | Bring a soft-deleted config back |
Config State
selected, active, enabled, and deleted are separate states.
| State | Meaning |
|---|---|
selected | The preferred config. This does not start a proxy process. |
active | The config used by the current managed runtime session. |
enabled | Included in normal list, test, and rotation workflows. |
disabled | Stored but skipped by enabled-only workflows. |
deleted | Soft-deleted and hidden unless --deleted or --all used. |
Use xrat connect <id> when you want to start a proxy runtime. Use
xrat proxy start when you want the daemon to manage automatic rotation.
add
Add a single config URI directly to the database.
xrat add <input>
Arguments
| Argument | Description |
|---|---|
input | Config URI: vless://..., vmess://..., ss://..., trojan://..., hysteria2://..., etc. |
Examples
xrat add "vless://uuid@example.com:443?type=ws&security=tls#Node"
Unlike xrat import, xrat add does not create or update a subscription source
record.
show
Show details for one stored config.
xrat show <id> [flags]
Arguments
| Argument | Description |
|---|---|
id | Config ID to show |
Flags
| Flag | Description |
|---|---|
--json | Print the result as JSON |
Examples
xrat show 42
xrat show 42 --json
select
Select a config as the current selection.
xrat select <id>
Arguments
| Argument | Description |
|---|---|
id | Config ID to select |
Selection is useful for workflows that need a preferred config, including the TUI. It does not start or stop the managed runtime.
enable
Enable a config.
xrat enable <id>
Arguments
| Argument | Description |
|---|---|
id | Config ID to enable |
Enabled configs are included in normal enabled-only workflows, such as:
xrat list configs --enabled-only
xrat test --enabled-only
disable
Disable a config.
xrat disable <id>
Arguments
| Argument | Description |
|---|---|
id | Config ID to disable |
Disabled configs remain in the database but are excluded from enabled-only queries, tests, and rotation candidate selection.
delete
Soft-delete a config by default.
xrat delete <id> [flags]
Arguments
| Argument | Description |
|---|---|
id | Config ID to delete |
Flags
| Flag | Description |
|---|---|
--hard | Permanently delete the config |
Soft-deleted configs are hidden from normal lists but can still be viewed with:
xrat list configs --deleted
xrat list configs --all
Use --hard only when the row should be permanently removed.
restore
Restore a soft-deleted config.
xrat restore <id>
Arguments
| Argument | Description |
|---|---|
id | Config ID to restore |
restore only applies to soft-deleted configs. It does not recreate a config
that was removed with delete --hard.
Related
listβ find config IDs and filter by stateruntimeβ connect, disconnect, and inspect active sessionstuiβ manage configs interactively
list
List stored configs or subscription sources.
xrat list <target> [flags]
Targets
| Target | Alias | Description |
|---|---|---|
configs | nodes | List stored proxy configs |
subscriptions | subs | List stored subscription sources |
list configs
xrat list configs [flags]
Flags
| Flag | Description |
|---|---|
--enabled-only | Show only enabled configs |
--active-only | Show only the active config |
--selected-only | Show only the selected config |
--deleted | Show only soft-deleted configs |
--all | Include soft-deleted configs in results |
--subscription <id> | Show only configs from the given subscription ID |
Examples
List all configs:
xrat list configs
List only enabled configs from subscription 3:
xrat list configs --enabled-only --subscription 3
List soft-deleted configs:
xrat list configs --deleted
list subscriptions
xrat list subscriptions [flags]
Flags
| Flag | Description |
|---|---|
--kind <kind> | Filter by source kind: url, file, or raw-text |
Examples
List all subscriptions:
xrat list subscriptions
List only URL-based subscriptions:
xrat list subscriptions --kind url
parse
Parse and validate config links without persisting to the database.
xrat parse [input] [flags]
Arguments
| Argument | Description |
|---|---|
input | Single config URI to parse (optional if using --file or --stdin) |
Flags
| Flag | Description |
|---|---|
--file <path> | Read config links (one per line) from a local file |
--stdin | Read config links (one per line) from stdin |
--json | Print the generated runtime JSON config for the parsed node |
--engine <engine> | Proxy engine for runtime config generation: auto, xray, sing-box (default: auto) |
Engine Modes
| Mode | Behavior |
|---|---|
auto | Uses sing-box for hysteria2, xray for everything else |
xray | Always use Xray-core (rejects hysteria2) |
sing-box | Always use sing-box |
This engine choice only affects parse-time validation and --json runtime
config preview. Managed runtime commands such as xrat connect use the
Xray/V2Ray lifecycle path.
Examples
Parse a single VLESS link:
xrat parse "vless://uuid@example.com:443?type=ws&security=tls&sni=cdn.example.com#Node"
Parse from a file:
xrat parse --file ./links.txt
Parse from stdin:
cat links.txt | xrat parse --stdin
Generate runtime JSON:
xrat parse --json "vless://uuid@example.com:443?type=tcp#Node"
Force sing-box engine:
xrat parse --engine sing-box "hy2://secret@example.com:443?sni=edge.example.com#HY2"
Output
Without --json, prints decoded node fields (protocol, address, port, network,
TLS, SNI, etc.).
With --json, prints the full Xray or sing-box runtime configuration JSON that
would be generated for the parsed node.
test
Test connectivity and latency for stored configs.
xrat test [id] [flags]
Arguments
| Argument | Description |
|---|---|
id | Config ID to test. Omit to bulk-test matching configs |
Filter Flags
When testing multiple configs (no id specified):
| Flag | Description |
|---|---|
--enabled-only | Filter: only enabled configs |
--active-only | Filter: only the active config |
--selected-only | Filter: only the selected config |
--subscription <id> | Filter: only configs from the given subscription ID |
Stage Skip Flags
| Flag | Description |
|---|---|
--skip-icmp | Skip the ICMP ping stage |
--skip-tcp | Skip the TCP connectivity stage |
--skip-real-delay | Skip the real-delay (HTTP round-trip) stage |
--skip-download | Skip the download speed stage |
--skip-upload | Skip the upload speed stage (disabled by default) |
URL Override Flags
| Flag | Description |
|---|---|
--test-url <url> | Override the URL used for real-delay checks |
--download-url <url> | Override the URL used for download speed checks |
--upload-url <url> | Enable upload speed stage and set the HTTP POST target URL |
Timeout Override Flags
| Flag | Description |
|---|---|
--icmp-timeout <ms> | Override ICMP timeout in milliseconds |
--tcp-timeout <ms> | Override TCP connect timeout in milliseconds |
--real-delay-timeout <ms> | Override real-delay HTTP request timeout in milliseconds |
--download-timeout <ms> | Override download speed request timeout in milliseconds |
--upload-timeout <ms> | Override upload speed request timeout in milliseconds |
Concurrency and Output Flags
| Flag | Description |
|---|---|
--concurrency <n> | Bulk-test concurrency. 0 = auto-detect |
--format <format> | Output format: tsv, csv, json (default: tsv) |
--output <file> | Write bulk results to a file instead of stdout |
--sort-by <field> | Sort order: status, icmp, real-delay, download-speed, protocol, address (default: status) |
--no-progress | Hide the animated progress bar |
Ping Loop Flags
| Flag | Description |
|---|---|
--ping | Continuously ping one config until Ctrl+C, printing a live summary |
--ping-interval <ms> | Interval between ping-loop iterations (default: 1000) |
Historical Summary Flags
| Flag | Description |
|---|---|
--latest-run-summary | Print a summary of the latest persisted test run and exit |
--country <iso> | Filter latest-run summary by endpoint country ISO code (e.g. US, DE) |
--asn <filter> | Filter latest-run summary by ASN (case-insensitive substring match) |
Test Stages
The test command can record up to 5 probe result types:
| Stage | Measures | Default |
|---|---|---|
| ICMP | ICMP ping success and latency | Enabled |
| TCP | TCP connect success and latency | Enabled |
| Real Delay | HTTP round-trip latency through proxy | Enabled |
| Download | Download throughput through proxy | Disabled |
| Upload | Upload throughput through proxy | Disabled |
Stage Order
The default order for ICMP, real-delay, and download is configurable via
config.toml:
[testing]
order = ["icmp", "real_delay", "download"]
TCP is used as a gate before real-delay when enabled. Upload runs after download
only when --upload-url <url> is provided; there is no [testing.upload]
config section.
Failure Policy
Controls behavior when a stage fails:
[testing]
failure_policy = "continue" # "continue" | "skip_remaining" | "mark_failed"
Examples
Test a single config:
xrat test 42
Bulk-test all enabled configs with 4 workers:
xrat test --enabled-only --concurrency 4
Test with custom URLs and timeouts:
xrat test 1 \
--test-url https://example.com/generate_204 \
--download-url https://example.com/10mb.test \
--real-delay-timeout 5000 \
--download-timeout 15000
Skip ICMP and download stages:
xrat test 1 --skip-icmp --skip-download
Export results to CSV:
xrat test --enabled-only --format csv --output results.csv
Continuous ping loop:
xrat test 1 --ping --ping-interval 2000
View latest test run summary:
xrat test --latest-run-summary
Filter by country and ASN:
xrat test --latest-run-summary --country US --asn cloudflare
Output Formats
TSV (default)
Tab-separated values, easy to read in terminal:
ID Protocol Address ICMP TCP Real Delay Download Status
1 vless example.com:443 15ms 12ms 145ms - alive
2 vmess edge.com:8443 - - - - timeout
CSV
Comma-separated values, spreadsheet compatible.
JSON
Machine-parseable JSON array with full test result details.
Failure Classification
Test failures are classified into categories:
| Category | Description |
|---|---|
DNS | DNS resolution failed |
Timeout | Connection or request timed out |
Refused | Connection refused |
Unreachable | Network unreachable |
PermissionDenied | Permission denied |
TLS | TLS handshake failed |
Auth | Authentication failed |
Process | Proxy process failed to start |
Proxy | Proxy returned an error |
Unknown | Unclassified failure |
Related
list configsβ view configs before testingconnectβ start a proxy for a tested config
scan
Scan candidate IPs for TCP reachability and persist results.
xrat scan [flags]
Flags
| Flag | Description |
|---|---|
--ips <ips> | Comma-separated IPs to scan, e.g. 1.1.1.1,8.8.8.8 |
--file <path> | Read newline-separated IPs from a file |
--port <port> | Target TCP port (default: 443) |
--timeout <ms> | TCP connect timeout in milliseconds (default: 4000) |
--history <limit> | Print the latest N persisted scan results and exit (skips scanning) |
Examples
Scan specific IPs:
xrat scan --ips 1.1.1.1,8.8.8.8,9.9.9.9
Scan from a file:
xrat scan --file ./candidate-ips.txt
Scan on a custom port with shorter timeout:
xrat scan --ips 1.1.1.1,8.8.8.8 --port 8443 --timeout 2000
View scan history:
xrat scan --history 20
Behavior
- Reads candidate IPs from
--ipsor--file - Attempts TCP connection to each IP on the specified port
- Measures connection latency
- Persists results to the
cf_scan_resultstable (upsert) - Prints scan results with latency and success/failure status
Use Cases
- Cloudflare IP scanning: Test candidate Cloudflare edge IPs for low latency
- CDN endpoint testing: Identify fast CDN nodes in your region
- Network reconnaissance: Discover reachable IPs in a range
Output
IP Port Latency Status
1.1.1.1 443 12ms reachable
8.8.8.8 443 15ms reachable
9.9.9.9 443 - timeout
Persistence
Results are persisted to the database and can be retrieved with --history.
This allows tracking IP reachability over time and identifying consistently fast
endpoints.
Related
testβ test stored proxy configs (not raw IPs)
Runtime Commands
Manage the local proxy runtime through the daemon: connect, disconnect, and check status.
connect
Start a managed proxy runtime for a stored config.
xrat connect <id> [flags]
Arguments
| Argument | Description |
|---|---|
id | Config ID to start as the active local proxy session |
Flags
| Flag | Description |
|---|---|
--json | Print the result as JSON |
Examples
xrat connect 42
xrat connect 42 --json
Behavior
- Sends a runtime-connect request to the daemon over local IPC
- The daemon loads the config from the database
- Generates an Xray (or V2Ray) runtime config with local inbounds
- Spawns the proxy process
- Waits for the SOCKS port to become ready
- Persists a
runtime_sessionsrecord with statusrunning - Prints connection details
If the daemon is not running, start it first:
xrat daemon start
Default Inbounds
| Protocol | Host | Port | Notes |
|---|---|---|---|
| SOCKS5 | 0.0.0.0 | 1080 | UDP support enabled by default |
| HTTP | 0.0.0.0 | 8080 | Disabled by default in config.toml |
| Shadowsocks | 0.0.0.0 | 1081 | Disabled by default, aes-128-gcm |
Configure inbounds in config.toml under [runtime.socks], [runtime.http],
and [runtime.shadowsocks].
Session Replacement
If replace_active_session = true in config.toml, connecting to a new config
automatically disconnects the previous session.
Engine Boundary
The managed runtime lifecycle is Xray/V2Ray-focused.
xrat parse --engine sing-box can generate sing-box JSON for diagnostics, but
connect, disconnect, and status manage the Xray/V2Ray runtime path.
disconnect
Stop the active managed proxy runtime.
xrat disconnect [flags]
Flags
| Flag | Description |
|---|---|
--json | Print the result as JSON |
Examples
xrat disconnect
Behavior
- Sends a runtime-disconnect request to the daemon over local IPC
- The daemon sends SIGTERM to the running proxy process
- Waits up to 5 seconds for graceful shutdown
- Sends SIGKILL if the process is still running
- Updates the session status to
stopped - Cleans up temporary config files
status
Show the managed proxy runtime status.
xrat status [flags]
Flags
| Flag | Description |
|---|---|
--json | Print the status as JSON |
Examples
xrat status
xrat status --json
Output
Displays:
- Session state:
starting,running,stopping,stopped,failed - Config details: protocol, address, port, name
- Process info: PID, liveness check
- Inbound health: TCP reachability of SOCKS, HTTP, Shadowsocks ports
- Uptime: time since session started
If no daemon is reachable, the command exits with a hint to run
xrat daemon start.
JSON Output
{
"status": "running",
"session_id": 5,
"config_id": 42,
"protocol": "vless",
"address": "example.com",
"port": 443,
"pid": 12345,
"pid_alive": true,
"socks_port": 1080,
"socks_reachable": true,
"http_port": 8080,
"http_reachable": false,
"started_at": "2026-05-28T10:30:00Z"
}
Related
daemonβ persistent daemon with auto-rotationproxyβ control auto-rotation schedulingtestβ test configs before connectingparseβ parse and preview Xray or sing-box runtime JSON
daemon
Run or control the daemon supervisor process.
xrat daemon <action>
Actions
| Action | Description |
|---|---|
start | Start the long-lived daemon process |
status | Show daemon IPC reachability and protocol information |
stop | Request daemon shutdown via local IPC |
install | Install xrat-daemon.service as a systemd user service |
uninstall | Remove the installed systemd user service |
The hidden internal run-server action is used by the daemon launcher and is
not a user-facing command.
daemon start
Start the long-lived daemon supervisor process.
xrat daemon start
Flags
No command-specific flags.
Behavior
- Forks a background daemon process
- Creates a Unix domain socket at
<runtime_dir>/daemon.sock - Runs the supervisor event loop with:
- Health checks every 15 seconds
- IPC event processing from CLI commands
- Auto-rotation scheduling (if enabled)
- Reattaches to any stale runtime sessions from previous daemon runs
Daemon Features
- IPC server: Listens for commands from
xrat connect,xrat disconnect, etc. - Health monitoring: Periodically checks proxy liveness, triggers rotation on failure
- Auto-rotation: Scheduled proxy switching with cooldown and candidate testing
- Session reconciliation: Detects and recovers from stale sessions on restart
daemon status
Show daemon IPC reachability and protocol information.
xrat daemon status
Flags
No command-specific flags.
Output
Daemon Status
βββββββββββββββββββββββββββββββββ
Socket: /home/user/.config/xrat/runtime/daemon.sock
Reachable: yes
Protocol: v1
If the daemon is not running or the socket is unreachable, prints an error.
daemon stop
Request daemon shutdown via local IPC.
xrat daemon stop
Flags
No command-specific flags.
Behavior
- Connects to the daemon socket
- Sends a shutdown request
- Daemon gracefully terminates:
- Stops the active proxy session (if running)
- Closes the IPC socket
- Exits cleanly
daemon install
Install xrat as a systemd user service. Linux only.
xrat daemon install [--start] [--with-api] [--dry-run]
Flags
| Flag | Description |
|---|---|
--start | Start the daemon immediately after enabling the service |
--with-api | Also install xrat-api.service (standalone HTTP API) |
--dry-run | Print the generated unit and planned actions without writing anything |
Behavior
- Resolves the current binary path via
std::env::current_exe() - Generates
xrat-daemon.servicefrom the template inpackaging/systemd/with the resolved binary path and configured XRAT root - Writes the service file to
~/.config/systemd/user/(respects$XDG_CONFIG_HOME) - Runs
systemctl --user daemon-reload - Runs
systemctl --user enable xrat-daemon.service - If
--start: runssystemctl --user start xrat-daemon.service - If
--with-api: generates and installsxrat-api.serviceas well
Example
xrat daemon install --start
Written: /home/user/.config/systemd/user/xrat-daemon.service
Reloaded systemd user daemon.
Enabled: xrat-daemon.service
Started: xrat-daemon.service
Daemon installed successfully.
Dry run
xrat daemon install --dry-run
Prints the generated service unit and the systemctl commands that would run, without writing any files or calling systemctl.
daemon uninstall
Remove the installed xrat-daemon.service systemd user service.
xrat daemon uninstall [--dry-run]
Flags
| Flag | Description |
|---|---|
--dry-run | Print planned actions without removing anything |
Behavior
- Stops
xrat-daemon.service(non-fatal if not running) - Disables
xrat-daemon.service - Removes
~/.config/systemd/user/xrat-daemon.service - Repeats for
xrat-api.serviceif present - Runs
systemctl --user daemon-reload
User config, database, logs, and all application state are preserved.
IPC Protocol
The daemon uses JSON over Unix domain socket with protocol version 1.
Request Types
| Request | Description |
|---|---|
DaemonPing | Check daemon reachability |
DaemonShutdown | Request graceful shutdown |
RuntimeStatus | Get proxy runtime status |
RuntimeConnect | Start a proxy session |
RuntimeReplace | Atomic disconnect-old + connect-new |
RuntimeDisconnect | Stop the active proxy session |
ProxyStart | Enable auto-rotation |
ProxyStatus | Get rotation status |
ProxyStop | Disable auto-rotation |
Manual proxy rotation uses RuntimeReplace with a manual trigger and optional
candidate config ID. There is no separate ProxyRotate IPC request type.
Response Envelope
{
"protocol_version": 1,
"ok": true,
"code": 200,
"message": "success",
"payload": { ... }
}
Related
proxyβ control auto-rotation schedulingconnectβ start a proxy via daemon IPCstatusβ check proxy status via daemon IPCinitβ initialize config directory before first use- systemd β full systemd deployment guide
proxy
Control auto-rotating proxy scheduling via the daemon.
xrat proxy <action> [flags]
All proxy actions require a running daemon:
xrat daemon start
Actions
| Action | Description |
|---|---|
start | Enable automatic proxy rotation on a fixed schedule |
status | Show the current proxy rotation status |
rotate | Trigger an immediate manual rotation |
stop | Disable automatic proxy rotation |
proxy start
Enable automatic proxy rotation on a fixed schedule.
xrat proxy start
Flags
No command-specific flags.
Behavior
- Sends a request to the daemon via IPC
- Daemon enables the rotation scheduler with settings from
config.toml:
[runtime.rotation]
enabled = true
interval_secs = 1800
health_trigger_enabled = true
cooldown_secs = 300
test_concurrency = 0
test_stages = ["real_delay", "download"]
- Daemon begins periodic rotation according to the interval
Rotation Triggers
| Trigger | Description |
|---|---|
| Timer | Scheduled rotation every interval_secs |
| Health check | Triggered when proxy health check fails (if health_trigger_enabled) |
| Manual | Triggered by xrat proxy rotate |
Cooldown
After a rotation, the daemon waits cooldown_secs before allowing another
rotation. This prevents rapid switching when multiple triggers fire.
proxy status
Show the current proxy rotation status.
xrat proxy status [flags]
Flags
| Flag | Description |
|---|---|
--json | Print rotation status as JSON |
Output
Proxy Rotation Status
βββββββββββββββββββββββββββββββββ
Enabled: yes
Interval: 1800s
Last rotation: 2026-05-28 10:30:00 (manual)
Next rotation: 2026-05-28 11:00:00
Cooldown: 300s (inactive)
Active config: 42 (vless://example.com:443)
JSON Output
{
"enabled": true,
"interval_secs": 1800,
"last_trigger": "manual",
"last_rotation_at": "2026-05-28T10:30:00Z",
"next_rotation_at": "2026-05-28T11:00:00Z",
"cooldown_secs": 300,
"cooldown_active": false,
"active_config_id": 42
}
proxy rotate
Trigger an immediate manual rotation.
xrat proxy rotate [flags]
Flags
| Flag | Description |
|---|---|
--config-id <id> | Force rotation to a specific enabled config ID |
Examples
Rotate to the best candidate automatically:
xrat proxy rotate
Rotate to a specific config:
xrat proxy rotate --config-id 99
Behavior
- If
--config-idis provided, rotates to that specific config - Otherwise, selects the best candidate from enabled configs:
- Tests candidates using
test_stagesfrom config.toml - Picks the config with the lowest real-delay latency
- Tests candidates using
- Atomically disconnects the old session and connects the new one
- Respects cooldown period (rotation is delayed if cooldown is active)
Candidate Selection
When rotating without --config-id:
- Loads all enabled configs from the database
- Excludes the currently active config
- Tests candidates concurrently (up to
test_concurrencyworkers) - Filters out configs that fail any test stage
- Sorts by real-delay latency (lowest first)
- Selects the top candidate
proxy stop
Disable automatic proxy rotation.
xrat proxy stop
Flags
No command-specific flags.
Behavior
- Sends a request to the daemon via IPC
- Daemon disables the rotation scheduler
- Active proxy session continues running (not disconnected)
Related
daemonβ daemon must be running for proxy commands to workconnectβ start one proxy session through the daemontestβ test configs before enabling rotation
geoip
Manage GeoLite2 MMDB assets and inspect GeoIP lookup configuration.
xrat geoip <command> [flags]
Subcommands
| Command | Description |
|---|---|
download | Download one or more GeoLite2 MMDB editions |
update | Refresh all supported GeoLite2 MMDB editions |
path | Print the resolved MMDB directory |
status | Show MMDB presence and size for each supported edition |
lookup | Look up a single IP through the configured GeoIP backend |
backend | Print the active GeoIP backend configuration |
download
Download one or more GeoLite2 MMDB editions.
xrat geoip download [flags]
| Flag | Description |
|---|---|
--edition <name> | Edition to download. Repeatable: GeoLite2-Country, GeoLite2-City, GeoLite2-ASN or country, city, asn |
--all | Download all supported editions |
--output <dir> | Override the MMDB target directory for this command |
--force | Re-download even when the destination file already exists |
--url <url> | Override the download URL template. Use {edition} as a placeholder |
--timeout <secs> | Override the HTTP request timeout in seconds |
--quiet | Suppress progress bar output |
If neither --edition nor --all is given, the configured default_editions
from [mmdb] are used.
Examples
Download all editions to the default MMDB directory:
xrat geoip download --all
Download a single edition:
xrat geoip download --edition city
Download to a custom directory:
xrat geoip download --all --output ./testdata/xrat/mmdb
update
Refresh all supported GeoLite2 MMDB editions. Equivalent to
download --all --force.
xrat geoip update [flags]
| Flag | Description |
|---|---|
--output <dir> | Override the MMDB target directory for this command |
--url <url> | Override the download URL template. Use {edition} as a placeholder |
--timeout <secs> | Override the HTTP request timeout in seconds |
--quiet | Suppress progress bar output |
Example
xrat geoip update
path
Print the resolved MMDB directory.
xrat geoip path [flags]
| Flag | Description |
|---|---|
--output <dir> | Override the MMDB target directory for this command |
Resolution order:
--outputflag, if provided[mmdb].dirfrom config (resolved relative toXRAT_PATHor config file location)- Default:
~/.config/xrat/mmdb
Examples
xrat geoip path
xrat geoip path --output /custom/path
status
Show MMDB presence and size for each supported edition.
xrat geoip status [flags]
| Flag | Description |
|---|---|
--output <dir> | Override the MMDB target directory for this command |
--strict | Exit non-zero when any supported edition is missing |
Example
xrat geoip status --strict
lookup
Look up a single IP address through the configured GeoIP backend.
xrat geoip lookup <ip> [flags]
| Argument | Description |
|---|---|
ip | IP address to look up |
| Flag | Description |
|---|---|
--backend <name> | Override backend: mmdb, ipwhois, ip-api |
--no-cache | Bypass the configured in-memory cache for this invocation |
--json | Print the lookup result as JSON |
The lookup returns country code, city/region, and ASN information when available.
Examples
xrat geoip lookup 8.8.8.8
xrat geoip lookup 8.8.8.8 --backend ipwhois
xrat geoip lookup 2001:4860:4860::8888 --json
backend
Print the active GeoIP backend configuration.
xrat geoip backend [flags]
| Flag | Description |
|---|---|
--backend <name> | Override backend: mmdb, ipwhois, ip-api |
--no-cache | Describe the backend chain without cache wrapping |
Shows the backend type, configured fallback, rate limiting, and cache settings.
Example
xrat geoip backend
Troubleshooting
If geoip status --strict reports missing files, download the supported MMDB
editions:
xrat geoip download --all
If MMDB lookup fails but remote lookup works, check the resolved directory:
xrat geoip path
xrat geoip lookup 8.8.8.8 --backend ipwhois
Remote backends can be rate-limited by the provider. Use the default cache
unless you specifically need --no-cache for diagnostics.
Related
[mmdb]config β MMDB asset configuration[testing.geoip]config β GeoIP lookup backend configuration- GeoIP Enrichment β test result enrichment feature
serve
Start the local HTTP API server.
xrat serve [flags]
Flags
| Flag | Description |
|---|---|
--host <host> | Override HTTP API bind host |
--port <port> | Override HTTP API bind port |
Examples
Start with default settings (from config.toml):
xrat serve
Override host and port:
xrat serve --host 0.0.0.0 --port 9090
Configuration
The HTTP API server is configured in config.toml:
[server]
enabled = false
host = "127.0.0.1"
port = 8080
key = { env = "XRAT_API_KEY" }
| Field | Description |
|---|---|
enabled | Enable daemon-hosted API (see below) |
host | Bind host (default: 127.0.0.1) |
port | Bind port (default: 8080) |
key | Optional API key for authentication |
Operating Modes
Foreground Mode
Run xrat serve to start the API server in the foreground. Useful for
development or standalone deployment.
Daemon-Hosted Mode
When enabled = true in config.toml, the daemon automatically starts the HTTP
API alongside IPC. The API runs in the same process as the daemon.
systemd Service
Run as a systemd user service:
[Unit]
Description=xrat HTTP API
After=network.target
[Service]
ExecStart=/usr/local/bin/xrat serve
Restart=on-failure
Environment=RUST_LOG=info
[Install]
WantedBy=default.target
API Routes
| Route | Method | Description |
|---|---|---|
/health | GET | Health check (no auth required) |
/json | GET | List configs with latest test results as JSON array |
/b64 | GET | Base64-encoded subscription text payload |
/configs | GET | Paginated config list with details |
/configs/{id} | GET | Single config detail with latest test results |
Query Parameters
/json
| Parameter | Description |
|---|---|
key | API key (if authentication is enabled) |
top | Return top N configs sorted by real-delay |
enabled | Filter: true for enabled configs only |
protocol | Filter by protocol: vless, vmess, ss, trojan, hy2 |
selected | Filter: true for selected configs only |
/b64
| Parameter | Description |
|---|---|
key | API key (if authentication is enabled) |
/configs
| Parameter | Description |
|---|---|
key | API key (if authentication is enabled) |
page | Page number (default: 1) |
per_page | Items per page (default: 20) |
enabled | Filter: true for enabled configs only |
protocol | Filter by protocol |
selected | Filter: true for selected configs only |
/configs/{id}
| Parameter | Description |
|---|---|
key | API key (if authentication is enabled) |
Authentication
If key is set in config.toml, all routes except /health require the key
query parameter:
curl "http://localhost:8080/json?key=secret"
curl "http://localhost:8080/configs?key=secret&page=1&per_page=10"
Response Formats
/health
{
"status": "ok"
}
/json
[
{
"id": 42,
"protocol": "vless",
"address": "example.com",
"port": 443,
"name": "My Node",
"is_enabled": true,
"is_active": false,
"is_selected": false,
"latest_test": {
"icmp_ok": true,
"icmp_ms": 15,
"tcp_ok": true,
"tcp_ms": 12,
"real_delay_ok": true,
"real_delay_ms": 145,
"download_mbps": null,
"tested_at": "2026-05-28T10:00:00Z"
}
}
]
/b64
Returns base64-encoded subscription text compatible with v2rayN, Clash, and other clients:
dmxlc3M6Ly91dWlkQGV4YW1wbGUuY29tOjQ0Mz90eXBlPXdzJnNlY3VyaXR5PXRscyNNeSBOb2RlCnZtZXNzOi8v...
/configs
{
"page": 1,
"per_page": 20,
"total": 150,
"configs": [
{
"id": 42,
"protocol": "vless",
"address": "example.com",
"port": 443,
"name": "My Node",
"is_enabled": true,
"is_active": false,
"is_selected": false,
"subscription_id": 1,
"created_at": "2026-05-20T08:00:00Z",
"updated_at": "2026-05-28T10:00:00Z",
"latest_test": { ... }
}
]
}
/configs/{id}
{
"id": 42,
"protocol": "vless",
"address": "example.com",
"port": 443,
"uuid": "uuid-123",
"network": "ws",
"tls": "tls",
"sni": "cdn.example.com",
"host": "cdn.example.com",
"path": "/ray",
"name": "My Node",
"is_enabled": true,
"is_active": false,
"is_selected": false,
"subscription_id": 1,
"created_at": "2026-05-20T08:00:00Z",
"updated_at": "2026-05-28T10:00:00Z",
"latest_test": { ... }
}
Use Cases
- Subscription server: Serve configs to mobile/desktop clients via
/b64 - Monitoring: Poll
/healthand/configsfor uptime monitoring - Integration: Build dashboards or automation around
/jsonand/configs - Proxy management: Query active configs and test results programmatically
Related
daemonβ daemon-hosted API modetestβ test results are exposed via the APIlistβ CLI equivalent of/configs
tui
Start the interactive terminal UI.
xrat tui
The TUI has no command-specific flags. It uses the same global flags as other
commands, including --database, --config, --xray, --v2ray, --sing-box,
-v, and -q.
The TUI is an interactive view over xratβs shared database, source, testing, runtime, and diagnostics services. It does not keep a separate copy of business logic: config changes, imports, tests, and runtime operations use the same app services as the CLI commands.
Views
| Key | View | Purpose |
|---|---|---|
1 | Configs | Browse, filter, select, enable, disable, delete, and share configs |
2 | Sources | Inspect subscription sources, refresh/import sources, and share source/API URLs |
3 | Tests | Start/cancel background test batches and inspect recent results |
4 | Diagnostics | Inspect paths, runtime/source summaries, and recent operation messages |
The TUI opens in the Configs view. The bottom bar shows view shortcuts, help and quit actions, active filters, task state, and the latest status message.
Global Keys
| Key | Action |
|---|---|
1-4 | Switch primary view |
Tab | Cycle primary views |
j, k | Move focus down/up |
| arrow keys | Move focus down/up |
? | Open help |
Esc | Close modal, leave search, or go back |
q, Ctrl+C | Quit |
Configs View
The Configs view shows stored configs with latest test summaries and config
state. The status marker column uses β for active, β for selected, β for
soft-deleted, β for disabled, and ! for failed configs. Long names are
truncated in the table. It supports focused actions, selected-config bulk
actions, test batches, and managed runtime controls.
| Key | Action |
|---|---|
/ | Edit config search |
Ctrl+U | Clear search while editing |
s | Cycle sort field |
F | Cycle filter: all, enabled, failed, has-delay |
P | Cycle protocol filter |
f | Show or hide soft-deleted configs |
Space | Mark the focused config as selected |
Enter | Select and start the focused config |
e, x | Enable or disable the focused config |
E, X | Enable or disable all selected configs |
d | Soft-delete the focused config after confirmation |
D | Purge the focused config after confirmation |
r | Restore the focused soft-deleted config |
t | Start a test batch for the current Configs scope |
a | Test all enabled, non-deleted configs |
v | Test visible configs matching current filters |
K | Stop/disconnect the managed runtime |
R | Restart the managed runtime |
y | Show a QR code for the focused config URI |
c | Copy the focused config URI |
C | Copy selected config URIs as newline-separated text |
Search matches the displayed config fields. Sorting can cycle through latency,
ID, name, protocol, source, last-tested time, and imported time. Deleted configs
are hidden by default; press f to include them.
Soft delete hides a config from normal views and workflows. Purge permanently deletes it. Both destructive actions require confirmation.
The Runtime panel inside the Configs view shows the current managed runtime
state, active config, selected config, current task, proxy endpoint, and failure
message when present. Runtime actions use the same runtime service as
xrat connect, xrat disconnect, and xrat status. The same runtime
prerequisites apply: the configured Xray/V2Ray binary must be available, runtime
paths must be writable, and daemon/runtime configuration must be valid.
Select a preferred config in the Configs view before starting or switching the runtime. Runtime operations run in the background and reload TUI data after completion.
Sources View
The Sources view lists subscription sources and source metadata. The detail panel also shows the local HTTP API base64 subscription URL when it is available.
| Key | Action |
|---|---|
r | Refresh the focused source |
R | Refresh all sources with stored values |
i | Open the import modal |
n | Rename the focused source |
d | Delete the focused source and its configs |
y | Show a QR code for the focused source URL |
c | Copy the focused source URL |
u | Show a QR code for the HTTP API /b64 subscription URL |
U | Copy the HTTP API /b64 subscription URL |
The import modal accepts the same input forms as xrat import: subscription
URL, file path, raw config link, raw link list, base64 subscription text, SIP008
JSON, or Xray JSON. Press Enter to import and Esc to cancel.
Source refresh and import run as background tasks. When they finish, the TUI reloads database-backed data so the Configs and Sources views reflect the new state.
Tests View
The Tests view shows the latest test run summary, current test settings, progress, and recent results.
| Key | Action |
|---|---|
s | Start a background test batch |
c | Cancel the running test batch |
The current implementation starts a batch for all enabled, non-deleted configs.
It runs TCP and real-delay tests with concurrency 4 and skips download,
upload, and ICMP stages. The scope, mode, and concurrency are displayed in the
view, but there are not yet keybindings to change them interactively.
While a batch is running, the progress bar updates without blocking navigation. Cancelling requests cooperative cancellation; the active operation reports cancelled once the shared test executor observes the cancellation request.
Diagnostics and Help
Press 4 to open Diagnostics. It summarizes important runtime, database,
source, API, and operation state, including recent TUI task messages.
Press ? from any view to open the help modal. Press Esc to close it.
QR and Clipboard Behavior
QR modals are available for focused config URIs, source URLs, and the HTTP API
subscription URL. Press Esc or q to close a QR modal.
Clipboard actions use the host clipboard. They can fail in SSH, tmux, Wayland, X11, or headless sessions depending on environment support. When clipboard access fails, the TUI reports the error in the status area.
QR generation can fail if a URI is too long for the QR renderer. When that happens, the QR modal reports the failure instead of crashing.
Related Commands
| Workflow | CLI equivalent |
|---|---|
| Manage config state | config management |
| Start or stop runtime | runtime |
| Run tests | test |
| Inspect sources | list subscriptions |
| Import sources | import |
| Serve API URL | serve |
Troubleshooting
If the TUI cannot start, check that the terminal supports alternate-screen raw mode and run with a higher log level:
xrat -vv tui
If runtime actions fail, verify the equivalent CLI flow first:
xrat daemon start
xrat connect <id>
xrat status
If source/API QR or copy actions report that a URL is unavailable, ensure the source has a stored value and that the HTTP API subscription URL can be built from the current app configuration.
completions
Generate shell completion scripts for xrat.
xrat completions <shell>
This is a hidden command (not shown in --help) intended for shell setup and
release packaging.
Arguments
| Argument | Description | Values |
|---|---|---|
<shell> | Shell to generate completions for (required) | bash, zsh, fish, powershell |
Per-shell installation
Bash
mkdir -p ~/.local/share/bash-completion/completions
xrat completions bash > ~/.local/share/bash-completion/completions/xrat
Reload: open a new shell or source ~/.bashrc.
Zsh
mkdir -p ~/.zfunc
xrat completions zsh > ~/.zfunc/_xrat
Add to ~/.zshrc if not already present:
fpath=(~/.zfunc $fpath)
autoload -Uz compinit && compinit
Reload: exec zsh.
Fish
xrat completions fish > ~/.config/fish/completions/xrat.fish
Reload: open a new Fish shell.
PowerShell
xrat completions powershell > xrat.ps1
. ./xrat.ps1
Add the dot-source line to your $PROFILE for persistence.
Release packaging
Pre-generated completion scripts are included in release archives under
completions/:
| File | Shell |
|---|---|
completions/xrat.bash | Bash |
completions/_xrat | Zsh |
completions/xrat.fish | Fish |
CI generates these during the release workflow using xrat completions <shell>.
Related
manpage
Generate roff-format man pages for xrat and all subcommands.
xrat manpage [--output <dir>]
This is a hidden command (not shown in --help) intended for use during release
packaging and local installation.
Flags
| Flag | Description | Default |
|---|---|---|
--output <dir> | Directory to write generated .1 files | . |
Behavior
Generates one man page per visible command and subcommand:
xrat.1β root command with global flagsxrat-init.1,xrat-import.1,xrat-daemon.1, β¦ β top-level subcommandsxrat-daemon-install.1,xrat-daemon-stop.1, β¦ β nested subcommands
Hidden commands (e.g., daemon run-server) are excluded.
Output format is roff/troff compatible with man(1).
Example
xrat manpage --output /tmp/man
/tmp/man/xrat.1
/tmp/man/xrat-init.1
/tmp/man/xrat-import.1
/tmp/man/xrat-daemon.1
/tmp/man/xrat-daemon-install.1
...
Installing locally
mkdir -p ~/.local/share/man/man1
xrat manpage --output ~/.local/share/man/man1
mandb ~/.local/share/man # update index (may require once)
man xrat
man xrat-daemon-install
Or system-wide:
sudo xrat manpage --output /usr/local/share/man/man1
sudo mandb
Release packaging
CI generates man pages during the release workflow and includes them in release
archives under man/:
xrat manpage --output dist/man/man1/
Related
Features
xrat provides a comprehensive set of features for managing proxy configurations and running local proxy services.
Core Features
| Feature | Description |
|---|---|
| Importing | Import subscriptions from URLs, files, raw text, base64, JSON |
| Testing | 5-stage probe pipeline with failure classification |
| Runtime Management | Connect lifecycle, session state, reattach |
| Daemon and IPC | Supervisor process with Unix socket IPC |
| Auto-Rotation | Scheduled proxy switching with cooldown |
| IP Scanning | TCP reachability scanning with persistence |
| HTTP API | RESTful API for config access and monitoring |
| Deduplication | Versioned dedup keys for config uniqueness |
Feature Highlights
Multi-Protocol Support
xrat supports 7 proxy protocols:
- VLESS β modern, lightweight protocol
- VMess β legacy protocol with encryption
- Shadowsocks β simple SOCKS5-like proxy
- Trojan β TLS-based proxy that mimics HTTPS
- HTTP/HTTPS β standard HTTP proxy
- SOCKS5 β classic SOCKS protocol
- Hysteria2 β QUIC-based protocol (via sing-box)
Dual Database Backend
- SQLite β single-user, file-based, zero configuration
- PostgreSQL β multi-user, connection pooling, production-ready
Engine Support
- Xray-core/V2Ray β managed runtime engines used by
xrat connect - sing-box β parse-time and runtime-config preview support, including
Hysteria2 diagnostics through
xrat parse --engine sing-box
Configurable Testing Pipeline
- 5 test stages: ICMP, TCP, real-delay, download, upload
- Configurable stage order and failure policy
- Bulk testing with concurrency control
- Failure classification with 10 categories
- GeoIP enrichment for endpoint metadata
Managed Runtime
- Automatic proxy process lifecycle management
- Session state tracking with database persistence
- Graceful shutdown with SIGTERM/SIGKILL fallback
- Stale session recovery on daemon restart
Daemon Supervisor
- Long-lived background process
- Unix domain socket IPC for CLI communication
- Health monitoring with automatic rotation on failure
- Scheduled rotation with cooldown protection
HTTP API
- RESTful endpoints for config access
- Base64 subscription output for mobile clients
- Optional API key authentication
- Paginated config listing with filters
Architecture
See Architecture for details on how these features are implemented.
Importing
xrat imports proxy configurations from multiple sources and formats, automatically detecting the input type and normalizing all configs into a unified internal representation.
Input Sources
Subscription URL
Fetch configs from a remote HTTP endpoint:
xrat import https://example.com/subscription
xrat:
- Fetches the URL content
- Parses
subscription-userinfoheaders for metadata (upload, download, total, expire) - Detects format (base64, plain list, JSON)
- Parses and normalizes each node
- Persists to database with subscription tracking
Local File
Import from a file on disk:
xrat import ./nodes.txt
Supports the same format detection as URLs.
Raw Text
Import inline subscription text:
xrat import "vless://uuid@example.com:443?type=ws#Node"
Useful for quick imports or scripting.
Input Formats
Single Share Link
A single proxy URI:
vless://uuid-123@example.com:443?type=ws&security=tls&sni=cdn.example.com&path=%2Fray#My%20Node
Supported schemes:
vless://vmess://ss://trojan://http:///https://socks5://hysteria2:///hy2://
Base64 Subscription
Standard v2rayN/Clash subscription format:
dmxlc3M6Ly91dWlkQGV4YW1wbGUuY29tOjQ0Mz90eXBlPXdzJnNlY3VyaXR5PXRscyNNeSBOb2RlCnZtZXNzOi8v...
xrat:
- Base64-decodes the payload
- Splits into lines
- Parses each line as a share link
Plain Link List
Multiple share links, one per line:
vless://uuid-1@example.com:443?type=tcp#Node1
vmess://eyJhZGQiOiJleGFtcGxlLmNvbSIsInBvcnQiOiI0NDMifQ==#Node2
ss://YWVzLTI1Ni1nY206c2VjcmV0@example.com:8388#Node3
Lines starting with # are treated as comments and skipped.
SIP008 JSON
Shadowsocks SIP008 format:
{
"version": 1,
"servers": [
{
"server": "example.com",
"server_port": 8388,
"method": "aes-256-gcm",
"password": "secret",
"remarks": "My SS Node"
}
]
}
Xray JSON
Full Xray configuration:
{
"inbounds": [...],
"outbounds": [
{
"protocol": "vless",
"settings": {
"vnext": [...]
}
}
]
}
xrat extracts outbound configs and converts them to internal nodes.
Format Detection
xrat automatically detects the input format using heuristics:
| Condition | Detected Format |
|---|---|
Starts with { and contains "version" or "inbounds" | Xray JSON |
Starts with { and contains "servers" | SIP008 JSON |
| Single line starting with a protocol scheme | Single share link |
| Multiple lines, first line starts with protocol scheme | Plain link list |
| Otherwise | Base64 subscription |
Normalization
After parsing, xrat normalizes each node:
- Network defaults: Empty network β
tcp - WebSocket defaults: Missing
hostβ copy fromsni, missingpathβ/ - gRPC defaults: Missing
pathβ/ - TLS cleanup: Empty string
tlsβNone
Deduplication
Before persisting, xrat generates a dedup key for each node and skips duplicates. See Deduplication for details.
Subscription Tracking
Each import creates or updates a subscriptions record:
| Field | Description |
|---|---|
source_url | Original URL or file path |
source_kind | url, file, or raw_text |
name | Optional name (from URL or user-provided) |
created_at | First import timestamp |
updated_at | Latest import timestamp |
Configs are linked to their subscription via subscription_id foreign key.
Metadata Extraction
For subscription URLs, xrat extracts metadata from HTTP headers:
subscription-userinfo: upload=1024; download=2048; total=10240; expire=1234567890
Parsed fields:
uploadβ bytes uploadeddownloadβ bytes downloadedtotalβ total quotaexpireβ expiration timestamp (Unix epoch)
Error Handling
xrat continues parsing even when individual lines fail:
Import Summary
βββββββββββββββββββββββββββββββββ
Source: https://example.com/sub.txt
Parsed: 45 nodes
Failed: 3 lines
Duplicates: 12 skipped
New: 33 configs added
Failed lines are logged with line numbers and error messages.
Related
importCLI β command reference- Deduplication β how duplicates are detected
- Protocols β supported protocol formats
Testing
xrat includes a comprehensive testing pipeline that measures connectivity, latency, and throughput for stored proxy configs.
Test Stages
The test command runs up to 5 stages in sequence:
| Stage | Measures | Default | Implementation |
|---|---|---|---|
| ICMP | Ping success and latency | Enabled | Spawns system ping command |
| TCP | TCP connect success and latency | Enabled | Direct TCP socket connection |
| Real Delay | HTTP round-trip latency through proxy | Enabled | Spawns proxy, makes HTTP request |
| Download | Download throughput through proxy | Disabled | Downloads file through proxy |
| Upload | Upload throughput through proxy | Disabled | POSTs data through proxy |
Stage Configuration
Configure stages in config.toml:
[testing]
concurrency = 0 # 0 = auto-detect
order = ["icmp", "real_delay", "download"]
failure_policy = "continue"
[testing.icmp]
enabled = true
timeout = 3000
attempts = 3
[testing.tcp]
enabled = true
timeout = 5000
[testing.real_delay]
enabled = true
url = "https://www.gstatic.com/generate_204"
timeout = 10_000
[testing.download]
enabled = false
url = "https://cachefly.cachefly.net/50mb.test"
timeout = 30_000
Stage Order
The order array controls ICMP, real-delay, and download ordering. TCP is a
gate before real-delay when enabled. Upload is optional and runs after download
only when --upload-url is provided.
Example: skip ICMP, run only real-delay and download:
order = ["real_delay", "download"]
Failure Policy
Controls behavior when a stage fails:
| Policy | Behavior |
|---|---|
continue | Run all stages regardless of failures |
skip_remaining | Stop testing this config after first failure |
mark_failed | Mark config as failed, skip remaining stages |
ICMP Stage
Measures ICMP ping latency by spawning the system ping command.
Configuration
[testing.icmp]
enabled = true
timeout = 3000 # ms per attempt
attempts = 3 # number of ping packets
Output
icmp_okβ boolean successicmp_msβ average latency in millisecondsicmp_attemptsβ number of packets sent
Implementation
xrat spawns ping -c <attempts> -W <timeout> <address> and parses stdout for
packet loss and round-trip times.
TCP Stage
Measures TCP connection latency to the proxyβs address:port.
Configuration
[testing.tcp]
enabled = true
timeout = 5000 # ms
Output
tcp_okβ boolean successtcp_msβ connection time in millisecondsfailure_kindβ failure classification (if failed)
Failure Classification
TCP failures are classified into categories:
| Category | Description |
|---|---|
DNS | DNS resolution failed |
Timeout | Connection timed out |
Refused | Connection refused (port closed) |
Unreachable | Network unreachable |
PermissionDenied | Permission denied |
TLS | TLS handshake failed |
Auth | Authentication failed |
Process | Proxy process failed to start |
Proxy | Proxy returned an error |
Unknown | Unclassified failure |
Real Delay Stage
Measures actual HTTP round-trip latency through the proxy.
How It Works
- Generates a temporary Xray probe config with a local SOCKS inbound
- Spawns a short-lived Xray process
- Waits for the SOCKS port to become ready
- Makes an HTTP request through the proxy to the test URL
- Measures connect time, TTFB, and total round-trip time
- Terminates the Xray process
Configuration
[testing.real_delay]
enabled = true
url = "https://www.gstatic.com/generate_204"
timeout = 10_000 # ms
Output
real_delay_okβ boolean successreal_delay_msβ total round-trip timeconnect_msβ TCP connection timettfb_msβ time to first bytehttp_statusβ HTTP response status code
Probe Config
The probe config uses a minimal setup:
{
"log": { "loglevel": "warning" },
"inbounds": [
{
"tag": "probe-in",
"port": <random>,
"listen": "127.0.0.1",
"protocol": "socks",
"settings": { "udp": false }
}
],
"outbounds": [
{
"tag": "proxy",
"protocol": "<node-protocol>",
"settings": { ... },
"stream_settings": { ... }
}
]
}
Download Stage
Measures download throughput by downloading a file through the proxy.
Configuration
[testing.download]
enabled = false
url = "https://cachefly.cachefly.net/50mb.test"
timeout = 30_000 # ms
Output
download_mbpsβ throughput in megabits per second
Implementation
- Spawns proxy with the config
- Downloads the file through the proxy
- Measures bytes transferred and elapsed time
- Calculates throughput:
(bytes * 8) / (seconds * 1_000_000)
Upload Stage
Measures upload throughput by POSTing data through the proxy.
Invocation
xrat test 42 --upload-url https://example.com/upload --upload-timeout 30000
Output
upload_mbpsβ throughput in megabits per second
Bulk Testing
Test multiple configs concurrently:
xrat test --enabled-only --concurrency 4
Concurrency
0= auto-detect based on CPU cores- Positive values set exact worker count
Progress Bar
Bulk tests display an animated progress bar (unless --no-progress is used):
Testing configs ββββββββββββββββββββ 45/150 30% 2m 15s
Output Formats
| Format | Description |
|---|---|
tsv | Tab-separated values (default, terminal-friendly) |
csv | Comma-separated values (spreadsheet-friendly) |
json | JSON array with full details |
Sorting
Sort results by:
| Field | Description |
|---|---|
status | Alive first, then by failure reason |
icmp | Lowest ICMP latency |
real-delay | Lowest real-delay latency |
download-speed | Highest download throughput |
protocol | Protocol name alphabetically |
address | Server address alphabetically |
Ping Loop
Continuous monitoring mode for a single config:
xrat test 42 --ping --ping-interval 2000
Runs the test repeatedly until Ctrl+C, printing a live summary:
Ping loop for config 42 (vless://example.com:443)
βββββββββββββββββββββββββββββββββββββββββββββββββββββ
#1 ICMP: 15ms TCP: 12ms Real Delay: 145ms β
#2 ICMP: 14ms TCP: 11ms Real Delay: 142ms β
#3 ICMP: - TCP: - Real Delay: - β timeout
#4 ICMP: 16ms TCP: 13ms Real Delay: 148ms β
GeoIP Enrichment
Optionally enrich test results with GeoIP data (country, city, ASN) using configurable lookup backends.
Backend Types
| Backend | Description |
|---|---|
mmdb | Local GeoLite2 MMDB files (default) |
ipwhois | Remote ipwhois.app API |
ip-api | Remote ip-api.com API |
chain | Local MMDB with remote fallback |
Configuration
[testing.geoip]
enabled = true
backend = "mmdb" # mmdb | ipwhois | ip-api | chain
mmdb backend
Paths for local GeoLite2 MMDB files. Relative paths are resolved from the config
file location, or from XRAT_PATH when set.
[testing.geoip]
country_path = "mmdb/GeoLite2-Country.mmdb"
city_path = "mmdb/GeoLite2-City.mmdb"
asn_path = "mmdb/GeoLite2-ASN.mmdb"
Download MMDB files with the geoip download
command.
Remote backends (ipwhois / ip-api)
[testing.geoip.remote]
provider = "ipwhois" # ipwhois | ip-api
endpoint = "" # override API endpoint (empty = provider default)
timeout_ms = 5000
api_key = "" # provider-specific (if required)
rate_limit_per_minute = 30
Chain backend
Primary is local MMDB; falls back to a remote service on cache/miss or MMDB absence.
[testing.geoip]
backend = "chain"
fallback = "ipwhois" # ipwhois | ip-api
Caching
Remote lookups are cached in memory to reduce API calls:
[testing.geoip.cache]
enabled = true
ttl_secs = 86400 # per-entry TTL
max_entries = 10000
Test Result Enrichment
When GeoIP enrichment is enabled, test results include:
endpoint_ipβ resolved IP addressendpoint_countryβ ISO country code (e.g.NL)endpoint_locationβ location label such as city/country when availableendpoint_asnβ Autonomous System Number and organization (e.g.AS15169 Google LLC)
Related
geoipCLI β manage MMDB assets, inspect backends, and run ad-hoc IP lookups[mmdb]config β MMDB asset configuration[testing.geoip]config β full configuration reference
Test Runs
Tests are grouped into runs for historical analysis:
| Table | Purpose |
|---|---|
connection_test_runs | Groups test results (id, kind, created_at) |
connection_tests | Individual test results linked to a run |
View the latest run summary:
xrat test --latest-run-summary
Filter by country or ASN:
xrat test --latest-run-summary --country US --asn cloudflare
Persistence
All test results are persisted to the database:
| Field | Description |
|---|---|
config_id | Foreign key to configs table |
run_id | Foreign key to connection_test_runs |
icmp_ok, icmp_ms | ICMP results |
tcp_ok, tcp_ms | TCP results |
real_delay_ok, real_delay_ms | Real delay results |
connect_ms, ttfb_ms, http_status | HTTP details |
download_mbps, upload_mbps | Throughput |
failure_kind, failure_reason | Failure details |
endpoint_ip, endpoint_country, endpoint_asn | GeoIP |
tested_at | Timestamp |
Related
testCLI β command reference- Runtime Management β uses probe configs for testing
- Database Schema β test result tables
Runtime Management
xrat manages the lifecycle of local proxy processes (Xray or V2Ray), providing automatic config generation, process spawning, health monitoring, and graceful shutdown.
Connect Flow
When you run xrat connect <id>:
- Load config β Fetch the config from the database by ID
- Generate runtime config β Create Xray JSON with local inbounds
- Spawn process β Launch Xray/V2Ray as a child process
- Wait for readiness β Poll the SOCKS port until it accepts connections
- Persist session β Insert a
runtime_sessionsrecord with statusrunning - Return result β Print connection details (ports, PID, config info)
Runtime Config Generation
xrat generates a complete Xray config with:
- Inbounds: SOCKS5, HTTP, Shadowsocks (as configured in config.toml)
- Outbound: Single outbound to the proxy node
- Logging: Configurable log level and file paths
- Stream settings: TLS, WebSocket, gRPC, TCP header obfuscation
Example generated config:
{
"log": { "loglevel": "warning" },
"inbounds": [
{
"tag": "socks-in",
"port": 1080,
"listen": "0.0.0.0",
"protocol": "socks",
"settings": { "udp": true }
},
{
"tag": "http-in",
"port": 8080,
"listen": "0.0.0.0",
"protocol": "http"
}
],
"outbounds": [
{
"tag": "proxy",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "example.com",
"port": 443,
"users": [{ "id": "uuid-123", "encryption": "none" }]
}
]
},
"stream_settings": {
"network": "ws",
"security": "tls",
"tls_settings": { "server_name": "cdn.example.com" },
"ws_settings": {
"path": "/ray",
"headers": { "Host": "cdn.example.com" }
}
}
}
]
}
Process Spawning
xrat spawns the proxy process with:
- Config file: Written to
<runtime_dir>/session-<id>.json - Stdout: Redirected to
<runtime_dir>/session-<id>.out.log - Stderr: Redirected to
<runtime_dir>/session-<id>.err.log - Detached mode: Process continues running after CLI exits (when using daemon)
Readiness Check
After spawning, xrat polls the SOCKS port every 100ms until:
- Port accepts TCP connections β success
- Process exits β error
- Timeout (default 10s) β error, process is killed
Session State
Each runtime session has a status:
| Status | Description |
|---|---|
starting | Process spawned, waiting for port readiness |
running | Port is ready, proxy is active |
stopping | Graceful shutdown in progress |
stopped | Process terminated cleanly |
failed | Process exited unexpectedly or startup failed |
State Transitions
starting β running β stopping β stopped
β β
failed failed
Session Record
Persisted to runtime_sessions table:
| Field | Description |
|---|---|
id | Session ID (primary key) |
config_id | Foreign key to configs table |
status | Current status |
process_id | OS process ID (PID) |
socks_host, socks_port | SOCKS inbound address |
http_host, http_port | HTTP inbound address |
shadowsocks_host, shadowsocks_port | Shadowsocks inbound address |
failure_reason | Error message (if failed) |
owner_kind | cli or daemon |
owner_instance_id | Daemon instance ID (if daemon-owned) |
started_at, stopped_at | Timestamps |
Disconnect Flow
When you run xrat disconnect:
- Load active session β Find the latest
runningsession - Send SIGTERM β Request graceful shutdown
- Wait for exit β Poll process status every 100ms (up to 5s)
- Send SIGKILL β Force kill if still running after timeout
- Update session β Set status to
stoppedorfailed - Cleanup β Remove temporary config files (if configured)
Graceful Shutdown
xrat attempts graceful shutdown:
#![allow(unused)]
fn main() {
terminate_process_gracefully(pid, Duration::from_secs(5))
}
- Check if process is running
- Send SIGTERM
- Poll every 100ms for up to 5 seconds
- If still running, send SIGKILL
- Return outcome:
Terminated,Killed, orNotRunning
Status Check
When you run xrat status:
- Load active session β Find the latest session (any status)
- Check PID liveness β Verify process is still running
- Check inbound health β Test TCP reachability of SOCKS/HTTP/Shadowsocks ports
- Return snapshot β Print status with config details and health
Health Check
For each inbound port:
| Status | Description |
|---|---|
reachable | TCP connection succeeded |
unreachable | TCP connection failed |
not_checked | Inbound is disabled or port is 0 |
Session Replacement
When replace_active_session = true in config.toml:
xrat connect 99
If a session is already running:
- Disconnect the old session (graceful shutdown)
- Connect the new session
- Atomic operation from the userβs perspective
This is useful for switching proxies without manual disconnect.
Reattach on Daemon Restart
When the daemon starts, it reconciles stale sessions:
- Find stale sessions β Query for
runningsessions with nostopped_at - Check PID liveness β For each stale session, check if PID is still running
- Verify process identity β Compare
/proc/<pid>/cmdlinewith expected binary path - Reattach or mark failed:
- PID alive + cmdline matches β reattach (keep as
running) - PID alive + cmdline mismatch β mark as
failed(different process reused PID) - PID dead β mark as
failed
- PID alive + cmdline matches β reattach (keep as
Reattach Validation
xrat validates that the PID still belongs to the expected proxy process:
#![allow(unused)]
fn main() {
fn validate_reattach(pid: i64, expected_binary: &Path) -> bool {
let cmdline = read_proc_cmdline(pid);
cmdline.contains(expected_binary.to_str().unwrap())
}
}
This prevents reattaching to a different process that happens to have the same PID.
Inbound Configuration
Configure local inbounds in config.toml:
SOCKS5
[runtime.socks]
enabled = true
host = "0.0.0.0"
port = 1080
udp = true
auth = { enabled = true, username = "xrat", password = { env = "XRAT_SOCKS_PASSWORD" } }
| Field | Description |
|---|---|
enabled | Enable SOCKS inbound |
host | Bind address |
port | Bind port |
udp | Enable UDP support |
auth | Optional username/password authentication |
HTTP
[runtime.http]
enabled = false
host = "0.0.0.0"
port = 8080
Shadowsocks
[runtime.shadowsocks]
enabled = false
host = "0.0.0.0"
port = 1081
method = "aes-128-gcm"
password = { env = "XRAT_SHADOWSOCKS_PASSWORD" }
network = "tcp,udp"
Sniffing
Enable traffic sniffing for better routing:
[runtime.sniffing]
enabled = true
dest_override = ["http", "tls", "quic"]
route_only = true
metadata_only = false
domains_excluded = []
ips_excluded = []
Logging
Configure proxy process logging:
[runtime.log]
enabled = true
mask = "none" # "quarter" | "half" | "full" | "none"
dir = "logs"
dns_log = false
level = "warning" # "debug" | "info" | "warning" | "error"
keep = true
| Field | Description |
|---|---|
enabled | Enable logging to files |
mask | Mask IP addresses in logs |
dir | Log directory (relative to config dir or absolute) |
dns_log | Enable DNS query logging |
level | Log level |
keep | Keep log files after session stops |
Engine Selection
Choose the proxy engine in config.toml:
[runtime]
engine = "xray" # "xray" | "v2ray" | "sing-box"
| Engine | Binary | Protocols |
|---|---|---|
xray | xray | All except Hysteria2 |
v2ray | v2ray | VLESS, VMess, Shadowsocks, Trojan, HTTP, SOCKS5 |
sing-box | sing-box | Parse-time/runtime-config preview only; managed process lifecycle is not yet sing-box parity |
Related
connectCLI β command reference- Daemon and IPC β daemon-managed sessions
- Auto-Rotation β automatic proxy switching
- Config Generation β how configs are generated
Daemon and IPC
xrat includes a long-lived daemon supervisor process that manages proxy sessions, monitors health, and handles auto-rotation. CLI commands communicate with the daemon via Unix domain socket IPC.
Daemon Architecture
The daemon runs as a background process with three main responsibilities:
- IPC Server β Listens for commands from CLI clients
- Health Monitor β Periodically checks proxy liveness
- Rotation Scheduler β Manages automatic proxy switching
Supervisor Event Loop
The daemon runs a tokio::select! loop:
#![allow(unused)]
fn main() {
loop {
tokio::select! {
_ = health_tick.tick() => {
check_proxy_health().await;
}
Some(event) = ipc_rx.recv() => {
handle_ipc_event(event).await;
}
_ = rotation_timer.tick() => {
trigger_rotation().await;
}
}
}
}
IPC Protocol
The daemon uses JSON over Unix domain socket with protocol version 1.
Socket Location
<runtime_dir>/daemon.sock
Default: ~/.config/xrat/runtime/daemon.sock
Connection Flow
- CLI command connects to the Unix socket
- Sends a JSON request
- Receives a JSON response
- Closes the connection
Request Format
{
"protocol_version": 1,
"request": {
"type": "RuntimeConnect",
"payload": {
"config_id": 42
}
}
}
Response Format
{
"protocol_version": 1,
"ok": true,
"code": 200,
"message": "success",
"payload": { ... }
}
Request Types
| Type | Description | Payload |
|---|---|---|
DaemonPing | Check daemon reachability | None |
DaemonShutdown | Request graceful shutdown | None |
RuntimeStatus | Get proxy runtime status | None |
RuntimeConnect | Start a proxy session | { config_id: i64 } |
RuntimeReplace | Atomic disconnect + connect | { trigger, candidate_id } |
RuntimeDisconnect | Stop the active proxy session | None |
ProxyStart | Enable auto-rotation | None |
ProxyStatus | Get rotation status | None |
ProxyStop | Disable auto-rotation | None |
Manual xrat proxy rotate calls RuntimeReplace with trigger = manual and an
optional candidate_id. There is no separate ProxyRotate request type.
Response Codes
| Code | Description |
|---|---|
200 | Success |
400 | Bad request (invalid payload) |
404 | Not found (no active session) |
409 | Conflict (session already running) |
500 | Internal error |
Daemon Lifecycle
Starting the Daemon
xrat daemon start
- Check if daemon is already running (try connecting to socket)
- Fork a background process
- Create the Unix socket
- Run the supervisor event loop
- Reattach to any stale sessions from previous daemon runs
Stopping the Daemon
xrat daemon stop
- Connect to the daemon socket
- Send
DaemonShutdownrequest - Daemon gracefully terminates:
- Stops the active proxy session (if running)
- Closes the IPC socket
- Exits cleanly
Checking Daemon Status
xrat daemon status
Attempts to connect to the socket and sends a DaemonPing request. Reports
whether the daemon is reachable and the protocol version.
Health Monitoring
The daemon periodically checks the health of the active proxy session.
Health Tick Interval
Every 15 seconds, the daemon:
- Loads the active session from the database
- Checks if the PID is still running
- Tests TCP reachability of the SOCKS port
- If health check fails:
- Logs the failure
- Triggers auto-rotation (if
health_trigger_enabled)
Health Check Implementation
#![allow(unused)]
fn main() {
async fn check_proxy_health() -> HealthStatus {
let session = db.get_latest_running_session().await?;
if !process_is_running(session.process_id) {
return HealthStatus::ProcessDead;
}
if !tcp_connect(session.socks_host, session.socks_port).await {
return HealthStatus::PortUnreachable;
}
HealthStatus::Healthy
}
}
Failure Handling
When a health check fails:
- Log the failure β Record in daemon logs
- Update session β Mark as
failedwith reason - Trigger rotation β If
health_trigger_enabled, start rotation - Respect cooldown β Donβt rotate if cooldown is active
Session Reconciliation
When the daemon starts, it reconciles stale sessions from previous runs.
Stale Session Detection
Query for sessions with:
status = 'running'stopped_at IS NULL
Reconciliation Logic
For each stale session:
- Check PID liveness β Is the process still running?
- Verify process identity β Does
/proc/<pid>/cmdlinematch the expected binary? - Decision:
- PID alive + cmdline matches β reattach (keep as
running) - PID alive + cmdline mismatch β mark failed (different process reused PID)
- PID dead β mark failed
- PID alive + cmdline matches β reattach (keep as
Reattach Validation
#![allow(unused)]
fn main() {
fn validate_reattach(pid: i64, expected_binary: &Path) -> bool {
let cmdline_path = format!("/proc/{}/cmdline", pid);
let cmdline = std::fs::read_to_string(cmdline_path).ok()?;
cmdline.contains(expected_binary.to_str()?)
}
}
This prevents reattaching to a different process that happens to have the same PID.
IPC Client
CLI commands use an IPC client to communicate with the daemon.
Connection Retry
The client attempts to connect with retry:
#![allow(unused)]
fn main() {
async fn connect_with_retry(socket_path: &Path, max_attempts: u32) -> Result<UnixStream> {
for attempt in 1..=max_attempts {
match UnixStream::connect(socket_path).await {
Ok(stream) => return Ok(stream),
Err(_) if attempt < max_attempts => {
sleep(Duration::from_millis(100)).await;
}
Err(e) => return Err(e),
}
}
}
}
Error Handling
If the daemon is not running or unreachable:
Error: daemon socket not reachable at /home/user/.config/xrat/runtime/daemon.sock
Hint: start the daemon with 'xrat daemon start'
Daemon Integration
CLI Commands via IPC
When the daemon is running, these commands route through IPC:
| Command | IPC Request |
|---|---|
xrat connect <id> | RuntimeConnect |
xrat disconnect | RuntimeDisconnect |
xrat status | RuntimeStatus |
xrat proxy start | ProxyStart |
xrat proxy status | ProxyStatus |
xrat proxy rotate | RuntimeReplace |
xrat proxy stop | ProxyStop |
Daemon Required
Runtime and proxy commands require the daemon IPC path. If the daemon is not
running, xrat connect, xrat status, xrat disconnect, and xrat proxy ...
return a hint to start it:
xrat daemon start
Supervisor State
The daemon maintains internal state:
#![allow(unused)]
fn main() {
struct SupervisorState {
rotation_enabled: bool,
last_rotation_at: Option<DateTime<Utc>>,
last_trigger: Option<RotationTrigger>,
cooldown_until: Option<DateTime<Utc>>,
next_timer_at: Option<DateTime<Utc>>,
instance_id: String,
}
}
Instance ID
Each daemon instance has a unique ID (UUID v4) used to track session ownership:
#![allow(unused)]
fn main() {
let instance_id = Uuid::new_v4().to_string();
}
Sessions created by the daemon include:
{
"owner_kind": "daemon",
"owner_instance_id": "550e8400-e29b-41d4-a716-446655440000"
}
Logging
The daemon logs to stderr and optionally to a file:
xrat daemon start 2> daemon.log
Or with systemd:
[Service]
StandardOutput=journal
StandardError=journal
Log Levels
Control verbosity with RUST_LOG:
RUST_LOG=info xrat daemon start
RUST_LOG=debug xrat daemon start
Security Considerations
Socket Permissions
The Unix socket is created with default permissions (usually 0755). On
multi-user systems, consider restricting access:
chmod 700 ~/.config/xrat/runtime/daemon.sock
API Key
The daemon does not enforce authentication for IPC. Security relies on Unix socket permissions and filesystem access control.
For the HTTP API (if enabled), use the key field in config.toml for
authentication.
Troubleshooting
Daemon Not Starting
Symptom: xrat daemon start fails or exits immediately
Check:
- Is a daemon already running?
xrat daemon status - Check logs:
RUST_LOG=debug xrat daemon start - Verify socket directory exists and is writable
IPC Connection Failed
Symptom: CLI commands fail with βdaemon socket not reachableβ
Check:
- Is the daemon running?
ps aux | grep xrat - Does the socket file exist?
ls -la ~/.config/xrat/runtime/daemon.sock - Check socket permissions
Stale Sessions
Symptom: xrat status shows a session but no proxy is running
Fix:
xrat disconnect
xrat daemon stop
xrat daemon start
The daemon will reconcile stale sessions on startup.
Related
daemonCLI β command reference- Auto-Rotation β rotation scheduling
- Runtime Management β session lifecycle
Auto-Rotation
xrat supports automatic proxy rotation, periodically switching between configs based on a schedule, health checks, or manual triggers.
Overview
Auto-rotation is managed by the daemon supervisor. When enabled, the daemon:
- Periodically tests candidate configs
- Selects the best candidate based on latency
- Atomically disconnects the old session and connects the new one
- Respects cooldown periods to prevent rapid switching
Configuration
Enable and configure rotation in config.toml:
[runtime.rotation]
enabled = false
interval_secs = 1800
health_trigger_enabled = true
cooldown_secs = 300
test_concurrency = 0
test_stages = ["real_delay", "download"]
| Field | Description | Default |
|---|---|---|
enabled | Enable scheduled rotation | false |
interval_secs | Rotation interval in seconds | 1800 (30 minutes) |
health_trigger_enabled | Trigger rotation on health check failure | true |
cooldown_secs | Minimum time between rotations | 300 (5 minutes) |
test_concurrency | Concurrent test workers (0 = auto) | 0 |
test_stages | Test stages to run for candidate selection | ["real_delay", "download"] |
Rotation Triggers
Timer Trigger
Scheduled rotation every interval_secs:
interval_secs = 1800 # rotate every 30 minutes
The daemon maintains a timer that fires at the specified interval, triggering rotation to the best available candidate.
Health Check Trigger
Triggered when the active proxy fails a health check:
health_trigger_enabled = true
The daemon monitors proxy health every 15 seconds. If the health check fails (process dead, port unreachable), rotation is triggered immediately.
Manual Trigger
Triggered by the user via CLI:
xrat proxy rotate
Manual rotation bypasses the timer but respects cooldown.
Forced Rotation
Rotate to a specific config:
xrat proxy rotate --config-id 99
Skips candidate selection and rotates to the specified config.
Cooldown Protection
After a rotation, the daemon enforces a cooldown period:
cooldown_secs = 300 # 5 minutes
During cooldown:
- Timer triggers are delayed until cooldown expires
- Health check triggers are suppressed (unless critical)
- Manual triggers are allowed (user override)
Cooldown State
#![allow(unused)]
fn main() {
struct SupervisorState {
cooldown_until: Option<DateTime<Utc>>,
// ...
}
}
When a rotation completes:
#![allow(unused)]
fn main() {
state.cooldown_until = Some(Utc::now() + Duration::from_secs(cooldown_secs));
}
Candidate Selection
When rotating without --config-id, the daemon selects the best candidate:
Step 1: Load Candidates
Query enabled configs from the database:
SELECT * FROM configs
WHERE is_enabled = true
AND is_deleted = false
AND id != <current_config_id>
Step 2: Test Candidates
Run test stages on all candidates concurrently:
test_concurrency = 4 # test 4 configs at once
test_stages = ["real_delay", "download"]
For each candidate:
- Generate a probe config
- Spawn a short-lived Xray process
- Run the specified test stages
- Collect results (latency, throughput, success/failure)
- Terminate the probe process
Step 3: Filter Failures
Exclude configs that failed any test stage:
#![allow(unused)]
fn main() {
let successful = candidates.into_iter()
.filter(|c| c.real_delay_ok && c.download_ok)
.collect();
}
Step 4: Sort by Latency
Sort by real-delay latency (lowest first):
#![allow(unused)]
fn main() {
successful.sort_by_key(|c| c.real_delay_ms);
}
Step 5: Select Top Candidate
Pick the first config from the sorted list:
#![allow(unused)]
fn main() {
let best = successful.first()?;
}
If no candidates pass testing, rotation is skipped.
Rotation Flow
When rotation is triggered:
- Check cooldown β If active, delay or skip
- Select candidate β Run candidate selection (or use
--config-id) - Atomic replace β Disconnect old session, connect new session
- Update state β Record rotation timestamp, trigger type, new config ID
- Reset timer β Schedule next timer-based rotation
Atomic Replace
The replace flow ensures minimal downtime:
#![allow(unused)]
fn main() {
async fn replace_session(old_id: i64, new_config_id: i64) -> Result<()> {
// 1. Start new session
let new_session = connect(new_config_id).await?;
// 2. Wait for new session to be ready
wait_for_ready(new_session.socks_port).await?;
// 3. Stop old session
disconnect(old_id).await?;
Ok(())
}
}
The new session is started before the old session is stopped, ensuring continuous connectivity.
Rotation Status
Check rotation status:
xrat proxy status
Output:
Proxy Rotation Status
βββββββββββββββββββββββββββββββββ
Enabled: yes
Interval: 1800s
Last rotation: 2026-05-28 10:30:00 (manual)
Next rotation: 2026-05-28 11:00:00
Cooldown: 300s (inactive)
Active config: 42 (vless://example.com:443)
JSON Output
xrat proxy status --json
{
"enabled": true,
"interval_secs": 1800,
"last_trigger": "manual",
"last_rotation_at": "2026-05-28T10:30:00Z",
"next_rotation_at": "2026-05-28T11:00:00Z",
"cooldown_secs": 300,
"cooldown_active": false,
"active_config_id": 42
}
Enabling Rotation
Start Rotation
xrat proxy start
Sends a ProxyStart request to the daemon, which enables the rotation
scheduler.
Stop Rotation
xrat proxy stop
Sends a ProxyStop request to the daemon, which disables the rotation
scheduler. The active proxy session continues running.
Rotation Strategies
Conservative Strategy
Long intervals, strict testing, long cooldown:
[runtime.rotation]
enabled = true
interval_secs = 3600 # 1 hour
health_trigger_enabled = true
cooldown_secs = 600 # 10 minutes
test_stages = ["real_delay", "download"]
Best for: stable connections, minimal disruption
Aggressive Strategy
Short intervals, fast testing, short cooldown:
[runtime.rotation]
enabled = true
interval_secs = 300 # 5 minutes
health_trigger_enabled = true
cooldown_secs = 60 # 1 minute
test_stages = ["real_delay"]
Best for: finding the fastest proxy, frequent optimization
Health-Only Strategy
No scheduled rotation, only rotate on failure:
[runtime.rotation]
enabled = true
interval_secs = 86400 # 24 hours (effectively disabled)
health_trigger_enabled = true
cooldown_secs = 300
test_stages = ["real_delay"]
Best for: stable connections with automatic failover
Persistence
Rotation state is tracked in memory (not persisted to database). On daemon restart:
- Rotation is disabled (must be re-enabled with
xrat proxy start) - Cooldown is reset
- Timer is reset
The active session is persisted and reattached on daemon restart.
Troubleshooting
Rotation Not Triggering
Symptom: Timer fires but rotation doesnβt happen
Check:
- Is cooldown active?
xrat proxy status - Are there enabled configs?
xrat list configs --enabled-only - Do candidates pass testing?
xrat test --enabled-only
Rotation Fails
Symptom: Rotation triggers but new session fails to connect
Check:
- Test the target config manually:
xrat test <id> - Check daemon logs for errors
- Verify Xray binary is available
Rapid Rotation
Symptom: Proxy switches too frequently
Fix: Increase cooldown:
cooldown_secs = 600 # 10 minutes
Or disable health trigger:
health_trigger_enabled = false
Related
proxyCLI β command reference- Daemon and IPC β daemon supervisor
- Testing β test stages used for candidate selection
- Runtime Management β session lifecycle
IP Scanning
xrat includes a TCP reachability scanner for testing candidate IP addresses, useful for discovering fast CDN endpoints or Cloudflare edge IPs.
Overview
The scan command:
- Reads candidate IPs from CLI flags or a file
- Attempts TCP connection to each IP on a specified port
- Measures connection latency
- Persists results to the database
- Prints a summary with reachability and latency
Usage
xrat scan [flags]
Scan Specific IPs
xrat scan --ips 1.1.1.1,8.8.8.8,9.9.9.9
Scan from a File
xrat scan --file ./candidate-ips.txt
File format (one IP per line):
1.1.1.1
8.8.8.8
9.9.9.9
104.16.0.1
Custom Port and Timeout
xrat scan --ips 1.1.1.1,8.8.8.8 --port 8443 --timeout 2000
View History
xrat scan --history 20
Configuration
| Flag | Description | Default |
|---|---|---|
--ips <list> | Comma-separated IPs to scan | - |
--file <path> | File with newline-separated IPs | - |
--port <port> | Target TCP port | 443 |
--timeout <ms> | TCP connect timeout | 4000 |
--history <n> | Print latest N results and exit | - |
Scan Process
Step 1: Load IPs
Read IPs from --ips or --file:
#![allow(unused)]
fn main() {
let ips = if !args.ips.is_empty() {
args.ips.clone()
} else if let Some(file) = &args.file {
std::fs::read_to_string(file)?
.lines()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
} else {
return Err("no IPs provided");
};
}
Step 2: TCP Connect
For each IP, attempt a TCP connection:
#![allow(unused)]
fn main() {
async fn tcp_connect(ip: &str, port: u16, timeout: Duration) -> Result<Duration> {
let start = Instant::now();
let addr = format!("{}:{}", ip, port);
match timeout(timeout, TcpStream::connect(&addr)).await {
Ok(Ok(_)) => Ok(start.elapsed()),
Ok(Err(e)) => Err(e.into()),
Err(_) => Err(Error::Timeout),
}
}
}
Step 3: Measure Latency
Record the connection time in milliseconds:
#![allow(unused)]
fn main() {
let latency_ms = start.elapsed().as_millis() as u64;
}
Step 4: Persist Results
Insert or update the result in cf_scan_results:
INSERT INTO cf_scan_results (ip, latency_ms, error, last_scanned_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(ip) DO UPDATE SET
latency_ms = excluded.latency_ms,
error = excluded.error,
last_scanned_at = excluded.last_scanned_at
Step 5: Print Summary
IP Port Latency Status
1.1.1.1 443 12ms reachable
8.8.8.8 443 15ms reachable
9.9.9.9 443 - timeout
104.16.0.1 443 8ms reachable
Persistence
Scan results are persisted to the cf_scan_results table:
| Column | Type | Description |
|---|---|---|
id | INTEGER | Primary key |
ip | TEXT | IP address (unique) |
latency_ms | INTEGER | Connection latency (NULL if failed) |
error | TEXT | Error message (NULL if successful) |
last_scanned_at | TIMESTAMP | Last scan timestamp |
Upsert Behavior
Results are upserted (insert or update on conflict):
- New IP β insert new row
- Existing IP β update latency, error, timestamp
This allows tracking IP reachability over time.
History
View persisted scan results:
xrat scan --history 20
Output:
Scan History (latest 20)
βββββββββββββββββββββββββββββββββββββββββββββββββββββ
IP Latency Last Scanned
1.1.1.1 12ms 2026-05-28 10:30:00
8.8.8.8 15ms 2026-05-28 10:30:00
104.16.0.1 8ms 2026-05-28 10:30:00
9.9.9.9 - 2026-05-28 10:30:00 (timeout)
Results are sorted by last_scanned_at descending.
Use Cases
Cloudflare IP Scanning
Discover fast Cloudflare edge IPs:
# Generate candidate IPs from Cloudflare ranges
cat > cf-ips.txt <<EOF
1.1.1.1
1.0.0.1
104.16.0.1
104.17.0.1
EOF
# Scan on port 443
xrat scan --file cf-ips.txt --port 443 --timeout 3000
# View results
xrat scan --history 10
CDN Endpoint Testing
Test CDN nodes in your region:
xrat scan --ips 151.101.1.69,151.101.65.69,151.101.129.69 --port 443
Network Reconnaissance
Discover reachable IPs in a range:
# Generate IP range
for i in {1..254}; do echo "192.168.1.$i"; done > ips.txt
# Scan on custom port
xrat scan --file ips.txt --port 8443 --timeout 1000
Output Format
Success
IP Port Latency Status
1.1.1.1 443 12ms reachable
Failure
IP Port Latency Status
9.9.9.9 443 - timeout
Error Types
| Error | Description |
|---|---|
timeout | Connection timed out |
refused | Connection refused (port closed) |
unreachable | Network unreachable |
dns | DNS resolution failed (for hostnames) |
io | I/O error |
Performance
Concurrency
Scans are performed sequentially to avoid overwhelming the network. For large IP lists, consider splitting into multiple runs.
Timeout
Adjust timeout based on network conditions:
- Fast network:
--timeout 2000(2 seconds) - Slow network:
--timeout 5000(5 seconds) - High latency:
--timeout 10000(10 seconds)
Limitations
- TCP only: Does not support UDP or ICMP
- Single port: Scans one port at a time
- No authentication: Does not test proxy authentication
- Sequential: No parallel scanning (yet)
Related
scanCLI β command reference- Testing β test stored proxy configs (not raw IPs)
- Database Schema β
cf_scan_resultstable
HTTP API
xrat includes an Axum-based HTTP API server that exposes stored configs, test results, and subscription-compatible output for integration with external tools and clients.
Overview
The HTTP API provides:
- Health check β verify server is running
- Config listing β query configs with filters and pagination
- Subscription output β base64-encoded subscription text for mobile clients
- JSON export β machine-readable config data with test results
Starting the Server
Foreground Mode
xrat serve
Starts the server in the foreground using settings from config.toml.
Override Host and Port
xrat serve --host 0.0.0.0 --port 9090
Daemon-Hosted Mode
When enabled = true in config.toml, the daemon automatically starts the HTTP
API alongside IPC:
[server]
enabled = true
host = "127.0.0.1"
port = 8080
Configuration
[server]
enabled = false
host = "127.0.0.1"
port = 8080
key = { env = "XRAT_API_KEY" }
| Field | Description | Default |
|---|---|---|
enabled | Enable daemon-hosted API | false |
host | Bind host | 127.0.0.1 |
port | Bind port | 8080 |
key | Optional API key for authentication | - |
Routes
| Route | Method | Description | Auth Required |
|---|---|---|---|
/health | GET | Health check | No |
/json | GET | List configs as JSON array | Yes (if key set) |
/b64 | GET | Base64 subscription text | Yes (if key set) |
/configs | GET | Paginated config list | Yes (if key set) |
/configs/{id} | GET | Single config detail | Yes (if key set) |
Authentication
If key is set in config.toml, all routes except /health require the key
query parameter:
curl "http://localhost:8080/json?key=secret"
The key can be a literal string or an environment variable:
key = "literal-secret"
key = { env = "XRAT_API_KEY" }
Route Details
GET /health
Health check endpoint (no authentication required).
Request:
curl http://localhost:8080/health
Response:
{
"status": "ok"
}
Status: 200 OK
GET /json
List configs with latest test results as a JSON array.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
key | string | API key (if authentication enabled) |
top | integer | Return top N configs sorted by real-delay |
enabled | boolean | Filter: true for enabled configs only |
protocol | string | Filter by protocol: vless, vmess, ss, trojan, hy2 |
selected | boolean | Filter: true for selected configs only |
Request:
curl "http://localhost:8080/json?key=secret&top=10&enabled=true"
Response:
[
{
"id": 42,
"protocol": "vless",
"address": "example.com",
"port": 443,
"name": "My Node",
"is_enabled": true,
"is_active": false,
"is_selected": false,
"latest_test": {
"icmp_ok": true,
"icmp_ms": 15,
"tcp_ok": true,
"tcp_ms": 12,
"real_delay_ok": true,
"real_delay_ms": 145,
"download_mbps": null,
"tested_at": "2026-05-28T10:00:00Z"
}
},
{
"id": 43,
"protocol": "vmess",
"address": "edge.com",
"port": 8443,
"name": "Edge Node",
"is_enabled": true,
"is_active": false,
"is_selected": false,
"latest_test": {
"icmp_ok": true,
"icmp_ms": 18,
"tcp_ok": true,
"tcp_ms": 14,
"real_delay_ok": true,
"real_delay_ms": 162,
"download_mbps": null,
"tested_at": "2026-05-28T10:00:00Z"
}
}
]
Status: 200 OK
GET /b64
Base64-encoded subscription text compatible with v2rayN, Clash, and other clients.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
key | string | API key (if authentication enabled) |
Request:
curl "http://localhost:8080/b64?key=secret"
Response:
dmxlc3M6Ly91dWlkQGV4YW1wbGUuY29tOjQ0Mz90eXBlPXdzJnNlY3VyaXR5PXRscyNNeSBOb2RlCnZtZXNzOi8v...
Status: 200 OK
Content-Type: text/plain
The response is a base64-encoded string containing one share link per line:
vless://uuid@example.com:443?type=ws&security=tls#My Node
vmess://eyJhZGQiOiJlZGdlLmNvbSIsInBvcnQiOiI4NDQzIn0=#Edge Node
GET /configs
Paginated config list with details.
Query Parameters:
| Parameter | Type | Description | Default |
|---|---|---|---|
key | string | API key (if authentication enabled) | - |
page | integer | Page number | 1 |
per_page | integer | Items per page | 20 |
enabled | boolean | Filter: true for enabled configs only | - |
protocol | string | Filter by protocol | - |
selected | boolean | Filter: true for selected configs only | - |
Request:
curl "http://localhost:8080/configs?key=secret&page=1&per_page=10&enabled=true"
Response:
{
"page": 1,
"per_page": 10,
"total": 150,
"configs": [
{
"id": 42,
"protocol": "vless",
"address": "example.com",
"port": 443,
"uuid": "uuid-123",
"network": "ws",
"tls": "tls",
"sni": "cdn.example.com",
"host": "cdn.example.com",
"path": "/ray",
"name": "My Node",
"is_enabled": true,
"is_active": false,
"is_selected": false,
"subscription_id": 1,
"created_at": "2026-05-20T08:00:00Z",
"updated_at": "2026-05-28T10:00:00Z",
"latest_test": {
"icmp_ok": true,
"icmp_ms": 15,
"tcp_ok": true,
"tcp_ms": 12,
"real_delay_ok": true,
"real_delay_ms": 145,
"download_mbps": null,
"tested_at": "2026-05-28T10:00:00Z"
}
}
]
}
Status: 200 OK
GET /configs/
Single config detail with latest test results.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | integer | Config ID |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
key | string | API key (if authentication enabled) |
Request:
curl "http://localhost:8080/configs/42?key=secret"
Response:
{
"id": 42,
"protocol": "vless",
"address": "example.com",
"port": 443,
"uuid": "uuid-123",
"password": null,
"method": null,
"network": "ws",
"tls": "tls",
"sni": "cdn.example.com",
"host": "cdn.example.com",
"path": "/ray",
"name": "My Node",
"is_enabled": true,
"is_active": false,
"is_selected": false,
"subscription_id": 1,
"created_at": "2026-05-20T08:00:00Z",
"updated_at": "2026-05-28T10:00:00Z",
"latest_test": {
"icmp_ok": true,
"icmp_ms": 15,
"tcp_ok": true,
"tcp_ms": 12,
"real_delay_ok": true,
"real_delay_ms": 145,
"connect_ms": 10,
"ttfb_ms": 120,
"http_status": 204,
"download_mbps": null,
"upload_mbps": null,
"failure_kind": null,
"failure_reason": null,
"endpoint_ip": "93.184.216.34",
"endpoint_country": "US",
"endpoint_asn": "AS15133",
"tested_at": "2026-05-28T10:00:00Z"
}
}
Status: 200 OK
Error Response (config not found):
{
"error": "config not found",
"id": 999
}
Status: 404 Not Found
Error Responses
Authentication Failed
{
"error": "unauthorized"
}
Status: 401 Unauthorized
Not Found
{
"error": "config not found",
"id": 999
}
Status: 404 Not Found
Internal Error
{
"error": "internal server error"
}
Status: 500 Internal Server Error
Use Cases
Subscription Server
Serve configs to mobile/desktop clients:
# v2rayN / Clash
curl "http://localhost:8080/b64?key=secret" > subscription.txt
Configure clients to fetch from http://your-server:8080/b64?key=secret.
Monitoring
Poll health and config status:
# Health check
curl http://localhost:8080/health
# Active configs
curl "http://localhost:8080/configs?key=secret&enabled=true"
Dashboard Integration
Build a web dashboard that queries the API:
fetch("http://localhost:8080/configs?key=secret&page=1&per_page=20")
.then((r) => r.json())
.then((data) => {
console.log(`Total configs: ${data.total}`);
data.configs.forEach((c) => {
console.log(`${c.name}: ${c.latest_test?.real_delay_ms}ms`);
});
});
Automation
Script proxy management:
# Get top 5 configs by latency
TOP=$(curl -s "http://localhost:8080/json?key=secret&top=5&enabled=true")
# Extract config IDs
IDS=$(echo "$TOP" | jq -r '.[].id')
# Test each config
for id in $IDS; do
xrat test $id
done
Security Considerations
Bind Address
By default, the server binds to 127.0.0.1 (localhost only). To expose
externally:
[server]
host = "0.0.0.0"
Warning: Only expose externally if authentication is enabled and the network is trusted.
API Key
Use a strong, random API key:
# Generate a random key
openssl rand -hex 32
# Set in config.toml
key = "a1b2c3d4e5f6..."
Or use an environment variable:
key = { env = "XRAT_API_KEY" }
export XRAT_API_KEY=$(openssl rand -hex 32)
xrat serve
HTTPS
The server does not support HTTPS natively. Use a reverse proxy (nginx, Caddy) for TLS termination:
server {
listen 443 ssl;
server_name xrat.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
}
}
systemd Service
Run as a systemd user service:
[Unit]
Description=xrat HTTP API
After=network.target
[Service]
ExecStart=/usr/local/bin/xrat serve
Restart=on-failure
Environment=RUST_LOG=info
Environment=XRAT_API_KEY=your-secret-key
[Install]
WantedBy=default.target
Enable and start:
systemctl --user daemon-reload
systemctl --user enable xrat-api
systemctl --user start xrat-api
Related
serveCLI β command reference- Daemon and IPC β daemon-hosted API mode
- Deployment β systemd service examples
Deduplication
xrat uses versioned, length-prefixed dedup keys to ensure config uniqueness
while distinguishing between None and empty string values.
Overview
When importing configs, xrat:
- Generates a dedup key for each node
- Checks if a config with the same key already exists
- Skips duplicates, only inserting new configs
This prevents the same proxy from being imported multiple times, even across different subscriptions.
Dedup Key Format
The dedup key is a string with the following format:
v1|protocol=<len>:<value>|address=<len>:<value>|port=<len>:<value>|...
Structure
- Version prefix:
v1(allows future format changes) - Field separator:
| - Field format:
<name>=<length>:<value>or<name>=-(for None)
Fields
| Field | Type | Description |
|---|---|---|
protocol | required | Protocol name (vless, vmess, ss, etc.) |
address | required | Server address |
port | required | Server port |
username | optional | Username (HTTP/SOCKS5) |
uuid | optional | UUID (VLESS/VMess) |
password | optional | Password (Trojan/SS/SOCKS5) |
method | optional | Encryption method (Shadowsocks) |
network | required | Network type (tcp, ws, grpc) |
tls | optional | TLS mode (tls, none) |
sni | optional | SNI hostname |
host | optional | Host header (WebSocket) |
path | optional | Path (WebSocket/gRPC) |
Length Prefix
Each value is prefixed with its character count:
protocol=5:vless
address=11:example.com
port=3:443
This prevents ambiguity when values contain the separator character (|).
None vs Empty String
- None: Represented as
- - Empty string: Represented as
0:
Example:
username=- # None
username=0: # Empty string
username=4:user # "user"
This distinction is important because some protocols treat None and empty string differently.
Example Keys
VLESS with WebSocket + TLS
v1|protocol=5:vless|address=11:example.com|port=3:443|username=-|uuid=8:uuid-123|password=-|method=-|network=2:ws|tls=3:tls|sni=15:cdn.example.com|host=15:cdn.example.com|path=4:/ray
VMess with TCP
v1|protocol=5:vmess|address=14:vmess.example.com|port=4:8443|username=-|uuid=8:uuid-456|password=-|method=-|network=3:tcp|tls=3:tls|sni=-|host=-|path=-
Shadowsocks
v1|protocol=2:ss|address=11:example.com|port=4:8388|username=-|uuid=-|password=6:secret|method=11:aes-256-gcm|network=3:tcp|tls=-|sni=-|host=-|path=-
Trojan
v1|protocol=6:trojan|address=11:example.com|port=3:443|username=-|uuid=-|password=8:password|method=-|network=3:tcp|tls=3:tls|sni=-|host=-|path=-
Generation
The dedup key is generated from the Node struct:
#![allow(unused)]
fn main() {
impl Node {
pub fn dedup_key(&self) -> NodeDedupKey {
NodeDedupKey {
protocol: self.protocol.clone(),
address: self.address.clone(),
port: self.port,
username: self.username.clone(),
uuid: self.uuid.clone(),
password: self.password.clone(),
method: self.method.clone(),
network: self.network.clone(),
tls: self.tls.clone(),
sni: self.sni.clone(),
host: self.host.clone(),
path: self.path.clone(),
}
}
pub fn dedup_key_string(&self) -> String {
self.dedup_key().to_string()
}
}
}
Formatting
#![allow(unused)]
fn main() {
impl fmt::Display for NodeDedupKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("v1")?;
write_required(f, "protocol", self.protocol.as_str())?;
write_required(f, "address", &self.address)?;
write_required(f, "port", &self.port.to_string())?;
write_optional(f, "username", self.username.as_deref())?;
write_optional(f, "uuid", self.uuid.as_deref())?;
write_optional(f, "password", self.password.as_deref())?;
write_optional(f, "method", self.method.as_deref())?;
write_required(f, "network", &self.network)?;
write_optional(f, "tls", self.tls.as_deref())?;
write_optional(f, "sni", self.sni.as_deref())?;
write_optional(f, "host", self.host.as_deref())?;
write_optional(f, "path", self.path.as_deref())?;
Ok(())
}
}
fn write_required(f: &mut fmt::Formatter<'_>, name: &str, value: &str) -> fmt::Result {
write!(f, "|{}={}:{}", name, value.chars().count(), value)
}
fn write_optional(f: &mut fmt::Formatter<'_>, name: &str, value: Option<&str>) -> fmt::Result {
match value {
Some(value) => write_required(f, name, value),
None => write!(f, "|{}=-", name),
}
}
}
Database Storage
The dedup key is stored in the configs table:
CREATE TABLE configs (
id INTEGER PRIMARY KEY,
subscription_id INTEGER,
dedup_key TEXT NOT NULL UNIQUE,
protocol TEXT NOT NULL,
address TEXT NOT NULL,
port INTEGER NOT NULL,
-- ... other fields
);
The UNIQUE constraint on dedup_key enforces uniqueness at the database
level.
Import Behavior
When importing configs:
#![allow(unused)]
fn main() {
pub async fn import_nodes(&self, nodes: Vec<Node>, subscription_id: i64) -> Result<ImportSummary> {
let mut inserted = 0;
let mut duplicates = 0;
for node in nodes {
let dedup_key = node.dedup_key_string();
match self.insert_config(&node, subscription_id, &dedup_key).await {
Ok(_) => inserted += 1,
Err(DbError::UniqueViolation) => duplicates += 1,
Err(e) => return Err(e),
}
}
Ok(ImportSummary { inserted, duplicates })
}
}
Unique Violation
If a config with the same dedup key already exists, the database returns a unique violation error, which is caught and counted as a duplicate.
Edge Cases
Different Names, Same Config
Two configs with different display names but identical connection parameters are considered duplicates:
vless://uuid@example.com:443?type=ws#Node1
vless://uuid@example.com:443?type=ws#Node2
Both generate the same dedup key (name is not included), so only one is imported.
None vs Empty String
These are treated as different:
#![allow(unused)]
fn main() {
let key1 = NodeDedupKey { password: None, .. };
let key2 = NodeDedupKey { password: Some("".to_string()), .. };
assert_ne!(key1.to_string(), key2.to_string());
}
Key 1: |password=- Key 2: |password=0:
Protocol Differences
Different protocols with the same address/port are not duplicates:
vless://uuid@example.com:443
vmess://uuid@example.com:443
Different protocol field β different dedup keys.
Versioning
The v1 prefix allows future format changes without breaking existing data:
- v1: Current format (length-prefixed)
- v2: Future format (if needed)
When reading dedup keys, xrat checks the version prefix and handles each version appropriately.
Testing
xrat includes tests to verify dedup behavior:
#![allow(unused)]
fn main() {
#[test]
fn distinguishes_none_from_empty_string() {
let none_key = NodeDedupKey {
protocol: Protocol::Ss,
address: "example.com".to_string(),
port: 8388,
username: None,
uuid: None,
password: None,
method: None,
network: "tcp".to_string(),
tls: None,
sni: None,
host: None,
path: None,
};
let empty_key = NodeDedupKey {
password: Some(String::new()),
..none_key.clone()
};
assert_ne!(none_key.to_string(), empty_key.to_string());
assert!(empty_key.to_string().contains("|password=0:"));
}
}
Performance
Dedup key generation is fast:
- String formatting: ~1-2 microseconds per key
- Database lookup: indexed on
dedup_keycolumn - Import performance: ~1000 configs/second (including dedup)
Related
- Importing β how dedup is used during import
- Database Schema β
configstable - Protocols β supported protocols
Deployment
xrat can be deployed in various configurations, from single-user desktop setups to multi-user server deployments with PostgreSQL.
Deployment Options
| Option | Description | Use Case |
|---|---|---|
| systemd | Run as a systemd user service | Persistent daemon, auto-start on boot |
| Database Backends | SQLite vs PostgreSQL | Single-user vs multi-user deployments |
Quick Deployment Checklist
- Build xrat:
cargo build --release - Install binary: Copy
target/release/xratto/usr/local/bin/ - Create config directory:
mkdir -p ~/.config/xrat - Write config.toml: Configure database, runtime, testing settings
- Import subscriptions:
xrat import https://example.com/sub.txt - Test configs:
xrat test --enabled-only - Start daemon:
xrat daemon startor use systemd - Enable rotation (optional):
xrat proxy start - Start HTTP API (optional):
xrat serveor enable in daemon
Environment Variables
xrat respects these environment variables:
| Variable | Description |
|---|---|
XRAT_PATH | Config directory path (default: ~/.config/xrat) |
RUST_LOG | Log level (overrides --verbose/--quiet) |
XRAT_API_KEY | HTTP API authentication key |
XRAT_SOCKS_PASSWORD | SOCKS inbound password |
XRAT_SHADOWSOCKS_PASSWORD | Shadowsocks inbound password |
XRAT_POSTGRES_USER | PostgreSQL username |
XRAT_POSTGRES_PASSWORD | PostgreSQL password |
Binary Dependencies
xrat requires external proxy binaries:
| Binary | Required For | Installation |
|---|---|---|
xray | Managed runtime, most parse/test/generate flows | Xray-core releases |
v2ray | Alternative managed runtime binary | V2Ray releases |
sing-box | Parse-time sing-box JSON preview and Hysteria2 diagnostics only | sing-box releases |
Ensure binaries are in PATH or specify paths in config.toml:
[paths]
xray = "/usr/local/bin/xray"
v2ray = "/usr/local/bin/v2ray"
sing_box = "/usr/local/bin/sing-box"
Managed runtime process lifecycle is Xray/V2Ray-focused. sing-box is not yet a
managed runtime replacement for xrat connect.
Security Considerations
File Permissions
Restrict access to config directory:
chmod 700 ~/.config/xrat
chmod 600 ~/.config/xrat/config.toml
chmod 600 ~/.config/xrat/db.sqlite
Network Exposure
By default, xrat binds to:
- SOCKS5:
0.0.0.0:1080(all interfaces) - HTTP API:
127.0.0.1:8080(localhost only)
To restrict SOCKS5 to localhost:
[runtime.socks]
host = "127.0.0.1"
To expose HTTP API externally (with authentication):
[server]
host = "0.0.0.0"
port = 8080
key = { env = "XRAT_API_KEY" }
Secrets Management
Use environment variables for sensitive values:
[server]
key = { env = "XRAT_API_KEY" }
[runtime.socks]
auth = { enabled = true, username = "xrat", password = { env = "XRAT_SOCKS_PASSWORD" } }
Set in shell profile or systemd service:
export XRAT_API_KEY=$(openssl rand -hex 32)
export XRAT_SOCKS_PASSWORD=$(openssl rand -hex 16)
Monitoring
Health Checks
Use the HTTP API for monitoring:
curl http://localhost:8080/health
Logs
View daemon logs:
journalctl --user -u xrat-daemon -f
Or with direct execution:
RUST_LOG=info xrat daemon start 2> daemon.log
Process Monitoring
Check if daemon is running:
xrat daemon status
ps aux | grep xrat
Backup and Recovery
SQLite
Backup the database file:
cp ~/.config/xrat/db.sqlite ~/backup/db.sqlite.$(date +%Y%m%d)
PostgreSQL
Use pg_dump:
pg_dump xrat > ~/backup/xrat.$(date +%Y%m%d).sql
Config Files
Backup config directory:
tar czf ~/backup/xrat-config.$(date +%Y%m%d).tar.gz ~/.config/xrat/
Troubleshooting
Daemon Wonβt Start
Check:
- Is a daemon already running?
xrat daemon status - Check logs:
RUST_LOG=debug xrat daemon start - Verify socket directory is writable
Connection Failed
Check:
- Is Xray binary available?
which xray - Test config manually:
xrat test <id> - Check runtime logs:
~/.config/xrat/runtime/session-*.err.log
Database Locked (SQLite)
Symptom: βdatabase is lockedβ errors
Fix:
- Only one process can write to SQLite at a time
- Use PostgreSQL for multi-user deployments
- Increase busy timeout in config.toml (if supported)
Related
- systemd β systemd service examples
- Database Backends β SQLite vs PostgreSQL
- Configuration β config.toml reference
systemd Services
Run xrat as a systemd user service for persistent operation and automatic startup on login.
systemd user services run under your user account (not root) and are managed
with systemctl --user.
Benefits:
- Auto-start: Service starts on login
- Restart on failure: Automatically restarts if the process crashes
- Logging: Integrated with
journalctl
Installation
Use xrat daemon install to generate and enable the service automatically. This
is the recommended approach β no manual file editing required.
xrat daemon install
To also start the daemon immediately:
xrat daemon install --start
To install the standalone HTTP API service alongside the daemon:
xrat daemon install --with-api
To preview what would be written without making changes:
xrat daemon install --dry-run
The command:
- Resolves the current binary path
- Generates
xrat-daemon.servicewith the correctExecStartandXRAT_PATH - Writes to
~/.config/systemd/user/(respects$XDG_CONFIG_HOME) - Runs
systemctl --user daemon-reload - Runs
systemctl --user enable xrat-daemon.service
Removal
xrat daemon uninstall
Stops, disables, and removes the service file. All user config, database, logs, and application state are preserved.
Preview first:
xrat daemon uninstall --dry-run
Management
systemctl --user start xrat-daemon
systemctl --user stop xrat-daemon
systemctl --user restart xrat-daemon
systemctl --user status xrat-daemon
View logs:
journalctl --user -u xrat-daemon -f
journalctl --user -u xrat-daemon --since today
journalctl --user -u xrat-daemon -n 100
Lingering
By default, user services stop when you log out. To keep the daemon running without an active login session (useful on servers):
loginctl enable-linger $USER
Environment Variables
The generated service file sets XRAT_PATH and RUST_LOG=info. To add secrets
(API key, passwords), use an environment file.
Create ~/.config/xrat/env:
XRAT_API_KEY=your-secret-key
XRAT_POSTGRES_PASSWORD=your-db-password
Then add to the service unit after running daemon install:
[Service]
EnvironmentFile=%h/.config/xrat/env
Troubleshooting
Service wonβt start:
systemctl --user status xrat-daemon
journalctl --user -u xrat-daemon -n 50
Common causes: binary not found (check ExecStart path), port already in use,
config parse error (test with xrat daemon start manually).
Service stops unexpectedly:
journalctl --user -u xrat-daemon --since "1 hour ago"
Logs not appearing: ensure Environment=RUST_LOG=info is set in the unit.
Reference: Manual Setup
If you cannot use xrat daemon install (e.g., the binary is not yet in PATH),
you can create the service file manually.
mkdir -p ~/.config/systemd/user
~/.config/systemd/user/xrat-daemon.service:
[Unit]
Description=XRAT Daemon
After=network.target
[Service]
Type=simple
ExecStart=/path/to/xrat daemon run-server
Restart=on-failure
RestartSec=5
Environment=XRAT_PATH=/home/user/.config/xrat
Environment=XRAT_API_KEY=
Environment=RUST_LOG=info
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/home/user/.config/xrat
PrivateTmp=true
[Install]
WantedBy=default.target
Replace /path/to/xrat with the actual binary location. Then:
systemctl --user daemon-reload
systemctl --user enable xrat-daemon.service
systemctl --user start xrat-daemon.service
The template files used by xrat daemon install are available in the repository
at packaging/systemd/.
Related
daemonβ daemon CLI reference including install/uninstall- Deployment β deployment overview
- HTTP API β API server details
- Daemon and IPC β daemon supervisor internals
Database Backends
xrat supports both SQLite and PostgreSQL as database backends, allowing flexibility from single-user desktop deployments to multi-user server setups.
Overview
| Backend | Use Case | Concurrency | Setup Complexity |
|---|---|---|---|
| SQLite | Single-user, desktop, testing | Single writer | Zero configuration |
| PostgreSQL | Multi-user, production, high concurrency | Connection pooling | Requires server |
Both backends use the same schema and support all xrat features.
Configuration
Configure the database backend in config.toml:
[database]
backend = "sqlite" # "sqlite" | "postgres"
[database.sqlite]
path = "db.sqlite"
[database.postgres]
user = { env = "XRAT_POSTGRES_USER" }
password = { env = "XRAT_POSTGRES_PASSWORD" }
host = "localhost"
port = 5432
db_name = "xrat"
max_connections = 10
min_connections = 1
connect_timeout_secs = 10
SQLite
SQLite is the default backend, ideal for single-user deployments.
Advantages
- Zero configuration: No server setup required
- Single file: Database is a single file on disk
- Portable: Easy to backup and move
- Fast: Excellent read performance
Limitations
- Single writer: Only one process can write at a time
- No concurrent access: Not suitable for multi-user deployments
- File locking: βdatabase is lockedβ errors under high concurrency
Configuration
[database]
backend = "sqlite"
[database.sqlite]
path = "db.sqlite" # relative to config directory or absolute
File Location
The database file is resolved in this order:
--database <path>CLI flag[database.sqlite].pathin config.toml[paths].databasein config.toml (deprecated)XRAT_PATH/db.sqlite~/.config/xrat/db.sqlite
Backup
Backup the database file:
cp ~/.config/xrat/db.sqlite ~/backup/db.sqlite.$(date +%Y%m%d)
Performance Tuning
For better write performance, consider:
- WAL mode: Enabled by default in xrat
- Busy timeout: Configured internally (5 seconds)
- Indexing: Automatic on frequently queried columns
Troubleshooting
βdatabase is lockedβ errors:
- Only one process can write to SQLite at a time
- Ensure no other xrat processes are running
- Consider PostgreSQL for multi-user deployments
PostgreSQL
PostgreSQL is recommended for multi-user deployments and high concurrency.
Advantages
- Concurrent access: Multiple readers and writers
- Connection pooling: Efficient connection management
- Scalability: Handles large datasets and high traffic
- Reliability: ACID compliance, crash recovery
Limitations
- Server required: Must install and configure PostgreSQL
- Network overhead: Slightly slower than SQLite for single-user
- Complexity: More setup and maintenance
Installation
Install PostgreSQL:
Ubuntu/Debian:
sudo apt install postgresql postgresql-contrib
macOS:
brew install postgresql
Docker:
docker run -d \
--name xrat-postgres \
-e POSTGRES_USER=xrat \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=xrat \
-p 5432:5432 \
postgres:15
Setup
- Create database and user:
sudo -u postgres psql
CREATE USER xrat WITH PASSWORD 'your-password';
CREATE DATABASE xrat OWNER xrat;
GRANT ALL PRIVILEGES ON DATABASE xrat TO xrat;
\q
- Configure xrat:
[database]
backend = "postgres"
[database.postgres]
user = "xrat"
password = "your-password"
host = "localhost"
port = 5432
db_name = "xrat"
max_connections = 10
min_connections = 1
connect_timeout_secs = 10
- Use environment variables (recommended):
[database.postgres]
user = { env = "XRAT_POSTGRES_USER" }
password = { env = "XRAT_POSTGRES_PASSWORD" }
host = "localhost"
port = 5432
db_name = "xrat"
export XRAT_POSTGRES_USER=xrat
export XRAT_POSTGRES_PASSWORD=your-password
xrat import https://example.com/sub.txt
Connection Pooling
xrat uses a connection pool for PostgreSQL:
| Setting | Description | Default |
|---|---|---|
max_connections | Maximum pool size | 10 |
min_connections | Minimum idle connections | 1 |
connect_timeout_secs | Connection timeout | 10 |
Tune based on your workload:
- Low traffic:
max_connections = 5 - Medium traffic:
max_connections = 10 - High traffic:
max_connections = 20-50
Backup
Use pg_dump for backups:
# Full backup
pg_dump xrat > ~/backup/xrat.$(date +%Y%m%d).sql
# Compressed backup
pg_dump -Fc xrat > ~/backup/xrat.$(date +%Y%m%d).dump
# Restore
pg_restore -d xrat ~/backup/xrat.20260528.dump
Performance Tuning
PostgreSQL configuration (postgresql.conf):
# Memory
shared_buffers = 256MB
effective_cache_size = 1GB
work_mem = 16MB
# WAL
wal_level = replica
max_wal_size = 2GB
# Connections
max_connections = 100
Indexing: xrat automatically creates indexes on:
configs.dedup_key(unique)configs.subscription_idconnection_tests.config_idconnection_tests.run_idruntime_sessions.config_id
High Availability
For production deployments, consider:
- Replication: Streaming replication for read replicas
- Connection pooling: PgBouncer or Pgpool-II
- Monitoring: pg_stat_statements, Prometheus exporter
- Backups: Automated daily backups with WAL archiving
Schema Migrations
xrat uses SQLx for schema migrations. Migrations run automatically on startup:
#![allow(unused)]
fn main() {
sqlx::migrate!("./migrations/sqlite").run(&pool).await?;
}
Migration Files
Located in migrations/sqlite/ and migrations/postgres/:
0001_init.sql
0002_add_connection_test_download_mbps.sql
0003_canonical_config_dedup_key.sql
...
0015_add_config_soft_delete.sql
Manual Migration
If migrations fail, run manually:
# SQLite
sqlite3 ~/.config/xrat/db.sqlite < migrations/sqlite/0001_init.sql
# PostgreSQL
psql xrat < migrations/postgres/0001_init.sql
Switching Backends
To switch from SQLite to PostgreSQL:
- Export data from SQLite:
sqlite3 ~/.config/xrat/db.sqlite .dump > xrat-data.sql
- Convert SQL (SQLite β PostgreSQL syntax):
# Manual conversion or use tools like pgloader
pgloader sqlite:///path/to/db.sqlite postgresql://xrat:password@localhost/xrat
- Update config.toml:
[database]
backend = "postgres"
- Import data:
psql xrat < xrat-data-converted.sql
Monitoring
SQLite
Check database size:
ls -lh ~/.config/xrat/db.sqlite
Check integrity:
sqlite3 ~/.config/xrat/db.sqlite "PRAGMA integrity_check;"
PostgreSQL
Check connection count:
SELECT count(*) FROM pg_stat_activity WHERE datname = 'xrat';
Check table sizes:
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
Check slow queries:
SELECT query, calls, total_time, mean_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;
Security
SQLite
- File permissions: Restrict access to database file
chmod 600 ~/.config/xrat/db.sqlite
PostgreSQL
- Authentication: Use strong passwords
- SSL: Enable SSL for remote connections
- Firewall: Restrict access to PostgreSQL port (5432)
- User permissions: Use dedicated user with minimal privileges
-- Read-only user for monitoring
CREATE USER xrat_read WITH PASSWORD 'read-password';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO xrat_read;
Related
- Deployment β deployment overview
- Database Schema β table definitions
- Configuration β config.toml reference
Reference
This section provides lookup material for xratβs configuration, protocols, database schema, and error codes.
Pages
| Page | Description |
|---|---|
| Protocols | Supported protocols, URI schemes, and engine routing |
| Config File | Full config.toml reference with all fields and defaults |
| Database Schema | Table definitions, columns, and migrations |
| Error Codes | AppError variants and FailureKind categories |
Protocols
xrat supports 7 proxy protocols, each with specific URI formats, configuration fields, and engine routing.
Supported Protocols
| Protocol | URI Scheme | Xray | sing-box | Parser |
|---|---|---|---|---|
| VLESS | vless:// | Yes | No | Yes |
| VMess | vmess:// | Yes | No | Yes |
| Shadowsocks | ss:// | Yes | No | Yes |
| Trojan | trojan:// | Yes | No | Yes |
| HTTP | http:// / https:// | Yes | No | Yes |
| SOCKS5 | socks5:// | Yes | No | Yes |
| Hysteria2 | hysteria2:// / hy2:// | No | Yes | Yes |
VLESS
Modern, lightweight protocol from the Xray project.
Scheme: vless://
Format:
vless://<uuid>@<address>:<port>?type=<network>&security=<tls>&sni=<sni>&host=<host>&path=<path>#<name>
Fields:
| Field | Location | Required | Description |
|---|---|---|---|
uuid | userinfo | Yes | VLESS user ID |
address | host | Yes | Server address |
port | port | Yes | Server port |
type | query | No | Network type (tcp, ws, grpc), default tcp |
security | query | No | TLS mode (tls, none), default none |
sni | query | No | SNI hostname |
host | query | No | Host header (WebSocket) |
path | query | No | Path (WebSocket, gRPC, TCP) |
name | fragment | No | Display name |
Examples:
vless://uuid-123@example.com:443?type=ws&security=tls&sni=cdn.example.com&path=%2Fray#My%20Node
vless://uuid-456@example.com:443?type=tcp#Direct
vless://uuid-789@example.com:8443?type=grpc&serviceName=service#gRPC%20Node
Engine: Xray (auto), Xray (explicit)
VMess
Legacy protocol with encryption, from V2Ray.
Scheme: vmess://
Format:
vmess://<base64-json>
Base64 JSON Fields:
{
"add": "example.com",
"port": "443",
"id": "uuid-456",
"net": "ws",
"tls": "tls",
"sni": "edge.example.com",
"host": "host.example.com",
"path": "/vmess",
"ps": "VMess Node"
}
| Field | Key | Required | Description |
|---|---|---|---|
add | JSON | Yes | Server address |
port | JSON | Yes | Server port |
id | JSON | No | UUID |
net | JSON | No | Network type (tcp, ws), default tcp |
tls | JSON | No | TLS mode (tls) |
sni | JSON | No | SNI hostname |
host | JSON | No | Host header (WebSocket) |
path | JSON | No | Path (WebSocket) |
ps | JSON | No | Display name |
Example:
vmess://eyJhZGQiOiJleGFtcGxlLmNvbSIsInBvcnQiOiI0NDMiLCJpZCI6InV1aWQtNDU2IiwibmV0Ijoid3MiLCJ0bHMiOiJ0bHMiLCJzbmkiOiJlZGdlLmV4YW1wbGUuY29tIiwiaG9zdCI6Imhvc3QuZXhhbXBsZS5jb20iLCJwYXRoIjoiL3ZtZXNzIiwicHMiOiJWTWVzcyBOb2RlIn0=
Engine: Xray (auto), Xray (explicit)
Shadowsocks
Simple, secure proxy protocol.
Scheme: ss://
Format:
ss://<base64(method:password)>@<address>:<port>#<name>
Fields:
| Field | Location | Required | Description |
|---|---|---|---|
method | base64 userinfo | Yes | Encryption method |
password | base64 userinfo | Yes | Password |
address | host | Yes | Server address |
port | port | Yes | Server port |
name | fragment | No | Display name |
Encryption methods: aes-128-gcm, aes-256-gcm, chacha20-ietf-poly1305,
xchacha20-ietf-poly1305, aes-128-cfb, aes-256-cfb, rc4-md5
Example:
ss://YWVzLTI1Ni1nY206c2VjcmV0@example.com:8388#SS%20Node
Engine: Xray (auto), Xray (explicit)
Trojan
TLS-based proxy that mimics HTTPS traffic.
Scheme: trojan://
Format:
trojan://<password>@<address>:<port>?type=<network>&sni=<sni>&host=<host>&path=<path>#<name>
Fields:
| Field | Location | Required | Description |
|---|---|---|---|
password | userinfo | Yes | Trojan password |
address | host | Yes | Server address |
port | port | Yes | Server port |
type | query | No | Network type (tcp, ws, grpc), default tcp |
sni | query | No | SNI hostname |
host | query | No | Host header (WebSocket) |
path | query | No | Path (WebSocket, gRPC) |
name | fragment | No | Display name |
Default TLS: Trojan always uses TLS (security=tls is added automatically).
Examples:
trojan://password@example.com:443?type=ws&sni=cdn.example.com&path=%2Ftrojan#Trojan%20Node
Engine: Xray (auto), Xray (explicit)
HTTP
Standard HTTP/HTTPS proxy.
Scheme: http:// / https://
Format:
http://<username>:<password>@<address>:<port>#<name>
https://<username>:<password>@<address>:<port>#<name>
Fields:
| Field | Location | Required | Description |
|---|---|---|---|
username | userinfo | No | Username |
password | userinfo | No | Password |
address | host | Yes | Server address |
port | port | Yes | Server port |
name | fragment | No | Display name |
TLS: https:// scheme automatically sets tls=tls.
Examples:
http://user:pass@example.com:8080#HTTP%20Node
https://example.com:443#HTTPS%20Node
Engine: Xray (auto), Xray (explicit)
SOCKS5
Standard SOCKS5 proxy.
Scheme: socks5://
Format:
socks5://<username>:<password>@<address>:<port>#<name>
Fields:
| Field | Location | Required | Description |
|---|---|---|---|
username | userinfo | No | Username |
password | userinfo | No | Password |
address | host | Yes | Server address |
port | port | Yes | Server port |
name | fragment | No | Display name |
Examples:
socks5://user:pass@example.com:1080#SOCKS%20Node
socks5://example.com:1080#Anonymous
Engine: Xray (auto), Xray (explicit)
Hysteria2
QUIC-based protocol designed for high-speed connections.
Scheme: hysteria2:// / hy2://
Format:
hysteria2://<password>@<address>:<port>?sni=<sni>&obfs=<type>&obfs-password=<pass>#<name>
hy2://<password>@<address>:<port>?sni=<sni>&obfs=<type>&obfs-password=<pass>#<name>
Fields:
| Field | Location | Required | Description |
|---|---|---|---|
password | userinfo | Yes | Authentication password |
address | host | Yes | Server address |
port | port | Yes | Server port |
sni | query | No | SNI hostname |
obfs | query | No | Obfuscation type |
obfs-password | query | No | Obfuscation password |
alpn | query | No | ALPN protocol |
insecure | query | No | Allow insecure TLS |
upmbps | query | No | Upload Mbps |
downmbps | query | No | Download Mbps |
name | fragment | No | Display name |
Default network: udp (not configurable) Default TLS: tls (always
enabled)
Examples:
hy2://password@example.com:443?sni=cdn.example.com&obfs=salamander&obfs-password=secret#HY2%20Node
hy2://password@example.com:8443#Simple%20HY2
hysteria2://password@example.com:443#Alias
Engine: sing-box (auto), sing-box (explicit)
Engine Routing
Engine selection is automatic but configurable.
Auto Mode (Default)
| Protocol | Engine |
|---|---|
| VLESS | Xray |
| VMess | Xray |
| Shadowsocks | Xray |
| Trojan | Xray |
| HTTP | Xray |
| SOCKS5 | Xray |
| Hysteria2 | sing-box |
Xray Mode
All protocols except Hysteria2. Errors on Hysteria2.
sing-box Mode
All protocols use sing-box (currently only Hysteria2 fully implemented).
Checking Engine
xrat parse --engine auto "vless://uuid@example.com:443"
xrat parse --engine sing-box "hy2://password@example.com:443"
Normalized Fields
All protocols are normalized to a common Node structure:
| Field | VLESS | VMess | SS | Trojan | HTTP | SOCKS5 | HY2 |
|---|---|---|---|---|---|---|---|
| protocol | vless | vmess | ss | trojan | http | socks5 | hy2 |
| address | host | add | host | host | host | host | host |
| port | port | port | port | port | port/80/443 | port | port |
| uuid | userinfo | id | - | - | - | - | - |
| password | - | - | base64 | userinfo | userinfo | userinfo | userinfo |
| method | - | - | base64 | - | - | - | - |
| network | type | net | tcp | type | tcp | tcp | udp |
| tls | security | tls | - | tls | scheme | - | tls |
| sni | sni | sni | - | sni | - | - | sni |
| host | host | host | - | host | - | - | - |
| path | path | path | - | path | - | - | - |
| name | fragment | ps | fragment | fragment | fragment | fragment | fragment |
Config File
Full reference for the config.toml file with all fields, defaults, and
accepted values.
File Location
Default: ~/.config/xrat/config.toml
Resolution order:
--config <path>CLI flagXRAT_PATH/config.tomlenvironment variable~/.config/xrat/config.toml
Top-Level Structure
[paths]
[database]
[server]
[runtime]
[routing]
[geo]
[parser]
[dns]
[testing]
[paths]
Binary paths for proxy engines. All fields are optional (defaults to $PATH).
[paths]
# Database file path (deprecated, use [database.sqlite].path)
database = "db.sqlite"
# Binary paths (optional, defaults to PATH lookup)
xray = "/usr/local/bin/xray"
v2ray = "/usr/local/bin/v2ray"
sing_box = "/usr/local/bin/sing-box"
| Field | Type | Default | Description |
|---|---|---|---|
database | string | - | Database path (deprecated, use [database.sqlite].path) |
xray | string | xray | Xray-core binary path |
v2ray | string | v2ray | V2Ray binary path |
sing_box | string | sing-box | sing-box binary path |
[database]
Database backend selection and connection settings.
[database]
backend = "sqlite" # "sqlite" | "postgres"
[database.sqlite]
path = "db.sqlite"
[database.postgres]
user = { env = "XRAT_POSTGRES_USER" }
password = { env = "XRAT_POSTGRES_PASSWORD" }
host = "localhost"
port = 5432
db_name = "xrat"
max_connections = 10
min_connections = 1
connect_timeout_secs = 10
| Field | Type | Default | Description |
|---|---|---|---|
backend | enum | sqlite | sqlite or postgres |
[sqlite].path | string | db.sqlite | SQLite database file path |
[postgres].user | string/env | - | PostgreSQL username |
[postgres].password | string/env | - | PostgreSQL password |
[postgres].host | string | localhost | PostgreSQL host |
[postgres].port | integer | 5432 | PostgreSQL port |
[postgres].db_name | string | - | PostgreSQL database name |
[postgres].max_connections | integer | 10 | Connection pool max size |
[postgres].min_connections | integer | 1 | Connection pool min size |
[postgres].connect_timeout_secs | integer | 10 | Connection timeout |
[server]
HTTP API server configuration.
[server]
enabled = false
host = "127.0.0.1"
port = 8080
key = { env = "XRAT_API_KEY" }
| Field | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable daemon-hosted API |
host | string | 127.0.0.1 | Bind host |
port | integer | 8080 | Bind port |
key | string/env | - | API key for authentication |
[runtime]
Runtime engine and proxy process configuration.
[runtime]
engine = "xray" # "xray" | "v2ray" | "sing-box"
replace_active_session = true
| Field | Type | Default | Description |
|---|---|---|---|
engine | enum | xray | Managed runtime engine. Xray/V2Ray are currently used for connect; sing-box is parse/preview oriented. |
replace_active_session | boolean | true | Auto-disconnect on new connect |
[runtime.rotation]
Proxy auto-rotation settings.
[runtime.rotation]
enabled = false
interval_secs = 1800
health_trigger_enabled = true
cooldown_secs = 300
test_concurrency = 0
test_stages = ["real_delay", "download"]
| Field | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable scheduled rotation |
interval_secs | integer | 1800 | Rotation interval in seconds |
health_trigger_enabled | boolean | true | Trigger rotation on health failure |
cooldown_secs | integer | 300 | Minimum time between rotations |
test_concurrency | integer | 0 | Test workers (0 = auto) |
test_stages | string[] | ["real_delay", "download"] | Candidate test stages |
[runtime.log]
Proxy process logging.
[runtime.log]
enabled = true
mask = "none" # "quarter" | "half" | "full" | "none"
dir = "logs"
dns_log = false
level = "warning" # "debug" | "info" | "warning" | "error"
keep = true
| Field | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable logging to files |
mask | enum | none | IP address masking |
dir | string | logs | Log directory |
dns_log | boolean | false | Enable DNS query logging |
level | enum | warning | Log level |
keep | boolean | true | Keep logs after session stop |
[runtime.socks]
SOCKS5 inbound configuration.
[runtime.socks]
enabled = true
host = "0.0.0.0"
port = 1080
udp = true
auth = { enabled = true, username = "xrat", password = { env = "XRAT_SOCKS_PASSWORD" } }
| Field | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable SOCKS inbound |
host | string | 0.0.0.0 | Bind address |
port | integer | 1080 | Bind port |
udp | boolean | true | Enable UDP support |
auth.enabled | boolean | false | Enable authentication |
auth.username | string | xrat | SOCKS username |
auth.password | string/env | - | SOCKS password |
[runtime.http]
HTTP proxy inbound configuration.
[runtime.http]
enabled = false
host = "0.0.0.0"
port = 8080
| Field | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable HTTP inbound |
host | string | 0.0.0.0 | Bind address |
port | integer | 8080 | Bind port |
[runtime.shadowsocks]
Shadowsocks inbound configuration.
[runtime.shadowsocks]
enabled = false
host = "0.0.0.0"
port = 1081
method = "aes-128-gcm"
password = { env = "XRAT_SHADOWSOCKS_PASSWORD" }
network = "tcp,udp"
| Field | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable Shadowsocks inbound |
host | string | 0.0.0.0 | Bind address |
port | integer | 1081 | Bind port |
method | string | aes-128-gcm | Encryption method |
password | string/env | - | Shadowsocks password |
network | string | tcp,udp | Network type |
[runtime.sniffing]
Traffic sniffing configuration.
[runtime.sniffing]
enabled = true
dest_override = ["http", "tls", "quic"]
route_only = true
metadata_only = false
domains_excluded = []
ips_excluded = []
| Field | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable traffic sniffing |
dest_override | string[] | ["http", "tls", "quic"] | Protocols for destination override |
route_only | boolean | true | Only sniff for routing |
metadata_only | boolean | false | Only sniff metadata |
domains_excluded | string[] | [] | Excluded domains |
ips_excluded | string[] | [] | Excluded IPs |
[routing]
Routing configuration.
[routing]
domain_strategy = "IPIfNonMatch" # "AsIs" | "IPIfNonMatch" | "IPOnDemand"
[routing.direct]
domain = []
ip = []
geosite = []
geoip = []
[routing.block]
domain = []
ip = []
geosite = []
geoip = []
| Field | Type | Default | Description |
|---|---|---|---|
domain_strategy | enum | IPIfNonMatch | Xray domain resolution strategy |
[direct].domain | string[] | [] | Direct-route domains |
[direct].ip | string[] | [] | Direct-route IPs |
[direct].geosite | string[] | [] | Direct-route geosite categories |
[direct].geoip | string[] | [] | Direct-route geoip categories |
[block].domain | string[] | [] | Blocked domains |
[block].ip | string[] | [] | Blocked IPs |
[block].geosite | string[] | [] | Blocked geosite categories |
[block].geoip | string[] | [] | Blocked geoip categories |
[geo]
GeoIP/geosite asset management.
[geo]
auto_update = false
update_interval_hours = 168
[[geo.profiles]]
name = "default"
geosite = "https://example.com/geosite.dat"
geoip = "https://example.com/geoip.dat"
[[geo.profiles]]
name = "local"
geosite = "geo/local/geosite.dat"
geoip = "geo/local/geoip.dat"
| Field | Type | Default | Description |
|---|---|---|---|
auto_update | boolean | false | Enable periodic geo asset updates |
update_interval_hours | integer | 168 | Update interval in hours |
[[profiles]].name | string | - | Profile name |
[[profiles]].geosite | string | - | Geosite file path or URL |
[[profiles]].geoip | string | - | GeoIP file path or URL |
[parser]
Xray JSON schema validation mode.
[parser]
parse_mode = "strict" # "strict" | "lenient" | "auto"
| Field | Type | Default | Description |
|---|---|---|---|
parse_mode | enum | strict | Xray JSON validation mode |
[dns]
DNS configuration for generated Xray configs.
[dns]
query_strategy = "UseSystem" # "UseIP" | "UseIPv4" | "UseIPv6" | "UseSystem"
servers = [
"8.8.8.8",
"https://1.1.1.1/dns-query",
]
use_system_hosts = true
disable_cache = false
disable_fallback = false
enable_parallel_query = true
[dns.hosts]
"domain:example.test" = "127.0.0.1"
"domain:lan.test" = ["192.168.1.10", "192.168.1.11"]
| Field | Type | Default | Description |
|---|---|---|---|
query_strategy | enum | UseSystem | DNS query strategy |
servers | string[] | - | DNS server list |
use_system_hosts | boolean | true | Use system hosts file |
disable_cache | boolean | false | Disable DNS cache |
disable_fallback | boolean | false | Disable fallback DNS |
enable_parallel_query | boolean | true | Enable parallel queries |
[hosts] | map | - | Static DNS entries |
[mmdb]
Dedicated MaxMind MMDB asset configuration, separate from [geo] routing
assets.
[mmdb]
dir = "mmdb"
download_url = "https://github.com/P3TERX/GeoLite.mmdb/releases/latest/download/{edition}.mmdb"
timeout_secs = 60
default_editions = ["country", "city", "asn"]
auto_update = false
update_interval_hours = 168
| Field | Type | Default | Description |
|---|---|---|---|
dir | string | mmdb | MMDB directory (absolute, or relative to the xrat runtime root) |
download_url | string | https://github.com/P3TERX/GeoLite.mmdb/releases/latest/download/{edition}.mmdb | Download URL template. {edition} is replaced with edition name |
timeout_secs | integer | 60 | HTTP request timeout for downloads |
default_editions | string[] | ["country", "city", "asn"] | Editions downloaded when no --edition or --all flag given |
auto_update | boolean | false | Enable periodic update checks |
update_interval_hours | integer | 168 | Update interval in hours |
The dir field is resolved relative to the xrat runtime root (XRAT_PATH when
set, otherwise the default app root). Absolute paths are used as-is. The default
per-edition MMDB paths under [testing.geoip] also resolve through this MMDB
directory; custom relative per-edition paths are resolved relative to the config
file directory.
[testing]
Connection testing configuration.
[testing]
concurrency = 0 # 0 = auto
order = ["icmp", "real_delay", "download"]
failure_policy = "continue" # "continue" | "skip_remaining" | "mark_failed"
[testing.real_delay]
enabled = true
url = "https://www.gstatic.com/generate_204"
timeout = 10_000
[testing.icmp]
enabled = true
timeout = 3000
attempts = 3
[testing.download]
enabled = false
url = "https://cachefly.cachefly.net/50mb.test"
timeout = 30_000
[testing.tcp]
enabled = true
timeout = 5000
[testing.geoip]
enabled = false
backend = "mmdb"
fallback = "none"
country_path = "mmdb/GeoLite2-Country.mmdb"
city_path = "mmdb/GeoLite2-City.mmdb"
asn_path = "mmdb/GeoLite2-ASN.mmdb"
[testing.geoip.remote]
provider = "ipwhois"
endpoint = ""
timeout_ms = 5000
api_key = ""
rate_limit_per_minute = 30
[testing.geoip.cache]
enabled = true
ttl_secs = 86400
max_entries = 10000
| Section | Field | Type | Default | Description |
|---|---|---|---|---|
[testing] | concurrency | integer | 0 | Test workers (0 = auto) |
[testing] | order | string[] | ["icmp", "real_delay", "download"] | Stage execution order |
[testing] | failure_policy | enum | continue | Behavior on stage failure |
[icmp] | enabled | boolean | true | Enable ICMP stage |
[icmp] | timeout | integer | 3000 | ICMP timeout (ms) |
[icmp] | attempts | integer | 3 | ICMP attempt count |
[tcp] | enabled | boolean | true | Enable TCP stage |
[tcp] | timeout | integer | 5000 | TCP timeout (ms) |
[real_delay] | enabled | boolean | true | Enable real-delay stage |
[real_delay] | url | string | https://www.gstatic.com/generate_204 | Test URL |
[real_delay] | timeout | integer | 10000 | HTTP request timeout (ms) |
[download] | enabled | boolean | false | Enable download stage |
[download] | url | string | - | Download URL |
[download] | timeout | integer | 30000 | Download timeout (ms) |
[testing.geoip] | enabled | boolean | false | Enable GeoIP enrichment |
[testing.geoip] | backend | enum | mmdb | Lookup backend: mmdb, ipwhois, ip-api, chain |
[testing.geoip] | fallback | enum | none | Fallback backend when primary is chain: ipwhois, ip-api, none |
[testing.geoip] | country_path | string | mmdb/GeoLite2-Country.mmdb | Country MMDB path (relative to config) |
[testing.geoip] | city_path | string | mmdb/GeoLite2-City.mmdb | City MMDB path (relative to config) |
[testing.geoip] | asn_path | string | mmdb/GeoLite2-ASN.mmdb | ASN MMDB path (relative to config) |
[remote] | provider | enum | ipwhois | Remote provider: ipwhois, ip-api |
[remote] | endpoint | string | "" (uses provider default) | Remote API endpoint override |
[remote] | timeout_ms | integer | 5000 | Remote request timeout in milliseconds |
[remote] | api_key | string | "" | API key (provider-specific) |
[remote] | rate_limit_per_minute | integer | 30 | Max remote requests per minute |
[cache] | enabled | boolean | true | Enable in-memory caching |
[cache] | ttl_secs | integer | 86400 | Cache entry TTL in seconds |
[cache] | max_entries | integer | 10000 | Maximum cache entries |
Upload tests are enabled per invocation with xrat test --upload-url <url>.
There is no [testing.upload] config section; --upload-timeout overrides the
default 30-second upload timeout.
Environment Variable References
Sensitive fields accept environment variable references:
# Literal value
password = "my-secret-password"
# Environment variable
password = { env = "XRAT_SOCKS_PASSWORD" }
Supported on these fields:
| Section | Field |
|---|---|
[server] | key |
[runtime.socks] | auth.password |
[runtime.shadowsocks] | password |
[database.postgres] | user |
[database.postgres] | password |
Example Config
See testdata/config.example.toml in the repository for a complete example with
all sections and comments.
Database Schema
xrat uses relational databases (SQLite or PostgreSQL) with the same schema across both backends.
Schema Overview
| Table | Version | Description |
|---|---|---|
subscriptions | 0001 | Import source tracking |
configs | 0001, 0003, 0015 | Stored proxy nodes |
connection_tests | 0001, 0002, 0008, 0009, 0010 | Test results per config |
connection_test_runs | 0007 | Groups test results into runs |
runtime_sessions | 0001, 0004, 0005, 0006, 0012, 0013, 0014 | Proxy process lifecycle |
cf_scan_results | 0011 | IP scan results |
Tables
subscriptions
Tracks import sources (URLs, files, raw text).
CREATE TABLE subscriptions (
id INTEGER PRIMARY KEY,
source_url TEXT,
source_kind TEXT NOT NULL CHECK(source_kind IN ('url', 'file', 'raw_text')),
name TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
| Column | Type | Description |
|---|---|---|
id | INTEGER | Primary key |
source_url | TEXT | Original URL, file path, or βraw_textβ |
source_kind | TEXT | url, file, or raw_text |
name | TEXT | Optional subscription name |
created_at | TIMESTAMP | First import timestamp |
updated_at | TIMESTAMP | Latest import timestamp |
configs
Stores normalized proxy nodes.
CREATE TABLE configs (
id INTEGER PRIMARY KEY,
subscription_id INTEGER REFERENCES subscriptions(id),
dedup_key TEXT NOT NULL UNIQUE,
protocol TEXT NOT NULL,
address TEXT NOT NULL,
port INTEGER NOT NULL,
username TEXT,
uuid TEXT,
password TEXT,
method TEXT,
network TEXT NOT NULL,
tls TEXT,
sni TEXT,
host TEXT,
path TEXT,
name TEXT,
raw_config TEXT NOT NULL,
is_active INTEGER NOT NULL DEFAULT 0,
is_enabled INTEGER NOT NULL DEFAULT 1,
is_selected INTEGER NOT NULL DEFAULT 0,
is_deleted INTEGER NOT NULL DEFAULT 0,
imported_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
| Column | Type | Description |
|---|---|---|
id | INTEGER | Primary key |
subscription_id | INTEGER | FK to subscriptions |
dedup_key | TEXT | Unique deduplication key |
protocol | TEXT | vless, vmess, ss, trojan, http, socks5, hy2 |
address | TEXT | Server address |
port | INTEGER | Server port |
username | TEXT | Username (HTTP/SOCKS5) |
uuid | TEXT | UUID (VLESS/VMess) |
password | TEXT | Password (Trojan/SS) |
method | TEXT | Encryption method (Shadowsocks) |
network | TEXT | tcp, ws, grpc, udp |
tls | TEXT | tls or NULL |
sni | TEXT | SNI hostname |
host | TEXT | Host header (WebSocket) |
path | TEXT | Path (WebSocket/gRPC/TCP) |
name | TEXT | Display name |
raw_config | TEXT | Original raw config line |
is_active | BOOLEAN | Currently active runtime config |
is_enabled | BOOLEAN | Included in bulk operations |
is_selected | BOOLEAN | User-selected config |
imported_at | TIMESTAMP | Import timestamp |
is_deleted | BOOLEAN | Soft-deleted flag |
deleted_at | TIMESTAMP | Deletion timestamp |
created_at | TIMESTAMP | Insertion timestamp |
updated_at | TIMESTAMP | Last update timestamp |
Indexes:
dedup_keyβ UNIQUEsubscription_idβ FK indexis_enabled,is_active,is_selectedβ filter queriesis_deletedβ soft-delete queries
connection_tests
Stores individual test results per config.
CREATE TABLE connection_tests (
id INTEGER PRIMARY KEY,
run_id INTEGER REFERENCES connection_test_runs(id),
config_id INTEGER NOT NULL REFERENCES configs(id),
icmp_ok INTEGER,
icmp_ms INTEGER,
tcp_ok INTEGER,
tcp_ms INTEGER,
real_delay_ok INTEGER,
real_delay_ms INTEGER,
connect_ms INTEGER,
ttfb_ms INTEGER,
http_status INTEGER,
download_mbps REAL,
upload_mbps REAL,
failure_kind TEXT,
failure_reason TEXT,
endpoint_ip TEXT,
endpoint_location TEXT,
endpoint_country TEXT,
endpoint_asn TEXT,
tested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
| Column | Type | Description |
|---|---|---|
id | INTEGER | Primary key |
run_id | INTEGER | FK to connection_test_runs |
config_id | INTEGER | FK to configs |
icmp_ok | BOOLEAN | ICMP ping success |
icmp_ms | INTEGER | ICMP latency |
tcp_ok | BOOLEAN | TCP connect success |
tcp_ms | INTEGER | TCP latency |
real_delay_ok | BOOLEAN | HTTP round-trip success |
real_delay_ms | INTEGER | HTTP round-trip latency |
connect_ms | INTEGER | TCP connect time |
ttfb_ms | INTEGER | Time to first byte |
http_status | INTEGER | HTTP response status |
download_mbps | REAL | Download throughput |
upload_mbps | REAL | Upload throughput |
failure_kind | TEXT | Failure classification |
failure_reason | TEXT | Human-readable error |
endpoint_ip | TEXT | Resolved IP address |
endpoint_location | TEXT | GeoIP location |
endpoint_country | TEXT | Country ISO code |
endpoint_asn | TEXT | ASN identifier |
tested_at | TIMESTAMP | Test timestamp |
Indexes:
config_idβ per-config queriesrun_idβ per-run queries(config_id, tested_at)β latest test per config
connection_test_runs
Groups test results into batches.
CREATE TABLE connection_test_runs (
id INTEGER PRIMARY KEY,
kind TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
| Column | Type | Description |
|---|---|---|
id | INTEGER | Primary key |
kind | TEXT | Run description (e.g., βbulkβ, βpingβ) |
created_at | TIMESTAMP | Run timestamp |
runtime_sessions
Tracks proxy process lifecycle.
CREATE TABLE runtime_sessions (
id INTEGER PRIMARY KEY,
config_id INTEGER REFERENCES configs(id),
status TEXT NOT NULL
CHECK(status IN ('starting', 'running', 'stopping', 'stopped', 'failed')),
socks_host TEXT,
socks_port INTEGER,
http_host TEXT,
http_port INTEGER,
shadowsocks_host TEXT,
shadowsocks_port INTEGER,
process_id INTEGER,
failure_reason TEXT,
owner_kind TEXT,
owner_instance_id TEXT,
last_transition_reason_code TEXT,
last_transition_reason_detail TEXT,
last_transition_origin TEXT,
cooldown_until TEXT,
last_failed_at TEXT,
last_failed_reason_code TEXT,
started_at TIMESTAMP,
stopped_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
| Column | Type | Description |
|---|---|---|
id | INTEGER | Primary key |
config_id | INTEGER | FK to configs |
status | TEXT | starting, running, stopping, stopped, failed |
socks_host | TEXT | SOCKS inbound host |
socks_port | INTEGER | SOCKS inbound port |
http_host | TEXT | HTTP inbound host (if enabled) |
http_port | INTEGER | HTTP inbound port |
shadowsocks_host | TEXT | Shadowsocks inbound host (if enabled) |
shadowsocks_port | INTEGER | Shadowsocks inbound port |
process_id | INTEGER | OS process ID |
failure_reason | TEXT | Error message (if failed) |
owner_kind | TEXT | cli or daemon |
owner_instance_id | TEXT | Daemon instance UUID |
last_transition_reason_code | TEXT | Machine-readable transition reason code |
last_transition_reason_detail | TEXT | Human-readable transition details |
last_transition_origin | TEXT | Transition source such as CLI, daemon, health, or rotation |
cooldown_until | TEXT | Rotation cooldown expiry as epoch seconds |
last_failed_at | TEXT | Last runtime/health failure time as epoch seconds |
last_failed_reason_code | TEXT | Machine-readable last failure reason code |
started_at | TIMESTAMP | Session start timestamp |
stopped_at | TIMESTAMP | Session stop timestamp |
created_at | TIMESTAMP | Record creation timestamp |
updated_at | TIMESTAMP | Last update timestamp |
Indexes:
config_idβ per-config queriesstatusβ running session lookup(owner_kind, owner_instance_id)β daemon reattach queries
cf_scan_results
Stores IP scan results.
CREATE TABLE cf_scan_results (
id INTEGER PRIMARY KEY,
ip TEXT NOT NULL UNIQUE,
latency_ms INTEGER,
download_mbps REAL,
upload_mbps REAL,
error TEXT,
last_scanned_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
| Column | Type | Description |
|---|---|---|
id | INTEGER | Primary key |
ip | TEXT | IP address (unique) |
latency_ms | INTEGER | Connection latency |
download_mbps | REAL | Download throughput (if measured) |
upload_mbps | REAL | Upload throughput (if measured) |
error | TEXT | Error message (if failed) |
last_scanned_at | TIMESTAMP | Last scan timestamp |
Migrations
Migrations are run automatically on startup using SQLx.
Migration List
| # | File | Description |
|---|---|---|
| 0001 | init.sql | Initial schema: subscriptions, configs, connection_tests, runtime_sessions |
| 0002 | add_connection_test_download_mbps.sql | Add download_mbps to connection_tests |
| 0003 | canonical_config_dedup_key.sql | Add dedup_key to configs |
| 0004 | add_runtime_session_inbound_ports.sql | Add inbound port columns to runtime_sessions |
| 0005 | drop_runtime_session_mixed_port.sql | Clean up mixed port column |
| 0006 | add_runtime_session_failure_reason.sql | Add failure tracking to runtime_sessions |
| 0007 | add_connection_test_runs.sql | Add connection_test_runs table |
| 0008 | add_connection_test_http_fields.sql | Add HTTP fields (connect_ms, ttfb_ms, http_status) |
| 0009 | add_connection_test_country_asn.sql | Add GeoIP fields (country, ASN) |
| 0010 | add_connection_test_upload_mbps.sql | Add upload_mbps to connection_tests |
| 0011 | add_cf_scan_results.sql | Add cf_scan_results table |
| 0012 | add_runtime_session_owner_transition_fields.sql | Add owner tracking to runtime_sessions |
| 0013 | add_runtime_session_transition_origin.sql | Add transition origin tracking |
| 0014 | add_runtime_session_cooldown_failure_fields.sql | Add cooldown and failure tracking |
| 0015 | add_config_soft_delete.sql | Add soft-delete fields to configs |
Migration Location
migrations/sqlite/0001_init.sql
migrations/sqlite/0002_add_connection_test_download_mbps.sql
...
migrations/sqlite/0015_add_config_soft_delete.sql
PostgreSQL migrations have equivalent files in migrations/postgres/.
Schema Diagram
subscriptions
β
βββ configs (1:N via subscription_id)
β β
β βββ connection_tests (1:N via config_id)
β β β
β β βββ connection_test_runs (1:N via run_id)
β β
β βββ runtime_sessions (1:N via config_id)
β
βββ (none)
cf_scan_results (standalone, not linked to configs)
Error Codes
xrat categorizes errors into application-level errors and test failure classifications.
AppError
AppError is the primary error type returned by command handlers and services.
| Variant | Description | Use Case |
|---|---|---|
ConfigNotFound | Config ID not found in database | xrat connect <id> with invalid ID |
NoActiveSession | No active proxy session | xrat disconnect with no session |
XraySpawn | Failed to spawn Xray process | Xray binary not found or invalid |
XrayExited | Xray process exited unexpectedly | Process crashed during startup |
XrayStartupTimeout | Xray port not ready within timeout | Slow startup or port conflict |
DaemonNotRunning | Daemon IPC socket not reachable | xrat proxy start without daemon |
DaemonConnect | Failed to connect to daemon socket | Permission denied or socket missing |
Database | Database query or connection error | Connection failure or constraint violation |
Io | Filesystem I/O error | Permission denied or disk full |
Config | Configuration file error | Invalid TOML or missing required field |
MissingPostgresUser | PostgreSQL user not configured | database.postgres.user is empty |
MissingPostgresDatabaseName | PostgreSQL database name not configured | database.postgres.db_name is empty |
InvalidConfigValue | Invalid configuration value | Unknown enum variant or out-of-range |
Serialization | JSON serialization/deserialization error | Invalid JSON or schema mismatch |
Probe | Probe test execution error | ICMP ping command failed |
Parse | Config link parsing error | Invalid URI format or unsupported scheme |
Error Messages
Errors implement Display for user-friendly messages:
Error: config not found (id: 42)
Error: daemon socket not reachable at /home/user/.config/xrat/runtime/daemon.sock
Error: Xray process failed to start: No such file or directory (os error 2)
DbError
DbError represents database-specific errors.
| Variant | Description |
|---|---|
Query | SQL query execution error |
Pool | Connection pool acquisition error |
Connection | Database connection error |
UniqueViolation | Duplicate key violation (used for dedup) |
ForeignKeyViolation | Referential integrity violation |
NotFound | Expected row not found |
Migration | Schema migration error |
Config | Database configuration error |
FailureKind
FailureKind classifies test stage failures. Used by the testing pipeline and
displayed in test results.
| Category | Description | Example |
|---|---|---|
DNS | DNS resolution failed | nodename nor servname provided, or not known |
Timeout | Connection or request timed out | connection timed out after 5000ms |
Refused | Connection refused | Connection refused (os error 111) |
Unreachable | Network unreachable | No route to host (os error 113) |
PermissionDenied | Permission denied | Operation not permitted |
TLS | TLS handshake failed | tls: first record does not look like a TLS handshake |
Auth | Authentication failed | proxy authentication required |
Process | Proxy process failed to start | xray binary not found |
Proxy | Proxy returned an error status | HTTP 503 Service Unavailable |
Unknown | Unclassified failure | Any other error |
Failure Classification Logic
TCP failures are classified by matching error strings:
#![allow(unused)]
fn main() {
fn classify_tcp_error(error: &io::Error) -> FailureKind {
match error.kind() {
io::ErrorKind::ConnectionRefused => FailureKind::Refused,
io::ErrorKind::ConnectionReset => FailureKind::Refused,
io::ErrorKind::TimedOut => FailureKind::Timeout,
io::ErrorKind::ConnectionAborted => FailureKind::Timeout,
io::ErrorKind::NotConnected => FailureKind::Unreachable,
io::ErrorKind::AddrInUse => FailureKind::PermissionDenied,
io::ErrorKind::AddrNotAvailable => FailureKind::PermissionDenied,
io::ErrorKind::PermissionDenied => FailureKind::PermissionDenied,
io::ErrorKind::HostUnreachable => FailureKind::Unreachable,
io::ErrorKind::NetworkUnreachable => FailureKind::Unreachable,
io::ErrorKind::InvalidInput => FailureKind::Unknown,
_ => {
let msg = error.to_string().to_lowercase();
if msg.contains("dns") || msg.contains("resolve") {
FailureKind::DNS
} else {
FailureKind::Unknown
}
}
}
}
}
ConfigParseError
ConfigParseError is returned by the config parser when parsing share links.
| Variant | Description |
|---|---|
Url | Invalid URL format |
Json | Invalid JSON (vmess://) |
Decode | Invalid base64 payload |
ParseInt | Invalid numeric value |
MissingAddressOrPort | URI missing address or port |
MissingBase64Userinfo | URI missing base64-encoded userinfo |
InvalidShadowsocksUserinfo | Invalid Shadowsocks userinfo format |
MissingRequiredField | Required field not found in JSON |
UnsupportedScheme | Unknown protocol scheme |
XrayProcessError
XrayProcessError is returned by the Xray process manager.
| Variant | Description |
|---|---|
TempFileError | Failed to create temporary config file |
SerializationError | Failed to serialize config JSON |
SpawnError | Failed to spawn Xray process |
StartupTimeout | Xray failed to start within timeout |
ProcessExited | Xray exited unexpectedly (with stderr) |
PortNotReady | Inbound port not ready within timeout |
ImportParseError
ImportParseError is returned by the import parser.
| Variant | Description |
|---|---|
InvalidShareLink | Input is not a valid share link |
Decode | Invalid base64 decoding |
Json | Invalid JSON |
MissingSip008Servers | SIP008 JSON missing servers array |
MissingSip008Field | SIP008 server missing required field |
Xray | Invalid Xray JSON |
Config | Invalid config node |
Error Handling Best Practices
CLI Commands
Command handlers return Result<(), AppError>:
#![allow(unused)]
fn main() {
pub async fn run(context: &AppContext, args: &ConnectArgs) -> Result<()> {
let config = context.db.get_config(args.id).await?
.ok_or(AppError::ConfigNotFound(args.id))?;
// ...
}
}
Logging
Errors are logged at error level before returning:
#![allow(unused)]
fn main() {
if let Err(err) = run(&context, &args.command).await {
tracing::error!(error = %err, "command failed");
std::process::exit(1);
}
}
User-Facing Messages
Errors display actionable messages when possible:
Error: daemon socket not reachable
Hint: start the daemon with 'xrat daemon start'
Architecture
This section describes xratβs internal architecture, data flow, and module structure for developers and contributors.
Context Diagram
graph TB
classDef user fill:#3a2c1a,stroke:#dfa85b,color:#e6edf3
classDef iface fill:#1a2e1a,stroke:#5bdf8a,color:#e6edf3
classDef core fill:#1a2c3a,stroke:#5b8def,color:#e6edf3
classDef engine fill:#1a3a1a,stroke:#5bdf5b,color:#e6edf3
classDef store fill:#1a3a3a,stroke:#5bdfd3,color:#e6edf3
User(("User")):::user
subgraph interfaces["User Interfaces"]
CLI["CLI (terminal)"]:::iface
TUI["TUI (ratatui)"]:::iface
API["HTTP API (axum)"]:::iface
end
subgraph xrat_core["xrat Core"]
Daemon["Daemon Supervisor"]:::core
end
subgraph engines["Proxy Engines"]
Xray["Xray-core"]:::engine
SingBox["sing-box"]:::engine
end
DB[("SQLite / Postgres")]:::store
User --> CLI
User --> TUI
User -- "HTTP" --> API
CLI -- "IPC" --> Daemon
TUI -- "IPC" --> Daemon
Daemon -- "spawns" --> Xray
Daemon -- "spawns" --> SingBox
CLI --> DB
Daemon --> DB
API --> DB
Pages
| Page | Description |
|---|---|
| Module Structure | Source tree, module responsibilities, dependency graph |
| Config Generation | How engine JSON configs are generated from nodes |
| Import Pipeline | End-to-end subscription import flow |
| Daemon Architecture | Daemon process, IPC protocol, supervisor event loop |
| Runtime Lifecycle | Session state machine, connect/replace/disconnect flows |
| Test Pipeline | Probe execution, test stages, output formatting |
| Database Schema | Full SQL DDL and per-table column reference |
Module Structure
xrat follows a modular architecture with clear separation of concerns across CLI parsing, command handlers, config parsing, database access, and engine integration.
Component Diagram
Arrows show dependency direction (A β B means A depends on B).
graph TB
classDef entry fill:#1a2744,stroke:#4a9eff,color:#e6edf3
classDef iface fill:#1a3a2a,stroke:#5bdf8a,color:#e6edf3
classDef app fill:#2a1a3a,stroke:#b070df,color:#e6edf3
classDef domain fill:#2e2a1a,stroke:#dfba5b,color:#e6edf3
classDef store fill:#1a2e2e,stroke:#5bcfdf,color:#e6edf3
classDef engine fill:#2e1a1a,stroke:#df6060,color:#e6edf3
classDef probe fill:#2a2a1a,stroke:#c0df5b,color:#e6edf3
main["main.rs"]:::entry
subgraph ui["User Interfaces"]
cli["cli/"]:::iface
server["server/"]:::iface
tui["tui/"]:::iface
end
subgraph app_layer["Application Layer"]
cmds["commands/"]:::app
daemon["daemon/"]:::app
rtsvc["runtime_service/"]:::app
end
subgraph domain_layer["Domain & Config"]
model["model/"]:::domain
config["config/"]:::domain
support["support/"]:::domain
end
db["db/"]:::store
subgraph engine_layer["Proxy Engines"]
xray["xray/"]:::engine
singbox["singbox/"]:::engine
end
prober["prober/"]:::probe
main --> cli
main --> cmds
cmds --> rtsvc
cmds --> daemon
daemon --> rtsvc
rtsvc --> xray
config --> model
support --> config
support --> db
db --> xray
db --> prober
prober --> xray
prober --> model
server --> db
server --> model
tui --> cmds
tui --> db
Module Responsibilities
| Module | Responsibility |
|---|---|
cli/ | Define CLI interface with Clap. Parse args and flags. Test parsing. |
app/ | Orchestrate command execution. Manage app lifecycle (context, config, daemon). |
model/ | Shared domain types (Node, Protocol, NodeDedupKey). No dependencies on other modules. |
config/ | Parse proxy URIs. Normalize nodes. Detect import formats. |
db/ | Database connection, migrations, queries, repositories. |
xray/ | Generate Xray JSON configs. Parse Xray JSON. Manage Xray processes. |
singbox/ | Generate sing-box JSON configs. Manage sing-box processes. |
prober/ | Connection testing probes: ICMP, TCP, HTTP real-delay, download, upload. |
server/ | HTTP API server using Axum. Auth, routes, response types. |
support/ | Shared utilities: base64 decode, GeoIP, network helpers. |
Data Flows
Import Flow
flowchart LR
classDef io fill:#1a2e1a,stroke:#5bdf8a,color:#e6edf3
classDef cfg fill:#2e2a1a,stroke:#dfba5b,color:#e6edf3
classDef db fill:#1a2e2e,stroke:#5bcfdf,color:#e6edf3
SRC["Input\n(URL / File / Stdin)"]:::io
APP_IN["app/input/"]:::io
DETECT["config/import/detect"]:::cfg
PARSE_FMT["config/import/parsers/"]:::cfg
PROTO["config/protocols/"]:::cfg
NORM["config/normalize/"]:::cfg
DEDUP["model/node_dedup_key/"]:::cfg
PERSIST["db/repository/configs/"]:::db
SUB["db/repository/subscriptions/"]:::db
SRC --> APP_IN --> DETECT --> PARSE_FMT --> PROTO --> NORM --> DEDUP --> PERSIST --> SUB
Test Flow
flowchart TD
classDef cli fill:#1a2744,stroke:#4a9eff,color:#e6edf3
classDef app fill:#2a1a3a,stroke:#b070df,color:#e6edf3
classDef engine fill:#2e1a1a,stroke:#df6060,color:#e6edf3
classDef probe fill:#2a2a1a,stroke:#c0df5b,color:#e6edf3
classDef store fill:#1a2e2e,stroke:#5bcfdf,color:#e6edf3
CLI["CLI args"]:::cli
SET["resolve settings\napp/commands/test/"]:::app
LOAD["load configs\ndb/repository/configs/"]:::store
LOOP{"For each config"}
GEN["generate probe config\nxray/config/generator/"]:::engine
SPAWN["spawn Xray\nxray/process/"]:::engine
ICMP["prober/icmp/"]:::probe
TCP["prober/tcp/"]:::probe
DELAY["prober/real_delay/"]:::probe
DL["prober/download/"]:::probe
UL["prober/upload/"]:::probe
KILL["kill probe\nxray/process/"]:::engine
SAVE["persist results\ndb/repository/connection_tests/"]:::store
OUT["format & print\napp/commands/test/output/"]:::app
CLI --> SET --> LOAD --> LOOP
LOOP --> GEN --> SPAWN --> ICMP --> TCP --> DELAY --> DL --> UL --> KILL --> LOOP
LOOP --> SAVE --> OUT
Connect Flow
flowchart LR
classDef cli fill:#1a2744,stroke:#4a9eff,color:#e6edf3
classDef app fill:#2a1a3a,stroke:#b070df,color:#e6edf3
classDef engine fill:#2e1a1a,stroke:#df6060,color:#e6edf3
classDef store fill:#1a2e2e,stroke:#5bcfdf,color:#e6edf3
CLI["CLI args"]:::cli
LOAD["load config\napp/commands/connect/"]:::app
RTSVC["start session\napp/runtime_service/connect/"]:::app
XGEN["build runtime config\nxray/config/generator/"]:::engine
XSPAWN["spawn detached\nxray/process_mgmt/"]:::engine
SAVE["persist session\ndb/repository/runtime_sessions/"]:::store
CLI --> LOAD --> RTSVC --> XGEN --> XSPAWN --> SAVE
Daemon Flow
flowchart TD
classDef cli fill:#1a2744,stroke:#4a9eff,color:#e6edf3
classDef app fill:#2a1a3a,stroke:#b070df,color:#e6edf3
classDef event fill:#1a2e1a,stroke:#5bdf8a,color:#e6edf3
START["xrat daemon start"]:::cli
FORK["fork child process"]:::app
SUP["event loop\napp/daemon/supervisor/"]:::app
REATTACH["reconcile stale sessions\napp/runtime_service/reattach/"]:::app
SELECT{"tokio::select!"}:::app
HEALTH["health check\n(every 15s)"]:::event
IPC["IPC events\n(Unix socket)"]:::event
ROTATE["rotation timer"]:::event
START --> FORK --> SUP --> REATTACH --> SELECT
SELECT --> HEALTH
SELECT --> IPC
SELECT --> ROTATE
Dependency Graph
Modules ordered from most foundational (left) to most dependent (right). An arrow means the target depends on the source.
graph LR
classDef entry fill:#1a2744,stroke:#4a9eff,color:#e6edf3
classDef domain fill:#2e2a1a,stroke:#dfba5b,color:#e6edf3
classDef store fill:#1a2e2e,stroke:#5bcfdf,color:#e6edf3
classDef engine fill:#2e1a1a,stroke:#df6060,color:#e6edf3
classDef probe fill:#2a2a1a,stroke:#c0df5b,color:#e6edf3
classDef app fill:#2a1a3a,stroke:#b070df,color:#e6edf3
classDef iface fill:#1a3a2a,stroke:#5bdf8a,color:#e6edf3
support["support/"]:::domain
model["model/"]:::domain
config["config/"]:::domain
db["db/"]:::store
xray["xray/"]:::engine
singbox["singbox/"]:::engine
prober["prober/"]:::probe
app["app/"]:::app
cli["cli/"]:::iface
server["server/"]:::iface
main["main.rs"]:::entry
support --> model
model --> config
config --> db
config --> xray
xray --> prober
xray --> singbox
prober --> app
db --> app
app --> cli
cli --> main
support --> server
db --> server
Source Tree
src/
βββ main.rs # Entrypoint: parse CLI, init tracing, dispatch command
βββ lib.rs # Re-exports all public modules
β
βββ cli/ # Clap command/flag definitions
β βββ mod.rs # Module root, pub re-exports
β βββ root.rs # Cli struct with global flags
β βββ command.rs # Command enum (all subcommands)
β βββ add.rs # AddArgs
β βββ connect.rs # ConnectArgs
β βββ daemon.rs # DaemonArgs + DaemonAction
β βββ disconnect.rs # DisconnectArgs
β βββ import.rs # ImportArgs
β βββ lifecycle.rs # select / enable / disable / delete / restore
β βββ list.rs # ListArgs + ListTarget
β βββ parse.rs # ParseArgs + ParseEngine
β βββ proxy.rs # ProxyArgs + ProxyAction
β βββ scan.rs # ScanArgs
β βββ serve.rs # ServeArgs
β βββ status.rs # StatusArgs
β βββ tui.rs # TuiArgs
β βββ test_cmd/ # TestArgs + TestFormat/TestSortBy
β βββ tests/ # CLI parsing tests (cases/test_command, cases/runtime_parse, ...)
β
βββ app/ # Application layer
β βββ mod.rs
β βββ app_paths.rs # Filesystem layout resolution
β βββ context.rs # AppContext: DB + config + runtime paths
β βββ context/
β β βββ paths.rs # Runtime path resolution
β β βββ tests/ # Context tests (binary, database resolution)
β βββ config/ # AppConfig TOML deserialization (proxy + testing)
β βββ daemon.rs # Daemon CLI dispatch glue
β βββ error.rs # AppError enum
β βββ import.rs # Top-level import orchestration
β βββ input/ # Input source reading (read_input, fetch_url)
β βββ runtime_service.rs # RuntimeService public re-exports
β βββ commands/ # Command handlers
β β βββ mod.rs
β β βββ add.rs
β β βββ connect.rs
β β βββ daemon.rs
β β βββ disconnect.rs
β β βββ import.rs
β β βββ lifecycle.rs
β β βββ list.rs
β β βββ parse.rs
β β βββ proxy.rs
β β βββ runtime_output.rs
β β βββ scan.rs
β β βββ serve.rs
β β βββ status/ # display + json + tests submodules
β β βββ test.rs
β β βββ test/
β β β βββ bulk/ # bulk executor
β β β β βββ bulk_executor/
β β β βββ execution/ # per-config probe loop
β β β βββ handlers/ # CLI arg handling helpers
β β β βββ output/ # table / TSV / CSV / JSON output
β β β βββ output_types/
β β β βββ settings/ # resolve / rows / validation
β β β βββ stages/ # endpoint / progress / throughput
β β β βββ tests/ # focused tests
β β βββ tui.rs
β βββ runtime_service/ # Proxy process lifecycle
β β βββ connect/ # Connect flow
β β βββ replace_flow/ # Atomic disconnect + connect (candidate, ports, stage)
β β βββ reattach/ # Stale session recovery (process inspector)
β β βββ session_state/# State transitions + inbound health
β β βββ types.rs
β β βββ tests/ # Integration tests
β βββ daemon/ # Daemon supervisor
β βββ ipc/ # Unix socket IPC protocol
β β βββ types.rs # Request/response types (DaemonRequest, RotationTrigger, ...)
β β βββ handler/ # dispatch.rs + io.rs
β β βββ client/ # unix_impl.rs + unsupported_impl.rs
β β βββ transport/ # ping_shutdown.rs, proxy.rs, runtime.rs
β β βββ tests/ # IPC integration tests
β βββ supervisor/ # Event loop
β βββ mod.rs
β βββ types.rs
β βββ health.rs
β βββ runtime.rs
β βββ test_support.rs
β βββ tests.rs
β βββ handlers/ # Health check, rotation, runtime
β βββ health.rs
β βββ mod.rs
β βββ runtime/ # runtime_lifecycle/, runtime_status_connect/
β βββ tests/ # tests_replace/
β
βββ model/ # Shared domain types
β βββ node.rs # Node struct
β βββ protocol.rs # Protocol enum
β βββ node_dedup_key.rs # Dedup key generation
β
βββ config/ # Config parsing and normalization
β βββ protocols/ # Protocol-specific parsers
β β βββ vless.rs # vless:// parser
β β βββ vmess.rs # vmess:// parser
β β βββ ss.rs # ss:// parser
β β βββ trojan.rs # trojan:// parser
β β βββ http.rs # http:// parser
β β βββ socks5.rs # socks5:// parser
β β βββ hy2.rs # hysteria2:// parser
β β βββ tests/ # Parser tests
β βββ line.rs # Line-by-line text parsing
β βββ normalize.rs # Node normalization defaults
β βββ parse_service.rs # Engine-aware parsing
β βββ import/ # Import format detection
β β βββ detect.rs # Format detection heuristics
β β βββ error.rs
β β βββ mod.rs # ImportMode / ImportResult / parse_import
β β βββ subscription.rs # URL fetch + metadata
β β βββ parsers/ # single_link, plain_list, base64, sip008, xray
β βββ parsing_helpers.rs # Shared URI helpers
β
βββ db/ # Database layer
β βββ connection.rs # Connection pool management
β βββ schema.rs # Migration runner
β βββ error.rs # DbError enum
β βββ mod.rs # DbPool + facade re-exports
β βββ database/ # Database query methods
β βββ repository/ # SQL implementations
β β βββ api/ # API-specific queries
β β βββ cf_scan_results.rs
β β βββ configs/
β β β βββ import_ops/ # Upsert on dedup_key
β β β βββ state_ops/ # enable/disable/select/delete
β β β βββ server_ops.rs
β β βββ connection_tests.rs
β β βββ row/ # Shared row helpers
β β βββ runtime_sessions.rs
β βββ record/ # Record types (DTOs)
β βββ cf_scan_results.rs
β βββ configs.rs
β βββ connection_tests.rs
β βββ import.rs # ImportSource, SubscriptionRecord, ...
β βββ mod.rs
β βββ runtime_sessions.rs
β
βββ xray/ # Xray-core integration
β βββ config/ # Config generation
β β βββ generator/ # Probe + runtime config builders
β β βββ outbound.rs # Protocol-to-outbound mapping
β β βββ stream.rs # Stream settings (TLS, WS, gRPC, TCP)
β β βββ types.rs # XrayConfig, Inbound, Outbound structs
β βββ parsing/ # Xray JSON config parsing
β β βββ core/ # Top-level config structure
β β βββ protocols/ # Inbound/outbound protocol parsers
β β β βββ inbound_settings/
β β β βββ outbound_settings/
β β βββ transports/ # Transport settings parsers
β β β βββ security/
β β βββ shared/ # Shared types (enums, strings)
β βββ process/ # Low-level process spawn + lifecycle
β β βββ errors.rs
β β βββ spawn.rs
β β βββ tests.rs
β βββ process_mgmt/ # High-level process management + signals
β βββ mod.rs
β βββ process.rs
β βββ signals.rs
β βββ tests.rs
β
βββ singbox/ # sing-box integration
β βββ mod.rs
β βββ config/ # sing-box config generation + process_mgmt helper
β βββ mod.rs
β βββ process_mgmt.rs
β
βββ prober/ # Connection testing probes
β βββ mod.rs # FailureKind + combined TestResult
β βββ icmp/ # ICMP ping (parse system ping output)
β β βββ mod.rs # icmp_ping, ping_with_system_command
β β βββ parsing.rs # parse_ping_latency, classify_ping_failure
β β βββ tests.rs
β βββ tcp/ # TCP connectivity check + failure classification
β β βββ check.rs # tcp_check
β β βββ classify.rs
β β βββ errors.rs
β β βββ model.rs # TcpResult
β β βββ mod.rs
β β βββ tests.rs
β βββ real_delay/ # HTTP round-trip latency via proxy
β β βββ check/ # execute, model, port, request, mod
β β βββ classify.rs
β β βββ mod.rs
β βββ download/ # Download speed measurement
β β βββ check/ # proxied, result, mod
β β βββ classify.rs
β β βββ mod.rs
β βββ upload/ # Upload speed measurement
β βββ classify.rs
β βββ mod.rs
β βββ request.rs
β
βββ server/ # Axum HTTP API
β βββ mod.rs
β βββ routes/ # b64, configs, health, json
β βββ auth.rs # API key authentication
β βββ response.rs # Response types
β βββ state.rs # ServerState
β βββ error.rs # Server error types
β
βββ tui/ # Ratatui TUI
β βββ mod.rs
β βββ run.rs # Terminal lifecycle + main loop
β βββ keymap.rs
β βββ task.rs # Background task primitives
β βββ theme.rs
β βββ app/ # App state, reducers, navigation
β βββ data/ # Data loading + tests
β βββ view/ # chrome, configs, sources, runtime, tests, modals
β
βββ support/ # Shared utilities
βββ decode.rs # Base64 decoding
βββ geoip.rs # MaxMind GeoIP lookups
βββ net.rs # Network utilities
βββ time.rs # Timestamp helpers
βββ url.rs # URL detection helpers
File Conventions
mod.rs: Module root, pub re-exports- Names: Snake_case for files/modules, PascalCase for types, snake_case for functions
- Tests:
#[cfg(test)] mod tests { ... }in same file ortests/submodule - Records/DTOs: In
db/record/β thin structs matching DB rows - Repository: In
db/repository/β SQL query functions separated by entity
Config Generation
xrat generates runtime configuration JSON from normalized Node objects for
both Xray-core and sing-box engines.
Overview
The config generation pipeline:
- Node (domain model) β Protocol-specific mapping β JSON config
- Supports probe configs (short-lived, for testing) and runtime configs (long-lived)
flowchart LR
classDef domain fill:#2e2a1a,stroke:#dfba5b,color:#e6edf3
classDef select fill:#1a2c3a,stroke:#5b8def,color:#e6edf3
classDef engine fill:#2e1a1a,stroke:#df6060,color:#e6edf3
classDef out fill:#1a2e1a,stroke:#5bdf8a,color:#e6edf3
NODE["model::Node\n(protocol, address, port, ...)"]:::domain
ENGINE{{"resolve_engine()\nAuto | Xray | SingBox"}}:::select
XGEN["xray/config/generator/\ngenerate_probe_config()\ngenerate_runtime_config()"]:::engine
SGEN["singbox/config/\ngenerate_probe_config()\ngenerate_runtime_config()"]:::engine
XOUT["Xray JSON config\n(XrayConfig)"]:::out
SOUT["sing-box JSON config\n(serde_json::Value)"]:::out
NODE --> ENGINE
ENGINE -- "VLESS, VMess, Trojan\nSS, SOCKS5, HTTP" --> XGEN
ENGINE -- "Hysteria2" --> SGEN
XGEN --> XOUT
SGEN --> SOUT
Xray Config Generation
Located in src/xray/config/.
Entry Points
#![allow(unused)]
fn main() {
// Probe config (used for testing)
pub fn generate_probe_config(node: &Node, local_port: u16) -> Result<XrayConfig, String>
// Runtime config (used for connect)
pub fn generate_runtime_config(node: &Node, socks_port: u16, http_port: Option<u16>) -> Result<XrayConfig, String>
// Runtime config with custom inbound hosts
pub fn generate_runtime_config_with_inbounds(
node: &Node,
socks_host: &str,
socks_port: u16,
http_host: Option<&str>,
http_port: Option<u16>,
) -> Result<XrayConfig, String>
// Runtime config with fully configurable inbounds
pub fn generate_runtime_config_for_inbounds(
node: &Node,
socks: Option<(&str, u16, bool)>,
http: Option<(&str, u16)>,
) -> Result<XrayConfig, String>
}
Probe Config
Used by the testing pipeline to measure real-delay latency:
{
"log": { "loglevel": "warning" },
"inbounds": [
{
"tag": "probe-in",
"port": <random>,
"listen": "127.0.0.1",
"protocol": "socks",
"settings": { "udp": false }
}
],
"outbounds": [
{
"tag": "proxy",
"protocol": "<node.protocol>",
"settings": { ... },
"stream_settings": { ... }
}
]
}
Runtime Config
Used by the managed proxy session:
{
"log": { "loglevel": "warning" },
"inbounds": [
{
"tag": "socks-in",
"port": 1080,
"listen": "0.0.0.0",
"protocol": "socks",
"settings": { "udp": true }
},
{
"tag": "http-in",
"port": 8080,
"listen": "0.0.0.0",
"protocol": "http"
}
],
"outbounds": [
{
"tag": "proxy",
"protocol": "<node.protocol>",
"settings": { ... },
"stream_settings": { ... }
}
]
}
XrayConfig Struct
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct XrayConfig {
pub log: LogConfig,
pub inbounds: Vec<Inbound>,
pub outbounds: Vec<Outbound>,
}
}
Outbound Generation
Each protocol maps to a specific outbound format:
VLESS
{
"vnext": [
{
"address": "example.com",
"port": 443,
"users": [
{
"id": "uuid-123",
"encryption": "none"
}
]
}
]
}
VMess
{
"vnext": [
{
"address": "example.com",
"port": 443,
"users": [
{
"id": "uuid-456",
"alterId": 0,
"security": "auto"
}
]
}
]
}
Trojan
{
"servers": [
{
"address": "example.com",
"port": 443,
"password": "password"
}
]
}
Shadowsocks
{
"servers": [
{
"address": "example.com",
"port": 8388,
"method": "aes-256-gcm",
"password": "secret"
}
]
}
SOCKS5
{
"servers": [
{
"address": "example.com",
"port": 1080,
"users": [
{
"user": "username",
"pass": "password"
}
]
}
]
}
HTTP
{
"servers": [
{
"address": "example.com",
"port": 8080,
"users": [
{
"user": "username",
"pass": "password"
}
]
}
]
}
Hysteria2
Hysteria2 is not supported by Xray. Returns an error if attempted:
Error: hysteria2/hy2 is not supported by xray config generator
Stream Settings
Generated based on node fields:
#![allow(unused)]
fn main() {
fn build_stream_settings(node: &Node) -> Result<Option<StreamSettings>, String> {
let network = node.network.as_str();
// SOCKS5 and HTTP have no stream settings
if matches!(node.protocol, Protocol::Socks5 | Protocol::Http) {
return Ok(None);
}
// Network-specific settings
StreamSettings {
network, // "tcp" | "ws" | "grpc" | "kcp" | "http"
security, // "tls" | "none" | None
tls_settings, // SNI, allow_insecure
ws_settings, // path, headers (Host)
tcp_settings, // HTTP header obfuscation
grpc_settings, // service_name
}
}
}
TLS Settings
{
"network": "tcp",
"security": "tls",
"tlsSettings": {
"serverName": "cdn.example.com",
"allowInsecure": false
}
}
WebSocket Settings
{
"network": "ws",
"security": "tls",
"tlsSettings": {
"serverName": "cdn.example.com"
},
"wsSettings": {
"path": "/ray",
"headers": {
"Host": "cdn.example.com"
}
}
}
gRPC Settings
{
"network": "grpc",
"grpcSettings": {
"serviceName": "service"
}
}
TCP HTTP Obfuscation
{
"network": "tcp",
"tcpSettings": {
"header": {
"type": "http",
"request": {
"path": ["/custom-path"]
}
}
}
}
Stream Settings Logic
#![allow(unused)]
fn main() {
pub(super) fn build_stream_settings(node: &Node) -> Result<Option<StreamSettings>, String> {
let network = node.network.as_str();
if matches!(node.protocol, Protocol::Socks5 | Protocol::Http) {
return Ok(None);
}
let security = node.tls.as_ref().map(|s| s.to_string());
let tls_settings = if node.tls.as_deref() == Some("tls") {
Some(TlsSettings {
server_name: node.sni.clone().unwrap_or_else(|| node.address.clone()),
allow_insecure: None,
})
} else {
None
};
let ws_settings = if network == "ws" {
let mut headers = HashMap::new();
if let Some(host) = &node.host {
headers.insert("Host".to_string(), host.clone());
}
Some(WsSettings {
path: node.path.clone().unwrap_or_else(|| "/".to_string()),
headers: if headers.is_empty() { None } else { Some(headers) },
})
} else {
None
};
let grpc_settings = if network == "grpc" {
Some(GrpcSettings {
service_name: node.path.clone().unwrap_or_default(),
})
} else {
None
};
let tcp_settings = if network == "tcp" {
node.path.as_ref().map(|path| TcpSettings {
header: Some(json!({
"type": "http",
"request": { "path": [path] }
})),
})
} else {
None
};
Ok(Some(StreamSettings {
network: network.to_string(),
security,
tls_settings,
ws_settings,
tcp_settings,
grpc_settings,
}))
}
}
sing-box Config Generation
Located in src/singbox/config/.
Supported Protocols
Currently only Hysteria2 is implemented.
Hysteria2 Config
#![allow(unused)]
fn main() {
pub fn generate_probe_config(node: &Node, local_port: u16) -> Result<Value, String>
pub fn generate_runtime_config(node: &Node, socks_port: u16) -> Result<Value, String>
}
Probe Config
{
"log": { "level": "warn" },
"inbounds": [
{
"type": "socks",
"tag": "socks-in",
"listen": "127.0.0.1",
"listen_port": <local_port>
}
],
"outbounds": [
{
"type": "hysteria2",
"tag": "proxy",
"server": "example.com",
"server_port": 443,
"password": "secret",
"tls": {
"enabled": true,
"server_name": "cdn.example.com"
}
},
{
"type": "direct",
"tag": "direct"
}
]
}
Engine Resolution
The parser service resolves which engine to use:
#![allow(unused)]
fn main() {
pub fn resolve_engine(mode: EngineMode, protocol: Protocol) -> Result<ResolvedEngine, ConfigParseError> {
match mode {
EngineMode::Auto => {
if matches!(protocol, Protocol::Hy2) {
Ok(ResolvedEngine::SingBox)
} else {
Ok(ResolvedEngine::Xray)
}
}
EngineMode::Xray => {
if matches!(protocol, Protocol::Hy2) {
return Err(ConfigParseError::UnsupportedScheme(
"hysteria2/hy2 is not compatible with xray engine".to_string()
));
}
Ok(ResolvedEngine::Xray)
}
EngineMode::SingBox => Ok(ResolvedEngine::SingBox),
}
}
}
Xray JSON Parsing
xrat includes a full Xray JSON config parser for reading existing configs.
Parser Modes
| Mode | Behavior |
|---|---|
strict | Rejects unknown fields using #[serde(deny_unknown_fields)] |
lenient | Allows unknown fields (default) |
auto | Same as lenient (reserved for future source-aware parsing) |
Parsed Structures
The parser can read:
- Log: access, error, loglevel, dns_log, mask_address
- API: tag, listen, services
- DNS: hosts, servers, client_ip, query_strategy, cache/fallback settings
- Routing: domain_strategy, rules, balancers
- Policy: levels (handshake, connIdle, etc.), system stats
- Inbounds: various protocol inbounds with full settings
- Outbounds: various protocol outbounds with full settings
- Transports: TCP, WebSocket, gRPC, HTTP/2, QUIC, KCP
- Features: stats, reverse, fakedns, metrics, observatory
Process Management
Xray Process Spawning
Low-level process lifecycle:
#![allow(unused)]
fn main() {
pub async fn spawn(config: &XrayConfig, startup_timeout: Duration) -> Result<XrayProcess, XrayProcessError>
}
- Write config JSON to temp file
- Spawn
xray run -c <config_path> - Poll SOCKS port every 100ms
- Return when port is ready or timeout
Managed Process Spawning
High-level process lifecycle for daemon:
#![allow(unused)]
fn main() {
pub async fn spawn_detached(
binary_path: &Path,
runtime_dir: &Path,
session_id: i64,
config: &XrayConfig,
ready_host: &str,
ready_port: u16,
startup_timeout: Duration,
) -> Result<ManagedXrayProcess, AppError>
}
- Write config to
runtime_dir/session-<id>.json - Create stdout/stderr log files
- Spawn detached process
- Poll for readiness
- Return ManagedXrayProcess (PID, port, paths)
Signal Handling
#![allow(unused)]
fn main() {
pub fn terminate_process_gracefully(
pid: i64,
timeout: Duration,
) -> Result<TerminationOutcome, AppError>
}
- Send SIGTERM
- Poll every 100ms up to timeout
- If still running, send SIGKILL
- Return outcome:
Terminated,Killed,NotRunning
Config Generation Tests
#![allow(unused)]
fn main() {
#[test]
fn test_generate_vless_probe_config() {
let node = Node {
protocol: Protocol::Vless,
address: "example.com".to_string(),
port: 443,
uuid: Some("test-uuid".to_string()),
network: "tcp".to_string(),
tls: Some("tls".to_string()),
sni: Some("example.com".to_string()),
// ...
};
let config = generate_probe_config(&node, 10808).unwrap();
assert_eq!(config.inbounds[0].port, 10808);
assert_eq!(config.outbounds[0].protocol, "vless");
}
}
Import Pipeline
The import pipeline converts raw input (URLs, files, raw text) into normalized
Node records, deduplicates them, and persists them as ConfigRecord rows
under their source SubscriptionRecord.
End-to-End Flow
flowchart TD
classDef io fill:#1a2e1a,stroke:#5bdf8a,color:#e6edf3
classDef cfg fill:#2e2a1a,stroke:#dfba5b,color:#e6edf3
classDef node fill:#1a2c3a,stroke:#5b8def,color:#e6edf3
classDef db fill:#1a2e2e,stroke:#5bcfdf,color:#e6edf3
INPUT["Raw Input\n(URL / file / stdin)"]:::io
READ["read_input()\napp/input/source.rs"]:::io
SRC["ImportSource\n{ kind, value, name }"]:::io
BYTES["raw bytes"]:::io
IMPORT["run_import()\napp/import.rs"]:::cfg
DETECT["detect format\nconfig/import/detect.rs"]:::cfg
MODE["ImportMode\n(Auto | SingleLink | Base64 | Plain | Sip008 | XrayJson)"]:::cfg
PARSE["parse_import()\nconfig/import/"]:::cfg
RESULT["ImportResult\n{ nodes, errors, metadata }"]:::cfg
NODE["model::Node"]:::node
NORM["normalize()\nconfig/normalize.rs"]:::node
DEDUP["dedup_key()\nmodel::Node"]:::node
PERSIST["upsert\ndb/repository/configs/import_ops"]:::db
SUB_REC["link subscription\ndb/repository/subscriptions/"]:::db
INPUT --> READ
READ --> SRC & BYTES
SRC --> IMPORT
BYTES --> IMPORT
IMPORT --> DETECT --> MODE --> PARSE --> RESULT --> NODE --> NORM --> DEDUP --> PERSIST --> SUB_REC
Input Sources
app/input/source.rs::read_input accepts a single string and decides how to
load the bytes. There is no separate InputSource enum at the call site β the
function returns an ImportSource plus the raw bytes.
#![allow(unused)]
fn main() {
pub fn read_input(input: &str) -> Result<(ImportSource, Vec<u8>), AppError>;
pub struct ImportSource {
pub kind: SourceKind, // Url | File | RawText
pub value: String, // original input or path
pub name: Option<String>,
}
}
Decision rules:
looks_like_url(input)β fetch withreqwest::blocking::get; status errors propagate asAppError.Path::new(input).exists()β read from disk; filename is used asnamewhen available.- otherwise β treat the input as raw text bytes.
Format Detection
config/import/detect.rs runs heuristics on the (decoded) bytes to pick an
ImportMode:
#![allow(unused)]
fn main() {
pub enum ImportMode {
Auto,
SingleLink,
Base64Subscription,
PlainList,
Sip008Json,
XrayJson,
}
}
| Detected as | Heuristic | Parser entry |
|---|---|---|
SingleLink | Starts with a known URI scheme | parsers::parse_single_link |
Sip008Json | JSON object with version: 1 and servers: [...] | parsers::parse_sip008_json |
XrayJson | JSON object with outbounds (or log/inbounds) | parsers::parse_xray_json |
Base64Subscription | Decodes to base64, then plain-list parses successfully | parsers::parse_base64_subscription |
PlainList | Newline-separated share links | parsers::parse_plain_list |
config/import::parse_import is the public entry point; it takes a string plus
an ImportMode (use Auto for detection, anything else to force a mode) and
returns ImportResult { nodes, errors, metadata }.
Protocol Parsers
Each supported protocol has a dedicated parser in config/protocols/:
flowchart TD
classDef input fill:#1a2e1a,stroke:#5bdf8a,color:#e6edf3
classDef parser fill:#2e2a1a,stroke:#dfba5b,color:#e6edf3
classDef node fill:#1a2c3a,stroke:#5b8def,color:#e6edf3
classDef err fill:#3a1a1a,stroke:#df5b5b,color:#e6edf3
LINK["share link string"]:::input
ROUTE{"URI scheme"}
VL["vless.rs"]:::parser
VM["vmess.rs"]:::parser
SS["ss.rs"]:::parser
TR["trojan.rs"]:::parser
HT["http.rs"]:::parser
SK["socks5.rs"]:::parser
HY["hy2.rs"]:::parser
ERR["ImportParseError\n::UnsupportedProtocol"]:::err
NODE["model::Node"]:::node
LINK --> ROUTE
ROUTE -- "vless://" --> VL
ROUTE -- "vmess://" --> VM
ROUTE -- "ss://" --> SS
ROUTE -- "trojan://" --> TR
ROUTE -- "http://" --> HT
ROUTE -- "socks5://" --> SK
ROUTE -- "hy2://" --> HY
ROUTE -- "unknown" --> ERR
VL & VM & SS & TR & HT & SK & HY --> NODE
The Protocol enum lives in model/protocol.rs and serializes lowercase
(vless, vmess, ss, trojan, http, socks5, hy2).
Node Struct
model::Node is the shared, in-memory domain type produced by every parser and
consumed by every engine generator. Fields are intentionally minimal β
protocol-specific quirks live in the parser or get dropped on the floor before
persistence.
#![allow(unused)]
fn main() {
pub struct Node {
pub protocol: Protocol,
pub address: String,
pub port: u16,
pub username: Option<String>,
pub uuid: Option<String>,
pub password: Option<String>,
pub method: Option<String>, // shadowsocks cipher
pub network: String, // tcp | ws | grpc | ...
pub tls: Option<String>,
pub sni: Option<String>,
pub host: Option<String>,
pub path: Option<String>,
pub name: Option<String>,
pub extensions: Option<BTreeMap<String, String>>,
pub raw_config: String,
}
}
Node is also the source for NodeDedupKey (see below).
Normalization
config/normalize.rs is a single fn normalize(node: &mut Node) that runs in
place after parsing:
- empty
networkbecomes"tcp" network == "ws"β copysnitohostifhostisNone; setpathto"/"ifpathisNonenetwork == "grpc"β setpathto"/"ifpathisNone- empty-string
tls(Some("")) is collapsed toNone
Deduplication
model::Node::dedup_key returns a NodeDedupKey. Its Display impl serializes
a length-prefixed, versioned string that the DB stores in configs.dedup_key
(UNIQUE).
#![allow(unused)]
fn main() {
pub struct NodeDedupKey {
pub protocol: Protocol,
pub address: String,
pub port: u16,
pub username: Option<String>,
pub uuid: Option<String>,
pub password: Option<String>,
pub method: Option<String>,
pub network: String,
pub tls: Option<String>,
pub sni: Option<String>,
pub host: Option<String>,
pub path: Option<String>,
}
}
The serialized form prefixes every field with name=<char_count>:<value> and
writes name=- for None values, so missing and empty-string values differ.
Example:
v1|protocol=5:vless|address=11:example.com|port=3:443|username=-|uuid=8:uuid|123|password=-|method=-|network=2:ws|tls=3:tls|sni=15:cdn.example.com|host=15:cdn.example.com|path=4:/ray
db/repository/configs/import_ops performs the upsert keyed on dedup_key;
duplicates are skipped and counted in ImportSummary.
Daemon Architecture
The daemon is a background process that owns the managed Xray runtime, accepts IPC requests from CLI/TUI clients, runs health checks, and drives auto-rotation.
Process Model
graph TB
classDef cli fill:#1a2744,stroke:#4a9eff,color:#e6edf3
classDef proc fill:#2a1a3a,stroke:#b070df,color:#e6edf3
classDef client fill:#1a2e1a,stroke:#5bdf8a,color:#e6edf3
classDef sock fill:#2e2a1a,stroke:#dfba5b,color:#e6edf3
CLI["xrat daemon start"]:::cli
PARENT["Parent process\nvalidates config, forks child"]:::proc
CHILD["Child process\nexecs 'xrat daemon run-server'"]:::proc
SOCK["Unix socket\n/path/to/xrat.sock"]:::sock
CLIENT1["xrat status"]:::client
CLIENT2["xrat connect"]:::client
CLIENT3["xrat proxy start"]:::client
CLI --> PARENT
PARENT -- "std::process::Command" --> CHILD
CHILD -- "creates" --> SOCK
CLIENT1 -- "IPC request" --> SOCK
CLIENT2 -- "IPC request" --> SOCK
CLIENT3 -- "IPC request" --> SOCK
Daemon Startup Sequence
sequenceDiagram
participant User as User
participant CLI as xrat daemon start
participant Parent as Parent Process
participant Child as Child (run-server)
participant Sock as Unix Socket
participant DB as Database
User->>CLI: xrat daemon start
CLI->>Parent: validate ports, clean old socket
Parent->>Child: fork + exec (XRAT_DAEMON_PARENT_PID)
Child->>Child: init SupervisorState
Child->>DB: reattach stale sessions
Child->>Sock: listen on Unix socket
Child->>Parent: IPC Ping response
Parent->>User: "Daemon started (pid: N)"
Note over Child: Enter event loop
Supervisor Event Loop
The supervisor runs a tokio::select! loop with three concurrent branches (IPC
accept, health-check tick, rotation tick). The structure of the loop itself is
internal β what matters is the set of SupervisorEvent messages the loop
handles and the resulting SupervisorState mutations.
stateDiagram-v2
[*] --> SupervisorRunning: daemon run-server
state SupervisorRunning {
[*] --> AcceptIPC: socket listener
[*] --> HealthCheck: interval tick
[*] --> RotationTick: interval tick
AcceptIPC --> HandleIPC: connection received
HandleIPC --> AcceptIPC: response sent
HealthCheck --> CheckEndpoints: tick
CheckEndpoints --> HealthCheck: done
RotationTick --> EvalRotation: tick
EvalRotation --> RotationTick: no trigger
EvalRotation --> DoRotation: trigger active
DoRotation --> RotationTick: done
}
SupervisorRunning --> SupervisorStopped: DaemonShutdown IPC
SupervisorStopped --> [*]: exit
IPC Protocol
The daemon communicates with CLI clients over a Unix socket using newline-
delimited JSON. The wire envelope is DaemonRequest / DaemonResponse<T> (see
app/daemon/ipc/types.rs); the actual operations are variants on
DaemonRequestKind.
Request Envelope
#![allow(unused)]
fn main() {
pub struct DaemonRequest {
pub protocol_version: u16,
pub request: DaemonRequestKind,
}
pub enum DaemonRequestKind {
DaemonPing,
DaemonShutdown,
RuntimeStatus,
RuntimeConnect { config_id: i64 },
RuntimeReplace {
trigger: RotationTrigger,
candidate_id: Option<i64>,
},
RuntimeDisconnect,
ProxyStart,
ProxyStatus,
ProxyStop,
}
}
RuntimeConnect and RuntimeReplace carry the operation inputs inline; the
other variants are unit-only. DaemonResponse<T> is generic, wraps a
DaemonResponseCode (Ok | Busy | NotFound | InvalidState | InternalError),
and carries a typed payload (PingPayload, RuntimeStatusPayload,
RuntimeConnectPayload, etc.).
Client and Server
sequenceDiagram
participant C as IPC Client (CLI / TUI)
participant S as IPC Server (Daemon)
C->>S: connect Unix socket
S-->>C: accept connection
rect rgb(40, 60, 90)
Note over C,S: One DaemonRequest / DaemonResponse exchange
end
C->>S: write JSON request + newline
S->>S: parse DaemonRequest
S->>S: dispatch_request
S->>S: supervisor handler
S->>S: build DaemonResponse
S-->>C: write JSON response + newline
C->>C: read JSON response + newline
Transport Routing
app/daemon/ipc/handler/dispatch.rs maps every DaemonRequestKind variant to a
*_response_via_supervisor helper. Those helpers live in
app/daemon/ipc/transport/ grouped by feature:
flowchart TD
classDef req fill:#1a2c3a,stroke:#5b8def,color:#e6edf3
classDef route fill:#2a1a3a,stroke:#b070df,color:#e6edf3
classDef trans fill:#2e2a1a,stroke:#dfba5b,color:#e6edf3
REQ["DaemonRequestKind"]:::req
TYPE{"dispatch"}:::route
TS["transport/ping_shutdown.rs"]:::trans
TP["transport/proxy.rs"]:::trans
TR["transport/runtime.rs"]:::trans
REQ --> TYPE
TYPE -- "DaemonPing\nDaemonShutdown" --> TS
TYPE -- "ProxyStart\nProxyStop\nProxyStatus" --> TP
TYPE -- "RuntimeConnect\nRuntimeDisconnect\nRuntimeReplace\nRuntimeStatus" --> TR
Health Checking
flowchart TD
classDef tick fill:#1a2744,stroke:#4a9eff,color:#e6edf3
classDef check fill:#2a1a3a,stroke:#b070df,color:#e6edf3
classDef ok fill:#1a3a1a,stroke:#5bdf8a,color:#e6edf3
classDef warn fill:#3a2a1a,stroke:#dfba5b,color:#e6edf3
classDef fail fill:#3a1a1a,stroke:#df5b5b,color:#e6edf3
TICK["health tick fires"]:::tick
CHECK{"active session?"}:::check
PROBE["probe inbound SOCKS / HTTP ports"]:::check
OPEN{"all inbounds reachable?"}:::check
RECORD["record success\nreset failure count"]:::ok
WAIT["skip"]:::ok
INCR["increment failure count"]:::warn
THRESH{"failures > threshold?"}:::warn
TRIGGER["trigger rotation\n(HealthCheckFailed)"]:::fail
COOLDOWN["enter cooldown period"]:::warn
TICK --> CHECK
CHECK -- "yes" --> PROBE
CHECK -- "no" --> WAIT
PROBE --> OPEN
OPEN -- "yes" --> RECORD
OPEN -- "no" --> INCR --> THRESH
THRESH -- "yes" --> TRIGGER
THRESH -- "no" --> COOLDOWN
Inbound health is reported as RuntimeInboundHealth with per-endpoint
RuntimeEndpointState::{Reachable, Unreachable, NotChecked} (see
Runtime Lifecycle).
Auto-Rotation
The daemon can rotate the active proxy on three triggers:
#![allow(unused)]
fn main() {
pub enum RotationTrigger {
Manual,
Timer,
HealthCheckFailed,
}
}
The trigger is carried inline in DaemonRequestKind::RuntimeReplace. The
supervisor stores rotation state in SupervisorState (per-AppContext) and
reports it via ProxyStatusPayload (rotation_enabled, interval_secs,
health_trigger_enabled, cooldown_secs, last_trigger, last_result,
cooldown_active, next_timer_epoch_secs).
Rotation Flow
flowchart TD
classDef trigger fill:#1a2744,stroke:#4a9eff,color:#e6edf3
classDef step fill:#2a1a3a,stroke:#b070df,color:#e6edf3
classDef ok fill:#1a3a1a,stroke:#5bdf8a,color:#e6edf3
classDef fail fill:#3a1a1a,stroke:#df5b5b,color:#e6edf3
classDef store fill:#1a2e2e,stroke:#5bcfdf,color:#e6edf3
TRIG{"trigger source"}:::trigger
NEXT["select next candidate config"]:::step
BUILD["handle_runtime_replace\nsupervisor/handlers/runtime/"]:::step
SPAWN["spawn new Xray\n(ephemeral ports)"]:::step
WAIT_HEALTH["wait for inbound health"]:::step
ATOMIC{"healthy?"}
SWITCH["atomically swap active config"]:::ok
CLEAN["kill new process\nkeep old session"]:::fail
STOP_OLD["stop old process"]:::ok
PERSIST["persist new session record"]:::store
TRIG -- "Timer" --> NEXT
TRIG -- "HealthCheckFailed" --> NEXT
TRIG -- "Manual IPC" --> NEXT
NEXT --> BUILD --> SPAWN --> WAIT_HEALTH --> ATOMIC
ATOMIC -- "yes" --> SWITCH --> STOP_OLD --> PERSIST
ATOMIC -- "no" --> CLEAN
Runtime Lifecycle
The runtime service owns the lifecycle of the managed Xray process: spawning sessions, swapping active configs, recovering state across daemon restarts, and reporting inbound health to clients.
The implementation lives in app/runtime_service/. The RuntimeService struct
is created from an AppContext and is consumed by the daemon supervisor, the
daemon IPC handlers, and TUI runtime flows. CLI runtime commands send IPC
requests to the daemon instead of constructing RuntimeService directly.
RuntimeService API
#![allow(unused)]
fn main() {
pub struct RuntimeService<'a> {
context: &'a AppContext,
}
impl RuntimeService<'_> {
pub async fn connect(&self, config_id: i64) -> Result<ConnectResult>;
pub async fn disconnect(&self) -> Result<DisconnectResult>;
pub async fn status(&self) -> Result<RuntimeStatusSnapshot>;
pub(super) async fn stage_replacement_runtime(
&self,
next_config_id: i64,
) -> Result<(i64, i64, u32)>; // (config_id, session_id, pid)
pub async fn reconcile_reattach_on_daemon_start(
&self,
daemon_instance_id: &str,
) -> Result<()>;
}
}
ConnectResult and ReplaceResult carry the new session id and pid;
RuntimeStatusSnapshot is the read-only view returned to the daemon supervisor
and the TUI.
Persisted Session Status
Sessions in the runtime_sessions table carry a RuntimeSessionStatus with
five plain variants (no data attached). All other βstatesβ reported to clients
are derived in memory at read time.
#![allow(unused)]
fn main() {
pub enum RuntimeSessionStatus {
Starting,
Running,
Stopping,
Stopped,
Failed,
}
}
as_str() returns the snake-case string stored in the DB column.
stateDiagram-v2
[*] --> Starting : connect / replace
Starting --> Running : process ready
Starting --> Failed : spawn error
Running --> Stopping : disconnect / replace
Running --> Failed : reattach rejected
Stopping --> Stopped : process exited
Stopped --> [*]
Failed --> [*]
Derived Runtime Display
The status snapshot folds in PID liveness and inbound reachability, so the caller can show βdegradedβ without separately checking the supervisor:
#![allow(unused)]
fn main() {
pub struct RuntimeStatusSnapshot {
pub status: RuntimeSessionDisplay,
pub session: Option<RuntimeSessionRecord>,
pub session_config: Option<ConfigRecord>,
pub active_config: Option<ConfigRecord>,
pub selected_config: Option<ConfigRecord>,
pub pid_running: bool,
pub inbound_health: RuntimeInboundHealth,
pub database_label: String,
}
pub enum RuntimeSessionDisplay {
Degraded,
Persisted(RuntimeSessionStatus),
Stale,
StaleReconciled,
Stopped,
}
}
ActiveSessionState is the internal pre-fold form used by the supervisor:
#![allow(unused)]
fn main() {
pub enum ActiveSessionState {
None,
Running(RuntimeSessionRecord),
Stale(RuntimeSessionRecord),
}
}
Connect Flow
sequenceDiagram
participant CLI as CLI/Client
participant D as Daemon IPC
participant RS as RuntimeService
participant DB as Database
participant XM as xray::process_mgmt
participant SP as Supervisor
CLI->>D: RuntimeConnect(config_id)
D->>RS: connect(config_id)
RS->>DB: load config
RS->>RS: resolve launch (endpoints, inbounds)
RS->>DB: insert RuntimeSession (Starting)
RS->>XM: spawn_detached(binary, runtime_dir, config, ready_host, ready_port)
XM-->>RS: ManagedXrayProcess { pid }
RS->>DB: update RuntimeSession (Running, pid, started_at)
RS-->>D: ConnectResult { config, session_id, pid, runtime_config_path, endpoints }
D-->>CLI: daemon response
Replace Flow (Hot-Swap Rotation)
The replace flow is staged: the new process is launched and observed healthy before the old one is killed, so a bad candidate never leaves the user without connectivity.
sequenceDiagram
participant CLI as CLI/Client
participant D as Daemon IPC
participant RS as RuntimeService
participant DB as Database
participant XM as xray::process_mgmt
CLI->>D: RuntimeReplace(trigger, candidate_id)
D->>RS: replace(trigger, candidate_id)
RS->>RS: pick candidate (or use candidate_id)
RS->>RS: stage_replacement_runtime(next_id)
RS->>DB: insert new RuntimeSession (Starting)
RS->>XM: spawn_detached(new config, ephemeral ports)
XM-->>RS: new process (ready)
RS->>DB: update new session (Running, pid)
RS->>XM: terminate old process
XM-->>RS: old stopped
RS->>DB: update old session (Stopped)
RS-->>D: ReplaceResult { old_session_id, new_config_id, new_session_id, new_pid }
D-->>CLI: daemon response
Ephemeral Port Allocation
The replace flow does not keep a fixed port range.
assign_ephemeral_inbound_ports asks the kernel for a free TCP port for each
inbound (socks / http / shadowsocks) by binding TcpListener::bind((host, 0)),
then drops the listener and uses the port the kernel assigned. The old process
keeps its ports until it is stopped.
#![allow(unused)]
fn main() {
fn allocate_port(host: &str) -> Result<u16> {
let listener = TcpListener::bind((connect_host_for_bind_host(host).as_str(), 0))?;
let port = listener.local_addr()?.port();
drop(listener);
Ok(port)
}
}
This eliminates the per-replace port rotation bookkeeping the previous design required.
Reattach Flow
On daemon restart the supervisor asks the runtime service whether the previously
persisted Running session is still alive. If the recorded PID no longer
matches an Xray executable, or the inbound is not reachable, the session is
marked Failed with a precise reason code.
flowchart TD
classDef start fill:#1a2744,stroke:#4a9eff,color:#e6edf3
classDef check fill:#2a1a3a,stroke:#b070df,color:#e6edf3
classDef ok fill:#1a3a1a,stroke:#5bdf8a,color:#e6edf3
classDef fail fill:#3a1a1a,stroke:#df5b5b,color:#e6edf3
START["daemon starts"]:::start
LOAD["get_running_runtime_session"]:::check
FOUND{"session found?"}:::check
NO["nothing to reattach"]:::ok
CHECK_PID{"PID still alive?"}:::check
VALIDATE{"exec + cmdline match?"}:::check
HEALTH{"any inbound reachable?"}:::check
RECONCILE["keep as Running"]:::ok
STALE["mark Failed\n(with reason code)"]:::fail
START --> LOAD --> FOUND
FOUND -- "no" --> NO
FOUND -- "yes" --> CHECK_PID
CHECK_PID -- "no" --> STALE
CHECK_PID -- "yes" --> VALIDATE
VALIDATE -- "no" --> STALE
VALIDATE -- "yes" --> HEALTH
HEALTH -- "ok" --> RECONCILE
HEALTH -- "none reachable" --> STALE
Reject reason codes:
daemon_restart_reattach_rejected_pid_missingβ the recorded PID has exited.daemon_restart_reattach_rejected_exec_mismatchβ the process is running but the executable path does not match.daemon_restart_reattach_rejected_cmdline_mismatchβ the executable matches but the cmdline does not reference the right runtime config.
The transition is also recorded in runtime_sessions.owner_kind,
owner_instance_id, and last_transition_* columns so the next daemon instance
can see who rejected the session and why.
Inbound Health Check
Inbound reachability is folded into RuntimeStatusSnapshot.inbound_health as a
per-endpoint view:
#![allow(unused)]
fn main() {
pub struct RuntimeInboundHealth {
pub socks: Option<RuntimeEndpointHealth>,
pub http: Option<RuntimeEndpointHealth>,
pub shadowsocks: Option<RuntimeEndpointHealth>,
}
pub enum RuntimeEndpointState {
Reachable,
Unreachable,
NotChecked,
}
}
NotChecked is returned when the recorded PID is no longer alive β the service
refuses to TCP-probe a port it knows is bound to a dead process. A session that
is Running with at least one Unreachable endpoint displays as Degraded.
Test Pipeline
The test command probes each selected config across one or more stages (ICMP,
TCP, real-delay, download, upload) and persists a per-config connection_tests
row plus a connection_test_runs row that groups the batch.
High-Level Flow
flowchart TD
classDef cli fill:#1a2744,stroke:#4a9eff,color:#e6edf3
classDef app fill:#2a1a3a,stroke:#b070df,color:#e6edf3
classDef engine fill:#2e1a1a,stroke:#df6060,color:#e6edf3
classDef probe fill:#2a2a1a,stroke:#c0df5b,color:#e6edf3
classDef store fill:#1a2e2e,stroke:#5bcfdf,color:#e6edf3
START["CLI: xrat test"]:::cli
RESOLVE["resolve_test_settings()\nsettings/resolve.rs"]:::app
LOAD["load target configs\ndb/repository/configs/"]:::store
EXEC["run for each config\nbulk/single.rs"]:::app
PROBE["generate probe config\nspawn Xray process"]:::engine
STAGES["run enabled stages\nsequentially"]:::probe
KILL["kill probe process"]:::engine
SAVE["persist results\ndb/repository/connection_tests/"]:::store
OUT["format & print\noutput/print.rs"]:::app
START --> RESOLVE --> LOAD --> EXEC --> PROBE --> STAGES --> KILL --> SAVE --> OUT
Probers
src/prober/ is the leaf-level measurement crate. Each prober is a small async
function that returns a *Result struct plus a FailureKind on failure. The
combined TestResult is the only thing the rest of the command touches.
#![allow(unused)]
fn main() {
pub use download::{DownloadResult, download_speed_check};
pub use icmp::{IcmpResult, icmp_ping};
pub use real_delay::{RealDelayResult, real_delay_check};
pub use tcp::{TcpResult, tcp_check};
pub use upload::{UploadResult, upload_speed_check};
pub struct TestResult {
pub icmp_ok: bool,
pub icmp_ms: Option<u32>,
pub tcp_ok: bool,
pub tcp_ms: Option<u32>,
pub real_delay_ok: bool,
pub real_delay_ms: Option<u32>,
pub download_ok: bool,
pub download_mbps: Option<f64>,
pub upload_ok: bool,
pub upload_mbps: Option<f64>,
pub ttfb_ms: Option<u32>,
pub http_status: Option<u16>,
pub endpoint_ip: Option<String>,
pub endpoint_location: Option<String>,
pub endpoint_country: Option<String>,
pub endpoint_asn: Option<String>,
pub failure_kind: Option<FailureKind>,
pub failure_reason: Option<String>,
}
}
Prober Source Layout
src/prober/
βββ icmp/
β βββ mod.rs β icmp_ping, ping_with_system_command
β βββ parsing.rs β parse_ping_latency, classify_ping_failure
βββ tcp/
β βββ check.rs β tcp_check
β βββ classify.rs β classify_dns_error, classify_tcp_error
β βββ model.rs β TcpResult
β βββ mod.rs
β βββ errors.rs
β βββ tests.rs
βββ real_delay/
β βββ check/
β β βββ execute.rs β proxied HTTP latency through Xray
β β βββ model.rs β RealDelayResult
β β βββ mod.rs
β β βββ port.rs β inbound port detection
β β βββ request.rs β request execution
β βββ classify.rs
β βββ mod.rs
βββ download/
β βββ check/
β β βββ proxied.rs β proxied download
β β βββ result.rs β throughput calculation
β β βββ mod.rs
β βββ classify.rs
β βββ mod.rs
βββ upload/
βββ classify.rs β classify_request_error, classify_xray_error
βββ mod.rs β upload_speed_check
βββ request.rs β make_proxied_upload
ICMP, TCP, and upload keep their logic in a single module + flat
classify/parsing files; real_delay and download push the proxied network
code one level deeper into check/.
Settings Resolution
The persisted TestingSettings (in app/config/testing/types.rs) is the
βconfig file + defaultsβ form. settings/resolve.rs::resolve_test_settings
merges that with the CLI flags into a ResolvedTestSettings value that the
executor consumes.
#![allow(unused)]
fn main() {
pub struct TestingSettings {
pub concurrency: i32,
pub order: Vec<ConnectionTestStage>,
pub failure_policy: TestFailurePolicy,
pub real_delay: RealDelayTestSettings,
pub icmp: IcmpTestSettings,
pub download: DownloadTestSettings,
pub tcp: TcpTestSettings,
pub geoip: GeoIpTestSettings,
}
pub enum ConnectionTestStage {
Icmp,
RealDelay,
Download,
}
pub enum TestFailurePolicy {
Continue,
SkipRemaining,
MarkFailed,
}
}
TestFailurePolicy::halts_after_failure() returns true for SkipRemaining
and MarkFailed; the executor uses this to decide whether to keep going after a
stage failure.
Resolved Settings
#![allow(unused)]
fn main() {
pub(crate) struct ResolvedTestSettings {
pub(crate) stage_order: Vec<ConnectionTestStage>,
pub(crate) failure_policy: TestFailurePolicy,
pub(crate) real_delay_url: String,
pub(crate) download_url: String,
pub(crate) upload_url: Option<String>, // upload only runs if Some
pub(crate) xray_binary_path: PathBuf,
pub(crate) icmp_timeout: Duration,
pub(crate) tcp_timeout: Duration,
pub(crate) xray_startup_timeout: Duration,
pub(crate) real_delay_timeout: Duration,
pub(crate) download_timeout: Duration,
pub(crate) upload_timeout: Duration,
pub(crate) upload_payload_bytes: usize,
pub(crate) run_icmp: bool,
pub(crate) run_tcp: bool,
pub(crate) run_real_delay: bool,
pub(crate) run_download: bool,
pub(crate) run_upload: bool,
pub(crate) concurrency: i32,
pub(crate) geoip_enabled: bool,
pub(crate) geoip_country_path: PathBuf,
pub(crate) geoip_city_path: PathBuf,
pub(crate) geoip_asn_path: PathBuf,
}
}
Note: upload_url is Option<String> and the upload stage is skipped when it
is None β ConnectionTestStage itself has no Upload variant.
Failure Classification
#![allow(unused)]
fn main() {
pub enum FailureKind {
Dns,
Timeout,
Refused,
Unreachable,
PermissionDenied,
Tls,
Auth,
Process,
Proxy,
Unknown,
}
}
as_str() is the canonical snake-case form stored in
connection_tests.failure_kind. Classifiers live next to each prober
(icmp/parsing.rs::classify_ping_failure,
tcp/classify.rs::classify_dns_error/classify_tcp_error,
upload/classify.rs::classify_request_error/classify_xray_error).
flowchart LR
classDef input fill:#1a2744,stroke:#4a9eff,color:#e6edf3
classDef icmp fill:#2e2a1a,stroke:#dfba5b,color:#e6edf3
classDef tcp fill:#2a1a3a,stroke:#b070df,color:#e6edf3
classDef http fill:#2e1a1a,stroke:#df6060,color:#e6edf3
FAIL["test failure"]:::input
CAT{"prober"}
ICMP["dns Β· timeout Β· permission_denied\nunreachable Β· unknown"]:::icmp
TCP["dns Β· timeout Β· refused\nunreachable Β· permission_denied Β· unknown"]:::tcp
RD["process Β· timeout Β· tls\nauth Β· proxy Β· unknown"]:::http
TH["process Β· timeout Β· tls\nauth Β· proxy Β· unknown"]:::http
FAIL --> CAT
CAT -- "ICMP" --> ICMP
CAT -- "TCP" --> TCP
CAT -- "RealDelay" --> RD
CAT -- "Download/Upload" --> TH
Stage Execution
Per config, the executor walks stage_order and runs the matching run_*_stage
function (stages/throughput.rs etc.) into a single TestResult. ICMP and TCP
run without spinning up Xray; real-delay, download, and upload each generate a
probe config and spawn a short-lived Xray process via
xray::XrayProcess::spawn_with_binary.
flowchart LR
classDef direct fill:#2e2a1a,stroke:#dfba5b,color:#e6edf3
classDef proxy fill:#2e1a1a,stroke:#df6060,color:#e6edf3
classDef opt fill:#1a2c3a,stroke:#5b8def,color:#e6edf3
ICMP["ICMP\n(direct)"]:::direct
TCP["TCP\n(direct)"]:::direct
REAL["Real Delay\n(via Xray)"]:::proxy
DL["Download\n(via Xray)"]:::proxy
UL["Upload\n(via Xray, optional)"]:::opt
ICMP --> TCP --> REAL --> DL -.-> UL
Stages marked (direct) probe the remote endpoint without a proxy process.
Stages marked (via Xray) spawn a short-lived Xray probe instance on a random
local port. Upload runs only when upload_url is configured.
Persistence
db/repository/connection_tests/ writes one row per config with the fields in
ConnectionTestRecord, and connection_test_runs records the batch metadata
(kind, created_at). The bulk executor groups results into a single run id so
callers can paginate by batch.
Output Formatting
output/print.rs and output/format.rs produce the per-row and tabular output
for the CLI. The TUI reuses the same in-process executors through
app::commands::test::bulk::* and does not shell out to a child xrat test
process.