Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

xrat β€” proxy manager for Xray-core and sing-box

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

SectionDescription
Getting StartedInstallation, quickstart, configuration
CLI ReferenceCommand reference for all subcommands
FeaturesDeep-dives into each major subsystem
Deploymentsystemd services, database backends
ReferenceProtocols, config file, database schema, errors
ArchitectureModule 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:

  1. --config <path> CLI flag
  2. XRAT_PATH environment variable
  3. ~/.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

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

ToolRequiredPurposeInstall
xrayYesManaged Xray runtime and real-delay testsXTLS/Xray-install
sing-boxNosing-box parsing and runtime-config preview for diagnosticssing-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

RequirementDetails
OSLinux x86_64 or aarch64
libcNone – release binaries are statically linked
SQLiteBundled – no system SQLite needed
PostgreSQLOptional – version 14+ if used instead of SQLite
NetworkOutbound HTTPS for imports and release downloads

Install

curl -fsSL https://raw.githubusercontent.com/mhyrzt/xrat/master/install.sh | bash

The installer will:

  1. Check for xray and warn if optional sing-box is missing.
  2. Detect x86_64 or aarch64.
  3. Download the latest GitHub release archive.
  4. Verify the archive against SHASUMS256.txt.
  5. Install xrat to ~/.local/bin/xrat.
  6. Offer to run xrat init.
  7. 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:

  1. Run cargo build --release inside the checkout.
  2. Generate man pages and shell completions from the built binary.
  3. Install xrat and extras the same way as the release path.
  4. 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

PathPurposeOverride
$HOME/.config/xrat/App rootXRAT_PATH env var
$HOME/.config/xrat/config.tomlConfiguration--config flag
$HOME/.config/xrat/db.sqliteSQLite 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:

FileArchitecture
xrat-vX.Y.Z-x86_64-unknown-linux-musl.tar.gzx86_64 (most PCs)
xrat-vX.Y.Z-aarch64-unknown-linux-musl.tar.gzARM64 (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
  • just
  • xray in PATH
  • sing-box if 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:

TargetPurpose
just checkRun cargo check --locked
just fmtFormat Rust, Markdown, and SQL
just fmt-checkCheck Rust, Markdown, and SQL formatting
just docsServe the mdBook locally
just cleanRemove Cargo build artifacts
just postgres-upStart the local PostgreSQL verification database
just test-postgresRun the PostgreSQL real-backend verification test
just postgres-downStop 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

SectionPurpose
[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:

FlagDescription
-v, --verboseIncrease log verbosity. Repeat: -v=info, -vv=debug, -vvv=trace
-q, --quietSuppress 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

CommandDescription
importImport a subscription URL, file, or raw text into the database
addAdd a single config URI directly to the database
listList stored configs or subscription sources
showShow details for a stored config
selectMark one config as the current selection
enableInclude a config in normal operations
disableExclude a config from normal operations
deleteSoft-delete or permanently delete a config
restoreRestore a soft-deleted config
parseParse and validate config links without persisting
testTest connectivity and latency for stored configs
scanScan candidate IPs for TCP reachability
connectStart a managed proxy runtime for a stored config
disconnectStop the active managed proxy runtime
statusShow the managed proxy runtime status
daemonRun or control the daemon supervisor process
proxyControl auto-rotating proxy scheduling via the daemon
geoipManage GeoLite2 MMDB assets and inspect GeoIP backend config
serveStart the local HTTP API server
tuiStart the interactive terminal UI

Common State Terms

These words appear across the CLI, TUI, API, and database:

TermMeaning
enabledIncluded in bulk tests, selection workflows, and rotation candidate sets
disabledStored but normally skipped by filtered workflows
selectedUser-selected config for workflows that need a preferred config
activeConfig attached to the current managed runtime session
deletedSoft-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 only
  • RUST_LOG environment variable: overrides all flags

Logs are written to stderr.

init

Initialize the xrat config directory, config file, and database.

xrat init [--dry-run]

Flags

FlagDescription
--dry-runPrint planned actions without creating anything

Behavior

  1. Creates the app root directory ($HOME/.config/xrat/ or $XRAT_PATH)
  2. Writes a default config.toml with sensible defaults if not already present
  3. Creates the SQLite database and runs all pending migrations
  4. Creates subdirectories: runtime/, logs/, mmdb/
  5. 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

PathPurposeOverride
$HOME/.config/xrat/App rootXRAT_PATH env var
$HOME/.config/xrat/config.tomlConfiguration--config flag
$HOME/.config/xrat/db.sqliteSQLite 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.

import

Import a subscription URL, file, or raw text into the database.

xrat import <input>

Arguments

ArgumentDescription
inputSubscription source: a URL, local file path, or raw subscription text

Input Formats

xrat automatically detects the input format:

FormatDetection
Subscription URLStarts with http:// or https://
Local filePath to an existing file on disk
Single share linkSingle line starting with a supported protocol scheme
Base64 subscriptionMulti-line or single-line base64-encoded text
Plain link listMultiple lines, each a valid share link
SIP008 JSONJSON with "servers" array
Xray JSONJSON 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

  1. Reads input from the specified source
  2. Detects format automatically
  3. Parses and normalizes each node
  4. Deduplicates against existing configs using a versioned key
  5. Persists new configs to the database
  6. Creates or updates the subscription source record
  7. Prints an import summary
  • add β€” add a single config URI without subscription tracking
  • list configs β€” view imported configs

add

Add a single config URI directly to the database.

xrat add <input>

Arguments

ArgumentDescription
inputConfig 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

CommandUse when you want to
addStore one share link without creating a subscription source record
showInspect one stored config
selectMark one config as the preferred config for interactive workflows
enableInclude a config in normal filtered workflows
disableKeep a config stored but skip it in normal filtered workflows
deleteHide a config from normal lists while preserving history
restoreBring a soft-deleted config back

Config State

selected, active, enabled, and deleted are separate states.

StateMeaning
selectedThe preferred config. This does not start a proxy process.
activeThe config used by the current managed runtime session.
enabledIncluded in normal list, test, and rotation workflows.
disabledStored but skipped by enabled-only workflows.
deletedSoft-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

ArgumentDescription
inputConfig 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

ArgumentDescription
idConfig ID to show

Flags

FlagDescription
--jsonPrint the result as JSON

Examples

xrat show 42
xrat show 42 --json

select

Select a config as the current selection.

xrat select <id>

Arguments

ArgumentDescription
idConfig 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

ArgumentDescription
idConfig 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

ArgumentDescription
idConfig 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

ArgumentDescription
idConfig ID to delete

Flags

FlagDescription
--hardPermanently 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

ArgumentDescription
idConfig ID to restore

restore only applies to soft-deleted configs. It does not recreate a config that was removed with delete --hard.

  • list β€” find config IDs and filter by state
  • runtime β€” connect, disconnect, and inspect active sessions
  • tui β€” manage configs interactively

list

List stored configs or subscription sources.

xrat list <target> [flags]

Targets

TargetAliasDescription
configsnodesList stored proxy configs
subscriptionssubsList stored subscription sources

list configs

xrat list configs [flags]

Flags

FlagDescription
--enabled-onlyShow only enabled configs
--active-onlyShow only the active config
--selected-onlyShow only the selected config
--deletedShow only soft-deleted configs
--allInclude 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

FlagDescription
--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

ArgumentDescription
inputSingle config URI to parse (optional if using --file or --stdin)

Flags

FlagDescription
--file <path>Read config links (one per line) from a local file
--stdinRead config links (one per line) from stdin
--jsonPrint 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

ModeBehavior
autoUses sing-box for hysteria2, xray for everything else
xrayAlways use Xray-core (rejects hysteria2)
sing-boxAlways 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

ArgumentDescription
idConfig ID to test. Omit to bulk-test matching configs

Filter Flags

When testing multiple configs (no id specified):

FlagDescription
--enabled-onlyFilter: only enabled configs
--active-onlyFilter: only the active config
--selected-onlyFilter: only the selected config
--subscription <id>Filter: only configs from the given subscription ID

Stage Skip Flags

FlagDescription
--skip-icmpSkip the ICMP ping stage
--skip-tcpSkip the TCP connectivity stage
--skip-real-delaySkip the real-delay (HTTP round-trip) stage
--skip-downloadSkip the download speed stage
--skip-uploadSkip the upload speed stage (disabled by default)

URL Override Flags

FlagDescription
--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

FlagDescription
--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

FlagDescription
--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-progressHide the animated progress bar

Ping Loop Flags

FlagDescription
--pingContinuously ping one config until Ctrl+C, printing a live summary
--ping-interval <ms>Interval between ping-loop iterations (default: 1000)

Historical Summary Flags

FlagDescription
--latest-run-summaryPrint 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:

StageMeasuresDefault
ICMPICMP ping success and latencyEnabled
TCPTCP connect success and latencyEnabled
Real DelayHTTP round-trip latency through proxyEnabled
DownloadDownload throughput through proxyDisabled
UploadUpload throughput through proxyDisabled

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:

CategoryDescription
DNSDNS resolution failed
TimeoutConnection or request timed out
RefusedConnection refused
UnreachableNetwork unreachable
PermissionDeniedPermission denied
TLSTLS handshake failed
AuthAuthentication failed
ProcessProxy process failed to start
ProxyProxy returned an error
UnknownUnclassified failure
  • list configs β€” view configs before testing
  • connect β€” start a proxy for a tested config

scan

Scan candidate IPs for TCP reachability and persist results.

xrat scan [flags]

Flags

FlagDescription
--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

  1. Reads candidate IPs from --ips or --file
  2. Attempts TCP connection to each IP on the specified port
  3. Measures connection latency
  4. Persists results to the cf_scan_results table (upsert)
  5. 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.

  • 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

ArgumentDescription
idConfig ID to start as the active local proxy session

Flags

FlagDescription
--jsonPrint the result as JSON

Examples

xrat connect 42
xrat connect 42 --json

Behavior

  1. Sends a runtime-connect request to the daemon over local IPC
  2. The daemon loads the config from the database
  3. Generates an Xray (or V2Ray) runtime config with local inbounds
  4. Spawns the proxy process
  5. Waits for the SOCKS port to become ready
  6. Persists a runtime_sessions record with status running
  7. Prints connection details

If the daemon is not running, start it first:

xrat daemon start

Default Inbounds

ProtocolHostPortNotes
SOCKS50.0.0.01080UDP support enabled by default
HTTP0.0.0.08080Disabled by default in config.toml
Shadowsocks0.0.0.01081Disabled 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

FlagDescription
--jsonPrint the result as JSON

Examples

xrat disconnect

Behavior

  1. Sends a runtime-disconnect request to the daemon over local IPC
  2. The daemon sends SIGTERM to the running proxy process
  3. Waits up to 5 seconds for graceful shutdown
  4. Sends SIGKILL if the process is still running
  5. Updates the session status to stopped
  6. Cleans up temporary config files

status

Show the managed proxy runtime status.

xrat status [flags]

Flags

FlagDescription
--jsonPrint 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"
}
  • daemon β€” persistent daemon with auto-rotation
  • proxy β€” control auto-rotation scheduling
  • test β€” test configs before connecting
  • parse β€” parse and preview Xray or sing-box runtime JSON

daemon

Run or control the daemon supervisor process.

xrat daemon <action>

Actions

ActionDescription
startStart the long-lived daemon process
statusShow daemon IPC reachability and protocol information
stopRequest daemon shutdown via local IPC
installInstall xrat-daemon.service as a systemd user service
uninstallRemove 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

  1. Forks a background daemon process
  2. Creates a Unix domain socket at <runtime_dir>/daemon.sock
  3. Runs the supervisor event loop with:
    • Health checks every 15 seconds
    • IPC event processing from CLI commands
    • Auto-rotation scheduling (if enabled)
  4. 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

  1. Connects to the daemon socket
  2. Sends a shutdown request
  3. 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

FlagDescription
--startStart the daemon immediately after enabling the service
--with-apiAlso install xrat-api.service (standalone HTTP API)
--dry-runPrint the generated unit and planned actions without writing anything

Behavior

  1. Resolves the current binary path via std::env::current_exe()
  2. Generates xrat-daemon.service from the template in packaging/systemd/ with the resolved binary path and configured XRAT root
  3. Writes the service file to ~/.config/systemd/user/ (respects $XDG_CONFIG_HOME)
  4. Runs systemctl --user daemon-reload
  5. Runs systemctl --user enable xrat-daemon.service
  6. If --start: runs systemctl --user start xrat-daemon.service
  7. If --with-api: generates and installs xrat-api.service as 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

FlagDescription
--dry-runPrint planned actions without removing anything

Behavior

  1. Stops xrat-daemon.service (non-fatal if not running)
  2. Disables xrat-daemon.service
  3. Removes ~/.config/systemd/user/xrat-daemon.service
  4. Repeats for xrat-api.service if present
  5. 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

RequestDescription
DaemonPingCheck daemon reachability
DaemonShutdownRequest graceful shutdown
RuntimeStatusGet proxy runtime status
RuntimeConnectStart a proxy session
RuntimeReplaceAtomic disconnect-old + connect-new
RuntimeDisconnectStop the active proxy session
ProxyStartEnable auto-rotation
ProxyStatusGet rotation status
ProxyStopDisable 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": { ... }
}
  • proxy β€” control auto-rotation scheduling
  • connect β€” start a proxy via daemon IPC
  • status β€” check proxy status via daemon IPC
  • init β€” 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

