If you've ever had a VPN blocked by a corporate firewall, a hotel network, or a restrictive ISP, you already know the problem. Most VPN protocols are trivially identifiable — they have distinct handshakes, use unusual ports, or simply don't look like anything a browser would generate. Trojan solves this.
Unlike traditional VPN obfuscation that wraps traffic in something VPN-shaped, Trojan tunnels your connection over real TLS on port 443. To any firewall doing deep packet inspection, it looks identical to a browser opening an HTTPS connection. To an active probe — where someone actually connects to your server to investigate — it responds with a real website.
This guide walks through the complete setup: building Caddy with the Trojan plugin on your VPS, then connecting from Windows, Linux, macOS, and Android.
What We're Building
Your Device → Trojan client (TLS/443) → VPS (Caddy + Trojan plugin)
↓ correct password
Your traffic forwarded
↓ wrong password / probe
Real website servedCaddy handles TLS termination, automatic Let's Encrypt certificates, and Trojan authentication all in one process — no separate Trojan server binary needed.
Prerequisites
- A Linux VPS (Ubuntu 22.04 or Debian 12 recommended)
- A domain name pointed at your VPS — a real domain, not just an IP, because you need a valid TLS certificate
- Port 443 open on your VPS firewall
- Root access
Part 1 — Building Caddy With the Trojan Plugin on Your VPS
Caddy's Trojan support comes as a plugin, which means you need to compile a custom Caddy binary rather than installing the standard package. This sounds harder than it is — the xcaddy build tool handles everything.
Step 1 — Install Go
xcaddy requires Go 1.21 or later. The version in Ubuntu's default repos is often outdated, so install directly from the official source:
wget https://go.dev/dl/go1.22.0.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
source ~/.bashrc
go versionYou should see go version go1.22.0 linux/amd64 or similar.
Step 2 — Install xcaddy
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latestxcaddy will be placed in ~/go/bin/. Add that to your path:
echo 'export PATH=$PATH:~/go/bin' >> ~/.bashrc
source ~/.bashrcStep 3 — Build Caddy With the Trojan Plugin
xcaddy build --with github.com/imgk/caddy-trojan@latestThis downloads Caddy's source, pulls in the Trojan plugin, and compiles them together. It takes 1–2 minutes. When it finishes you'll have a caddy binary in your current directory.
Move it into place:
sudo mv caddy /usr/local/bin/caddy
sudo chmod +x /usr/local/bin/caddy
caddy versionStep 4 — Create a Fallback Web Page
The whole point of Trojan is that unauthenticated connections get a real website back. Create a minimal but convincing one:
sudo mkdir -p /var/www/fallback
sudo cat > /var/www/fallback/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head><title>Welcome</title></head>
<body>
<h1>Welcome</h1>
<p>This site is under construction.</p>
</body>
</html>
EOFYou can make this more elaborate — a real static site is more convincing than a placeholder.
Step 5 — Write the Caddyfile
Create /etc/caddy/Caddyfile:
sudo mkdir -p /etc/caddy
sudo nano /etc/caddy/CaddyfilePaste the following, replacing yourdomain.com and the password:
{
email test@example.com
# Make the trojan HTTP handler placeable in the chain
order trojan before file_server
# Enable the trojan listener wrapper on 443
servers :443 {
listener_wrappers {
trojan
}
}
# Global trojan app config (auth + fallback behavior)
trojan {
caddy
no_proxy
# passwords (space-separated)
users pass1 pass2
}
}
www.yourdomain.com {
# Your TLS cert for this host
tls test@example.com
# Trojan handler (allowed usage)
trojan {
connect_method
# add websocket if your client uses WS:
# websocket
}
# What "normal HTTPS" users/probes see
reverse_proxy 127.0.0.1:8080
}
:8080 {
root * /var/www/fallback
file_server
}Generate a strong password if you need one:
openssl rand -base64 32Step 6 — Set Up Caddy as a Systemd Service
Create the service file at /etc/systemd/system/caddy.service:
[Unit]
Description=Caddy with Trojan
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target
[Service]
Type=notify
User=root
ExecStart=/usr/local/bin/caddy run --config /etc/caddy/Caddyfile
ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile
TimeoutStopSec=5s
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_BIND_SERVICE
Restart=on-failure
[Install]
WantedBy=multi-user.targetEnable and start it:
systemctl daemon-reload
systemctl enable caddy
systemctl start caddy
systemctl status caddyWatch the logs to confirm Caddy has obtained your TLS certificate:
journalctl -u caddy -fYou should see a line confirming the certificate was issued. If it fails, make sure port 80 is open on your VPS firewall — Let's Encrypt needs it for the HTTP-01 challenge.
Step 7 — Verify the Fallback
Open a browser and navigate to https://yourdomain.com. You should see your fallback page served over HTTPS with a valid certificate. This confirms Caddy is running, TLS is working, and unauthenticated requests hit the fallback correctly.
Your server is ready. Now for the clients.
Part 2 — Connecting From Windows
The best Trojan client for Windows is v2rayN — it's actively maintained, has a clean system tray interface, and handles routing at the OS level.
Step 1 — Download v2rayN
Go to the v2rayN GitHub releases page and download v2rayN-With-Core.zip. This bundle includes the Xray core engine that handles Trojan, so no separate installs are needed.
Extract the ZIP to a permanent location — C:\v2rayN\ works well. Run v2rayN.exe. It will appear in your system tray (bottom right, near the clock) as a small V icon.
Step 2 — Add Your Server
Right-click the tray icon → Servers → Add [Trojan] server
Fill in the fields:
Field Value Address yourdomain.com Port 443 Password your password from the Caddyfile SNI yourdomain.com Allow Insecure OFF Alias Anything — e.g. "Home VPS"
Click OK.
Step 3 — Enable TUN Mode
System Proxy mode routes browser traffic. TUN mode routes everything at the OS level — all apps, all protocols. For most use cases TUN mode is what you want.
Right-click tray → TUN Mode → toggle on. When prompted to install a virtual network adapter, allow it.
If TUN mode fails to enable, right-click v2rayN.exe and choose Run as Administrator.
Step 4 — Test
Right-click tray → Server: Test Latency (all). A ping value should appear next to your server. Then open a browser and visit https://whatismyip.com — it should show your VPS IP, not your home IP.
Step 5 — Auto-Start on Boot (Optional)
Right-click tray → Settings → check Auto-run at startup.
Part 3 — Connecting From Linux
On Linux you can run the Xray core directly — it's a single binary with no GUI needed.
Step 1 — Install Xray
bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ installThis installs Xray to /usr/local/bin/xray and sets it up as a systemd service.
Step 2 — Configure Xray as a Trojan Client
Edit /usr/local/etc/xray/config.json:
{
"log": {
"loglevel": "warning"
},
"inbounds": [
{
"port": 1080,
"protocol": "socks",
"listen": "127.0.0.1",
"settings": {
"udp": true
}
},
{
"port": 1081,
"protocol": "http",
"listen": "127.0.0.1"
}
],
"outbounds": [
{
"protocol": "trojan",
"settings": {
"servers": [
{
"address": "yourdomain.com",
"port": 443,
"password": "your_very_strong_password_here"
}
]
},
"streamSettings": {
"network": "tcp",
"security": "tls",
"tlsSettings": {
"serverName": "yourdomain.com",
"allowInsecure": false
}
}
}
]
}Step 3 — Start Xray
systemctl enable xray
systemctl start xray
systemctl status xrayStep 4 — Route Traffic Through the Proxy
Xray creates a SOCKS5 proxy at 127.0.0.1:1080 and an HTTP proxy at 127.0.0.1:1081.
For system-wide routing, set your proxy in GNOME:
Settings → Network → Network Proxy → Manual
Set SOCKS Host to 127.0.0.1, port 1080.
For terminal-only routing, set the environment variables:
export https_proxy=http://127.0.0.1:1081
export http_proxy=http://127.0.0.1:1081For full OS-level routing (all traffic, including non-proxy-aware apps), use tun2socks:
apt install tun2socks -y
ip tuntap add mode tun dev tun0
ip addr add 198.18.0.1/15 dev tun0
ip link set dev tun0 up
tun2socks -device tun0 -proxy socks5://127.0.0.1:1080Then adjust your routing table to send traffic through tun0. This is more involved but routes literally everything.
Part 4 — Connecting From macOS
The cleanest macOS client is V2RayXS — a native macOS app that wraps Xray with a menu bar interface.
Step 1 — Download V2RayXS
Download the latest release from the V2RayXS GitHub releases page. Open the DMG and drag V2RayXS to your Applications folder.
On first launch, macOS may block it because it's not from the App Store. Go to System Settings → Privacy & Security and click Open Anyway.
Step 2 — Add Your Server
Click the V2RayXS icon in the menu bar → Servers → Add Server
Choose Trojan as the protocol and fill in:
Field Value Server Address yourdomain.com Port 443 Password your password SNI yourdomain.com Allow Insecure off
Save and select the server.
Step 3 — Set Proxy Mode
Menu bar icon → choose one of:
- PAC Mode — routes only traffic to sites on a list (good for selective use)
- Global Mode — routes all traffic through the tunnel
- TPROXY/TUN Mode — full OS-level routing, requires admin password
Global Mode is the most straightforward for full traffic routing.
Step 4 — Test
Visit https://whatismyip.com in your browser — you should see your VPS IP.
Part 5 — Connecting From Android
Android has the best Trojan client ecosystem of any mobile platform. v2rayNG is the recommended choice — lightweight, actively maintained, and available on GitHub.
Step 1 — Install v2rayNG
v2rayNG is on the Google Play Store, but the GitHub releases are often newer. Search for v2rayNG on GitHub and download the APK directly if you prefer.
Install it and grant any permissions it requests (VPN permission is required — Android will show a standard VPN consent dialog).
Step 2 — Add Your Server
Tap the + button (top right) → Add Trojan server
Fill in:
Field Value Address yourdomain.com Port 443 Password your password SNI yourdomain.com Allow Insecure off Remarks anything — e.g. "Home VPS"
Tap the checkmark to save.
Step 3 — Connect
Tap the paper plane icon at the bottom right to connect. Android will show the standard VPN consent dialog on first connection — tap OK.
The icon turns green when connected. Your traffic is now routed through the Trojan tunnel.
Step 4 — Test
Open a browser and visit https://whatismyip.com — you should see your VPS IP.
Step 5 — Per-App Routing (Optional)
v2rayNG supports split tunneling — you can choose which apps use the tunnel and which connect directly.
Go to Settings → VPN Configuration and toggle on Per-app proxy. A list of installed apps appears. Select only the ones you want routed through the tunnel, and everything else connects normally. Useful for keeping local streaming services working while routing everything else through the VPN.
Verifying Your Setup Looks Like Normal HTTPS
To confirm that your traffic genuinely looks like HTTPS and not a VPN, you can check it from the outside.
On any machine not connected to your tunnel, run:
curl -v https://yourdomain.comYou should get your fallback webpage back — the same response anyone would get from a normal HTTPS site. Nothing in the response reveals the existence of the Trojan service.
For deeper verification, use a packet capture on your client machine while connected:
tcpdump -i eth0 port 443 -w capture.pcapOpen the capture in Wireshark. All you'll see is TLS 1.3 traffic to port 443 on your domain. No WireGuard handshake, no WebSocket upgrade headers, no unusual protocol identifiers — just standard HTTPS.
Troubleshooting
Caddy fails to start — certificate error
Make sure port 80 is open. Let's Encrypt's HTTP-01 challenge requires it even though your service runs on 443. Check: ufw allow 80 and try systemctl restart caddy.
Client connects but traffic doesn't route
On Windows, try switching from System Proxy to TUN Mode. On Linux, make sure your proxy environment variables or system proxy settings are pointing at the right port.
Fallback page not appearing
Confirm the :8080 block in your Caddyfile is correct and that /var/www/fallback/index.html exists. Test it locally on the VPS: curl http://localhost:8080.
Android won't connect
Double-check that Allow Insecure is off and your domain exactly matches the SNI field. A mismatch in the SNI causes a TLS handshake failure.
v2rayN TUN mode fails on Windows
Run v2rayN as Administrator. TUN mode requires elevated privileges to install the virtual network adapter.
Summary
The full stack in one picture:
Device
└── Trojan client (v2rayN / v2rayNG / Xray / V2RayXS)
└── TLS 1.3 on port 443
└── VPS running Caddy + caddy-trojan
├── Valid Let's Encrypt certificate
├── Correct password → traffic forwarded
└── Wrong password / probe → real website servedTo any observer on the network between your device and the VPS, this is an HTTPS connection to a website. The certificate is real, the TLS handshake is standard, and the server genuinely responds to unauthenticated requests with a webpage. There is nothing to identify as a VPN unless they can observe both ends of the connection simultaneously — which is not a capability available to most firewalls.