skip to content

Setting up a personal VLESS VPN on GCP with XTLS-Reality

/ 18 min read

Why bother with VLESS when WireGuard and OpenVPN already exist? A few reasons.

The EU is moving to restrict VPN use, and the proposals on the table target exactly the kind of traffic WireGuard and OpenVPN produce — protocols with distinctive network signatures that DPI boxes can fingerprint in milliseconds. Whatever you think of the policy, the practical implication is that the VPN app on your phone right now has a shelf life.

And the bigger trend behind it: every government wants to be China. China is the one country that actually pulled off building its own internet, and every regulator that watched it happen took notes. By 2030 the “global internet” will be sharded into a handful of loosely connected regional nets, each with its own rules about what’s allowed to cross the border.

China started, Russia followed, the EU is catching up. The US is heading in that direction too, though with a twist: they’re making platforms liable for what users do via VPN. The EU is taking notes.

If you want access to all of them from wherever you happen to be, you need a protocol that censors can’t fingerprint — and the only one that’s consistently survived contact with the Great Firewall is VLESS over XTLS-Reality.

So: a real-world walkthrough of running your own censorship-resistant proxy on Google Cloud — including all the gotchas I hit (and the Claude tokens I burned through figuring them out) so you don’t have to.

What you’ll build

  • A GCP VM running the 3X-UI panel with an XTLS-Reality inbound on port 443
  • A WARP outbound for routing specific traffic through CloudFlare instead of your server IP
  • A working vless:// URL you can paste into any modern client

Theory: what are VLESS, XTLS-Reality, Xray, and 3X-UI?

Before the setup, a quick map of the landscape. If you’ve used mainstream VPN tools like Outline, AmneziaVPN, or a commercial WireGuard client, some of this is new.

Why not just use WireGuard or OpenVPN?

WireGuard and OpenVPN are great protocols — fast, well-audited, easy to set up. But they were designed for corporate remote access, not censorship circumvention. They have distinctive network signatures (packet sizes, handshake patterns, port usage) that deep packet inspection (DPI) systems can identify and block. Countries that actively censor the internet have been blocking these protocols for years.

Shadowsocks tried to fix this by encrypting traffic so it looks random. That worked for a while until DPI systems got smart enough to detect “random-looking” traffic as suspicious by itself. If your traffic doesn’t look like anything recognizable, it looks like a proxy.

The modern approach: pretend to be HTTPS

The current generation of censorship-resistant protocols takes a different approach: don’t hide that you’re doing TLS, hide that you’re doing a proxy. HTTPS to a major website is the most common traffic on the internet. If your proxy traffic is indistinguishable from HTTPS to Google, there’s nothing for DPI to flag.

This is what VLESS does. It’s a minimal proxy protocol (successor to VMess) designed to run inside a real TLS connection to what looks like a real website. The protocol itself is thin and fast — the security comes from the TLS layer around it.

XTLS-Reality: the clever part

Normal VLESS-over-TLS has a subtle problem: when you encrypt already-encrypted traffic (like HTTPS pages), you get a detectable “TLS-in-TLS” signature. DPI systems can spot this.

XTLS-Reality solves both problems at once:

  1. No TLS-in-TLS: XTLS detects when the inner traffic is already TLS 1.3 encrypted and skips its own encryption layer.
  2. Real certificate from a real site: Reality proxies the TLS handshake to an actual third-party website (like www.microsoft.com). The client sees that real site’s real certificate. If a censor connects to your server to probe, they get forwarded to the real site and see a legitimate website. There’s no fake certificate to detect.

So an observer looking at your traffic sees what appears to be a normal HTTPS connection to Microsoft. Your server IP looks like it’s hosting a Microsoft website. There’s no protocol fingerprint, no weird certificate, no detectable proxy pattern.

As of 2026, Reality is not blocked anywhere by DPI alone. Blocks happen only when operators break the rules. One of the big ones is sharing too widely.

Why sharing breaks Reality