ActionDescription
startEnable automatic proxy rotation on a fixed schedule
statusShow the current proxy rotation status
rotateTrigger an immediate manual rotation
stopDisable automatic proxy rotation

proxy start

Enable automatic proxy rotation on a fixed schedule.

xrat proxy start

Flags

No command-specific flags.

Behavior

  1. Sends a request to the daemon via IPC
  2. 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"]
  1. Daemon begins periodic rotation according to the interval

Rotation Triggers

TriggerDescription
TimerScheduled rotation every interval_secs
Health checkTriggered when proxy health check fails (if health_trigger_enabled)
ManualTriggered 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

FlagDescription
--jsonPrint 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

FlagDescription
--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

  1. If --config-id is provided, rotates to that specific config
  2. Otherwise, selects the best candidate from enabled configs:
    • Tests candidates using test_stages from config.toml
    • Picks the config with the lowest real-delay latency
  3. Atomically disconnects the old session and connects the new one
  4. Respects cooldown period (rotation is delayed if cooldown is active)

Candidate Selection

When rotating without --config-id:

  1. Loads all enabled configs from the database
  2. Excludes the currently active config
  3. Tests candidates concurrently (up to test_concurrency workers)
  4. Filters out configs that fail any test stage
  5. Sorts by real-delay latency (lowest first)
  6. Selects the top candidate

proxy stop

Disable automatic proxy rotation.

xrat proxy stop

Flags

No command-specific flags.

Behavior

  1. Sends a request to the daemon via IPC
  2. Daemon disables the rotation scheduler
  3. Active proxy session continues running (not disconnected)
  • daemon β€” daemon must be running for proxy commands to work
  • connect β€” start one proxy session through the daemon
  • test β€” test configs before enabling rotation

geoip

