English | Русский | فارسی | 中文
still-ui is a fork of 3x-ui by MHSanaei — the web panel for Xray-core. It starts from 3x-ui v3.11.0 and keeps everything that panel can do: the same protocols, the same interface, the same database.
The difference is focus. still-ui fixes the things that hurt when a panel runs for months on a small server with real users on mobile networks: client settings that quietly get lost, handshakes that die on lossy links, and a panel process that slowly eats the server's memory.
The name says what we are after: a server you set up once and then leave alone.
Important
still-ui is an independent fork. It is not affiliated with or endorsed by the 3x-ui authors. Please report still-ui problems here, not upstream.
- Partial client edits no longer break clients. In 3x-ui v3.11.0, updating a client through the API (
/panel/api/clients/update/:email) with only some of its fields disabled the client (enable=false) and resetlimitIp,totalGB,tgId,comment,group,flow, the renewal settings (reset,resetMax) andlimitHwid. In still-ui a field you do not send is taken from the stored client. An explicit zero or empty value still clears a field. WireGuard/AmneziaWG keys and peer addresses were already kept in v3.11.0; a test pins that. - The Xray core accepts the
iosfingerprint and sing-box clients again. Since Xray 26.9.9, an official REALITY server accepts only a ClientHello that carries an X25519MLKEM768 key share. That refuses Xray clients with theios,edgeandqqfingerprints, every sing-box client (Hiddify, Karing) with any fingerprint, and mihomo unlesssupport-x25519mlkem768is on.iosis the short one (about 517 bytes, one TCP segment), and on mobile networks that drop longer ClientHellos it is the fingerprint that works. still-ui 1.4.0 ships Xray v26.10.31 built from source with a one-hunk REALITY patch: a ClientHello without that key share is accepted again, duplicated key shares are still refused, nothing else changes. The build is reproducible: recipe, patch and sha256 in scripts/xray-build. New REALITY inbounds still start withchrome, as in 3x-ui. Switching Xray to an official release from the panel (the Version button next to Xray on the overview page) brings the old behaviour back;still-ui updaterestores the still-ui build. The dialog marks the still-ui build apart from the official releases and, before installing an official one, shows this warning and asks you to confirm. - The installer puts a memory cap on the panel. The service gets systemd limits:
MemoryMaxis 35% of RAM (at least 256 MiB, at most 1 GiB),MemoryHighis 85% of that, and the panel is not allowed to use swap. A runaway panel is restarted by systemd instead of pushing the whole server into swap and freezing it. If the server has no swap and 2 GiB of RAM or less, the installer offers to create a 2 GiB/swapfilefor the rest of the system. - Xray keeps running when the panel restarts. Xray runs as its own service
still-xray: an unprivileged user, onlyCAP_NET_BIND_SERVICEandCAP_NET_ADMIN(socket marks, TPROXY),ProtectSystem=strict,Restart=alwaysand its own memory cap. Restarting, updating or crashing the panel does not drop clients. The panel still adds and removes clients through the Xray API without a restart, and restarts the core only when the inbound structure changes — after checking the config withxray run -test; a config that passes the check but does not start is replaced with the last working one. This is the default on every server with systemd; an existing install moves over on its nextstill-ui update, unless its config needs something the service cannot do (see below). Docker images keep Xray inside the panel, as in 3x-ui. The embedded AmneziaWG, MTProto and TUIC still live inside the panel and reconnect when it restarts. - Scheduled, encrypted backups and a restore command. The panel can back up its database on a schedule to
/etc/still-ui/backups, keep the last N copies, encrypt them to your age or SSH public key (the private key never touches the server) and send the encrypted copy to Telegram.still-ui restore <file>checks the backup, stops the panel, swaps the database (keeping the old one) and starts it again. - Health alerts in Telegram. Xray not running, Xray restarting too often, panel or Xray memory near its limit, a full disk, a certificate about to expire, a failed backup. One message when it starts, a reminder while it lasts, one when it is fixed.
- Renewed certificates are picked up automatically. Xray rereads the certificate files of TLS and Hysteria inbounds by itself once an hour (or every
ocspStaplingseconds). Once a minute the panel does a handshake (TLS, or QUIC for Hysteria) to see which certificate each inbound really serves; if the core has not picked up a renewed file within that time (or the inbound usesoneTimeLoading), or the served certificate expires within an hour, only Xray is restarted: by itself with thestill-xrayservice, and with Xray inside the panel (Docker, or STILL_XRAY_UNIT=off) only withSTILL_CERT_RESTART=onin/etc/default/still-ui, because that restart drops every client; without it the tag just says the certificate was renewed. The “Certificates” tag on the overview page shows what each inbound serves and until when, and warns in advance (a week ahead, or a quarter of the lifetime for short-lived IP certificates). - AmneziaWG relay buffers are reused. Every TCP connection through the embedded AmneziaWG allocated two fresh 32 KiB copy buffers; they now come from a pool. The relay alone: about 69 KB → 2.9 KB allocated per connection. The whole panel on a 2 vCPU / 2 GiB test server, 100 short connections per second through AmneziaWG for 5 minutes, 3 runs each, 3x-ui v3.11.0 vs still-ui: panel memory under load 403 → 176 MiB on average, peak 525 → 211 MiB, the same at rest. Method, limits and raw data: bench/awg-relay-churn.
- An Auto group in Clash/Mihomo subscriptions. With two or more servers, the Clash/Mihomo profile gets an
Autogroup: the app tests every server through the tunnel and moves to a working one by itself (fastest with a 50 ms tolerance, checked every 300 s, or the first that answers).Autois the default choice inPROXY; picking a server by hand still works. On for new installs; on a panel that existed before, turn on Auto group in the subscription settings. - Subscriptions follow the reachability probes. A link whose endpoint the probes report down (two failed checks in a row) can be moved to the end of the subscription or left out, in every format: links, JSON and Clash. A user always keeps at least one link, and without fresh probe results nothing changes. Off by default.
- An egress filter for clients. Clients can be kept from sending mail on port 25 (on by default for new installs), from reaching private networks behind the server and the server's own panel and SSH ports, and from addresses and ports you list. It applies to every client inbound through Xray and, with
nft, to AmneziaWG kernel-mode clients; a master can hand it to its nodes. Details. - A monthly traffic limit for each node. The master counts the traffic of a node's own inbounds per month (from the reset day you pick), sends a Telegram alert at 80% (adjustable) and at the limit, and can turn the node's inbounds off until the next period. An accounting multiplier from 0.1 to 100 scales that node's traffic in client totals, from the moment it is set (earlier traffic keeps its old rate). The node checks each client's quota by its own master's count, so masters with different multipliers can share a node. No limit by default.
- Adding a node takes one command. Nodes → Add with a command makes a one-time invitation (valid 30 minutes by default, can be revoked); running the command it shows as root on a new server installs still-ui from the release, sets it up as a node and adds it to the master. No API tokens or certificate pins to copy by hand; the node checks the master's key before it changes anything. Guide.
- Panel memory over several days. The growth under AmneziaWG connection churn is found and fixed (see above); what is left is a multi-day measurement under load to confirm there is no other source of growth.
The full plan (in Russian) is in docs/ROADMAP.md.
Run as root:
bash <(curl -Ls https://github.com/Dv2Corn/still-ui/releases/latest/download/install.sh)To install a specific release, pass its tag (tags look like vX.Y.Z-still.N):
bash <(curl -Ls https://github.com/Dv2Corn/still-ui/releases/latest/download/install.sh) v1.4.0These commands run the installer of the latest release: since 1.5.1 every release publishes install.sh itself and lists it in its SHA256SUMS, so nothing is run from a branch. To check the installer before running it:
curl -fsSLO https://github.com/Dv2Corn/still-ui/releases/latest/download/install.sh
curl -fsSLO https://github.com/Dv2Corn/still-ui/releases/latest/download/SHA256SUMS
sha256sum -c SHA256SUMS --ignore-missing && bash install.shThe installer refuses an archive whose .sha256 is missing or does not match, and installs the still-ui menu from that checked archive.
To install from a release archive you already downloaded:
STILL_UI_ARCHIVE=/path/still-ui-linux-amd64.tar.gz bash install.shThe installer generates a random username, password and panel path and prints them at the end — save them. After that, run still-ui to open the management menu: start and stop the service, show or reset the login, change the port, manage certificates and so on. The subcommands are the same as in 3x-ui's x-ui script (still-ui start, stop, restart, status, log, update, legacy, uninstall and others).
Release builds exist for Linux amd64 and arm64 only. The current release is still-ui 1.4.0, based on 3x-ui 3.11.0 and the fixes merged after it, with Xray v26.10.31 in still-ui's own build (what differs).
| What | Where |
|---|---|
| Service | still-ui (systemctl status still-ui) |
| Program | /usr/local/still-ui |
| Database | /etc/still-ui/x-ui.db (the file name is kept from 3x-ui) |
| Logs | /var/log/still-ui |
| Settings | /etc/default/still-ui |
| Xray | service still-xray, config /etc/still-xray, logs /var/log/still-xray |
| Backups | /etc/still-ui/backups (XUI_BACKUP_FOLDER in the settings file moves them) |
Check the current cap:
systemctl show still-ui -p MemoryMaxTo set your own value, add a line to /etc/default/still-ui — for example STILL_UI_MEMORY_MAX=800M, or STILL_UI_MEMORY_MAX=off to remove the cap.
Xray runs as the systemd service still-xray, apart from the panel. systemctl restart still-ui, a panel update (from the command line or the update button) and a panel crash leave it alone, so clients stay connected. Tested in a throwaway Debian 12 container: a 20 MB download through a SOCKS inbound at 400 KB/s, with systemctl restart still-ui, kill -9 of the panel and a full still-ui update in the middle — each time the file arrived complete and the Xray process was the same.
When does Xray restart? When the inbound structure changes (the panel says so), when you press Restart Xray, after still-ui restore, and once when an install moves to the service. A new Xray from an update runs only after you restart Xray; the overview page says so.
Check it: systemctl status still-xray. The settings file /etc/default/still-ui has XUI_XRAY_UNIT=still-xray while the service is in use.
On update, x-ui xray-unit-check looks at the current config first. Xray stays inside the panel, and the update prints why, when an inbound
- listens on a unix socket outside
/run/still-xray/and/dev/shm/(the service has its own/tmpand sees the rest read-only; use e.g./dev/shm/xray.sock,0666), - is a TUN inbound: it needs
/dev/net/tun, which the service's sandbox hides; a drop-in of your own in/etc/systemd/system/still-xray.service.d/withPrivateDevices=noallows it, then run the update again.
TPROXY and socket marks (policy routing, WARP) work in the service: it has CAP_NET_ADMIN. The panel refuses a change the running service cannot do and says why, for example a socket mark after a local drop-in took the capability away; a core still running from before the update that gave the service the capability is restarted once for a change that needs it.
Settings in /etc/default/still-ui: STILL_XRAY_UNIT=off keeps Xray inside the panel (as in 3x-ui), STILL_XRAY_UNIT=on forces the service, unset means automatic. STILL_XRAY_MEMORY_MAX caps the core (half the RAM by default, 256M to 4G). After a change run still-ui update. Docker images have no systemd and always keep Xray inside the panel.
Settings → Backups in the panel:
- Scheduled backups on, and Schedule (daily by default). The panel takes a consistent snapshot (SQLite backup API;
pg_dumpwith PostgreSQL), checks it reads back, and writesstill-ui-<host>-<date>-<time>.db(.pgdump) to/etc/still-ui/backups(0600, folder 0700). Restart the panel after changing the schedule. - Copies to keep — older ones are deleted after each new backup; other files in the folder are never touched.
- Encryption keys — age public keys (
age1…, fromage-keygenon your own computer) or SSH public keys (ssh-ed25519 …), one per line. With a key set, backups are written as….db.ageand nothing unencrypted is stored. The private key stays with you. The daily Telegram backup (Telegram Bot → Notifications → Database Backup) is encrypted to the same keys, the Xray config too. - Send to Telegram — each encrypted backup also goes to the Telegram admins (up to 50 MB). Unencrypted backups are never sent this way.
- Back up now and the list of backups on the server. From the command line:
still-ui backup.
Restore:
still-ui restore /etc/still-ui/backups/still-ui-<host>-<date>-<time>.db.ageFor an encrypted backup the script asks you to paste the private key (AGE-SECRET-KEY-1…); it goes to the panel binary through a pipe and is not stored. You can also pass a key file as the second argument (an age identity file or an unencrypted OpenSSH private key) — copy it to the server only for the restore. The backup is decrypted and checked first (PRAGMA integrity_check, pg_restore --list); a wrong key or a damaged file changes nothing. Then the panel stops, the database is replaced, the old one is kept as /etc/still-ui/x-ui.db.before-restore-<time>, Xray restarts once to load the restored inbounds, and the panel starts again. On PostgreSQL the old database is kept as a dump beside it.
You can also decrypt elsewhere: age -d -i key.txt backup.db.age > x-ui.db.
Settings → Telegram → Notifications → Server health (the bot must be on):
| Alert | Fires when | Default |
|---|---|---|
| Xray is not running | Xray is down for two checks in a row (a minute), and you did not stop it | on |
| Xray restarts | Xray crashed or was restarted by systemd N times in M minutes | 3 in 10 min |
| Panel or Xray memory | the panel or Xray uses X% of its systemd MemoryMax (without file cache), or of the RAM when there is no cap; three checks in a row; clears after three checks 15 points below |
90% |
| Disk full | the disk with /, the database or the backups is X% full; clears after three checks 5 points below |
90% |
| Certificate expires | the panel's or the subscription's certificate file, or the certificate a TLS/Hysteria inbound actually serves, expires within N days — at most a quarter of its lifetime, so a 6-day IP certificate is reported only in its last day and a half | 7 days |
| Backup failed | a scheduled backup (or its Telegram upload) failed | on |
Checks run every minute. An alert is sent once when the problem starts, again every Remind every hours while it lasts (6 by default, 0 = never), and once more when it is fixed. The state lives in memory: after a panel restart a problem that is still there is reported once again.
Update to the latest release — from the command line or with the update button in the panel:
still-ui updateBoth download update.sh of the release they install (releases/download/<tag>/update.sh) together with that release's SHA256SUMS, and run it only if its sha256 is listed there; nothing is run from a branch. still-ui has no dev channel: every update installs a release. The first update from 1.5.0 or older still runs the old updater (it takes update.sh from main, which only changes through reviewed pull requests); from the next release on the release's own file is used.
Roll back to an earlier release — still-ui legacy asks for the version; or run the installer with that tag. The database is kept.
still-ui legacy
bash <(curl -Ls https://github.com/Dv2Corn/still-ui/releases/latest/download/install.sh) v<tag>Rolling back to a release without the still-xray service (for example v3.11.0-still.1): still-ui legacy stops the service before installing; if you run an older installer by hand, run systemctl disable --now still-xray first.
Back up /etc/still-ui before updating or rolling back: an older version may not understand a database written by a newer one.
Remove the panel:
still-ui uninstallThis removes the program, both services, the still-xray user and the menu, and keeps the data: the database, backups and install-result.env in /etc/still-ui and the env file (/etc/default/still-ui). Installing still-ui again picks them up, with the same login, inbounds and clients. The menu asks "Also delete all data?" (default No); without a terminal the data is always kept. For scripts: still-ui uninstall --keep-data keeps it, still-ui uninstall --purge deletes it too (and offers to purge a local PostgreSQL). Unlike 3x-ui, nothing is deleted unless you ask.
Russian mobile operators inspect traffic (DPI), and in "whitelist" mode they cut most foreign TCP connections after the first 12–16 KB. Two things in still-ui help.
Presets for new inbounds. In Add Inbound, pick a Preset:
- Mobile networks (Russia): VLESS REALITY — VLESS on TCP 443 with REALITY, a fresh key pair and short IDs, and the
iosfingerprint in the links and subscriptions of this inbound only (other new inbounds still start withchrome). TheiosClientHello is about 517 bytes and fits in one TCP segment; longer ones lose their second segment on some networks. still-ui's Xray acceptsios; official Xray 26.9.9 and newer refuses it. Then pick a target on the Security tab. Find Targets checks candidate sites from your server; prefer a site that is popular and not blocked in your clients' country over one well-known foreign site. Give clients thextls-rprx-visionflow. - Mobile networks (Russia): Hysteria2 (UDP) — Hysteria2 on UDP 443 with the
h3ALPN and no Salamander obfuscation, so that most clients can connect. UDP often keeps working in whitelist mode when TCP is cut. Set the certificate on the Security tab.
Block torrents for clients. Xray Configs → Routing → Basic Routing → Block torrents for clients. It is off by default, and clients get no notice. When it is on:
- Every Xray config sends BitTorrent (as Xray's sniffer sees it) to a blackhole before any other rule. Inbounds with sniffing off get content sniffing without destination override, because Xray only recognises BitTorrent when it sniffs.
- On Linux with
nft, the panel loads the nftables tableinet still_torrent_block. It looks only at what the server sends out: connections the server itself opens, and traffic forwarded from AmneziaWG kernel-mode interfaces (xui-awg*), which never passes through Xray. Outgoing BitTorrent handshakes, DHT messages, UDP tracker connects and uTP SYNs are dropped; an AmneziaWG kernel-mode client that sent one keeps only ports 53/80/443/853 for 30 minutes (a client can only cut itself). A peer endpoint (address and port) is cut for 30 minutes only after that peer itself answers with a BitTorrent handshake or a DHT response, so a client cannot get some other site blocked by sending it a fake handshake. One risk remains: a TCP service that echoes what it receives back at the start of its reply can be made to send a 68-byte BitTorrent handshake, and then its own address and port are cut for 30 minutes. Answers from ports 53/80/443/853, from the panel, subscription, inbound, node and SSH ports, and from the server's own addresses are never learned, and each remote address can teach at most 30 endpoints a minute. The server's replies on connections that clients open to it are never inspected. The panel loads the table on start and again if it disappears; turning the option off removes it. Withoutnft(or not on Linux) only the Xray part works, and the page says so: install thenftablespackage. The Docker image includes it.
BitTorrent encrypted inside a client's own extra tunnel cannot be seen. Traffic that Xray passes on to another proxy is covered by the Xray rule only. With nodes, turn the option on in each node's own panel.
Egress filter for clients. Xray Configs → Routing → Basic Routing → Egress filter for clients. It keeps clients from using the server for mail spam, from reaching networks behind it and from reaching its management ports; hosters suspend servers over complaints about exactly these. Tick what to refuse and press Apply:
- Mail to mail servers (port 25) — on by default on new installs, off on existing ones. Spam from VPN clients goes out on port 25, while mail apps send on 465/587.
- Mail submission (ports 465 and 587).
- Private and local networks: 10/8, 172.16/12, 192.168/16, 100.64/10 (carrier-grade NAT), 169.254/16, loopback, multicast, reserved, fc00::/7, fe80::/10. The stock template already sends
geoip:privateto a blackhole; the panel sees that rule and adds no second one. - This server's panel and SSH ports, read from the panel settings and
sshd_config, and the server's loopback services. Besides the interface addresses, the block covers IP literals in the panel's listen and domain settings and, on a node, the IP address its master reaches it at; names are never resolved, so a domain behind a CDN or a shared host blocks nothing. A public address on no interface (behind NAT or a floating IP) that is not listed there goes into Custom addresses and ports. The subscription port stays open, because client apps refresh their subscription through the tunnel they are connected to; a port that also serves an inbound stays open too. - Custom addresses and ports: an address or CIDR is refused on every port, a port or range (
6667,1000-2000) on every address. Port 53 cannot be listed: DNS always works.
Refused TCP connections are closed at once instead of hanging. Every generated Xray config gets these rules ahead of all others, for client inbounds only (including AmneziaWG, TUIC and MTProto relayed through Xray), plus block final rules in each freedom outbound for the server's own ports and for the private, loopback and listed addresses (every port but 53), since a client that names such an address by a domain is only resolved there. On Linux with nft, the table inet still_egress_filter refuses the same for AmneziaWG kernel-mode clients (xui-awg*): it judges only connections a tunnel client opens, never the server's own traffic, SSH sessions, DNS or replies, and traffic between two clients of the same tunnel. It is loaded on start and reloaded if it disappears.
With nodes, choose All nodes or Selected nodes: the panel hands the filter to those nodes and keeps retrying the ones that are offline; nodes taken out of the list keep their last filter as their own. A node shows that its filter was set by the central panel; saving it on the node makes it the node's own until the central panel sends it again.
Release archives are built by GitHub Actions from the tagged commit (release workflow), not on a developer's machine, and every archive comes with a signed build provenance attestation. Releases up to and including 1.4.0 were built by hand with scripts/still-release.sh and have only the .sha256 files.
| File | What it is |
|---|---|
still-ui-linux-<arch>.tar.gz |
the archive install.sh and still-ui update download |
still-ui-linux-<arch>.tar.gz.sha256 |
its checksum; the installer checks it |
still-ui-linux-<arch>.tar.gz.spdx.json |
SBOM (SPDX): every file in the archive with its sha256, and the Go modules inside x-ui, xray and mtg |
still-probe-linux-<arch>.tar.gz (+ .sha256) |
the reachability probe agent that scripts/still-probe-install.sh downloads; attested like the panel archives |
install.sh, update.sh, x-ui.sh, migrate.sh |
the installer, the updater, the still-ui menu and the 3x-ui migration of this release (the same files are inside the archive); the panel's update button and still-ui update run update.sh only when its sum is in SHA256SUMS |
SHA256SUMS |
checksums of the archives, SBOMs, probe archives and scripts |
Download and check the checksums (replace v1.6.0 with the release you install):
gh release download v1.6.0 -R Dv2Corn/still-ui -p 'still-ui-linux-amd64.tar.gz*' -p SHA256SUMS
sha256sum -c SHA256SUMS --ignore-missingCheck that the archive was built by this repository's release workflow from that tag (needs the GitHub CLI):
gh attestation verify still-ui-linux-amd64.tar.gz -R Dv2Corn/still-ui \
--signer-workflow Dv2Corn/still-ui/.github/workflows/release.yml --source-ref refs/tags/v1.6.0The SBOM is attested too:
gh attestation verify still-ui-linux-amd64.tar.gz -R Dv2Corn/still-ui --predicate-type https://spdx.dev/Document/v2.3The Xray core inside the archive can be rebuilt bit for bit. On a Linux host with git and docker:
tar -xzf still-ui-linux-amd64.tar.gz still-ui/bin/xray-linux-amd64
sha256sum still-ui/bin/xray-linux-amd64
git clone https://github.com/Dv2Corn/still-ui && cd still-ui && git checkout v1.6.0
scripts/xray-build/build.sh /tmp/xray-out amd64 arm64build.sh prints the sums of the binaries it built and fails if they differ from the ones pinned in it; the sum of xray-linux-amd64 from the archive must be the same. The Xray reproducible build workflow does this on every change to the recipe and once a week, on an x86_64 and an arm64 runner. What the patch changes: scripts/xray-build.
Everything else that builds or goes into the archive is pinned in scripts/still-release.sh, and the build fails on any other sum: the node and golang build images by digest, the Bootlin musl toolchain that links the panel binary by file name and sha256, and mtg by release and the sha256 of each archive. The geo files are data that has to stay current, so they are the latest releases of their projects at build time; the build log prints the repository, release tag and sha256 of each, and the SBOM lists them with their sha256. The archive as a whole is still not bit-for-bit reproducible (archive timestamps, the geo files), so the provenance attestation is what ties it to the commit and the workflow run.
One command moves a running 3x-ui to still-ui and keeps everything clients and you rely on:
bash <(curl -Ls https://github.com/Dv2Corn/still-ui/releases/latest/download/install.sh) migrateWhat it does:
- Finds 3x-ui (
/usr/local/x-ui,x-ui.service, the SQLite database or the PostgreSQL one named in/etc/default/x-ui) and refuses what it cannot move safely: other x-ui forks, 3x-ui older than 2.0.0 (update it first withx-ui update), a 3x-ui newer than the release still-ui is based on, hosts without systemd, Docker installs, hosts that already run still-ui. - While 3x-ui still runs, records what has to survive: inbounds, clients, the panel login, the panel and subscription addresses, and the subscriptions of up to five clients as 3x-ui serves them.
- Backs up everything to
/root/still-ui-migrate-<UTC time>/(readable by root only) before changing anything: a consistent copy of the database (or apg_dump),/usr/local/x-ui, the unit and its drop-ins,/usr/bin/x-ui, the env file, and a manifest with checksums. - Installs the packages still-ui needs while 3x-ui still serves clients, then stops 3x-ui and installs still-ui with a copy of its database (an installer that has not finished after 10 minutes,
STILL_UI_MIGRATE_INSTALL_TIMEOUT, counts as failed). still-ui lives in its own folders (/usr/local/still-ui,/etc/still-ui,still-ui.service); ports, certificates, the panel base path, subscription URLs, client links and the panel login (with 2FA) come from the database and stay the same. - Checks still-ui: the panel answers at the same address, Xray runs, inbounds, clients, login and listener settings match, and the sampled subscriptions are byte-identical. A 3x-ui 2.x writes fewer link parameters than still-ui does; then every parameter a link had must still be there with the same value.
- If any step after stopping 3x-ui fails, it restores 3x-ui on its own and says why.
When it is done, x-ui.service is removed (it is in the backup) so 3x-ui cannot start next to still-ui, the x-ui command opens still-ui's menu, and acme.sh renewals that restarted x-ui restart still-ui (the rollback puts them back). 3x-ui's own files and database are not modified.
Undo it at any time — this restores 3x-ui's files, unit and database from the newest backup, starts 3x-ui, removes still-ui and keeps still-ui's database in the backup folder:
still-ui migrate-rollbackChanges made in still-ui after the migration (new clients, traffic counters) do not travel back to 3x-ui. Without a terminal, set STILL_UI_MIGRATE_YES=1. A PostgreSQL setup needs pg_dump and pg_restore on the server. Tested on Debian 12 and Ubuntu 24.04 with 3x-ui 3.11.0 (SQLite and PostgreSQL), 3.7.0, 2.9.4, 2.6.7, 2.4.11, 2.3.15, 2.2.8 and 2.0.2 (scripts/test-migrate.sh).
A plain install also offers to take over /etc/x-ui/x-ui.db (non-interactive: STILL_UI_IMPORT_3XUI=yes), without the checks and the backup above. It stops 3x-ui, moves its unit to /root/still-ui-3x-ui-retired/ and points acme.sh renewals at still-ui too; bash /root/still-ui-3x-ui-retired/migrate.sh restore-import /root/still-ui-3x-ui-retired puts both back (still-ui uninstall --purge runs it; an uninstall that keeps the data for a reinstall leaves 3x-ui retired).
The panel and the subscription server pick up a renewed certificate file within half a minute, without a restart.
A panel cannot tell from the inside whether its inbounds work for users in a censored network: the server sees a handshake, while the censor may freeze the connection a few kilobytes later. A probe answers "does my VPN work from there right now?". It is a small agent (still-probe, a static binary with the still-ui Xray) that you run on any Linux box inside that network: a home server, a Raspberry Pi or a cheap local VPS.
- In the panel open Probes → Add probe, give it a name and choose the inbounds to test (VLESS, VMess, Trojan, Shadowsocks, Hysteria). The panel creates a service client
still-probe-…for it on those inbounds, with no traffic or time limit; do not edit or delete that client. - Copy the install command shown once with the token and run it as root on the probe box (amd64 or arm64, systemd):
STILL_PROBE_TOKEN='<token>' bash <(curl -fsSL https://raw.githubusercontent.com/Dv2Corn/still-ui/main/scripts/still-probe-install.sh) \
--panel 'https://<server>:<port>/<base path>/'The token is passed in the environment, which other local users cannot read; --token-file FILE works too, and without either the installer asks for it. --token '<token>' still works but prints a warning: a command line is visible to every user in ps.
The installer downloads still-probe-linux-<arch>.tar.gz from the latest still-ui release and checks its sha256, installs the agent to /usr/local/still-probe, its config to /etc/still-probe/config.json (root:still-probe 0640) and a systemd timer, and runs the first check. Add --pin <sha256> for a panel with a self-signed certificate, --uninstall to remove everything. An http:// panel address is refused: the token, the panel's secret path and the keys of every tested inbound would cross the censored network in clear text. --insecure-http accepts that anyway and prints a warning.
Every interval (10 minutes by default) the agent fetches its job from the panel, starts Xray with one SOCKS port per inbound endpoint (the same address, SNI and keys the subscription hands out), downloads the test URL through each of them (by default 256 KiB from Cloudflare: a censor that cuts connections after the first kilobytes shows up as a failure) and, optionally, a direct URL without the VPN, such as your site or subscription page. It reports success, time to first byte, total time and bytes. If the panel itself is blocked from there, the report goes through an inbound that just worked.
The Probes page shows the last result of every endpoint, its uptime over 7 days and an hourly history strip; a probe that has not reported for three intervals is marked "Not reporting". The Telegram bot sends an alert when an endpoint fails twice in a row, when it works again, and when a probe goes silent: turn on the "Reachability probes" events in Panel Settings → Telegram.
Subscriptions can use the probe results too (Subscription → Use probe results, off by default). With Move to the end, a link whose endpoint the probes report down goes to the end of every format (links, JSON, Clash); with Hide, it is left out, and Clash groups and JSON balancers stop naming it. An endpoint counts as down only after two failed checks in a row on enough probes: by default 1 when only one probe checks that inbound, otherwise 2. Results of a probe that has not reported within the expiry window (30 minutes by default) are ignored, a probe that fails on every endpoint at once is ignored when its own check without the VPN fails too, or, if it has no such check, when the failing endpoints are on more than one server (its own network is the likely cause); one whole server failing on a probe whose direct check passes is believed, because that is what a blocked server address looks like; and an endpoint without results stays where it is. A user always keeps at least one link: when every link is down the normal list is served. The Probes page shows which endpoints subscriptions move down or hide. The panel's own link and QR views and the subscription web page always show every link.
The probe token opens only the two agent routes (<base path>probe/v1/job and probe/v1/report), nothing under /panel/api; the panel stores only its SHA-256, and a job holds only the probe client's own credentials. New token revokes the old token and also gives the probe client new keys, so a leaked agent config or link stops working; a disabled or deleted probe has no client at all. WireGuard, AmneziaWG, TUIC and MTProto inbounds are not probed yet.
xui-doctor is a small read-only checker for still-ui and 3x-ui. It looks at the Xray core version, REALITY and TLS settings, open ports, clients and panel security, and prints what to fix. It changes nothing on the server.
go install github.com/Dv2Corn/xui-doctor/cmd/xui-doctor@latest
XUI_API_TOKEN=<token> xui-doctor check --url https://<server>:<port>/<base path>/Exit code 0 means no problems, 1 means warnings, 2 means something critical. Add --json for scripts.
If still-ui saves you time, you can support its development:
- USDT (TRC20):
TE18ocTtSRa855uCzKFnPFgnSAtJ5Jny2x - BTC:
bc1qneuy3u36lrln042uezc7a7ycxd30n5fk29f43e
Card payments via DonationAlerts: https://www.donationalerts.com/r/dv2corn
A star on GitHub helps too.
- 3x-ui by MHSanaei and its contributors — still-ui is built on their work. You can support the 3x-ui author too: GitHub Sponsors, donate.sanaei.dev.
- x-ui by vaxilu — where it all started — and alireza0.
- Xray-core by Project X — the engine underneath.
still-ui is distributed under the GNU General Public License v3.0, the same license as 3x-ui. If you distribute a modified version, you must publish your changes under the same license.