The protocol itself stays invisible, but traffic patterns don’t. A single user’s proxy looks like one person browsing one website — unremarkable. Ten of your friends sharing the same server looks similar. A hundred people looks like a business with employees.

But a thousand strangers connecting to what’s supposedly a static Microsoft marketing page is a giant red flag:

  • Real websites have a bell curve of traffic across geographies and times. A proxy has concentrated traffic from one country at predictable hours.
  • Real HTTPS sessions are short and bursty (page load, done). Proxy sessions are long and steady (minutes to hours of streaming).
  • Real visitors to www.microsoft.com don’t do much. Proxy users transfer gigabytes.
  • TLS connection counts and byte volumes to a single IP skew way outside normal website statistics.

DPI systems don’t need to decrypt the traffic to notice any of this. Researchers have published papers on detecting proxy servers purely from connection-metadata anomalies — exactly what operators in China do to identify and block servers before they get widely used.

So the practical rule: keep it under ~10 people who are geographically dispersed. Your traffic stays in the noise floor of normal internet activity. Once you start listing your server in public Telegram channels or selling access, you’ve turned it into a detectable anomaly — the protocol can’t save you from that.

Xray vs 3X-UI

Xray-core is the actual proxy server software — a fork of the original V2Ray project. It implements VLESS, VMess, Trojan, Shadowsocks, XTLS, Reality, and a dozen other protocols. It’s a single Go binary configured by a JSON file. Powerful but fiddly to configure by hand.

3X-UI is a web panel that generates Xray configurations for you. You click buttons in a UI, it writes the Xray JSON, restarts the Xray process, and shows you traffic stats. It’s a fork of the original X-UI project with more features and better maintenance.

When you see “inbound” and “outbound” in 3X-UI, those are Xray terms:

  • Inbound: a port/protocol your server listens on for clients
  • Outbound: where your server sends traffic after receiving it (direct to the internet, through WARP, blocked, etc.)

Where WARP fits in

CloudFlare WARP is a free VPN service by CloudFlare. On a server, you can run it as a local SOCKS/WireGuard proxy. Your Xray config uses it as one of its possible outbounds.

Why would you send traffic through WARP instead of directly out your server?

  • Some services block or rate-limit datacenter IPs. Going through WARP makes you look like a residential CloudFlare user.
  • You want to avoid your VPS IP appearing in access logs of specific destinations.
  • You want to keep your VPS IP “clean” — only used for the Reality masquerade, never making outbound requests that could link it to suspicious activity.

WARP is optional. You can skip it and route all traffic directly. But it’s easy to set up and useful, so this guide includes it.

Prerequisites

  • A Google Cloud Platform account
  • A credit card (GCP’s free tier covers part of it)
  • ~30 minutes

Part 1 — Create the GCP VM

  1. Go to Google Cloud Console → Compute Engine → Create Instance
  2. Configuration:
    • Name: proxy-server (or whatever)
    • Region: anywhere outside your country
    • Machine type: e2-small (~$13/month) or e2-micro for free tier
    • Boot disk: Debian 12, 10 GB SSD
    • Firewall: check both Allow HTTP and Allow HTTPS
  3. Click Create
  4. Note the External IP — you’ll use it everywhere
  5. Optional: reserve a static IP (Networking → IP Addresses) so it doesn’t change on restart

Open the firewall ports

GCP has its own firewall separate from the OS. Go to VPC Network → Firewall → Create Firewall Rule:

  • Name: allow-proxy-ports
  • Direction: Ingress
  • Targets: All instances in the network
  • Source IP ranges: 0.0.0.0/0
  • Protocols and ports: TCP443, 11223

(Replace 11223 with whatever random port you pick for the 3X-UI panel.)

Part 2 — Install 3X-UI

SSH into the VM (GCP’s browser SSH button works fine). Then:

Terminal window
sudo apt update && sudo apt full-upgrade -y
sudo reboot

Reconnect after ~30 seconds, then:

Terminal window
sudo apt install docker.io docker-compose git curl bash openssl nano -y

Clone 3X-UI and start it:

