3x-ui install and VLESS Reality setup

How to install 3x-ui and set up VLESS Reality with XTLS Vision on your VPS and domain: a locked-down panel, your own cover site, client apps and fixing blocks.

With Reality, Xray answers the TLS handshake on behalf of a target website — in this article, your own site on the same server. It recognises its own clients by a key hidden inside the handshake and passes everyone else, active probes included, through to the site. So censorship systems — Russia's TSPU, Iran's filtering, China's Great Firewall — go after Reality mostly through indirect signs: the server's IP, how plausible the "address + SNI" pair looks, the client fingerprint, traffic volume and connection count. Nearly every setting below exists to keep those signs from giving the server away.

Current as of September 2026: 3x-ui v3.8.5 with the bundled Xray-core v26.9.9; commands and defaults are checked against the official repository.

What you'll need

  • A ForestsNet Cloud VPS with a current Ubuntu or Debian and SSH access as root — see VNC console and first connection. If the server already runs other things, start from a clean system.
  • Your own domain or subdomain with an A record pointing at the server's IP — see Pointing a domain at your server. The record must lead straight to the server, not through the Cloudflare proxy.
  • A free port 443: no nginx or other web server may listen on it — the cover site will live on local port 8443.
  • 1 vCPU and 1 GB of RAM are enough for a personal VPN.

Step 1. Prepare the server

Update the system and close all inbound traffic except SSH, 80 and 443. Port 80 is needed to issue and renew your site's certificate; the panel port stays closed, since in step 3 you'll reach the panel through an SSH tunnel.

apt update && apt full-upgrade -y
apt install -y curl ufw
ufw default deny incoming
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
Important: if SSH runs on a non-standard port, allow that port instead, or you'll lock yourself out after ufw enable.

More on the rules in The ufw firewall: managing ports.

Step 2. Install 3x-ui

bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)

To install a specific version, append its tag, for example v3.8.5 — handy when a fresh release breaks something. Since v3.8.0 the script checks the archive against its published SHA-256 sum.

The installer asks three questions:

  1. Database — SQLite (option 1). PostgreSQL is for hundreds of clients.
  2. Panel port — answer n and the script picks a random port between 1024 and 62000.
  3. Panel SSL — don't just hit Enter. The default is a Let's Encrypt certificate for the IP address: it lives about six days and renews through port 80, which your site's web server takes over in step 4. Choose option 4 (no SSL) and answer y when asked to bind the panel to 127.0.0.1 — the panel then isn't visible from the internet at all.

At the end the script prints the username, password, port and WebBasePath — a random 18-character panel path. The same data is saved to /etc/x-ui/install-result.env, readable by root only. x-ui settings shows the current settings, and x-ui with no arguments opens the management menu.

Step 3. Take the panel off the internet

Scanners hunt for panels, and every project has vulnerabilities sooner or later. There's also a Reality-specific reason: port 443 impersonates someone else's website, while a 3x-ui login page on the next port gives away what the server really does.

A panel bound to 127.0.0.1 is opened through an SSH tunnel. On your computer (OpenSSH ships with Linux, macOS and Windows 10/11) run:

ssh -L 2222:127.0.0.1:PANEL_PORT root@SERVER_IP

While the session is open, the panel is at http://localhost:2222/WEBBASEPATH/, and the traffic travels inside SSH. If you didn't bind the panel to localhost during installation, do it now:

/usr/local/x-ui/x-ui setting -listenIP 127.0.0.1
x-ui restart

If you do need the panel from outside, use a domain certificate, a random port, a long WebBasePath and 2FA, and open the port to your own address only: ufw allow from YOUR_IP to any port PANEL_PORT proto tcp.

After the first login, change the username and password and enable two-factor authentication (TOTP) in the panel settings. The subscription server is on by default and listens on port 2096 on all addresses: if you don't need subscriptions, turn it off under Subscription; if you do, give it a certificate, otherwise the link and all connection parameters travel in plain text.

Step 4. Set up your own cover site

The target (the Target field; target in the Xray config, formerly dest) is the website whose TLS handshake the server borrows, and the SNI (serverNames) is its name, which the client sends in plain text. The network sees exactly this "server IP + SNI" pair. Here the target is your own site on the same server: the domain points at the server's address, the certificate is issued for that domain, and the "IP + SNI" pair is honest.

On ForestsNet Cloud servers this is the only way Reality is allowed: making the server pose as someone else's website — putting a domain you don't own in the SNI, the target or a certificate — is prohibited by the Terms of Service. Such setups get addresses blocked, and those blocks hit every customer on the network.

A borrowed target is a bad idea technically, too. www.microsoft.com, www.apple.com, dl.google.com sit on thousands of servers, and such a server shows up in one log query: popular SNI, gigabytes of traffic, a network the domain's owner doesn't own. Xray 26.9 even logs a warning if the SNI contains apple, icloud or microsoft or ends in .ru, .ir or .cn.

The layout: nginx serves the site on 127.0.0.1:8443 with a Let's Encrypt certificate and keeps port 80 for issuing it; Xray on 443 passes everyone who fails the Reality check through to nginx. The examples use cloud.example.com. Pick a neutral name without vpn or proxy in it: the network sees the SNI in plain text. Install nginx and certbot, create the site directory and remove nginx's default site, so the server's IP doesn't show a stock page:

apt install -y nginx certbot
mkdir -p /var/www/cloud
rm /etc/nginx/sites-enabled/default

Create /etc/nginx/sites-available/cloud with a port 80 block — certbot verifies the domain through it:

server {
    listen 80;
    server_name cloud.example.com;
    root /var/www/cloud;
    location /.well-known/acme-challenge/ { }
    location / { return 301 https://$host$request_uri; }
}

Enable the site and get the certificate. Plain certbot --nginx without certonly won't do here: it would add port 443 to the config, and Xray holds that port.

ln -s /etc/nginx/sites-available/cloud /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
certbot certonly --webroot -w /var/www/cloud -d cloud.example.com --deploy-hook "systemctl reload nginx"

Append the site block itself to the same file and run nginx -t && systemctl reload nginx again:

server {
    listen 127.0.0.1:8443 ssl http2;
    server_name cloud.example.com;
    ssl_certificate /etc/letsencrypt/live/cloud.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cloud.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    root /var/www/cloud;
}

Reality needs TLS 1.3, hence the ssl_protocols line; the http2 flag on listen works even in the older nginx versions shipped by Debian and Ubuntu. Caddy works instead of nginx on the same principle: the site on 127.0.0.1:8443, the certificate via port 80.

The site must be alive and fit the domain name: a few pages, images, a favicon — not a "Welcome to nginx" page. Also put a file of a couple of megabytes there, such as photo.jpg: it's handy for the tests at the end of the article. Check the site on the server; the answer should start with HTTP/2 200:

curl -sI --resolve cloud.example.com:8443:127.0.0.1 https://cloud.example.com:8443/ | head -1

Step 5. Create the VLESS + Reality + Vision inbound

In the panel, open Inbounds → Add Inbound:

  1. Basics tab: protocol vless, port 443.
  2. Stream tab: transport RAW — plain TCP.
  3. Security tab: Reality, uTLS chrome, Target 127.0.0.1:8443 (the port is required, otherwise Xray won't start), SNI cloud.example.com.
  4. Get New Cert generates the x25519 key pair. Fill empty Short IDs and SpiderX with the regenerate buttons next to the fields.
  5. Leave Min Client Ver, Max Time Diff, mldsa65 and the fallback limits empty.
  6. Sniffing tab: enable it with http, tls and quic — without sniffing, routing rules don't work, including the built-in BitTorrent block.

Why:

  • Port 443. Real HTTPS lives on 443, and Xray logs a warning when Reality listens on any other port.
  • The chrome fingerprint. The client disguises its TLS greeting (ClientHello) as a browser's. Since Xray v26.9.8 the Reality server rejects any ClientHello without an X25519MLKEM768 key share, as sent by current browsers. Of the Xray client presets, chrome, firefox and safari qualify; random and randomized are currently discouraged by the XTLS developers.
  • Short ID — a hex string of even length, up to 16 characters. Remove a value from the list and you revoke access for clients using it.
  • The private key stays on the server; clients get the public one. Don't hand the public key out freely either: Xray even renamed its field to password.
  • Min Client Ver stays empty. In Xray v26.7.x an empty field meant a minimum of 26.3.27, and third-party clients were refused despite correct keys; since v26.9.8 empty means no limit.
  • Don't enable mldsa65 without a reason: if the target's certificate chain is shorter than 3,500 bytes, connections silently fall back to the target.

The resulting inbound JSON looks like this:

"realitySettings": {
  "target": "127.0.0.1:8443",
  "serverNames": ["cloud.example.com"],
  "privateKey": "<x25519 private key>",
  "shortIds": ["6ba85179e30d4fc2"]
}

After saving, check journalctl -u x-ui -n 50 --no-pager | grep XRAY: Xray's warnings about the target or port show up there. Then open https://cloud.example.com in a browser without the VPN — your site should appear with a valid certificate. That's Xray failing to recognise you as a client and passing you to nginx; any scanner will see the same.

Step 6. Add clients and connect your devices

Under Clients → Add Client, create one client per device: you'll see who uses how much and can disable one without touching the rest. The Email is just a unique name, such as anna-phone. Attach the client to the inbound and set Flow to xtls-rprx-vision; the traffic limit, expiry and IP Limit (simultaneous addresses) live here too.

The client card gives you a link and a QR code:

vless://UUID@SERVER_IP:443?type=tcp&security=reality&pbk=PUBLIC_KEY&fp=chrome&sni=cloud.example.com&sid=6ba85179e30d4fc2&spx=%2F&flow=xtls-rprx-vision#anna-phone

Treat the link like a password. A subscription is more convenient — the app picks up changes to the target or port by itself — but for a couple of your own devices a link is enough.

Apps as of September 2026 — prefer ones built on Xray-core:

  • Windows: v2rayN (open source, supports XHTTP; TUN mode needs administrator rights). Fallback: Throne (formerly NekoRay) with Xray profiles.
  • macOS: v2rayN (not signed by Apple, allow the first launch manually), Happ or OneXray.
  • Android: v2rayNG, from GitHub only (2dust/v2rayNG) — it isn't on Google Play, and clones abound. Alternative: Happ.
  • iOS: Happ, Streisand, V2Box, OneXray or incy. With a Russian Apple ID only OneXray and incy of these are available, and the Russian App Store is full of clones with the same names: the original Happ, for example, is published by Flyfrog LLC.

Import works the same everywhere: from the clipboard, by QR code or by subscription URL; then check the fingerprint and flow in the profile. Turn on split routing so sites in your home country go direct: local banks and government services often refuse foreign IPs, and a device sending all its traffic to one foreign address is one more VPN sign in the operator's logs.

Done: switch the connection on and open any IP-check site — it should show your server's address.
Important: since Xray v26.9.8 (3x-ui 3.8), Reality refuses clients whose ClientHello lacks X25519MLKEM768. As of late September 2026 this locks out sing-box-based clients (Karing, Hiddify, NekoBox, the official sing-box apps) and Shadowrocket 2.2.92. Mihomo (Clash Verge Rev, for example) works only with the chrome fingerprint and the support-x25519mlkem768 option, which the 3x-ui /mihomo/ subscription sets but a plain vless:// link does not. Last resort: roll the core back to v26.7.28 on the panel dashboard and set Min Client Ver to 0.0.0 — on 26.7.x cores an empty field means a minimum of 26.3.27.

Step 7. Keep the panel updated and back it up

Update with x-ui update, but read the release notes first: as the ML-KEM story shows, a new core can cut off some clients. Menu item 5 of x-ui installs a specific panel version.

Inbounds, clients and keys live in /etc/x-ui/x-ui.db. It's SQLite in WAL mode, so copying the file while the panel runs can lose the latest changes still in x-ui.db-wal. Download a backup from the dashboard (Backup & Restore) or stop the panel while copying:

x-ui stop
cp /etc/x-ui/x-ui.db /root/x-ui-$(date +%F).db
x-ui start

The database holds the Reality private keys and client UUIDs, so keep copies off the server. The panel's Telegram bot can send the database and config.json on a schedule.

Step 8. Check the security basics

  • Firewall: only SSH, 80, 443 and, if used, the subscription port are open from outside. Avoid the Firewall Management → Install item of the x-ui menu: it allows ports 22, 80, 443, 2053 and 2096, but not your random panel port and not a non-standard SSH port.
  • fail2ban: the installer set it up for IP Limit (the 3x-ipl jail), but SSH brute-force protection needs a separate sshd jail — see Fail2ban: protection against brute-force.
  • SSH: key-only login — see How to harden SSH on your VPS.
  • Panel: listens on 127.0.0.1, 2FA on. The built-in limiter locks login for 15 minutes after five failed attempts.
  • System updates install automatically — see the VPS security checklist.
  • Torrents: copyright complaints land on you, and the built-in rule doesn't catch everything — see How to block torrents on your VPN or proxy.

When Reality stops working

According to ntc.party threads and Habr write-ups from 2025–2026, Russia's TSPU doesn't recognise the Reality protocol itself. It relies on blunt measures: address bans, freezing connections to hosting subnets, connection limits, and whitelists on mobile internet. The mechanisms below are documented for Russia; other censors have their own toolkits, but the diagnostics are the same. Each problem has a different cure, so first find out which one hit you.

Control test first, then tests from inside the censored network

Connect with the same client from outside the censored country — over roaming, or ask someone abroad. If it fails there too, the server is at fault: check x-ui status, ss -ltnp | grep ':443' and journalctl -u x-ui -n 100 --no-pager. A common cause is another process holding port 443.

If the server is alive, test the path from the ISP. Reality shows outsiders the real target, so plain curl opens your server as the target site. Run it from the network where things fail; on Windows use PowerShell, curl.exe and NUL instead of /dev/null:

# 1. TCP and TLS: connect time, handshake time, response code
curl -sk -o /dev/null --resolve cloud.example.com:443:SERVER_IP -w 'tcp=%{time_connect} tls=%{time_appconnect} code=%{http_code}\n' https://cloud.example.com/
# 2. Volume: a file on your site larger than 64 KB, such as photo.jpg from step 4
curl -sk -o /dev/null --max-time 10 --resolve cloud.example.com:443:SERVER_IP -w 'bytes=%{size_download}\n' https://cloud.example.com/photo.jpg
# 3. Connection count: 50 parallel TLS sessions
curl -sk -I --parallel --parallel-max 50 -m 10 -o /dev/null -w '%{http_code}\n' --resolve cloud.example.com:443:SERVER_IP "https://cloud.example.com/?[1-50]"
  • TCP never connects, while from abroad everything works — the address is banned outright; only a new IP helps.
  • TCP connects, the handshake hangs. Wait a couple of minutes and repeat the first command with a different name pointed at the same IP, such as your apex domain example.com. If that hangs too, the address is banned; if an answer arrives, even an error, your SNI is blocked: set up a new subdomain with a certificate and change the SNI in the inbound and on the clients.
  • 14–25 KB arrive, then silence — a volume freeze.
  • The first responses come, then timeouts — a connection limit. The test itself will trigger a block for a couple of minutes.
Important: ping and open-port checks (nc -z, Test-NetConnection) prove nothing: with some bans ICMP and the TCP handshake pass while data doesn't. Don't test many addresses in parallel, or you'll get a false "ban". Online checkers run their nodes in data centres, where filtering differs from home and mobile networks.

Stalls after a few kilobytes

Since June 2025, TSPU at many Russian ISPs freezes TCP connections to the subnets of large hosters and CDNs: Hetzner, OVH, DigitalOcean, AWS, Oracle, Contabo, Scaleway, Cloudflare and others. The first 14–25 KB get through, then data stops without an RST, on every connection: the VPN "connects", but pages hang. It varies by ISP and region; in summer 2026 some operators lifted the restriction.

Only SNIs on a whitelist get through — about a thousand popular domains — so your own domain won't pass on a restricted hoster. That leaves two routes:

  • A hoster that isn't restricted — then Reality on your own site works. The hyperion-cs "TCP 16-20" web checker shows whether your ISP throttles connections to your server: put your domain into its host parameter.
  • An intermediate server inside Russia — rules for traffic leaving Russian data centres are softer. But after the ban wave of 21 September 2026, banned addresses aren't reachable from there either.

What doesn't help: XHTTP, mux and gRPC (the response still flows over one long connection), fragmentation or pauses. The classic fallback — XHTTP through Cloudflare — works poorly in Russia too, since Cloudflare's own subnets are restricted.

Freezes on connection bursts

Since November 2025, at some ISPs — in 2026 mostly the MegaFon and Yota mobile networks — a series of about a dozen TLS handshakes with one SNI to one server freezes new connections for a couple of minutes. According to the hyperion-cs analysis (June 2026), more than three near-simultaneous attempts are enough when the fingerprint is Chrome, Safari or iOS and the address is in a "suspicious" subnet; port and TLS version don't matter.

Vision is vulnerable by design: it opens a separate TLS connection for every application connection, and a browser easily produces a dozen handshakes per second. What helps:

  • XHTTP — it packs application connections into a few TLS connections. Since Xray v26.7.28 the client keeps at most three by default: the value was lowered precisely because of TSPU.
  • The firefox fingerprint instead of chrome, in a client on a recent Xray core (26.3.27 or newer) — in June 2026 this switch restored service.

For XHTTP, create a second inbound: VLESS, transport XHTTP, security Reality, the same target 127.0.0.1:8443 and the same SNI. In the XHTTP settings set only the path, for example /api/v2/c7f3, and leave the mode on auto: with Reality the client picks stream-one, which reportedly works well on mobile networks too. Don't enable XMUX in the panel; Flow isn't used with XHTTP. Two inbounds can't share a port, so either move 443 to XHTTP or give the backup inbound another port.

Important: 3x-ui 3.0 through 3.3.0 injected scMaxEachPostBytes: 1000000 and scMinPostsIntervalMs: 30 into XHTTP configs and links, and on mobile networks TSPU recognised the traffic by these values. If your XHTTP inbound was created in those versions, remove both parameters from the inbound JSON and reissue the links.

IP address bans

VPN server addresses get banned in waves — for example, on 4 August and 21 September 2026. ValdikSS distinguishes two kinds of ban. In the first, neither TCP, UDP nor ping gets through from Russian home networks, at every ISP at once. In the second, the TCP handshake passes but silence follows the first data packet, and only at some ISPs. Only a new address helps.

ForestsNet Cloud checks its addresses from Russia every day over TCP and TLS and withdraws blocked ones from allocation, so a new server gets an address that was reachable at check time. A ban can still come later, and the check covers neither your particular mix of target, volume and connection count nor censors outside Russia. If the tests above show the address itself is banned, contact support and attach their output.

Mobile whitelists in Russia

In this mode the operator lets through only approved IP ranges, and a foreign VPS isn't directly reachable over any protocol. The workaround is an entry server in a Russian cloud with a whitelisted address — a separate setup.

Troubleshooting

  • The client is "connected", but sites don't open. The client log shows received real certificate or processed invalid connection: the server didn't recognise the client and handed it the target's certificate. Compare the public key, Short ID and SNI with the panel, update the app, set the fingerprint to chrome.
  • Some clients broke after updating to 3x-ui 3.8. The X25519MLKEM768 requirement — see step 6. On panels 3.5–3.7 a similar refusal of third-party clients comes from the 26.3.27 minimum version in the 26.7.x core.
  • Nothing works, neither from inside the censored country nor from abroad. Xray didn't start: port 443 is taken or the target lacks a port.
  • The handshake hangs from inside the censored network. An address ban, an SNI block or a burst freeze — tell them apart with the tests.
  • The first kilobytes arrive, then silence. A freeze on the hoster's subnet: another hoster or a relay through a server inside Russia.
  • Heavy pages freeze everything for a couple of minutes. A connection limit: XHTTP, the firefox fingerprint.
  • All clients dropped at once, and the site doesn't open by its domain. Reality contacts the target on every connection, so without nginx even your own clients can't get through. Check systemctl status nginx and certificate renewal: certbot renew --dry-run.
  • The panel doesn't open. It listens on 127.0.0.1 — use the SSH tunnel.
  • A client's traffic in the panel grows in jumps. Normal for Vision: data goes through splice in the Linux kernel, and Xray counts it when the connection closes.
  • An app that needs QUIC doesn't work. Vision intercepts UDP to port 443 so that browsers fall back to TCP. For such a client, choose the xtls-rprx-vision-udp443 flow.

What's next

Multi-server panels: Marzban and Remnawave. A protocol comparison is in Self-hosted VPN: which protocol to choose; for a backup channel, see Amnezia VPN or Hysteria2.

Sources