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 always none (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

ParameterValuePurpose
Address / Portthe VPS IP / 443direct to the server, no domain involved
UUIDgenerated aboveclient identity, must match the server
flowxtls-rprx-visionthe Vision flow control matching the server
securityrealityenable REALITY
SNIwww.microsoft.comthe “front” site impersonated in the handshake; must equal the server’s serverNames
publicKey (pbk)the Public key from abovepairs with the server’s private key to authenticate
shortId (sid)generated abovemust be one of the server’s shortIds
fingerprint (fp)chromethe 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:

  1. 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);
  2. 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.pem and /etc/ssl/cf-origin.key.

If you’d rather not use the Origin Certificate, you can issue a Let’s Encrypt certificate with acme.sh via 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

ParameterValuePurpose
Addressthe domain (or a Cloudflare preferred IP)you connect to a Cloudflare node, not the origin
Port443the CDN’s public HTTPS port
UUIDmatches the serverclient identity
securitytls, SNI = the domainstandard HTTPS between you and the CDN
networkws, path = /mypathWebSocket transport; the path must match the server and Nginx exactly
hostthe domainthe 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 / SNI don’t match the server.
  • WS + CDN 502 / handshake failure: check that Nginx has the Upgrade / Connection headers, that the path matches 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 uses tcp + reality, CDN uses ws wrapped 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.