Terminal window
git clone https://github.com/MHSanaei/3x-ui.git
cd 3x-ui
sudo docker-compose up -d

This pulls the latest image tag. As of this writing that’s 3X-UI v2.9.x bundled with Xray-core v26.4.x.

A word on version pinning

The docker-compose.yml references ghcr.io/mhsanaei/3x-ui:latest. Pinning to an older image tag (e.g. v2.0.2) was the recommended fix in earlier guides because the current Xray ships with a few behaviours that break compatibility with popular client apps:

  • Post-quantum TLS (X25519MLKEM768) in the Reality target — current client apps (V2Box, Hiddify, Streisand, FoxRay, Nekobox) all fail the handshake with tls: unknown certificate. The server sees repeated connection attempts that never complete.
  • Vision Seed defaults (900, 500, 900, 256) that some clients stumble on
  • mldsa65 post-quantum signature fields that, if populated, produce URLs like vless://...encryption=mlkem768x25519plus.native.0rtt... which most client apps refuse to parse

Rather than pinning to an old version and missing bug fixes, you can stay on latest and avoid these features in the inbound configuration. The Gotcha callouts in Part 7 show exactly what to leave empty. This is the approach this guide uses.

If you’d rather downgrade, change the image tag in docker-compose.yml before running docker-compose up -d:

image: ghcr.io/mhsanaei/3x-ui:v2.0.2

Just be aware you’ll miss security fixes and later client compatibility improvements.

Verify it’s running:

Terminal window
sudo docker ps

Note the container name (likely 3xui_app or 3x-ui-3x-ui-1).

Gotcha: sudo bash <(curl ...) doesn’t work

You’ll see this pattern in many guides. It breaks under sudo because process substitution doesn’t survive privilege escalation. Download first, then run:

Terminal window
curl -sSL https://example.com/some-script.sh -o /tmp/script.sh
sudo bash /tmp/script.sh

Part 3 — Install WARP

The standard installer:

Terminal window
curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh -o /tmp/install_warp.sh
sudo bash /tmp/install_warp.sh

Enter 40000 when prompted for a port.

Note: guides often say to run warp u first to uninstall an existing WARP. Fresh GCP VMs don’t have WARP, so that command fails with “command not found”. Ignore it.

Part 4 — Generate the panel TLS certificate

The 3X-UI panel needs a certificate for HTTPS. Self-signed is fine for personal use.

Terminal window
cd ~
openssl req -x509 -newkey rsa:4096 -nodes -sha256 -keyout private.key -out public.key -days 3650 -subj "/CN=APP"

Copy into the container:

Terminal window
sudo docker cp private.key 3xui_app:/private.key
sudo docker cp public.key 3xui_app:/public.key

Gotcha: wrong container name

If you get No such container: 3xui_app:, your container has a different name. Find it:

Terminal window
sudo docker ps --format "{{.Names}}"

Use that name in the docker cp commands.

Part 5 — Configure the panel

Open in browser: http://YOUR_IP:2053/ (HTTP, not HTTPS yet).

Login with admin / admin.

Go to Panel Settings:

  • Panel Port: pick a random number like 11223 (not 40000 — that’s WARP’s port)
  • Panel Certificate Public Key Path: /public.key
  • Panel Certificate Private Key Path: /private.key
  • Panel URL Root Path: /somerandomsecret/ (make up your own, must start and end with /)

Save → Restart Panel.

Reconnect at: https://YOUR_IP:11223/somerandomsecret/ (now HTTPS, with your custom port and path). Accept the self-signed cert warning.

Go to Security Settings and change the admin password.

Gotcha: 404 after saving settings

