Introduction

So you want a VPN that doesn’t scream “I am a VPN” to every censor and firewall out there?
Welcome to the world of Trojan over WebSocket + TLS behind Cloudflare.

This guide not only shows you how to set it up but also sprinkles in some debugging magic so you can figure out why things break (and they will break, trust me).

We’ll anonymise domains and secrets, so substitute with your own:

  • VPN domain: web.example.com
  • Panel domain: panel.example.com
  • Secret WS path: /stealth-path_abcd1234
  • Password: <PASSWORD>

Architecture at a Glance

Think of it as a disguise party:

  • Trojan = the shy guest (your VPN protocol)
  • Nginx = the bouncer checking IDs (reverse proxy)
  • Cloudflare = the doorman who makes sure nobody sees who’s inside (CDN & proxy)
  • Your fake website = the mask (camouflage page)

Traffic flow:

Client → Cloudflare (443) → Nginx (443) → Trojan (localhost:54321)

Step 1: Panel Setup (panel.example.com)

  • Bind the 3x-ui panel to localhost (e.g., 127.0.0.1:46309).
  • Choose a funky web base path like /panel-bananas_42/.
  • Proxy it through Nginx with HTTPS + Basic Auth.
  • Test it at https://panel.example.com/panel-bananas_42/.

Pro tip: If you see a blank page → your base path is mismatched or Nginx is eating it. Check logs!


Step 2: Trojan Inbound

In 3x-ui, create a Trojan inbound:

  • Local port: 54321
  • Transport: WebSocket
  • Path: /stealth-path_abcd1234
  • Security: none
  • Password: <PASSWORD>

Step 3: Nginx for VPN Domain (web.example.com)

Your Nginx is the gatekeeper. Sample config:

server {
    listen 443 ssl http2;
    server_name web.example.com;

    ssl_certificate     /etc/letsencrypt/live/web.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/web.example.com/privkey.pem;

    # Fake website at root
    location / {
        root /var/www/html;
        index index.html;
    }

    # Real VPN under secret WS path
    location /stealth-path_abcd1234 {
        proxy_pass http://127.0.0.1:54321;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }
}

Step 4: Firewall Rules

Lock things down! Only Cloudflare should reach you:

ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw deny 54321/tcp

Step 5: Client Config

Trojan URI:

trojan://<PASSWORD>@web.example.com:443?type=ws&security=tls&host=web.example.com&path=%2Fstealth-path_abcd1234&sni=web.example.com#MyStealthVPN

iOS clients: Shadowrocket, Stash, FoXray
Android clients: v2rayNG, Clash Meta, NekoBox


Debugging Time (a.k.a. “Why the heck doesn’t it work?!”)

Symptom: 520 Unknown Error (Cloudflare)

  • Likely cause: Nginx couldn’t talk to Trojan.
  • Check Nginx error log:
    tail -n 50 /var/log/nginx/error.log
    
  • If you see upstream sent no valid HTTP/1.0 header → you’re mixing HTTPS/HTTP between Nginx and Trojan.

Symptom: 526 Invalid SSL certificate

  • Cloudflare → Nginx cert mismatch.
  • Run:
    openssl s_client -connect web.example.com:443 -servername web.example.com -showcerts
    
  • Make sure CN = web.example.com. If not, fix your cert.

Symptom: Blank page on panel

  • Check if the base path matches exactly (case-sensitive, slash-sensitive).
  • Watch for sneaky trailing spaces!
  • Test locally:
    curl -s -D- http://127.0.0.1:46309/panel-bananas_42/
    

Symptom: Client won’t connect

  • Run a WebSocket test:
    curl -i -k -H "Connection: Upgrade" -H "Upgrade: websocket" https://web.example.com/stealth-path_abcd1234
    
    Expect 101 Switching Protocols. If not, check Nginx config.

Symptom: Censor still blocks you

  • Did you expose another service (like SSH, Matrix, or Minecraft) on the same IP?
    → They’ll find your origin IP. Use a second VPS or lock those ports down.
  • Did you use an obvious path like /ws?
    → Use a random-looking one like /cdn-assets-329df/.

Pro Tips & Fun Tricks

  • Serve a fake blog or portfolio at root so your domain looks legit.
  • Rotate WebSocket paths occasionally.
  • Use Cloudflare Page Rules to disable Rocket Loader & Minify for your VPN domain.
  • Make friends with your Nginx error.log — it will roast you but it tells the truth.

Conclusion

By putting Trojan behind Cloudflare, you’ve given your VPN a shiny new disguise:

  • Looks like normal HTTPS.
  • Hides your origin IP.
  • Forces censors into a tough choice: block Cloudflare (and half the web) or let you pass.

Congrats — you’ve built a VPN with both style and stealth. 🥷


Downloads: Markdown · Signature (.asc)

View OpenPGP signature
-----BEGIN PGP SIGNATURE-----

iQIzBAABCgAdFiEEVaKl3oR5K6zGyu4/tYgoUOBMjSoFAmpkbuAACgkQtYgoUOBM
jSo4Yw//T40cakj/BOL/Zh0gxoW4PnH014B7h67Yirq6pB3epUix6bFaZCJnndbD
9ZIhmnWMRzbkcA3SVhW1Y3NLpHvhK5UMXNY1qGqrGATQcoVCP7EewhL/3vXgTo5l
jFsQFzNxcgOXKKWevuRtL5MY8RuC+PbsfG2ry2jiOtJa9H2UixVJ5E8Imj+VS47O
S3XAlxr85O9CEh/TFkS/TuY5P5+VPoOLn6WfdCH5W2IdkW+Vi3bZFJ46LBm6cNBh
Iydo+r2msTrqOozimc9hVorPjHZVZLMADJhcsa4pSZ4pDShBYMOZWATQErROPsdl
5iyRbmdFb4zWcG5SgjngHnRHFrJ8PVgnFXObb3yZMqA+38RGmRNFgtR0mhMdtKNt
QxQDOO/IfO/oNfZzIERUqUnUy/iXs44v8ttTXXE6SkYYoHW335xmDuk8ntrmQA6g
YfXO4rJCXxsMaT6RkJaoK5YirKF/9hJYaUYY62SzdfhTmY1TFGGK0+CD+M1kQILC
E8pXSfGhaG2bFCzQ+BRMe4o1BXLNjUhvovUgWEBnYTdGF5oXNS1MM29WxD/HC3md
d17noEPwOujrtLxEQcAOUcQe02VOfYcX36pbr+ql32hsQMQHZtho1nx5HEGCoCWG
3sO7WQTpdBDtkwdHnRJqKBcuWqrVd3YNMwtZE0S6oXVyWkEbB4Y=
=IovZ
-----END PGP SIGNATURE-----

Verify locally:

curl -fSLO https://blog.alipour.eu/sources/posts/vpn.md
curl -fSLO https://blog.alipour.eu/sources/posts/vpn.md.asc
gpg --verify vpn.md.asc vpn.md