3X-UI 部署 Trojan over TLS 443:证书续期、Fallback 伪装与端口收紧
从 SSH 登录服务器开始,完整走一遍证书自动续期、Trojan TLS 443 入站、fallback 伪装到防火墙收紧的配置流程。
自建代理节点最常见的失败方式来自暴露面太明显:管理面板直接挂在公网、Trojan 节点用 IP 直连、443 端口一探测就能看出不是正常网站。这些问题和协议本身无关,纯粹是配置习惯问题。
这篇文章记录一套完整的配置流程:用 3X-UI 管理 Xray,在 443 端口跑 Trojan over TLS,证书用 Cloudflare DNS-01 自动续期,非代理流量走 fallback 落到一个普通网页,最后把服务器公网端口收紧到最小集合。从 SSH 登录服务器开始,到验证和排错结束,中间每一步为什么这样做也会一并说明。
文中出现的 trojan.example.com、example.com、服务器 IP 都是占位符,实际操作时替换成自己的域名和 IP。Cloudflare API Token、Trojan 客户端密码、SSH 私钥、3X-UI 登录密码这四类信息不要写进任何公开文档,包括这篇文章的副本。
确认现状:3X-UI 和 Xray 是否在跑#
先登录服务器:
ssh root@你的服务器IPbash如果 SSH 端口不是默认的 22:
ssh -p 你的SSH端口 root@你的服务器IPbash进去之后先看服务状态,而不是直接改配置:
systemctl status x-ui --no-pager
ss -lntp | grep -E '443|54321|x-ui|xray'bash正常情况下能看到 443 和 54321(面板端口)分别被 xray 和 x-ui 占用。如果面板没反应,先看日志再重启:
journalctl -u x-ui -n 100 --no-pager
systemctl restart x-ui
systemctl status x-ui --no-pagerbash后面每改一次配置,基本都会回到这几条命令来确认服务是否正常。
证书从哪来:Cloudflare DNS-01 自动续期#
Trojan 依赖真实的 TLS 证书,证书又需要定期续期,手动续期迟早会忘。DNS-01 验证方式的好处是不需要在 80 端口跑一个临时 Web 服务器去响应 HTTP-01 挑战,只要证书申请工具能替你在 Cloudflare 上写一条验证用的 DNS 记录就行。这也是选 acme.sh 配合 Cloudflare API 的原因。
在此之前,trojan.example.com 的 DNS 记录要设置成灰云(DNS only),不能走 Cloudflare 的橙云代理——橙云会终止 TLS 并换成 Cloudflare 自己的证书,Trojan 客户端看到的证书就对不上了。
Cloudflare API Token 只需要两个权限:
Zone / Zone / Read
Zone / DNS / Edittext资源范围限定在目标 zone:
Include / Specific zone / example.comtext申请证书的脚本#
把下面这段保存成脚本执行,中途会提示输入 Cloudflare Account ID、Zone ID 和 API Token:
cat > /root/fix-cloudflare-acme.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
DOMAIN="trojan.example.com"
ZONE="example.com"
CERT_DIR="/etc/trojan/cert/${DOMAIN}"
read -rp "Cloudflare Account ID: " CF_ACCOUNT_ID
read -rp "Cloudflare Zone ID: " CF_ZONE_ID
read -rsp "Cloudflare API Token: " CF_TOKEN
echo
apt update
apt install -y curl jq socat ca-certificates cron
echo "验证 Cloudflare Token..."
curl -fsS \
-H "Authorization: Bearer ${CF_TOKEN}" \
-H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/user/tokens/verify" | jq .
echo "验证 Zone ID..."
curl -fsS \
-H "Authorization: Bearer ${CF_TOKEN}" \
-H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}" | jq .
if [ ! -x /root/.acme.sh/acme.sh ]; then
curl https://get.acme.sh | sh -s email=admin@${ZONE}
fi
ACME="/root/.acme.sh/acme.sh"
"${ACME}" --set-default-ca --server letsencrypt
export CF_Token="${CF_TOKEN}"
export CF_Account_ID="${CF_ACCOUNT_ID}"
export CF_Zone_ID="${CF_ZONE_ID}"
"${ACME}" --issue --dns dns_cf -d "${DOMAIN}" --keylength ec-256 --force
mkdir -p "${CERT_DIR}"
chmod 700 "${CERT_DIR}"
"${ACME}" --install-cert -d "${DOMAIN}" --ecc \
--fullchain-file "${CERT_DIR}/fullchain.cer" \
--key-file "${CERT_DIR}/private.key" \
--ca-file "${CERT_DIR}/ca.cer" \
--cert-file "${CERT_DIR}/cert.cer" \
--reloadcmd "systemctl restart x-ui"
chmod 600 "${CERT_DIR}/private.key"
echo "完成。"
echo "证书:${CERT_DIR}/fullchain.cer"
echo "私钥:${CERT_DIR}/private.key"
EOF
chmod +x /root/fix-cloudflare-acme.sh
bash /root/fix-cloudflare-acme.shbash脚本里的 --reloadcmd "systemctl restart x-ui" 是关键一步:每次续期成功后自动重启 x-ui,让新证书生效,不需要人工介入。
确认续期真的会自动发生#
acme.sh 安装时会自己写一条 crontab,用来确认:
crontab -lbash应该能看到类似:
6 12 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/nullplaintext可以用 --dry-run 提前验证续期流程本身是否走得通,而不用等到证书真的快过期:
/root/.acme.sh/acme.sh --renew -d trojan.example.com --ecc --dry-runbash查看当前证书信息:
/root/.acme.sh/acme.sh --listbash配置 3X-UI 的 Trojan 入站#
证书就绪之后,接下来是让 Xray 在 443 端口对外提供 Trojan over TLS 服务。
选择 443 加真实 TLS,理由并不复杂:443 是标准 HTTPS 端口,不会因为用了一个奇怪的端口号而被重点扫描;配合真实证书和正确的 SNI,连接形态在外部看来和普通 HTTPS 服务没有区别;再加上后面会配置的 fallback,非代理流量访问时会看到一个正常网页,而不是端口空跑或者返回异常响应。这套组合压低的是明显的代理特征,流量本身谈不上彻底隐身,真正的安全性仍然要靠防火墙规则、强密码和收紧暴露面这些基本功来支撑。
面板地址是 https://你的服务器IP:54321/,如果面板还没开 HTTPS 就用 http:// 前缀。
入站配置#
在 Inbounds 里添加一个新入站,关键字段是:
Remark / 备注:agent-trojan-443
Protocol / 协议:trojan
Listen IP / 监听 IP:留空 或 0.0.0.0
Port / 端口:443
Network / 传输:tcp
Security / 安全:tls
SNI / Server Name:trojan.example.com
ALPN:http/1.1
TLS:开启
Reality:关闭text证书路径指向前面 acme.sh 安装的位置:
Certificate File:/etc/trojan/cert/trojan.example.com/fullchain.cer
Key File:/etc/trojan/cert/trojan.example.com/private.keytext客户端密码#
在这个入站的 Clients 里添加客户端,密码是 Trojan 认证的唯一凭据,建议用随机字符串而不是自己拍脑袋想一个:
openssl rand -base64 24bash其余字段留空即可:不限流量、不限 IP、不设过期时间,除非有明确的多用户管理需求。保存后重启 x-ui 让入站生效:
systemctl restart x-uibashFallback:给 443 端口一张正常网站的脸#
Trojan 的认证发生在 TLS 握手之后。这意味着一个不知道密码、只是随手拿浏览器访问 https://trojan.example.com/ 的人,握手依然会成功,只是后续认证会失败——如果这时候直接断开连接或者返回错误,本身就是一个可被识别的特征。fallback 解决的正是这个问题:认证失败的流量不会被拒绝,而是转发到本地一个普通的 Web 服务,返回一个正常网页。
整体结构是:
公网 443 -> Xray Trojan
非 Trojan 流量 -> fallback -> 127.0.0.1:8080 -> Nginx 普通网页text装 Nginx,放一个占位页面#
apt update
apt install -y nginxbash页面内容本身不重要,重要的是它看起来像一个真实存在的网站,而不是空白页或者默认的 Nginx 欢迎页。下面是一个可以直接替换文案使用的占位页:
mkdir -p /var/www/fallback
cat > /var/www/fallback/index.html <<'EOF'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Example Service</title>
<meta name="description" content="Example Service provides a simple public landing page.">
<style>
:root {
--bg: #f7f8fa;
--text: #1c2430;
--muted: #697386;
--line: #d8dee8;
--panel: #ffffff;
--accent: #1f6feb;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--bg);
color: var(--text);
}
.wrap {
width: min(920px, calc(100% - 40px));
margin: 0 auto;
}
header {
border-bottom: 1px solid var(--line);
background: var(--panel);
}
.nav {
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
}
.brand {
font-size: 20px;
font-weight: 700;
letter-spacing: 0;
}
.nav span {
color: var(--muted);
font-size: 14px;
}
main {
padding: 64px 0;
}
.hero {
max-width: 680px;
}
h1 {
margin: 0;
font-size: 42px;
line-height: 1.12;
letter-spacing: 0;
font-weight: 720;
}
.lead {
margin: 18px 0 0;
max-width: 580px;
font-size: 18px;
line-height: 1.7;
color: var(--muted);
}
.panel {
margin-top: 36px;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
padding: 22px 24px;
}
.note {
margin: 0;
color: var(--muted);
font-size: 15px;
line-height: 1.7;
}
footer {
border-top: 1px solid var(--line);
padding: 22px 0;
color: var(--muted);
font-size: 13px;
background: #ffffff;
}
@media (max-width: 760px) {
.wrap { width: min(100% - 28px, 1040px); }
main { padding: 46px 0 48px; }
.nav span { display: none; }
h1 { font-size: 34px; }
}
</style>
</head>
<body>
<header>
<div class="wrap nav">
<div class="brand">Example Service</div>
<span>System status</span>
</div>
</header>
<main class="wrap">
<section class="hero">
<h1>Service is running normally.</h1>
<p class="lead">
This page is reserved for a small public website. Replace this example
with your own ordinary product, tool, blog, or company introduction.
</p>
</section>
<section class="panel">
<p class="note">
Current status: online. For support, contact the site administrator.
</p>
</section>
</main>
<footer>
<div class="wrap">© 2026 Example Service. All rights reserved.</div>
</footer>
</body>
</html>
EOFbash记得把 Example Service 和文案换成自己的内容,不要直接照抄示例品牌名。
Nginx 只监听本机#
Nginx 不需要、也不应该直接暴露在公网上——它只服务于 Xray 转发过来的 fallback 流量:
cat > /etc/nginx/sites-available/fallback.conf <<'EOF'
server {
listen 127.0.0.1:8080;
server_name trojan.example.com;
root /var/www/fallback;
index index.html;
etag on;
if_modified_since exact;
location = / {
try_files /index.html =404;
add_header Cache-Control "public, max-age=31536000, immutable" always;
expires 365d;
}
location = /index.html {
try_files /index.html =404;
add_header Cache-Control "public, max-age=31536000, immutable" always;
expires 365d;
}
location / {
try_files $uri $uri/ =404;
add_header Cache-Control "public, max-age=31536000, immutable" always;
expires 365d;
}
}
EOF
rm -f /etc/nginx/sites-enabled/default
ln -sf /etc/nginx/sites-available/fallback.conf /etc/nginx/sites-enabled/fallback.conf
nginx -t
systemctl enable --now nginx
systemctl reload nginxbash强缓存头是为了让这个页面尽量少产生动态请求特征,看起来更像一个静态的普通站点。测试本机是否正常返回:
curl -I http://127.0.0.1:8080/bash应该看到 HTTP/1.1 200 OK。
在 3X-UI 入站里接上 fallback#
回到 Trojan 入站的配置,加一条 fallback 规则,把认证失败的流量指向 8080:
Dest / 目标:8080
Xver:0
Path:留空
ALPN:留空 或 http/1.1
SNI:留空text对应的高级配置 JSON 大致是:
"settings": {
"clients": [
{
"password": "你的Trojan客户端密码",
"email": "client-01"
}
],
"fallbacks": [
{
"dest": 8080,
"xver": 0
}
]
}json保存后重启 x-ui:
systemctl restart x-uibash验证时用浏览器或 curl 直接访问域名,应该看到刚才做的占位页面:
curl -vk https://trojan.example.com/bash收紧公网端口#
前面的配置解决的是”443 端口本身长什么样”,这一步解决的是”服务器还有哪些端口不该被外面看到”。管理面板端口、SSH 端口,这些如果对全网开放,本身就是一个明显的攻击面,和 Trojan 配得再好都没有关系。
服务器本机防火墙用 ufw:确认 SSH 能正常连通之后再执行,否则容易把自己锁在外面。
ufw default deny incoming
ufw default allow outgoing
ufw allow 443/tcp
ufw allow from 你的本地公网IP to any port 22 proto tcp
ufw allow from 你的本地公网IP to any port 54321 proto tcp
ufw enable
ufw statusbash如果暂时不确定自己的公网 IP 会不会变化,先不要在 ufw 里限制 22,避免规则生效后连不上服务器;可以先在云平台的防火墙层面收紧明显不需要的端口,等确认自己的访问方式稳定之后,再回来限制管理面板和 SSH。
云平台的防火墙(以 GCP 为例)建议规则:
TCP 443:允许 0.0.0.0/0
TCP 22:只允许本地公网 IP
TCP 54321:不开放,或只允许本地公网 IP
其他端口:关闭text两层防火墙叠加的意义在于:即便某一层规则被误改或者遗漏,另一层还能兜底。
验证与排错#
配置完成后按下面几条依次核对:
端口是否落在预期的进程上:
ss -lntp | grep -E ':443|:8080'bash期望看到 *:443 是 xray,127.0.0.1:8080 是 nginx。
证书是否正确:
openssl s_client -connect trojan.example.com:443 -servername trojan.example.com </dev/nullbash证书里的域名应该是 trojan.example.com。
网页是否正常返回:
curl -vk https://trojan.example.com/bash应该能看到占位页面的 HTML。之后再用实际的 Trojan 客户端添加节点,地址、端口、SNI 分别填域名、443、域名,密码填 3X-UI 里生成的那一串,测试是否能连上。
面板打不开时: 先看服务状态和端口,再区分是不是 HTTP/HTTPS 协议不匹配导致的:
systemctl status x-ui --no-pager
ss -lntp | grep 54321
curl -v http://127.0.0.1:54321/
curl -vk https://127.0.0.1:54321/bash如果用 curl -v http:// 访问时报 Unsupported HTTP version,通常说明面板现在跑的是 HTTPS,用错协议访问自然会失败——这个报错本身就是排错的线索,不用怀疑是面板挂了。
Trojan 节点连不上时: 看日志、看证书路径、看端口有没有被其他服务占用:
journalctl -u x-ui -n 100 --no-pager
ls -l /etc/trojan/cert/trojan.example.com/
ss -lntp | grep ':443'bash三条命令基本能定位问题出在配置层、证书层还是端口冲突层。
优先级和该避开的坑#
把这套配置的关键点收敛成一份清单,方便后续检查:
- 公网只开放 443,其余端口不直接暴露
- 54321 面板端口不对全网开放
- SSH 和面板只允许可信来源访问
- Cloudflare 保持灰云 DNS only,不要切换成橙云代理
- 证书到期前自动续期,并联动重启 x-ui
- fallback 页面能正常返回,而不是空白或报错
几个容易踩的坑:不要用服务器 IP 直连 Trojan 节点,SNI 和证书都对不上;不要让 Nginx 直接监听公网 443,它只应该处理 fallback 转发过来的本地流量;也不要图省事在多人之间共享同一个客户端密码,出问题时无法区分是谁的连接。
一张图收尾#
整套流程落到架构图上很简单:
客户端 -> trojan.example.com:443 -> Xray Trojan
普通浏览器 -> trojan.example.com:443 -> Xray fallback -> 127.0.0.1:8080 -> Nginx 示例页面text证书自动续期保证这条链路长期有效,fallback 保证 443 端口在被随手访问时不会露出异常,防火墙收紧保证除了这一条链路之外,服务器不再有多余的暴露面。三者分别解决的是”能不能用”“像不像正常网站”和”还剩多少攻击面”这三个不同层面的问题,缺一层,前面做的另外两层都会打折扣。