Important
The 0.2 line is an active ground-up rewrite targeting the complete 3x-ui
v3.6.0 API. The full tagged HTTP and WebSocket surface is now implemented;
stabilization toward 1.0 is still in progress. Do not expect compatibility
with 0.1.
- Strong request and response types instead of unstructured JSON.
- A cheap-to-clone client designed for concurrent async workloads.
- API-token and cookie-session authentication with secrets redacted by default.
- Correct base-path handling for panels installed below a custom URL prefix.
- Contract tests derived from the upstream 3x-ui API surface.
- A reviewed rustdoc API baseline and exhaustive downstream re-export contract.
- RustSec, license, dependency-source, package, and SemVer release gates.
- A typed, forward-compatible real-time stream with explicit reconnect semantics.
- One explicit HTTP/HTTPS/SOCKS5 proxy configuration shared by HTTP, subscriptions, and WebSocket transports.
- Strict formatting, linting, documentation, MSRV, and cross-platform CI gates.
API tokens are the recommended choice for services, bots, and automation. They avoid browser-session state and 3x-ui's CSRF flow.
use xui_rs::Client;
let client = Client::builder("https://panel.example.com/secret/")?
.bearer_token(std::env::var("XUI_API_TOKEN")?)
.build()?;
# Ok::<(), Box<dyn std::error::Error>>(())Cookie login remains available for panels where an API token cannot be used. The SDK obtains a pre-auth CSRF token, stores only cookie name/value pairs in a standards-compliant jar, and reuses the CSRF token for unsafe requests.
use xui_rs::{Client, LoginRequest};
# async fn example() -> xui_rs::Result<()> {
let client = Client::new("https://panel.example.com/secret/")?;
let login = LoginRequest::new("admin", "password")
.with_two_factor_code("123456");
client.auth().login(login).await?;
client.auth().logout().await?;
# Ok(())
# }Credentials are never retained for automatic re-login. An expired session is
reported as Error::Unauthorized,
allowing the application to decide how and when credentials may be requested.
See the authentication design for the rationale.
The complete v3.6.0 inbound surface is available through Client::inbounds,
including the source-only allLinks route omitted from upstream OpenAPI.
use xui_rs::{Client, InboundConfig, InboundProtocol};
# async fn example() -> xui_rs::Result<()> {
let client = Client::builder("https://panel.example.com/secret/")?
.bearer_token("api-token")
.build()?;
let mut config = InboundConfig::new(InboundProtocol::Vless, 443);
config.remark = "production-vless".into();
config.settings = serde_json::json!({ "clients": [] });
config.stream_settings = serde_json::json!({
"network": "tcp",
"security": "reality"
});
let created = client.inbounds().create(&config).await?;
// Update is a full replacement in 3x-ui, so preserve the fetched state.
let mut edited = created.to_config();
edited.remark = "renamed-vless".into();
client.inbounds().update(created.id, &edited).await?;
# Ok(())
# }See the inbound API guide for the operation inventory, replacement/import semantics, and intentionally open-ended Xray JSON fields.
Client::clients() covers all 43 client and group routes registered by v3.6.0,
including server-side paging, portable import/export, bulk operations, online
and IP attribution, and group traffic baselines.
use xui_rs::{
Client, ClientConfig, ClientCreateRequest, ClientPageRequest,
ClientStatusFilter,
};
# async fn example(client: &Client) -> xui_rs::Result<()> {
let page = client
.clients()
.list_paged(&ClientPageRequest {
statuses: vec![ClientStatusFilter::Online],
..ClientPageRequest::default()
})
.await?;
let config = ClientConfig::new("alice@example.com");
let request = ClientCreateRequest::new(config, vec![7, 9]);
// Uncomment only when creation is intended.
// client.clients().create(&request).await?;
println!("{} online clients", page.summary.online_count);
# Ok(())
# }Every email, subscription ID, and group name is encoded as one URL path
segment. Client credentials, subscription IDs, external links, and private
keys are redacted from Debug. See the client API guide.
Client::server() covers all 38 routes registered by the v3.6.0 server
controller: typed host/Xray status and history, observatory data, lifecycle and
updates, logs, cryptographic helpers, REALITY target scanning, database
backup/restore, and cluster IP synchronization.
use xui_rs::{Client, HistoryBucket, SystemMetric};
# async fn example(client: &Client) -> xui_rs::Result<()> {
let status = client.server().status().await?;
let cpu = client
.server()
.system_history(SystemMetric::Cpu, HistoryBucket::Hour1)
.await?;
println!("Xray {:?}; {} CPU samples", status.xray.state, cpu.len());
# Ok(())
# }Database downloads stay in memory and preserve attachment metadata. Restore,
self-update, and Xray lifecycle methods are explicit operations and never run
implicitly. Generated private material is redacted from Debug. See the
server API guide.
Client::settings() covers all 14 v3.6.0 panel-settings routes, including API
tokens, notification tests, credential replacement, and the two source-only
factory-default and regex-validation operations. Client::xray_settings()
covers all 21 Xray settings, WARP/NordVPN, outbound-test, routing-test, and
remote outbound-subscription routes.
use xui_rs::{Client, PanelSettingsUpdate};
# async fn example(client: &Client) -> xui_rs::Result<()> {
let view = client.settings().all().await?;
let mut update = PanelSettingsUpdate::new(view.settings);
update.settings.display.page_size = 100;
// This endpoint is a full replacement; update the fetched settings object.
client.settings().update(&update).await?;
# Ok(())
# }The ergonomic grouped settings model flattens to exact upstream wire names.
Nested Xray JSON is encoded and decoded automatically. Stored notification and
LDAP secrets, integration payloads, subscription URLs/outbounds, and API-token
plaintext are redacted from Debug. See the settings API guide.
Client::hosts() covers all 12 host-override routes registered by v3.6.0,
including the source-only bulk-create alias. Logical groups span multiple
inbounds and addresses, while create/update results expose every physical row
produced by that Cartesian expansion.
use xui_rs::{Client, HostGroup, HostSecurity};
# async fn example(client: &Client) -> xui_rs::Result<()> {
let mut group = HostGroup::new(vec![7, 9], "production CDN");
group.hosts = vec!["cdn.example.com".into()];
group.options.port = 443;
group.options.security = HostSecurity::Tls;
// Creation is explicit and persistent.
// client.hosts().create(&group).await?;
# Ok(())
# }Nested mux, sockopt, and final-mask JSON is encoded automatically, path values are segment-safe, and the source's nullable empty list is normalized. See the Hosts API guide.
Client::nodes() covers all 15 v3.6.0 node routes: registration and lifecycle,
saved/unsaved probes, remote inbound discovery, health history, bulk panel
updates, certificate pinning, and node mTLS.
use xui_rs::{Client, NodeRequest};
# async fn example(client: &Client) -> xui_rs::Result<()> {
let request = NodeRequest::new("edge-de", "node.example.com", 2053)
.with_api_token(std::env::var("NODE_API_TOKEN").unwrap());
let probe = client.nodes().test_connection(&request).await?;
println!("candidate node is {:?}", probe.status);
// Registration is explicit because it persists and starts synchronization.
// client.nodes().create(&request).await?;
# Ok(())
# }Read models never expose write-only node API tokens. Credential retain/replace/ clear states are mutually exclusive, legacy nullable tags are normalized, and unknown enum/protocol values remain forward-compatible. See the Nodes API guide.
SubscriptionClient covers all six routes exposed by 3x-ui's separate public
subscription server: GET and HEAD for raw, Xray JSON, and Clash/Mihomo
formats. It intentionally carries no panel authentication because this server
can run on another origin and port.
use xui_rs::SubscriptionClient;
# async fn example() -> xui_rs::Result<()> {
let subscriptions = SubscriptionClient::new("https://sub.example.com:2096")?;
let metadata = subscriptions
.raw_metadata("secret-subscription-id")
.await?;
println!("profile: {:?}; traffic: {:?}", metadata.profile_title, metadata.traffic);
# Ok(())
# }Custom raw/JSON/Clash paths and construction from panel settings are supported.
Subscription identifiers, documents, generated links, routing rules, and
represented emails are redacted from Debug and request errors. The raw body
can be decoded through SubscriptionDocument::decode_base64 when subEncrypt
is enabled.
Client::panel() adds the two remaining panel-wide HTTP operations: retrieve
the runtime OpenAPI document and explicitly send a fresh database backup to
configured Telegram administrators. See the public subscription and
panel-wide operations guide.
Panel HTTP, public subscriptions, and WebSocket connections support the same
typed ProxyConfig. HTTP, HTTPS, SOCKS5 with local DNS, and socks5h with
proxy-side DNS are supported, including Basic/username-password authentication.
use xui_rs::{Client, ProxyConfig};
# fn example() -> xui_rs::Result<()> {
let proxy = ProxyConfig::new("socks5h://proxy.example.com:1080")?
.with_basic_auth("service", "proxy-password")?;
let client = Client::builder("https://panel.example.com/secret/")?
.proxy(proxy)
.build()?;
# let _ = client;
# Ok(())
# }Proxy credentials are separate from the credential-free URL and redacted from
Debug and errors. Environment proxy variables are intentionally ignored, so
HTTP and WebSocket routing stays deterministic. Cookies remain scoped to the
target panel and continue to use the same standards-compliant jar through the
proxy. See the outbound proxy guide for DNS, TLS, timeout,
and security behavior.
Response bodies are bounded in memory even when a server uses chunked transfer encoding or reports an incorrect length. Ordinary panel API and public subscription responses default to 64 MiB; explicit database downloads have an independent 512 MiB default. All three limits are configurable.
use xui_rs::{Client, SubscriptionClient};
# fn example() -> xui_rs::Result<()> {
let panel = Client::builder("https://panel.example.com/secret/")?
.response_body_limit(16 * 1024 * 1024)
.download_body_limit(1024 * 1024 * 1024)
.build()?;
let subscriptions = SubscriptionClient::builder("https://panel.example.com")?
.response_body_limit(32 * 1024 * 1024)
.build()?;
# let _ = (panel, subscriptions);
# Ok(())
# }Oversized responses return the typed ErrorKind::ResponseTooLarge. Optional
tracing events correlate a request with its response headers using a request
ID, method, status, outcome, and latency while deliberately omitting URLs,
paths, queries, headers, cookies, tokens, and bodies. See the
transport safety and observability guide for exact
semantics and introspection.
Client::events() covers the authenticated /ws handshake and all ten
message names declared by the v3.6.0 source. Status, traffic, inbounds,
outbounds, nodes, notifications, Xray transitions, client counters, reserved
clients payloads, and invalidations have distinct typed variants.
use xui_rs::{Client, LoginRequest, PanelEventKind};
# async fn example() -> xui_rs::Result<()> {
let client = Client::new("https://panel.example.com/secret/")?;
client
.auth()
.login(LoginRequest::new("admin", "password"))
.await?;
let mut events = client.events().connect().await?;
while let Some(event) = events.next_event().await? {
if let PanelEventKind::Invalidate(value) = event.kind {
println!("refresh {} through HTTP", value.target.as_str());
}
}
# Ok(())
# }The endpoint does not support bearer-token authentication. HTTP and WebSocket share one standards-compliant cookie jar, but cookie values are never exposed or copied manually. Control frames and the 10 MiB source limit are handled internally. Reconnect is explicit because 3x-ui does not replay events missed during a disconnect. See the real-time events guide.
All operations return xui_rs::Result<T>. ErrorKind provides a stable,
copyable classification for metrics and policy, while status(), method(),
url(), and authentication/rate-limit/server/timeout helpers expose context
without destructuring variants.
use xui_rs::Error;
fn action(error: &Error) -> &'static str {
if error.is_unauthorized() {
"refresh credentials"
} else if error.is_rate_limited() {
"back off"
} else if error.is_timeout() || error.is_server_error() {
"reconcile and maybe retry"
} else {
"inspect the typed error"
}
}The SDK never retries API mutations implicitly: a timeout does not prove that the server failed to apply a request. See the error and retry guide for idempotency-aware recommendations and WebSocket recovery semantics.
The complete proposed 1.0 Rust surface is recorded in a reproducible rustdoc snapshot. Downstream contract tests compile every concise crate-root re-export, important trait guarantee, and representative multithreaded-runtime future. Extensible enums are non-exhaustive, so matches should retain a wildcard arm.
See the API stability policy for the exact 1.x SemVer guarantees and the boundary between Rust source compatibility and upstream 3x-ui server behavior.
| xui-rs | 3x-ui | Status |
|---|---|---|
0.2.x |
3.6.0 |
Complete HTTP and WebSocket API; pre-1.0 stabilization |
0.1.x |
legacy API | Superseded |
Rust 1.88.0 is the minimum supported compiler. Development and CI use the pinned Rust 1.98.0 toolchain.
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-targets --all-features
cargo test --doc --all-features
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
actionlint
cargo deny check
scripts/public-api.sh check
cargo package --locked --allow-dirtySee CONTRIBUTING.md for contribution guidelines and SECURITY.md for private vulnerability reporting. Maintainers should follow the OIDC-based release procedure; releases never require a long-lived crates.io token.
Released into the public domain under the Unlicense.