Manage GeoLite2 MMDB assets and inspect GeoIP lookup configuration.

xrat geoip <command> [flags]

Subcommands

CommandDescription
downloadDownload one or more GeoLite2 MMDB editions
updateRefresh all supported GeoLite2 MMDB editions
pathPrint the resolved MMDB directory
statusShow MMDB presence and size for each supported edition
lookupLook up a single IP through the configured GeoIP backend
backendPrint the active GeoIP backend configuration

download

Download one or more GeoLite2 MMDB editions.

xrat geoip download [flags]
FlagDescription
--edition <name>Edition to download. Repeatable: GeoLite2-Country, GeoLite2-City, GeoLite2-ASN or country, city, asn
--allDownload all supported editions
--output <dir>Override the MMDB target directory for this command
--forceRe-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
--quietSuppress 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]
FlagDescription
--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
--quietSuppress progress bar output

Example

xrat geoip update

path

Print the resolved MMDB directory.

xrat geoip path [flags]
FlagDescription
--output <dir>Override the MMDB target directory for this command

Resolution order:

  1. --output flag, if provided
  2. [mmdb].dir from config (resolved relative to XRAT_PATH or config file location)
  3. 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]
FlagDescription
--output <dir>Override the MMDB target directory for this command
--strictExit 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]
ArgumentDescription
ipIP address to look up
FlagDescription
--backend <name>Override backend: mmdb, ipwhois, ip-api
--no-cacheBypass the configured in-memory cache for this invocation
--jsonPrint 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]
FlagDescription
--backend <name>Override backend: mmdb, ipwhois, ip-api
--no-cacheDescribe 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.

serve

Start the local HTTP API server.

xrat serve [flags]

Flags

FlagDescription
--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" }
FieldDescription
enabledEnable daemon-hosted API (see below)
hostBind host (default: 127.0.0.1)
portBind port (default: 8080)
keyOptional 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

RouteMethodDescription
/healthGETHealth check (no auth required)
/jsonGETList configs with latest test results as JSON array
/b64GETBase64-encoded subscription text payload
/configsGETPaginated config list with details
/configs/{id}GETSingle config detail with latest test results

Query Parameters

/json

ParameterDescription
keyAPI key (if authentication is enabled)
topReturn top N configs sorted by real-delay
enabledFilter: true for enabled configs only
protocolFilter by protocol: vless, vmess, ss, trojan, hy2
selectedFilter: true for selected configs only

/b64

ParameterDescription
keyAPI key (if authentication is enabled)

/configs

ParameterDescription
keyAPI key (if authentication is enabled)
pagePage number (default: 1)
per_pageItems per page (default: 20)
enabledFilter: true for enabled configs only
protocolFilter by protocol
selectedFilter: true for selected configs only

/configs/{id}

ParameterDescription
keyAPI 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 /health and /configs for uptime monitoring
  • Integration: Build dashboards or automation around /json and /configs
  • Proxy management: Query active configs and test results programmatically
  • daemon β€” daemon-hosted API mode
  • test β€” test results are exposed via the API
  • list β€” 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

KeyViewPurpose
1ConfigsBrowse, filter, select, enable, disable, delete, and share configs
2SourcesInspect subscription sources, refresh/import sources, and share source/API URLs
3TestsStart/cancel background test batches and inspect recent results
4DiagnosticsInspect 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

KeyAction
1-4Switch primary view
TabCycle primary views
j, kMove focus down/up
arrow keysMove focus down/up
?Open help
EscClose modal, leave search, or go back
q, Ctrl+CQuit

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.

KeyAction
/Edit config search
Ctrl+UClear search while editing
sCycle sort field
FCycle filter: all, enabled, failed, has-delay
PCycle protocol filter
fShow or hide soft-deleted configs
SpaceMark the focused config as selected
EnterSelect and start the focused config
e, xEnable or disable the focused config
E, XEnable or disable all selected configs
dSoft-delete the focused config after confirmation
DPurge the focused config after confirmation
rRestore the focused soft-deleted config
tStart a test batch for the current Configs scope
aTest all enabled, non-deleted configs
vTest visible configs matching current filters
KStop/disconnect the managed runtime
RRestart the managed runtime
yShow a QR code for the focused config URI
cCopy the focused config URI
CCopy 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.

KeyAction
rRefresh the focused source
RRefresh all sources with stored values
iOpen the import modal
nRename the focused source
dDelete the focused source and its configs
yShow a QR code for the focused source URL
cCopy the focused source URL
uShow a QR code for the HTTP API /b64 subscription URL
UCopy 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.

KeyAction
sStart a background test batch
cCancel 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.

WorkflowCLI equivalent
Manage config stateconfig management
Start or stop runtimeruntime
Run teststest
Inspect sourceslist subscriptions
Import sourcesimport
Serve API URLserve

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

ArgumentDescriptionValues
<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/:

FileShell
completions/xrat.bashBash
completions/_xratZsh
completions/xrat.fishFish

CI generates these during the release workflow using xrat completions <shell>.

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

FlagDescriptionDefault
--output <dir>Directory to write generated .1 files.

Behavior

Generates one man page per visible command and subcommand:

  • xrat.1 β€” root command with global flags
  • xrat-init.1, xrat-import.1, xrat-daemon.1, … β€” top-level subcommands
  • xrat-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/

Features

xrat provides a comprehensive set of features for managing proxy configurations and running local proxy services.

Core Features

FeatureDescription
ImportingImport subscriptions from URLs, files, raw text, base64, JSON
Testing5-stage probe pipeline with failure classification
Runtime ManagementConnect lifecycle, session state, reattach
Daemon and IPCSupervisor process with Unix socket IPC
Auto-RotationScheduled proxy switching with cooldown
IP ScanningTCP reachability scanning with persistence
HTTP APIRESTful API for config access and monitoring
DeduplicationVersioned 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:

  1. Fetches the URL content
  2. Parses subscription-userinfo headers for metadata (upload, download, total, expire)
  3. Detects format (base64, plain list, JSON)
  4. Parses and normalizes each node
  5. 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

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:

  1. Base64-decodes the payload
  2. Splits into lines
  3. Parses each line as a share link

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:

ConditionDetected Format
Starts with { and contains "version" or "inbounds"Xray JSON
Starts with { and contains "servers"SIP008 JSON
Single line starting with a protocol schemeSingle share link
Multiple lines, first line starts with protocol schemePlain link list
OtherwiseBase64 subscription

Normalization

After parsing, xrat normalizes each node:

  1. Network defaults: Empty network β†’ tcp
  2. WebSocket defaults: Missing host β†’ copy from sni, missing path β†’ /
  3. gRPC defaults: Missing path β†’ /
  4. 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:

FieldDescription
source_urlOriginal URL or file path
source_kindurl, file, or raw_text
nameOptional name (from URL or user-provided)
created_atFirst import timestamp
updated_atLatest 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 uploaded
  • download β€” bytes downloaded
  • total β€” total quota
  • expire β€” 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.

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:

StageMeasuresDefaultImplementation
ICMPPing success and latencyEnabledSpawns system ping command
TCPTCP connect success and latencyEnabledDirect TCP socket connection
Real DelayHTTP round-trip latency through proxyEnabledSpawns proxy, makes HTTP request
DownloadDownload throughput through proxyDisabledDownloads file through proxy
UploadUpload throughput through proxyDisabledPOSTs 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:

PolicyBehavior
continueRun all stages regardless of failures
skip_remainingStop testing this config after first failure
mark_failedMark 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 success
  • icmp_ms β€” average latency in milliseconds
  • icmp_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 success
  • tcp_ms β€” connection time in milliseconds
  • failure_kind β€” failure classification (if failed)

Failure Classification

TCP failures are classified into categories:

CategoryDescription
DNSDNS resolution failed
TimeoutConnection timed out
RefusedConnection refused (port closed)
UnreachableNetwork unreachable
PermissionDeniedPermission denied
TLSTLS handshake failed
AuthAuthentication failed
ProcessProxy process failed to start
ProxyProxy returned an error
UnknownUnclassified failure

Real Delay Stage

Measures actual HTTP round-trip latency through the proxy.

How It Works

  1. Generates a temporary Xray probe config with a local SOCKS inbound
  2. Spawns a short-lived Xray process
  3. Waits for the SOCKS port to become ready
  4. Makes an HTTP request through the proxy to the test URL
  5. Measures connect time, TTFB, and total round-trip time
  6. 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 success
  • real_delay_ms β€” total round-trip time
  • connect_ms β€” TCP connection time
  • ttfb_ms β€” time to first byte
  • http_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

  1. Spawns proxy with the config
  2. Downloads the file through the proxy
  3. Measures bytes transferred and elapsed time
  4. 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

FormatDescription
tsvTab-separated values (default, terminal-friendly)
csvComma-separated values (spreadsheet-friendly)
jsonJSON array with full details

Sorting

Sort results by:

FieldDescription
statusAlive first, then by failure reason
icmpLowest ICMP latency
real-delayLowest real-delay latency
download-speedHighest download throughput
protocolProtocol name alphabetically
addressServer 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

BackendDescription
mmdbLocal GeoLite2 MMDB files (default)
ipwhoisRemote ipwhois.app API
ip-apiRemote ip-api.com API
chainLocal 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 address
  • endpoint_country β€” ISO country code (e.g. NL)
  • endpoint_location β€” location label such as city/country when available
  • endpoint_asn β€” Autonomous System Number and organization (e.g. AS15169 Google LLC)

Test Runs

Tests are grouped into runs for historical analysis:

TablePurpose
connection_test_runsGroups test results (id, kind, created_at)
connection_testsIndividual 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:

FieldDescription
config_idForeign key to configs table
run_idForeign key to connection_test_runs
icmp_ok, icmp_msICMP results
tcp_ok, tcp_msTCP results
real_delay_ok, real_delay_msReal delay results
connect_ms, ttfb_ms, http_statusHTTP details
download_mbps, upload_mbpsThroughput
failure_kind, failure_reasonFailure details
endpoint_ip, endpoint_country, endpoint_asnGeoIP
tested_atTimestamp

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>:

  1. Load config β€” Fetch the config from the database by ID
  2. Generate runtime config β€” Create Xray JSON with local inbounds
  3. Spawn process β€” Launch Xray/V2Ray as a child process
  4. Wait for readiness β€” Poll the SOCKS port until it accepts connections
  5. Persist session β€” Insert a runtime_sessions record with status running
  6. 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:

StatusDescription
startingProcess spawned, waiting for port readiness
runningPort is ready, proxy is active
stoppingGraceful shutdown in progress
stoppedProcess terminated cleanly
failedProcess exited unexpectedly or startup failed

State Transitions

starting β†’ running β†’ stopping β†’ stopped
   ↓                      ↓
 failed                failed

Session Record

Persisted to runtime_sessions table:

FieldDescription
idSession ID (primary key)
config_idForeign key to configs table
statusCurrent status
process_idOS process ID (PID)
socks_host, socks_portSOCKS inbound address
http_host, http_portHTTP inbound address
shadowsocks_host, shadowsocks_portShadowsocks inbound address
failure_reasonError message (if failed)
owner_kindcli or daemon
owner_instance_idDaemon instance ID (if daemon-owned)
started_at, stopped_atTimestamps

Disconnect Flow

When you run xrat disconnect:

  1. Load active session β€” Find the latest running session
  2. Send SIGTERM β€” Request graceful shutdown
  3. Wait for exit β€” Poll process status every 100ms (up to 5s)
  4. Send SIGKILL β€” Force kill if still running after timeout
  5. Update session β€” Set status to stopped or failed
  6. 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))
}
  1. Check if process is running
  2. Send SIGTERM
  3. Poll every 100ms for up to 5 seconds
  4. If still running, send SIGKILL
  5. Return outcome: Terminated, Killed, or NotRunning

Status Check

When you run xrat status:

  1. Load active session β€” Find the latest session (any status)
  2. Check PID liveness β€” Verify process is still running
  3. Check inbound health β€” Test TCP reachability of SOCKS/HTTP/Shadowsocks ports
  4. Return snapshot β€” Print status with config details and health

Health Check

For each inbound port:

StatusDescription
reachableTCP connection succeeded
unreachableTCP connection failed
not_checkedInbound 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:

  1. Disconnect the old session (graceful shutdown)
  2. Connect the new session
  3. 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:

  1. Find stale sessions β€” Query for running sessions with no stopped_at
  2. Check PID liveness β€” For each stale session, check if PID is still running
  3. Verify process identity β€” Compare /proc/<pid>/cmdline with expected binary path
  4. 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

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" } }
FieldDescription
enabledEnable SOCKS inbound
hostBind address
portBind port
udpEnable UDP support
authOptional 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
FieldDescription
enabledEnable logging to files
maskMask IP addresses in logs
dirLog directory (relative to config dir or absolute)
dns_logEnable DNS query logging
levelLog level
keepKeep log files after session stops

Engine Selection

Choose the proxy engine in config.toml:

[runtime]
engine = "xray"  # "xray" | "v2ray" | "sing-box"
EngineBinaryProtocols
xrayxrayAll except Hysteria2
v2rayv2rayVLESS, VMess, Shadowsocks, Trojan, HTTP, SOCKS5
sing-boxsing-boxParse-time/runtime-config preview only; managed process lifecycle is not yet sing-box parity

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:

  1. IPC Server β€” Listens for commands from CLI clients
  2. Health Monitor β€” Periodically checks proxy liveness
  3. 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

  1. CLI command connects to the Unix socket
  2. Sends a JSON request
  3. Receives a JSON response
  4. 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

TypeDescriptionPayload
DaemonPingCheck daemon reachabilityNone
DaemonShutdownRequest graceful shutdownNone
RuntimeStatusGet proxy runtime statusNone
RuntimeConnectStart a proxy session{ config_id: i64 }
RuntimeReplaceAtomic disconnect + connect{ trigger, candidate_id }
RuntimeDisconnectStop the active proxy sessionNone
ProxyStartEnable auto-rotationNone
ProxyStatusGet rotation statusNone
ProxyStopDisable auto-rotationNone

Manual xrat proxy rotate calls RuntimeReplace with trigger = manual and an optional candidate_id. There is no separate ProxyRotate request type.

Response Codes

CodeDescription
200Success
400Bad request (invalid payload)
404Not found (no active session)
409Conflict (session already running)
500Internal error

Daemon Lifecycle

Starting the Daemon

xrat daemon start
  1. Check if daemon is already running (try connecting to socket)
  2. Fork a background process
  3. Create the Unix socket
  4. Run the supervisor event loop
  5. Reattach to any stale sessions from previous daemon runs

Stopping the Daemon

xrat daemon stop
  1. Connect to the daemon socket
  2. Send DaemonShutdown request
  3. 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:

  1. Loads the active session from the database
  2. Checks if the PID is still running
  3. Tests TCP reachability of the SOCKS port
  4. 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:

  1. Log the failure β€” Record in daemon logs
  2. Update session β€” Mark as failed with reason
  3. Trigger rotation β€” If health_trigger_enabled, start rotation
  4. 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:

  1. Check PID liveness β€” Is the process still running?
  2. Verify process identity β€” Does /proc/<pid>/cmdline match the expected binary?
  3. Decision:
    • PID alive + cmdline matches β†’ reattach (keep as running)
    • PID alive + cmdline mismatch β†’ mark failed (different process reused PID)
    • PID dead β†’ mark failed

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:

CommandIPC Request
xrat connect <id>RuntimeConnect
xrat disconnectRuntimeDisconnect
xrat statusRuntimeStatus
xrat proxy startProxyStart
xrat proxy statusProxyStatus
xrat proxy rotateRuntimeReplace
xrat proxy stopProxyStop

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.

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:

  1. Periodically tests candidate configs
  2. Selects the best candidate based on latency
  3. Atomically disconnects the old session and connects the new one
  4. 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"]
FieldDescriptionDefault
enabledEnable scheduled rotationfalse
interval_secsRotation interval in seconds1800 (30 minutes)
health_trigger_enabledTrigger rotation on health check failuretrue
cooldown_secsMinimum time between rotations300 (5 minutes)
test_concurrencyConcurrent test workers (0 = auto)0
test_stagesTest 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:

  1. Generate a probe config
  2. Spawn a short-lived Xray process
  3. Run the specified test stages
  4. Collect results (latency, throughput, success/failure)
  5. 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:

  1. Check cooldown β€” If active, delay or skip
  2. Select candidate β€” Run candidate selection (or use --config-id)
  3. Atomic replace β€” Disconnect old session, connect new session
  4. Update state β€” Record rotation timestamp, trigger type, new config ID
  5. 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

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:

  1. Reads candidate IPs from CLI flags or a file
  2. Attempts TCP connection to each IP on a specified port
  3. Measures connection latency
  4. Persists results to the database
  5. 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

FlagDescriptionDefault
--ips <list>Comma-separated IPs to scan-
--file <path>File with newline-separated IPs-
--port <port>Target TCP port443
--timeout <ms>TCP connect timeout4000
--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:

ColumnTypeDescription
idINTEGERPrimary key
ipTEXTIP address (unique)
latency_msINTEGERConnection latency (NULL if failed)
errorTEXTError message (NULL if successful)
last_scanned_atTIMESTAMPLast 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

ErrorDescription
timeoutConnection timed out
refusedConnection refused (port closed)
unreachableNetwork unreachable
dnsDNS resolution failed (for hostnames)
ioI/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)

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" }
FieldDescriptionDefault
enabledEnable daemon-hosted APIfalse
hostBind host127.0.0.1
portBind port8080
keyOptional API key for authentication-

Routes

RouteMethodDescriptionAuth Required
/healthGETHealth checkNo
/jsonGETList configs as JSON arrayYes (if key set)
/b64GETBase64 subscription textYes (if key set)
/configsGETPaginated config listYes (if key set)
/configs/{id}GETSingle config detailYes (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:

ParameterTypeDescription
keystringAPI key (if authentication enabled)
topintegerReturn top N configs sorted by real-delay
enabledbooleanFilter: true for enabled configs only
protocolstringFilter by protocol: vless, vmess, ss, trojan, hy2
selectedbooleanFilter: 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:

ParameterTypeDescription
keystringAPI 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:

ParameterTypeDescriptionDefault
keystringAPI key (if authentication enabled)-
pageintegerPage number1
per_pageintegerItems per page20
enabledbooleanFilter: true for enabled configs only-
protocolstringFilter by protocol-
selectedbooleanFilter: 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:

ParameterTypeDescription
idintegerConfig ID

Query Parameters:

ParameterTypeDescription
keystringAPI 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

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:

  1. Generates a dedup key for each node
  2. Checks if a config with the same key already exists
  3. 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

FieldTypeDescription
protocolrequiredProtocol name (vless, vmess, ss, etc.)
addressrequiredServer address
portrequiredServer port
usernameoptionalUsername (HTTP/SOCKS5)
uuidoptionalUUID (VLESS/VMess)
passwordoptionalPassword (Trojan/SS/SOCKS5)
methodoptionalEncryption method (Shadowsocks)
networkrequiredNetwork type (tcp, ws, grpc)
tlsoptionalTLS mode (tls, none)
snioptionalSNI hostname
hostoptionalHost header (WebSocket)
pathoptionalPath (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_key column
  • Import performance: ~1000 configs/second (including dedup)

Deployment

xrat can be deployed in various configurations, from single-user desktop setups to multi-user server deployments with PostgreSQL.

Deployment Options

OptionDescriptionUse Case
systemdRun as a systemd user servicePersistent daemon, auto-start on boot
Database BackendsSQLite vs PostgreSQLSingle-user vs multi-user deployments

Quick Deployment Checklist

  1. Build xrat: cargo build --release
  2. Install binary: Copy target/release/xrat to /usr/local/bin/
  3. Create config directory: mkdir -p ~/.config/xrat
  4. Write config.toml: Configure database, runtime, testing settings
  5. Import subscriptions: xrat import https://example.com/sub.txt
  6. Test configs: xrat test --enabled-only
  7. Start daemon: xrat daemon start or use systemd
  8. Enable rotation (optional): xrat proxy start
  9. Start HTTP API (optional): xrat serve or enable in daemon

Environment Variables

xrat respects these environment variables:

VariableDescription
XRAT_PATHConfig directory path (default: ~/.config/xrat)
RUST_LOGLog level (overrides --verbose/--quiet)
XRAT_API_KEYHTTP API authentication key
XRAT_SOCKS_PASSWORDSOCKS inbound password
XRAT_SHADOWSOCKS_PASSWORDShadowsocks inbound password
XRAT_POSTGRES_USERPostgreSQL username
XRAT_POSTGRES_PASSWORDPostgreSQL password

Binary Dependencies

xrat requires external proxy binaries:

BinaryRequired ForInstallation
xrayManaged runtime, most parse/test/generate flowsXray-core releases
v2rayAlternative managed runtime binaryV2Ray releases
sing-boxParse-time sing-box JSON preview and Hysteria2 diagnostics onlysing-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)

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:

  1. Resolves the current binary path
  2. Generates xrat-daemon.service with the correct ExecStart and XRAT_PATH
  3. Writes to ~/.config/systemd/user/ (respects $XDG_CONFIG_HOME)
  4. Runs systemctl --user daemon-reload
  5. 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/.


Database Backends

xrat supports both SQLite and PostgreSQL as database backends, allowing flexibility from single-user desktop deployments to multi-user server setups.

Overview

BackendUse CaseConcurrencySetup Complexity
SQLiteSingle-user, desktop, testingSingle writerZero configuration
PostgreSQLMulti-user, production, high concurrencyConnection poolingRequires 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:

  1. --database <path> CLI flag
  2. [database.sqlite].path in config.toml
  3. [paths].database in config.toml (deprecated)
  4. XRAT_PATH/db.sqlite
  5. ~/.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

  1. 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
  1. 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
  1. 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:

SettingDescriptionDefault
max_connectionsMaximum pool size10
min_connectionsMinimum idle connections1
connect_timeout_secsConnection timeout10

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_id
  • connection_tests.config_id
  • connection_tests.run_id
  • runtime_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:

  1. Export data from SQLite:
sqlite3 ~/.config/xrat/db.sqlite .dump > xrat-data.sql
  1. Convert SQL (SQLite β†’ PostgreSQL syntax):
# Manual conversion or use tools like pgloader
pgloader sqlite:///path/to/db.sqlite postgresql://xrat:password@localhost/xrat
  1. Update config.toml:
[database]
backend = "postgres"
  1. 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;

Reference

This section provides lookup material for xrat’s configuration, protocols, database schema, and error codes.

Pages

PageDescription
ProtocolsSupported protocols, URI schemes, and engine routing
Config FileFull config.toml reference with all fields and defaults
Database SchemaTable definitions, columns, and migrations
Error CodesAppError variants and FailureKind categories

Protocols

xrat supports 7 proxy protocols, each with specific URI formats, configuration fields, and engine routing.

Supported Protocols

ProtocolURI SchemeXraysing-boxParser
VLESSvless://YesNoYes
VMessvmess://YesNoYes
Shadowsocksss://YesNoYes
Trojantrojan://YesNoYes
HTTPhttp:// / https://YesNoYes
SOCKS5socks5://YesNoYes
Hysteria2hysteria2:// / hy2://NoYesYes

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:

FieldLocationRequiredDescription
uuiduserinfoYesVLESS user ID
addresshostYesServer address
portportYesServer port
typequeryNoNetwork type (tcp, ws, grpc), default tcp
securityqueryNoTLS mode (tls, none), default none
sniqueryNoSNI hostname
hostqueryNoHost header (WebSocket)
pathqueryNoPath (WebSocket, gRPC, TCP)
namefragmentNoDisplay 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"
}
FieldKeyRequiredDescription
addJSONYesServer address
portJSONYesServer port
idJSONNoUUID
netJSONNoNetwork type (tcp, ws), default tcp
tlsJSONNoTLS mode (tls)
sniJSONNoSNI hostname
hostJSONNoHost header (WebSocket)
pathJSONNoPath (WebSocket)
psJSONNoDisplay 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:

FieldLocationRequiredDescription
methodbase64 userinfoYesEncryption method
passwordbase64 userinfoYesPassword
addresshostYesServer address
portportYesServer port
namefragmentNoDisplay 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:

FieldLocationRequiredDescription
passworduserinfoYesTrojan password
addresshostYesServer address
portportYesServer port
typequeryNoNetwork type (tcp, ws, grpc), default tcp
sniqueryNoSNI hostname
hostqueryNoHost header (WebSocket)
pathqueryNoPath (WebSocket, gRPC)
namefragmentNoDisplay 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:

FieldLocationRequiredDescription
usernameuserinfoNoUsername
passworduserinfoNoPassword
addresshostYesServer address
portportYesServer port
namefragmentNoDisplay 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:

FieldLocationRequiredDescription
usernameuserinfoNoUsername
passworduserinfoNoPassword
addresshostYesServer address
portportYesServer port
namefragmentNoDisplay 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:

FieldLocationRequiredDescription
passworduserinfoYesAuthentication password
addresshostYesServer address
portportYesServer port
sniqueryNoSNI hostname
obfsqueryNoObfuscation type
obfs-passwordqueryNoObfuscation password
alpnqueryNoALPN protocol
insecurequeryNoAllow insecure TLS
upmbpsqueryNoUpload Mbps
downmbpsqueryNoDownload Mbps
namefragmentNoDisplay 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)

ProtocolEngine
VLESSXray
VMessXray
ShadowsocksXray
TrojanXray
HTTPXray
SOCKS5Xray
Hysteria2sing-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:

FieldVLESSVMessSSTrojanHTTPSOCKS5HY2
protocolvlessvmesssstrojanhttpsocks5hy2
addresshostaddhosthosthosthosthost
portportportportportport/80/443portport
uuiduserinfoid-----
password--base64userinfouserinfouserinfouserinfo
method--base64----
networktypenettcptypetcptcpudp
tlssecuritytls-tlsscheme-tls
snisnisni-sni--sni
hosthosthost-host---
pathpathpath-path---
namefragmentpsfragmentfragmentfragmentfragmentfragment

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:

  1. --config <path> CLI flag
  2. XRAT_PATH/config.toml environment variable
  3. ~/.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"
FieldTypeDefaultDescription
databasestring-Database path (deprecated, use [database.sqlite].path)
xraystringxrayXray-core binary path
v2raystringv2rayV2Ray binary path
sing_boxstringsing-boxsing-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
FieldTypeDefaultDescription
backendenumsqlitesqlite or postgres
[sqlite].pathstringdb.sqliteSQLite database file path
[postgres].userstring/env-PostgreSQL username
[postgres].passwordstring/env-PostgreSQL password
[postgres].hoststringlocalhostPostgreSQL host
[postgres].portinteger5432PostgreSQL port
[postgres].db_namestring-PostgreSQL database name
[postgres].max_connectionsinteger10Connection pool max size
[postgres].min_connectionsinteger1Connection pool min size
[postgres].connect_timeout_secsinteger10Connection timeout

[server]

HTTP API server configuration.

[server]
enabled = false
host = "127.0.0.1"
port = 8080
key = { env = "XRAT_API_KEY" }
FieldTypeDefaultDescription
enabledbooleanfalseEnable daemon-hosted API
hoststring127.0.0.1Bind host
portinteger8080Bind port
keystring/env-API key for authentication

[runtime]

Runtime engine and proxy process configuration.

