From tunpilot
Deploys production Xray-core Trojan proxy nodes via SSH with server probing for OS/CPU/memory/ports/firewall, TLS cert pinning, and TunPilot registration.
npx claudepluginhub buywatermelon/tunpilot --plugin tunpilotThis skill uses the workspace's default tool permissions.
Deploy a production-grade Xray-core Trojan proxy node with automatic performance tuning, security hardening, and certificate fingerprint pinning. Follow each phase in order.
Deploys production Hysteria2 proxy nodes on Linux servers via SSH: probes capabilities, tunes performance/security, configures TLS, registers in TunPilot.
Deploys and manages 3X-UI on Ubuntu/Debian VPS with Docker Compose, nginx proxy, ACME certs, SSH tunneling, UFW hardening, and Xray VLESS over XHTTP on port 443. For fresh installs, repairs, client adds, or safe updates.
Provisions, hardens, and deploys apps to Hetzner Cloud VPS with Docker, Nginx/Caddy reverse proxy, SSL certs, database setup, monitoring, and backups.
Share bugs, ideas, or general feedback.
Deploy a production-grade Xray-core Trojan proxy node with automatic performance tuning, security hardening, and certificate fingerprint pinning. Follow each phase in order.
Prerequisite: TunPilot server must be running and CLI must be configured (use getting-started skill if not).
Collect the following from the user:
root@node1.example.com or an SSH config aliastokyo-trojan, bwg-trojan)ssh <server> "echo ok"
Run ALL probes in a single SSH session to minimize round trips:
ssh <server> bash <<'PROBE'
echo "=== OS/ARCH ==="
uname -s -m
cat /etc/os-release 2>/dev/null | grep -E '^(ID|VERSION_ID)='
echo "=== CPU ==="
nproc
echo "=== MEMORY ==="
free -b | awk '/Mem/{print $2}'
echo "=== PORT CONFLICTS ==="
ss -tulnp | grep -E ':443|:80' || echo "no conflicts"
echo "=== FIREWALL ==="
if command -v ufw &>/dev/null; then echo "ufw"; ufw status 2>/dev/null
elif command -v firewall-cmd &>/dev/null; then echo "firewalld"; firewall-cmd --state 2>/dev/null
elif command -v nft &>/dev/null; then echo "nftables"
else echo "none"
fi
echo "=== EXISTING XRAY ==="
xray version 2>/dev/null || echo "not installed"
echo "=== NETWORK ==="
ip -4 addr show scope global 2>/dev/null
ip -6 addr show scope global 2>/dev/null
echo "=== SYSCTL ==="
sysctl -n net.core.rmem_max 2>/dev/null
sysctl -n net.core.wmem_max 2>/dev/null
sysctl -n net.core.somaxconn 2>/dev/null
sysctl -n net.ipv4.tcp_congestion_control 2>/dev/null
sysctl -n net.core.default_qdisc 2>/dev/null
sysctl -n net.ipv4.tcp_fastopen 2>/dev/null
echo "=== DISK ==="
df -h / 2>/dev/null
PROBE
Using the probe results, build a server profile table:
| Parameter | Source | Derived Setting |
|---|---|---|
| Memory | free -b | TCP buffer sizes (rmem/wmem) |
| CPU cores | nproc | Connection capacity |
| Port conflicts | ss -tulnp | Whether 443/TCP and 80/TCP are available |
| Firewall type | probe | Which firewall commands to use (ufw/firewall-cmd/iptables/none) |
| Kernel tuning | sysctl values | Whether TCP sysctl tuning is needed (BBR, somaxconn, fastopen) |
| Existing Xray | version check | Whether to install fresh or upgrade |
Present the server profile and confirm:
10085 for Xray stats APIApply TCP-optimized sysctl settings. Skip if the probe shows values are already tuned.
ssh <server> bash <<'SYSCTL'
cat > /etc/sysctl.d/99-xray.conf << 'EOF'
# TCP buffer sizes
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
# Connection backlog
net.core.somaxconn = 4096
# TCP Fast Open (client + server)
net.ipv4.tcp_fastopen = 3
# BBR congestion control
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
EOF
sysctl -p /etc/sysctl.d/99-xray.conf
SYSCTL
Ensure prerequisites are installed, then install Xray:
ssh <server> 'apt-get update -qq && apt-get install -y -qq unzip curl && bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install'
Verify installation:
ssh <server> "xray version"
Install tools required by the diagnostic scripts (IPQuality + NetQuality):
ssh <server> "apt-get update -qq && apt-get install -y -qq jq curl bc netcat-openbsd dnsutils iproute2 iperf3 mtr"
Deploy the tunpilot-diag script for clean JSON diagnostics output:
ssh <server> bash <<'DIAG_INSTALL'
curl -fsSL https://raw.githubusercontent.com/Buywatermelon/tunpilot/main/scripts/tunpilot-diag.sh \
-o /usr/local/bin/tunpilot-diag
chmod +x /usr/local/bin/tunpilot-diag
tunpilot-diag --version
DIAG_INSTALL
Config A — With domain (ACME via standalone or webroot):
Use certbot or acme.sh to obtain a Let's Encrypt certificate independently of Xray. This keeps certificate management separate from the proxy:
ssh <server> bash <<'ACME'
# Install certbot if not present
command -v certbot &>/dev/null || apt-get install -y certbot
# Ensure port 80 is free for HTTP-01 challenge
ss -tlnp | grep ':80 ' && echo "WARNING: port 80 in use — stop the service first" || echo "port 80 available"
# Obtain certificate (standalone mode — no web server needed)
certbot certonly --standalone -d {{DOMAIN}} --non-interactive --agree-tos --email admin@{{DOMAIN}}
# Set up auto-renewal with Xray restart
mkdir -p /etc/xray
ln -sf /etc/letsencrypt/live/{{DOMAIN}}/fullchain.pem /etc/xray/cert.pem
ln -sf /etc/letsencrypt/live/{{DOMAIN}}/privkey.pem /etc/xray/key.pem
# Add post-renewal hook to restart Xray
cat > /etc/letsencrypt/renewal-hooks/post/restart-xray.sh << 'HOOK'
#!/bin/bash
systemctl restart xray
HOOK
chmod +x /etc/letsencrypt/renewal-hooks/post/restart-xray.sh
ACME
Config B — Without domain (self-signed EC P-256 + fingerprint pinning):
ssh <server> bash <<'SELFSIGN'
mkdir -p /etc/xray
openssl req -x509 -newkey ec \
-pkeyopt ec_paramgen_curve:prime256v1 \
-keyout /etc/xray/key.pem \
-out /etc/xray/cert.pem \
-days 3650 -nodes \
-subj '/CN=bing.com'
# Calculate SHA-256 fingerprint for certificate pinning
echo "=== Certificate SHA-256 Fingerprint ==="
openssl x509 -in /etc/xray/cert.pem -noout -fingerprint -sha256 | sed 's/://g' | cut -d= -f2
SELFSIGN
Save the fingerprint — it will be used when registering the node in TunPilot and is included in subscription configs for clients to verify the certificate.
Use the CLI to register the node:
tunpilot node add \
--name=<node-name> \
--host=<server-ip-or-domain> \
--port=443 \
--protocol=trojan \
--stats_port=10085 \
--sni=<domain> \
--ssh_user=root \
--ssh_port=22 \
--insecure=1 \
--cert_fingerprint=<sha256-fingerprint>
Required flags: --name, --host, --port, --protocol
Optional flags: --stats_port, --sni, --cert_path, --ssh_user, --ssh_port, --ssh_alias, --insecure (1 for self-signed, 0 for ACME), --cert_fingerprint (Config B only)
Read the config template from xray-template.md in this skill directory. Choose the appropriate config variant:
Fill all placeholders using values from the server profile. Write the config:
ssh <server> "cat > /usr/local/etc/xray/config.json << 'CONF'
<filled config from template>
CONF"
Create a systemd drop-in to harden the Xray service:
ssh <server> bash <<'SYSTEMD'
mkdir -p /etc/systemd/system/xray.service.d
cat > /etc/systemd/system/xray.service.d/hardening.conf << 'EOF'
[Service]
LimitNOFILE=65536
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/usr/local/etc/xray /etc/xray /var/log/xray
EOF
systemctl daemon-reload
SYSTEMD
Open required ports using the firewall type detected in Phase 1.3. Trojan uses TCP (not UDP like Hysteria2):
ssh <server> bash <<'FIREWALL'
if command -v ufw &>/dev/null; then
ufw allow 443/tcp
ufw allow 80/tcp
ufw reload
elif command -v firewall-cmd &>/dev/null; then
firewall-cmd --permanent --add-port=443/tcp
firewall-cmd --permanent --add-port=80/tcp
firewall-cmd --reload
else
echo "No firewall manager detected — ensure TCP/443 and TCP/80 are open at the provider level"
fi
FIREWALL
ssh <server> "systemctl enable --now xray && sleep 2 && systemctl is-active xray"
If the service fails to start, check logs immediately:
ssh <server> "journalctl -u xray --no-pager -n 50"
After the service is running, sync users to the node:
tunpilot node sync
Use the CLI to check node health:
tunpilot health
Test the Xray gRPC stats API from the node itself via SSH:
ssh <server> "xray api statsquery --server=127.0.0.1:{{API_PORT}}"
This should return stats output (may be empty if no traffic yet).
Review recent logs for any errors or warnings:
ssh <server> "journalctl -u xray --no-pager -n 30 --since '5 minutes ago'"
Present a final report to the user:
tunpilot user update <id> --nodes=<node-id> to grant users access, then tunpilot node sync to push users)| Symptom | Diagnosis | Fix |
|---|---|---|
tunpilot health unreachable | gRPC API not accessible | Verify stats_port matches Xray config api.listen port, check SSH connectivity |
| Service won't start | Config syntax error | Run journalctl -u xray --no-pager -n 50 and validate JSON syntax with xray run -test -c /usr/local/etc/xray/config.json |
| ACME cert fails | DNS not pointing to server | Check dig <domain>, ensure port 80 is open and not occupied |
| Clients can't connect | Firewall blocking TCP/443 | Check `ss -tlnp |
| gRPC sync fails | Xray API not listening | Verify api block in config, check `ss -tlnp |
| Auth failures | Users not synced | Run tunpilot node sync to push users to the node |
| Certificate pinning errors | Fingerprint mismatch | Re-extract fingerprint: openssl x509 -in /etc/xray/cert.pem -noout -fingerprint -sha256 and update via tunpilot node update <id> --cert_fingerprint=... |
| Command | Use When |
|---|---|
tunpilot node list | See all registered nodes |
tunpilot node add --name=... --host=... --port=... --protocol=trojan | Register a new node (Phase 2.5) |
tunpilot node update <id> --name=... | Change node config (port, SNI, fingerprint, enable/disable) |
tunpilot node remove <id> | Delete a node (cascades user assignments) |
tunpilot node sync | Push users to all nodes via gRPC (Phase 2.9) |
tunpilot health | Verify all nodes are reachable |
tunpilot traffic --node=<id> | Query traffic usage by node |
tunpilot user update <id> --nodes=<id1>,<id2> | Grant a user access to specific nodes |
tunpilot sub create --user=<id> --format=surge | Generate client subscription link for a user |