* fix(node): stop stale expiry sync from undoing client extensions (#6228) After an expired client is extended on the master, a lagging node could overwrite client_traffics with an older absolute expiry and latch enable=false. Reject older absolute expiries on merge, ignore expiry-stale disables when the master is not over quota, lift stale lifecycle fields out of adopted settings, stamp reconcile fingerprints from the pre-lift node blob, and mark the node dirty so the next tick re-pushes. * fix(node): lockstep client_traffics expiry/enable SQL with review fixes (#6228) Make expiry merge keep any master absolute (node only activates when master is unset/duration). Include this tick's up/down deltas in the enable stale-disable quota check so a crossing-tick disable is not dropped. * fix(node): add settings absolute helper for renew/lift guards (#6228) Expose settingsClientAbsoluteExpiry so traffic merge can tell a real node auto-renew (settings+stats later) from lagging ClientStats after a master shorten. Trim lift godoc to the invariant. * fix(node): authority-aware lifecycle merge for multi-node sync (#6228) While config_dirty, accumulate traffic only — do not adopt node expiry/enable/total/reset (and preserve dipped baselines so a false renew cannot fire after clear). On clean ticks, master absolute expiry wins; node auto-renew still goes through nodeClientRenewed when settings also show the later deadline. Defer settings lifecycle lift until after traffic deltas land, align SyncInbound via applyMasterClientLifecycle, and avoid re-MarkNodeDirty when already dirty. * fix(node): clear config_dirty only after the post-reconcile traffic merge (#6228) After a successful ReconcileNode, keep the node dirty through the same tick's SetRemoteTraffic so lagging ClientStats cannot clobber the just-pushed master lifecycle, then ClearNodeDirty. * test(node): cover dirty-gate, master-absolute, and renew false-positives (#6228) Add regressions for extend/shorten while dirty, clean-sibling shorten, settings vs lagging disable, renew recovery after dirty, renew with matching settings, and shorten+Reset lagging stats not treated as renew. * fix(node): address the review findings on the lifecycle merge (#6228) The automated review on #6231 flagged a blocking regression and six smaller issues. All of them are fixed here. Blocking: making the master's absolute expiry always win left nodeClientRenewed as the only channel for a node-side auto-renew, and that required a counter dip. A client that used no traffic in the period never dips, so its renewal was dropped, the master kept the expired deadline and disableInvalidClients removed it with no way back (master-side autoRenewClients skips node inbounds). The node bumps reset_count on every renewal, so that counter is now an independent renewal signal and is persisted with the renewal so it keeps converging. The deferred ClearNodeDirty made every reconcile-success tick merge in dirty mode, which suppressed inbound adoption, new client_traffics rows, the orphan sweeps and the whole SyncInbound record loop -- and left the node dirty forever whenever SetRemoteTraffic errored. The clear goes back to where it was; a separate justPushed flag now freezes only the client lifecycle merge for the tick whose push just landed. staleNodeDisable only recognised a lagging disable by an older expiry, so a quota top-up (raise totalGB, leave the expiry alone) was re-latched to disabled by the next lagging snapshot -- the #6228 symptom on a second axis. The reviewer's suggestion of dropping the expiry precondition outright fails TestNodeQuotaDisable_SameExpiryStillLatches, because the master's own counters legitimately sit below a node's after a seeded-at-zero adoption. nodeDisableIsStale instead compares the limits the node judged the client against with the master's own: matching limits mean a genuine verdict that still latches (#4917), differing limits mean the node has not seen the master's change yet. It also now measures the master deadline against wall-clock now, so an expired master row no longer looks "extended" merely because the node's copy is older still. Also: the settings lift now writes enable in both directions, so a blob fetched before a master disable cannot carry enable=true back into central settings and on to the node; the renewal guard parses the inbound settings once per inbound instead of once per renewing client; the adoption loop only writes settings when they actually changed; and two comments that described mechanisms the code does not use were corrected. The test deadlines are now relative to the run: the merge compares against now, so fixed timestamps would have rotted into the wrong side of it. --------- Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
English | فارسی | العربية | 中文 | Español | Русский | Türkçe
3X-UI is an advanced, open-source web control panel for managing Xray-core servers. It provides a clean, multi-language interface for deploying, configuring, and monitoring a wide range of proxy and VPN protocols — from a single VPS to multi-node deployments.
Built as an enhanced fork of the original X-UI project, 3X-UI adds broader protocol support, improved stability, per-client traffic accounting, and many quality-of-life features.
Important
This project is intended for personal use only. Please do not use it for illegal purposes or in a production environment.
Features
- Multi-protocol inbounds — VLESS, VMess, Trojan, Shadowsocks, WireGuard, Hysteria2, HTTP, SOCKS (Mixed), Dokodemo-door / Tunnel, and TUN.
- Modern transports & security — TCP (Raw), mKCP, WebSocket, gRPC, HTTPUpgrade, and XHTTP, secured with TLS, XTLS, and REALITY.
- Fallbacks — serve multiple protocols on a single port (e.g. VLESS and Trojan on 443) using Xray's fallback support.
- Per-client management — traffic quotas, expiry dates, IP limits, live online status, and one-click share links, QR codes, and subscriptions.
- Traffic statistics — per inbound, per client, and per outbound, with reset controls.
- Multi-node support — manage and scale across multiple servers from a single panel.
- Outbound & routing — WARP, NordVPN, custom routing rules, load balancers, and outbound proxy chaining.
- Built-in subscription server with multiple output formats and custom page templates.
- Telegram bot for remote monitoring and management.
- RESTful API with in-panel Swagger documentation.
- Flexible storage — SQLite (default) or PostgreSQL.
- 13 UI languages with dark and light themes.
- Fail2ban integration for enforcing per-client IP limits.
Screenshots
Quick Start
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
To install a specific version, append its tag (e.g. v3.4.0):
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v3.4.0
To install the rolling dev build (latest per-commit pre-release from main, not a stable release), pass dev-latest:
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) dev-latest
During installation a random username, password, and access path are generated. After installation, run x-ui to open the management menu, where you can start/stop the service, view or reset your login credentials, manage SSL certificates, and more.
For full documentation, please visit the project Wiki.
Unattended install
The installer also runs non-interactively for cloud-init.
Set XUI_NONINTERACTIVE=1 (or pipe with no TTY) and it installs end-to-end with
zero prompts, generating random credentials and writing them to
/etc/x-ui/install-result.env. See deploy/ for:
- Cloud-init user-data — unattended install on any cloud (Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle)
- Hetzner Cloud notes — cloud-init deployment on Hetzner
Supported Platforms
Operating systems: Ubuntu, Debian, Armbian, Fedora, CentOS, RHEL, AlmaLinux, Rocky Linux, Oracle Linux, Amazon Linux, Virtuozzo, Arch, Manjaro, Parch, openSUSE (Tumbleweed / Leap), Alpine, and Windows.
Architectures: amd64 · 386 · arm64 (aarch64) · armv7 · armv6 · armv5 · s390x.
Database Options
3X-UI supports two backends, chosen during the install:
- SQLite (default) — a single file at
/etc/x-ui/x-ui.db. Zero setup, ideal for small and medium deployments. - PostgreSQL — recommended for high client counts or multi-node setups. The installer can install PostgreSQL locally for you, or accept a DSN to an existing server.
At runtime the backend is selected via environment variables (the installer writes these to /etc/default/x-ui for you):
XUI_DB_TYPE=postgres
XUI_DB_DSN=postgres://xui:password@127.0.0.1:5432/xui?sslmode=disable
Migrating an existing SQLite install to PostgreSQL
x-ui migrate-db --dsn "postgres://xui:password@127.0.0.1:5432/xui?sslmode=disable"
# then set XUI_DB_TYPE and XUI_DB_DSN in /etc/default/x-ui and restart:
systemctl restart x-ui
The source SQLite file is left untouched; remove it manually once you have verified the new backend.
Docker
The default docker compose up -d keeps using SQLite. To run with the bundled PostgreSQL service, uncomment the two XUI_DB_* env lines in docker-compose.yml and start with the profile:
docker compose --profile postgres up -d
The image bundles Fail2ban (enabled by default) to enforce per-client IP limits. Fail2ban bans offenders with iptables, which requires the NET_ADMIN capability. docker-compose.yml already grants it via cap_add; if you start the container with docker run instead, add the capabilities yourself, otherwise bans are logged but never applied:
docker run -d --cap-add=NET_ADMIN --cap-add=NET_RAW ... ghcr.io/mhsanaei/3x-ui
Environment Variables
| Variable | Description | Default |
|---|---|---|
XUI_DB_TYPE |
Database backend: sqlite or postgres |
sqlite |
XUI_DB_DSN |
PostgreSQL connection string (when XUI_DB_TYPE=postgres) |
— |
XUI_DB_FOLDER |
Directory for the SQLite database file | /etc/x-ui |
XUI_DB_MAX_OPEN_CONNS |
Maximum open connections (PostgreSQL pool) | — |
XUI_DB_MAX_IDLE_CONNS |
Maximum idle connections (PostgreSQL pool) | — |
XUI_INIT_WEB_BASE_PATH |
The initial URI path for the web panel | / |
XUI_ENABLE_FAIL2BAN |
Enable Fail2ban-based IP-limit enforcement | true |
XUI_LOG_LEVEL |
Log verbosity (debug, info, warning, error) |
info |
XUI_DEBUG |
Enable debug mode | false |
XUI_TUNNEL_HEALTH_MONITOR |
Enable the tunnel health monitor (probes a URL and restarts xray after repeated failures; a restart drops all clients) | false |
XUI_TUNNEL_HEALTH_PROXY |
Proxy the probe is sent through; point it at a local xray inbound so the probe tests the tunnel (e.g. socks5://127.0.0.1:1080). Empty means the probe only checks host connectivity |
— |
XUI_TUNNEL_HEALTH_URL |
URL probed for tunnel health | https://www.cloudflare.com/cdn-cgi/trace |
XUI_TUNNEL_HEALTH_INTERVAL |
Interval between probes | 30s |
XUI_TUNNEL_HEALTH_TIMEOUT |
Per-probe timeout | 10s |
XUI_TUNNEL_HEALTH_FAILURES |
Consecutive failures before a restart is triggered | 3 |
XUI_TUNNEL_HEALTH_COOLDOWN |
Minimum delay between consecutive restarts | 5m |
Supported Languages
The panel UI is available in 13 languages:
English · فارسی · العربية · 中文(简体) · 中文(繁體) · Español · Русский · Українська · Türkçe · Tiếng Việt · 日本語 · Bahasa Indonesia · Português (Brasil)
Contributing
Contributions are welcome. Please read the Contributing Guide before opening an issue or pull request.
A Special Thanks to
Acknowledgment
- Iran v2ray rules (License: GPL-3.0): Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking.
- Russia v2ray rules (License: GPL-3.0): This repository contains automatically updated V2Ray routing rules based on data on blocked domains and addresses in Russia.
Community Tools
Tools and integrations built by the community around 3x-ui.
- terraform-provider-3x-ui (License: MIT): Manage inbounds, clients, panel settings, and Xray configuration as code with Terraform / OpenTofu.
Support project
If this project is helpful to you, you may wish to give it a🌟




