This is a record of lawful network experiments on a server you own, for learning and reference only. Please comply with the laws of your jurisdiction.
The previous post, VLESS, REALITY and CDN, covered the theory. This one gets hands-on: on a single VPS, we’ll bring up both REALITY (direct, no domain needed) and WS + CDN (hides the origin, needs a domain). Both run on Xray, and the core difference is only in streamSettings.
Prerequisites
- An overseas VPS (Debian / Ubuntu here) with root access;
- Port 443 opened in the firewall / security group;
- The WS + CDN option also needs a domain and a Cloudflare account.
First install Xray with the official script:
bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install
The config file lives at /usr/local/etc/xray/config.json; after editing, restart with systemctl restart xray.
Option 1: VLESS + REALITY (direct, no domain needed)
REALITY needs no domain or certificate — it’s the simplest, and a good fit for a fresh machine whose IP hasn’t been blocked yet.
Step 1: Generate the keys and IDs
xray uuid # generate a UUID (client identity)
xray x25519 # generate a key pair: Private key / Public key
openssl rand -hex 8 # generate a shortId
Note down: UUID, Private key (for the server), Public key (for the client), and shortId.
Step 2: Server config
{
"inbounds": [
{
"listen": "0.0.0.0",
"port": 443,
"protocol": "vless",
"settings": {
"clients": [
{ "id": "<UUID>", "flow": "xtls-rprx-vision" }
],
"decryption": "none"
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"dest": "www.microsoft.com:443",
"serverNames": ["www.microsoft.com"],
"privateKey": "<Private key>",
"shortIds": ["<shortId>"]
}
}
}
],
"outbounds": [{ "protocol": "freedom" }]
}
What the key parameters mean:
decryption: none: VLESS doesn’t encrypt on its own, so this is alwaysnone(encryption is handled by REALITY/TLS);flow: xtls-rprx-vision: XTLS Vision flow control, which avoids the double-encryption overhead of “TLS inside TLS”; for REALITY / direct TLS only;dest/serverNames: the real website to “borrow”; the two must match. Pick a major site that actually exists, supports TLS 1.3, and isn’t blocked locally as the “front”;privateKey: the private half of the REALITY key pair, kept on the server; the matching Public key goes to the client;shortIds: client identifiers — leave empty""or use the value generated above; the server can hold several to distinguish clients.
Step 3: Key client parameters
| Parameter | Value | Purpose |
|---|---|---|
| Address / Port | the VPS IP / 443 | direct to the server, no domain involved |
| UUID | generated above | client identity, must match the server |
| flow | xtls-rprx-vision | the Vision flow control matching the server |
| security | reality | enable REALITY |
| SNI | www.microsoft.com | the “front” site impersonated in the handshake; must equal the server’s serverNames |
| publicKey (pbk) | the Public key from above | pairs with the server’s private key to authenticate |
| shortId (sid) | generated above | must be one of the server’s shortIds |
| fingerprint (fp) | chrome | the simulated TLS fingerprint, making the handshake look like Chrome |
Import via a share link
Rather than filling everything in by hand, it’s more common to import a single vless:// link (v2rayN, v2rayNG, NekoBox, etc. all support “import from clipboard”). The link format for a REALITY node:
vless://<UUID>@<VPS-IP>:443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=www.microsoft.com&fp=chrome&pbk=<Public key>&sid=<shortId>&type=tcp#REALITY
Each parameter after ? maps to a row in the table above, and the text after # is the node label. Restart with systemctl restart xray, import it, and you’re connected.
Option 2: VLESS + WS + CDN (hides the origin, needs a domain)
When the machine’s IP is prone to being blocked, use Cloudflare to hide the origin.
This option adds one component over REALITY: Nginx. It’s the most widely used web server / reverse proxy, and here it does two jobs:
- Serves HTTPS with a certificate on port 443 (the CDN requires the origin to be valid HTTPS, while Xray’s WS is just a bare WebSocket);
- Reverse-proxies only the agreed secret path (e.g.
/mypath) to the local Xray, and returns an ordinary web page on every other path to disguise the server as a normal website.
Nginx needs you to provide the certificate yourself. But since we’re already behind Cloudflare, the easiest route is Cloudflare’s free Origin Certificate — issued by Cloudflare, valid for up to 15 years, and only needs to be trusted by the CDN.
Step 1: Cloudflare DNS + Origin Certificate
- Point the domain’s A record at the VPS IP and enable the orange cloud (proxied);
- Set the SSL/TLS mode to Full (strict);
- Under SSL/TLS → Origin Server → Create Certificate, generate a certificate and save the cert and key to
/etc/ssl/cf-origin.pemand/etc/ssl/cf-origin.key.
If you’d rather not use the Origin Certificate, you can issue a Let’s Encrypt certificate with
acme.shvia the Cloudflare DNS API; the Nginx config is the same afterward.
Step 2: Xray listening on a local WS
{
"inbounds": [
{
"listen": "127.0.0.1",
"port": 10000,
"protocol": "vless",
"settings": {
"clients": [{ "id": "<UUID>" }],
"decryption": "none"
},
"streamSettings": {
"network": "ws",
"wsSettings": { "path": "/mypath" }
}
}
],
"outbounds": [{ "protocol": "freedom" }]
}
Note the WS option needs no flow (Vision is only for REALITY / direct TLS).
Step 3: Nginx HTTPS + reverse proxy
/etc/nginx/conf.d/proxy.conf:
server {
listen 443 ssl;
http2 on;
server_name your.domain.com;
ssl_certificate /etc/ssl/cf-origin.pem;
ssl_certificate_key /etc/ssl/cf-origin.key;
# reverse-proxy the secret path to the local Xray
location /mypath {
proxy_pass http://127.0.0.1:10000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # WebSocket upgrade
proxy_set_header Connection "upgrade"; # WebSocket upgrade
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# everything else returns a plain page as disguise
location / {
return 200 "hello";
}
}
The easiest thing to miss here is the Upgrade / Connection "upgrade" headers — without them the WebSocket never completes its handshake; Nginx requires you to add them explicitly when proxying WebSocket. Run nginx -t to check the syntax, then systemctl restart nginx.
Step 4: Key client parameters
| Parameter | Value | Purpose |
|---|---|---|
| Address | the domain (or a Cloudflare preferred IP) | you connect to a Cloudflare node, not the origin |
| Port | 443 | the CDN’s public HTTPS port |
| UUID | matches the server | client identity |
| security | tls, SNI = the domain | standard HTTPS between you and the CDN |
| network | ws, path = /mypath | WebSocket transport; the path must match the server and Nginx exactly |
| host | the domain | the WS Host header; required when using a preferred IP, since the CDN uses it to decide which site to route to |
Import via a share link
The vless:// link format for a WS + CDN node:
vless://<UUID>@<domain-or-preferred-IP>:443?encryption=none&security=tls&sni=<domain>&type=ws&host=<domain>&path=%2Fmypath#WS-CDN
Note that path must be URL-encoded in the link: /mypath becomes %2Fmypath. The WS option carries no flow (Vision is only for REALITY / direct TLS).
Verifying and troubleshooting
systemctl status xray nginx # are both services running?
journalctl -u xray -e # check Xray error logs
nginx -t # check Nginx config syntax
ss -tlnp | grep -E '443|10000' # are the ports listening?
Common pitfalls:
- REALITY won’t connect: usually the client’s
publicKey/shortId/SNIdon’t match the server. - WS + CDN 502 / handshake failure: check that Nginx has the
Upgrade/Connectionheaders, that thepathmatches Xray exactly, and that Cloudflare is proxied (orange cloud) with SSL mode Full (strict). - Connects but no traffic: confirm both the VPS firewall and the cloud provider’s security group allow 443.
Summary
- The Xray config differs between the two options almost only in
streamSettings: REALITY usestcp + reality, CDN useswswrapped in a layer of Nginx/CDN TLS; - New machine, want speed → start with REALITY;
- IP being targeted, want to hide the origin → switch to WS + CDN;
- It’s worth setting up both and switching as the network conditions demand — which lines up exactly with the choice from the previous post.