After saving, the panel URL changes completely. If you get 404:

  • Check you’re using https (not http)

  • Check the new port — and watch this closely, because it can shift between restarts. If you set a port but the panel restart fails (bad cert path, port conflict, etc.), 3X-UI silently falls back to another port. Always confirm the actual listening port before troubleshooting anything else:

    Terminal window
    sudo docker logs 3xui_app 2>&1 | grep "Web server running"

    That tells you the port Xray/x-ui is actually serving on right now, which may not match what you typed in the UI.

  • Include the secret path with trailing slash

  • Confirm your cloud provider’s firewall allows TCP traffic on that exact port. Some providers (GCP, AWS, Oracle Cloud) have a separate firewall layer outside the VM. If you changed the panel port from 2053 to 11223, you need to open 11223 in the provider firewall too — and opening “HTTPS” in the provider console usually only opens 443, not your custom port.

If you forgot what you set, recover it from the database:

Terminal window
sudo docker exec 3xui_app grep -a "webBasePath" /etc/x-ui/x-ui.db

Or reset to defaults:

Terminal window
sudo docker exec 3xui_app /app/x-ui setting -reset
sudo docker exec 3xui_app /app/x-ui setting -username admin -password admin
sudo docker restart 3xui_app

Part 6 — Configure the WARP outbound

Gotcha: WARP IPv6 on GCP

GCP VMs don’t have IPv6 by default. The WARP installer sets up an IPv6 endpoint that will never work, flooding your logs with errors like:

Failed to send handshake initiation: write udp [::]:43058->[2606:4700:d0::a29f:c001]:2408: sendto: network is unreachable

In the panel → Xray Settings → Outbounds → click the WARP button at top. If no outbound exists yet, click Add Outbound in the WARP dialog.

Then edit the WARP outbound:

  • Address: remove IPv6, keep only 172.16.0.2/32
  • Endpoint: change engage.cloudflareclient.com:2408 to 162.159.193.10:2408 (forces IPv4)
  • Allowed IPs: remove ::/0, keep only 0.0.0.0/0

Save → Restart Xray. Test WARP with the lightning bolt icon — should pass.

Part 7 — Create the VLESS Reality inbound

Inbounds → Add Inbound:

FieldValue
Protocolvless
Listen IPLEAVE EMPTY
Port443
Client Emailany identifier
Flowxtls-rprx-vision (appears after selecting Reality)
TransmissionTCP (RAW)
SecurityReality
uTLSchrome
Targetwww.microsoft.com:443
SNI / Server Nameswww.microsoft.com
Public/Private Keyclick Get New Cert
mldsa65 Seed / VerifyLEAVE EMPTY — don’t click Get New Seed
Min/Max Client VerLEAVE EMPTY
Vision Seedclick Reset to clear defaults

Gotcha: bind: cannot assign requested address

If you set Listen IP to your external GCP IP, Xray will fail to bind because GCP uses NAT — the external IP isn’t actually on any network interface on the VM. Leave Listen IP empty so Xray binds to 0.0.0.0 (all interfaces).

Check logs and port:

Terminal window
sudo docker logs 3xui_app 2>&1 | tail -10
sudo ss -tlnp | grep 443

You should see xray-linux-amd6 listening on *:443.

Gotcha: post-quantum TLS breaks current clients

3X-UI v2.9.3+ ships with Xray 26+ which enables post-quantum TLS (X25519MLKEM768) by default. Current client apps (V2Box, Hiddify, Streisand, etc.) can’t handle it and fail with tls: unknown certificate.

Symptoms:

  • Client URL contains encryption=mlkem768x25519plus.native.0rtt — that’s the marker
  • Clients connect but fail during handshake
  • Server logs show TLS handshake error ... tls: unknown certificate

To avoid it:

  • Leave mldsa65 Seed and mldsa65 Verify empty (don’t click Get New Seed)

  • Leave Min/Max Client Ver empty

  • Pick a donor site that doesn’t negotiate post-quantum. Test with:

    Terminal window
    sudo docker exec 3xui_app /app/bin/xray-linux-amd64 tls ping www.microsoft.com

    If the output says TLS Post-Quantum key exchange: true, pick a different donor. Most Google properties now use post-quantum. Microsoft properties currently don’t.

Choosing a donor site