[runtime]
engine = "xray"     # "xray" | "v2ray" | "sing-box"
replace_active_session = true
FieldTypeDefaultDescription
engineenumxrayManaged runtime engine. Xray/V2Ray are currently used for connect; sing-box is parse/preview oriented.
replace_active_sessionbooleantrueAuto-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"]
FieldTypeDefaultDescription
enabledbooleanfalseEnable scheduled rotation
interval_secsinteger1800Rotation interval in seconds
health_trigger_enabledbooleantrueTrigger rotation on health failure
cooldown_secsinteger300Minimum time between rotations
test_concurrencyinteger0Test workers (0 = auto)
test_stagesstring[]["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
FieldTypeDefaultDescription
enabledbooleantrueEnable logging to files
maskenumnoneIP address masking
dirstringlogsLog directory
dns_logbooleanfalseEnable DNS query logging
levelenumwarningLog level
keepbooleantrueKeep 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" } }
FieldTypeDefaultDescription
enabledbooleantrueEnable SOCKS inbound
hoststring0.0.0.0Bind address
portinteger1080Bind port
udpbooleantrueEnable UDP support
auth.enabledbooleanfalseEnable authentication
auth.usernamestringxratSOCKS username
auth.passwordstring/env-SOCKS password

[runtime.http]

HTTP proxy inbound configuration.

[runtime.http]
enabled = false
host = "0.0.0.0"
port = 8080
FieldTypeDefaultDescription
enabledbooleanfalseEnable HTTP inbound
hoststring0.0.0.0Bind address
portinteger8080Bind 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"
FieldTypeDefaultDescription
enabledbooleanfalseEnable Shadowsocks inbound
hoststring0.0.0.0Bind address
portinteger1081Bind port
methodstringaes-128-gcmEncryption method
passwordstring/env-Shadowsocks password
networkstringtcp,udpNetwork 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 = []
FieldTypeDefaultDescription
enabledbooleantrueEnable traffic sniffing
dest_overridestring[]["http", "tls", "quic"]Protocols for destination override
route_onlybooleantrueOnly sniff for routing
metadata_onlybooleanfalseOnly sniff metadata
domains_excludedstring[][]Excluded domains
ips_excludedstring[][]Excluded IPs

[routing]

Routing configuration.

[routing]
domain_strategy = "IPIfNonMatch" # "AsIs" | "IPIfNonMatch" | "IPOnDemand"

[routing.direct]
domain = []
ip = []
geosite = []
geoip = []

[routing.block]
domain = []
ip = []
geosite = []
geoip = []
FieldTypeDefaultDescription
domain_strategyenumIPIfNonMatchXray domain resolution strategy
[direct].domainstring[][]Direct-route domains
[direct].ipstring[][]Direct-route IPs
[direct].geositestring[][]Direct-route geosite categories
[direct].geoipstring[][]Direct-route geoip categories
[block].domainstring[][]Blocked domains
[block].ipstring[][]Blocked IPs
[block].geositestring[][]Blocked geosite categories
[block].geoipstring[][]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"
FieldTypeDefaultDescription
auto_updatebooleanfalseEnable periodic geo asset updates
update_interval_hoursinteger168Update interval in hours
[[profiles]].namestring-Profile name
[[profiles]].geositestring-Geosite file path or URL
[[profiles]].geoipstring-GeoIP file path or URL

[parser]

Xray JSON schema validation mode.

[parser]
parse_mode = "strict" # "strict" | "lenient" | "auto"
FieldTypeDefaultDescription
parse_modeenumstrictXray 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"]
FieldTypeDefaultDescription
query_strategyenumUseSystemDNS query strategy
serversstring[]-DNS server list
use_system_hostsbooleantrueUse system hosts file
disable_cachebooleanfalseDisable DNS cache
disable_fallbackbooleanfalseDisable fallback DNS
enable_parallel_querybooleantrueEnable 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
FieldTypeDefaultDescription
dirstringmmdbMMDB directory (absolute, or relative to the xrat runtime root)
download_urlstringhttps://github.com/P3TERX/GeoLite.mmdb/releases/latest/download/{edition}.mmdbDownload URL template. {edition} is replaced with edition name
timeout_secsinteger60HTTP request timeout for downloads
default_editionsstring[]["country", "city", "asn"]Editions downloaded when no --edition or --all flag given
auto_updatebooleanfalseEnable periodic update checks
update_interval_hoursinteger168Update 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
SectionFieldTypeDefaultDescription
[testing]concurrencyinteger0Test workers (0 = auto)
[testing]orderstring[]["icmp", "real_delay", "download"]Stage execution order
[testing]failure_policyenumcontinueBehavior on stage failure
[icmp]enabledbooleantrueEnable ICMP stage
[icmp]timeoutinteger3000ICMP timeout (ms)
[icmp]attemptsinteger3ICMP attempt count
[tcp]enabledbooleantrueEnable TCP stage
[tcp]timeoutinteger5000TCP timeout (ms)
[real_delay]enabledbooleantrueEnable real-delay stage
[real_delay]urlstringhttps://www.gstatic.com/generate_204Test URL
[real_delay]timeoutinteger10000HTTP request timeout (ms)
[download]enabledbooleanfalseEnable download stage
[download]urlstring-Download URL
[download]timeoutinteger30000Download timeout (ms)
[testing.geoip]enabledbooleanfalseEnable GeoIP enrichment
[testing.geoip]backendenummmdbLookup backend: mmdb, ipwhois, ip-api, chain
[testing.geoip]fallbackenumnoneFallback backend when primary is chain: ipwhois, ip-api, none
[testing.geoip]country_pathstringmmdb/GeoLite2-Country.mmdbCountry MMDB path (relative to config)
[testing.geoip]city_pathstringmmdb/GeoLite2-City.mmdbCity MMDB path (relative to config)
[testing.geoip]asn_pathstringmmdb/GeoLite2-ASN.mmdbASN MMDB path (relative to config)
[remote]providerenumipwhoisRemote provider: ipwhois, ip-api
[remote]endpointstring"" (uses provider default)Remote API endpoint override
[remote]timeout_msinteger5000Remote request timeout in milliseconds
[remote]api_keystring""API key (provider-specific)
[remote]rate_limit_per_minuteinteger30Max remote requests per minute
[cache]enabledbooleantrueEnable in-memory caching
[cache]ttl_secsinteger86400Cache entry TTL in seconds
[cache]max_entriesinteger10000Maximum 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:

SectionField
[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

TableVersionDescription
subscriptions0001Import source tracking
configs0001, 0003, 0015Stored proxy nodes
connection_tests0001, 0002, 0008, 0009, 0010Test results per config
connection_test_runs0007Groups test results into runs
runtime_sessions0001, 0004, 0005, 0006, 0012, 0013, 0014Proxy process lifecycle
cf_scan_results0011IP 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
);
ColumnTypeDescription
idINTEGERPrimary key
source_urlTEXTOriginal URL, file path, or β€œraw_text”
source_kindTEXTurl, file, or raw_text
nameTEXTOptional subscription name
created_atTIMESTAMPFirst import timestamp
updated_atTIMESTAMPLatest 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
);
ColumnTypeDescription
idINTEGERPrimary key
subscription_idINTEGERFK to subscriptions
dedup_keyTEXTUnique deduplication key
protocolTEXTvless, vmess, ss, trojan, http, socks5, hy2
addressTEXTServer address
portINTEGERServer port
usernameTEXTUsername (HTTP/SOCKS5)
uuidTEXTUUID (VLESS/VMess)
passwordTEXTPassword (Trojan/SS)
methodTEXTEncryption method (Shadowsocks)
networkTEXTtcp, ws, grpc, udp
tlsTEXTtls or NULL
sniTEXTSNI hostname
hostTEXTHost header (WebSocket)
pathTEXTPath (WebSocket/gRPC/TCP)
nameTEXTDisplay name
raw_configTEXTOriginal raw config line
is_activeBOOLEANCurrently active runtime config
is_enabledBOOLEANIncluded in bulk operations
is_selectedBOOLEANUser-selected config
imported_atTIMESTAMPImport timestamp
is_deletedBOOLEANSoft-deleted flag
deleted_atTIMESTAMPDeletion timestamp
created_atTIMESTAMPInsertion timestamp
updated_atTIMESTAMPLast update timestamp

Indexes:

  • dedup_key β€” UNIQUE
  • subscription_id β€” FK index
  • is_enabled, is_active, is_selected β€” filter queries
  • is_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
);
ColumnTypeDescription
idINTEGERPrimary key
run_idINTEGERFK to connection_test_runs
config_idINTEGERFK to configs
icmp_okBOOLEANICMP ping success
icmp_msINTEGERICMP latency
tcp_okBOOLEANTCP connect success
tcp_msINTEGERTCP latency
real_delay_okBOOLEANHTTP round-trip success
real_delay_msINTEGERHTTP round-trip latency
connect_msINTEGERTCP connect time
ttfb_msINTEGERTime to first byte
http_statusINTEGERHTTP response status
download_mbpsREALDownload throughput
upload_mbpsREALUpload throughput
failure_kindTEXTFailure classification
failure_reasonTEXTHuman-readable error
endpoint_ipTEXTResolved IP address
endpoint_locationTEXTGeoIP location
endpoint_countryTEXTCountry ISO code
endpoint_asnTEXTASN identifier
tested_atTIMESTAMPTest timestamp

Indexes:

  • config_id β€” per-config queries
  • run_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
);
ColumnTypeDescription
idINTEGERPrimary key
kindTEXTRun description (e.g., β€œbulk”, β€œping”)
created_atTIMESTAMPRun 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
);
ColumnTypeDescription
idINTEGERPrimary key
config_idINTEGERFK to configs
statusTEXTstarting, running, stopping, stopped, failed
socks_hostTEXTSOCKS inbound host
socks_portINTEGERSOCKS inbound port
http_hostTEXTHTTP inbound host (if enabled)
http_portINTEGERHTTP inbound port
shadowsocks_hostTEXTShadowsocks inbound host (if enabled)
shadowsocks_portINTEGERShadowsocks inbound port
process_idINTEGEROS process ID
failure_reasonTEXTError message (if failed)
owner_kindTEXTcli or daemon
owner_instance_idTEXTDaemon instance UUID
last_transition_reason_codeTEXTMachine-readable transition reason code
last_transition_reason_detailTEXTHuman-readable transition details
last_transition_originTEXTTransition source such as CLI, daemon, health, or rotation
cooldown_untilTEXTRotation cooldown expiry as epoch seconds
last_failed_atTEXTLast runtime/health failure time as epoch seconds
last_failed_reason_codeTEXTMachine-readable last failure reason code
started_atTIMESTAMPSession start timestamp
stopped_atTIMESTAMPSession stop timestamp
created_atTIMESTAMPRecord creation timestamp
updated_atTIMESTAMPLast update timestamp

Indexes:

  • config_id β€” per-config queries
  • status β€” 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
);
ColumnTypeDescription
idINTEGERPrimary key
ipTEXTIP address (unique)
latency_msINTEGERConnection latency
download_mbpsREALDownload throughput (if measured)
upload_mbpsREALUpload throughput (if measured)
errorTEXTError message (if failed)
last_scanned_atTIMESTAMPLast scan timestamp

Migrations

Migrations are run automatically on startup using SQLx.

Migration List

#FileDescription
0001init.sqlInitial schema: subscriptions, configs, connection_tests, runtime_sessions
0002add_connection_test_download_mbps.sqlAdd download_mbps to connection_tests
0003canonical_config_dedup_key.sqlAdd dedup_key to configs
0004add_runtime_session_inbound_ports.sqlAdd inbound port columns to runtime_sessions
0005drop_runtime_session_mixed_port.sqlClean up mixed port column
0006add_runtime_session_failure_reason.sqlAdd failure tracking to runtime_sessions
0007add_connection_test_runs.sqlAdd connection_test_runs table
0008add_connection_test_http_fields.sqlAdd HTTP fields (connect_ms, ttfb_ms, http_status)
0009add_connection_test_country_asn.sqlAdd GeoIP fields (country, ASN)
0010add_connection_test_upload_mbps.sqlAdd upload_mbps to connection_tests
0011add_cf_scan_results.sqlAdd cf_scan_results table
0012add_runtime_session_owner_transition_fields.sqlAdd owner tracking to runtime_sessions
0013add_runtime_session_transition_origin.sqlAdd transition origin tracking
0014add_runtime_session_cooldown_failure_fields.sqlAdd cooldown and failure tracking
0015add_config_soft_delete.sqlAdd 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.

VariantDescriptionUse Case
ConfigNotFoundConfig ID not found in databasexrat connect <id> with invalid ID
NoActiveSessionNo active proxy sessionxrat disconnect with no session
XraySpawnFailed to spawn Xray processXray binary not found or invalid
XrayExitedXray process exited unexpectedlyProcess crashed during startup
XrayStartupTimeoutXray port not ready within timeoutSlow startup or port conflict
DaemonNotRunningDaemon IPC socket not reachablexrat proxy start without daemon
DaemonConnectFailed to connect to daemon socketPermission denied or socket missing
DatabaseDatabase query or connection errorConnection failure or constraint violation
IoFilesystem I/O errorPermission denied or disk full
ConfigConfiguration file errorInvalid TOML or missing required field
MissingPostgresUserPostgreSQL user not configureddatabase.postgres.user is empty
MissingPostgresDatabaseNamePostgreSQL database name not configureddatabase.postgres.db_name is empty
InvalidConfigValueInvalid configuration valueUnknown enum variant or out-of-range
SerializationJSON serialization/deserialization errorInvalid JSON or schema mismatch
ProbeProbe test execution errorICMP ping command failed
ParseConfig link parsing errorInvalid 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.

VariantDescription
QuerySQL query execution error
PoolConnection pool acquisition error
ConnectionDatabase connection error
UniqueViolationDuplicate key violation (used for dedup)
ForeignKeyViolationReferential integrity violation
NotFoundExpected row not found
MigrationSchema migration error
ConfigDatabase configuration error

FailureKind

FailureKind classifies test stage failures. Used by the testing pipeline and displayed in test results.

CategoryDescriptionExample
DNSDNS resolution failednodename nor servname provided, or not known
TimeoutConnection or request timed outconnection timed out after 5000ms
RefusedConnection refusedConnection refused (os error 111)
UnreachableNetwork unreachableNo route to host (os error 113)
PermissionDeniedPermission deniedOperation not permitted
TLSTLS handshake failedtls: first record does not look like a TLS handshake
AuthAuthentication failedproxy authentication required
ProcessProxy process failed to startxray binary not found
ProxyProxy returned an error statusHTTP 503 Service Unavailable
UnknownUnclassified failureAny 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.

VariantDescription
UrlInvalid URL format
JsonInvalid JSON (vmess://)
DecodeInvalid base64 payload
ParseIntInvalid numeric value
MissingAddressOrPortURI missing address or port
MissingBase64UserinfoURI missing base64-encoded userinfo
InvalidShadowsocksUserinfoInvalid Shadowsocks userinfo format
MissingRequiredFieldRequired field not found in JSON
UnsupportedSchemeUnknown protocol scheme

XrayProcessError

XrayProcessError is returned by the Xray process manager.

VariantDescription
TempFileErrorFailed to create temporary config file
SerializationErrorFailed to serialize config JSON
SpawnErrorFailed to spawn Xray process
StartupTimeoutXray failed to start within timeout
ProcessExitedXray exited unexpectedly (with stderr)
PortNotReadyInbound port not ready within timeout

ImportParseError

ImportParseError is returned by the import parser.

VariantDescription
InvalidShareLinkInput is not a valid share link
DecodeInvalid base64 decoding
JsonInvalid JSON
MissingSip008ServersSIP008 JSON missing servers array
MissingSip008FieldSIP008 server missing required field
XrayInvalid Xray JSON
ConfigInvalid 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

PageDescription
Module StructureSource tree, module responsibilities, dependency graph
Config GenerationHow engine JSON configs are generated from nodes
Import PipelineEnd-to-end subscription import flow
Daemon ArchitectureDaemon process, IPC protocol, supervisor event loop
Runtime LifecycleSession state machine, connect/replace/disconnect flows
Test PipelineProbe execution, test stages, output formatting
Database SchemaFull 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

ModuleResponsibility
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 or tests/ 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:

  1. Node (domain model) β†’ Protocol-specific mapping β†’ JSON config
  2. 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

ModeBehavior
strictRejects unknown fields using #[serde(deny_unknown_fields)]
lenientAllows unknown fields (default)
autoSame 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>
}
  1. Write config JSON to temp file
  2. Spawn xray run -c <config_path>
  3. Poll SOCKS port every 100ms
  4. 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>
}
  1. Write config to runtime_dir/session-<id>.json
  2. Create stdout/stderr log files
  3. Spawn detached process
  4. Poll for readiness
  5. Return ManagedXrayProcess (PID, port, paths)

Signal Handling

#![allow(unused)]
fn main() {
pub fn terminate_process_gracefully(
    pid: i64,
    timeout: Duration,
) -> Result<TerminationOutcome, AppError>
}
  1. Send SIGTERM
  2. Poll every 100ms up to timeout
  3. If still running, send SIGKILL
  4. 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 with reqwest::blocking::get; status errors propagate as AppError.
  • Path::new(input).exists() β†’ read from disk; filename is used as name when 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 asHeuristicParser entry
SingleLinkStarts with a known URI schemeparsers::parse_single_link
Sip008JsonJSON object with version: 1 and servers: [...]parsers::parse_sip008_json
XrayJsonJSON object with outbounds (or log/inbounds)parsers::parse_xray_json
Base64SubscriptionDecodes to base64, then plain-list parses successfullyparsers::parse_base64_subscription
PlainListNewline-separated share linksparsers::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 network becomes "tcp"
  • network == "ws" β†’ copy sni to host if host is None; set path to "/" if path is None
  • network == "grpc" β†’ set path to "/" if path is None
  • empty-string tls (Some("")) is collapsed to None

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.