The “Target” and “SNI” fields decide which real website your server masquerades as. Requirements:

  • Supports TLS 1.3 and HTTP/2
  • Has a proper landing page (not a redirect)
  • Isn’t negotiating post-quantum TLS (see above)
  • Ideally hosted in the same cloud provider as your server, for latency

Safe choices: www.microsoft.com, www.asus.com, www.samsung.com, www.cisco.com, www.amd.com, www.nvidia.com.

Things to watch out for:

  • Redirects break everything. Country-localised subdomains like www.microsoft.co.uk or www.google.co.uk usually 301 to their main site, which breaks the Reality handshake. Always test with curl -I https://your-donor-site first — if you see a 301/302 response, pick a different site.
  • Some big sites actively block proxy misuse. Amazon (including www.amazon.com, www.amazon.co.uk) caught on to people using their domains as Reality donors and now behaves in ways that break the handshake. It worked a year ago, it doesn’t anymore. Same story will probably happen to other popular choices over time — if a site that worked stops working, try a different one.
  • Your own domains are a bad idea. The whole point is that a censor investigating the site can’t link it to you. Using yourname.com defeats that.
  • Small sites are fragile — if they change their TLS config or renew their cert, your proxy breaks with no warning.

Part 8 — Routing rules

⚠️ This part is not optional. If you skip it, your proxy will technically work but will be much easier to identify and block. Read on for why.

The “round trip” detection problem

Here’s the pattern censorship systems look for to find proxy servers without decrypting anything:

  1. Alice in country X browses a website hosted in country X
  2. Her request goes: X → your VPS (outside X) → back to country X
  3. The censor sees a foreign IP making lots of requests back into their country at residential-internet hours

That U-shape is a dead giveaway for a proxy. A real foreign tourist browsing the site wouldn’t do it at that volume and consistency. China’s Great Firewall already uses this signal to identify and block proxy servers before they ever get reported. Other countries will learn the trick.

The fix is: when the proxy needs to fetch a site hosted in your country, don’t let your VPS make the request directly. Route it through CloudFlare WARP, so the fetch looks like it originated from CloudFlare’s network instead of your VPS IP. The U-shape collapses. No round trip is visible.

The same logic applies to any site where having your VPS IP in the access logs would be bad — whether for pattern-matching reasons or because the destination actively flags datacenter IPs (Google, banks, some government sites).

The default routing rules

3X-UI starts you with three default rules. Don’t change these:

  1. inboundTag: apiapi (internal panel traffic)
  2. ip: geoip:privateblocked (private network addresses)
  3. protocol: bittorrentblocked (BitTorrent traffic — keep this to avoid complaints to your cloud provider)

Rules you should add

In Xray Settings → Routing Rules, add these two rules targeted at your country. The examples below use ru as the country code — replace with your actual country code (cn for China, ir for Iran, by for Belarus, etc.):

Rule 4 — route your country’s domains through WARP:

  • Domain: geosite:category-gov-ru,regexp:.*\.ru$
  • Outbound Tag: warp

Rule 5 — route your country’s IPs through WARP:

  • IP: geoip:ru
  • Outbound Tag: warp

Rule 4 catches traffic by domain name (anything ending in .ru, plus the curated list of .ru government sites). Rule 5 catches traffic by destination IP in case DNS doesn’t give a matching domain. You need both.

Optional additions if you also want to avoid datacenter-IP blocks on western services:

  • Domain: geosite:google,geosite:openaiwarp — some Google and OpenAI services limit or block datacenter IPs

Save Settings → Restart Xray.

Verifying the WARP routing works

SSH to your server and watch logs while you browse a site in your country from the client:

Terminal window
sudo docker logs -f 3xui_app 2>&1 | grep -i warp

You should see connections being dispatched via the warp outbound. If everything goes through direct, your rules aren’t matching — double-check the country code and rule positions.

Part 9 — Export and test

Export the Reality URL: Inbounds → three dots on your Reality inbound → Export All URLs.

Gotcha: auto-filled hostname in exported URL

If you access the panel via a domain name, the exported URL will use that domain — but Reality needs a direct IP connection. Manually replace the domain with the server’s IP in the URL:

vless://UUID@YOUR.SERVER.IP.HERE:443?type=tcp&encryption=none&security=reality&pbk=...

Client apps

Both apps below are free.

For macOS: Rabbithole VPN Client — App Store, modern UI, handles Reality well. My daily driver on Mac.

For iOS: Streisand — App Store, clean UI, good VLESS/Reality support. My daily driver on phone.

Other clients I tested on macOS that also work:

  • Command-line xray (brew install xray) — best for troubleshooting, shows detailed debug info
  • FoxRay — has TUN mode on macOS (Rabbithole/Streisand don’t)
  • V2Box — works once you clear post-quantum fields on the server
  • Hiddify — had issues parsing newer URL formats in my testing

Paste the vless:// URL into the app, connect, then verify at ipinfo.io — should show your GCP IP.

Debugging commands cheat sheet

Terminal window
# What ports are listening
sudo ss -tlnp
# Xray logs (follow mode)
sudo docker logs -f 3xui_app 2>&1 | tail -30
# Current Xray config
sudo docker exec 3xui_app cat /app/bin/config.json
# Check if donor site supports TLS 1.3 and post-quantum status
sudo docker exec 3xui_app /app/bin/xray-linux-amd64 tls ping www.microsoft.com
# Verify Reality key pair matches (regenerate public from private)
sudo docker exec 3xui_app /app/bin/xray-linux-amd64 x25519 -i "YOUR_PRIVATE_KEY"
# Restart container
sudo docker restart 3xui_app

Test with command-line xray when clients misbehave

When client apps fail mysteriously, test from the command line on your Mac to isolate server vs client issues.

Install xray:

Terminal window
brew install xray

Create ~/xray-test.json:

{
"log": { "loglevel": "debug" },
"inbounds": [
{
"port": 10808,
"listen": "127.0.0.1",
"protocol": "socks",
"settings": { "udp": true }
}
],
"outbounds": [
{
"protocol": "vless",
"settings": {
"vnext": [{
"address": "YOUR.SERVER.IP",
"port": 443,
"users": [{
"id": "YOUR_UUID",
"flow": "xtls-rprx-vision",
"encryption": "none"
}]
}]
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"fingerprint": "chrome",
"serverName": "www.microsoft.com",
"publicKey": "YOUR_PUBLIC_KEY",
"shortId": "YOUR_SHORT_ID",
"spiderX": "/"
}
}
}
]
}

Run:

Terminal window
xray run -c ~/xray-test.json

In another terminal:

Terminal window
curl -x socks5://127.0.0.1:10808 https://ipinfo.io/ip

Should return your GCP IP. If it does, the server works and any client app problems are on the client side.

Operational notes

  • GCP costs: e2-small is about $13/month. Set a billing alert. Traffic is 1 GB egress free per month, then ~$0.12/GB. Watch your usage.
  • Reverse DNS: GCP lets you set a PTR record on the VM page. Change it to match your donor site’s domain for better masquerading.
  • SSH port: move SSH off port 22 to something in the 40000+ range. Port 22 is a common DPI trigger.
  • Don’t install other VPN protocols on this VPS. WireGuard/OpenVPN on the same IP would ruin the Reality masquerade.
  • Don’t share widely. A small number of users is invisible. A hundred users routing through one IP becomes an anomaly.

Summary of gotchas

  1. Download script + sudo bash — never sudo bash <(...)
  2. Find the actual Docker container name before using docker cp
  3. Panel URL changes after saving — remember the port and path
  4. Remove IPv6 from WARP on GCP (no IPv6 on the VM)
  5. Leave Listen IP empty on inbounds — GCP external IP is NAT’d
  6. Post-quantum TLS breaks current clients — leave mldsa65 empty, pick donor sites carefully
  7. Exported URL uses the hostname you used to reach the panel — swap to the IP manually
  8. Check xray tls ping DONOR before picking a donor site

Credits

Based on articles by MiraclePTR and the “Personal proxy for dummies” author on Habr: