BemoDB 2.0

Back

自建代理节点最常见的失败方式来自暴露面太明显:管理面板直接挂在公网、Trojan 节点用 IP 直连、443 端口一探测就能看出不是正常网站。这些问题和协议本身无关,纯粹是配置习惯问题。

这篇文章记录一套完整的配置流程:用 3X-UI 管理 Xray,在 443 端口跑 Trojan over TLS,证书用 Cloudflare DNS-01 自动续期,非代理流量走 fallback 落到一个普通网页,最后把服务器公网端口收紧到最小集合。从 SSH 登录服务器开始,到验证和排错结束,中间每一步为什么这样做也会一并说明。

文中出现的 trojan.example.comexample.com、服务器 IP 都是占位符,实际操作时替换成自己的域名和 IP。Cloudflare API Token、Trojan 客户端密码、SSH 私钥、3X-UI 登录密码这四类信息不要写进任何公开文档,包括这篇文章的副本。

确认现状:3X-UI 和 Xray 是否在跑#

先登录服务器:

ssh root@你的服务器IP
bash

如果 SSH 端口不是默认的 22:

ssh -p 你的SSH端口 root@你的服务器IP
bash

进去之后先看服务状态,而不是直接改配置:

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-pager
bash

后面每改一次配置,基本都会回到这几条命令来确认服务是否正常。

证书从哪来: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 / Edit
text

资源范围限定在目标 zone:

Include / Specific zone / example.com
text

申请证书的脚本#

把下面这段保存成脚本执行,中途会提示输入 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.sh
bash

脚本里的 --reloadcmd "systemctl restart x-ui" 是关键一步:每次续期成功后自动重启 x-ui,让新证书生效,不需要人工介入。

确认续期真的会自动发生#

acme.sh 安装时会自己写一条 crontab,用来确认:

crontab -l
bash

应该能看到类似:

6 12 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null
plaintext

可以用 --dry-run 提前验证续期流程本身是否走得通,而不用等到证书真的快过期:

/root/.acme.sh/acme.sh --renew -d trojan.example.com --ecc --dry-run
bash

查看当前证书信息:

/root/.acme.sh/acme.sh --list
bash

配置 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.key
text

客户端密码#

在这个入站的 Clients 里添加客户端,密码是 Trojan 认证的唯一凭据,建议用随机字符串而不是自己拍脑袋想一个:

openssl rand -base64 24
bash

其余字段留空即可:不限流量、不限 IP、不设过期时间,除非有明确的多用户管理需求。保存后重启 x-ui 让入站生效:

systemctl restart x-ui
bash

Fallback:给 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 nginx
bash

页面内容本身不重要,重要的是它看起来像一个真实存在的网站,而不是空白页或者默认的 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>
EOF
bash

记得把 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 nginx
bash

强缓存头是为了让这个页面尽量少产生动态请求特征,看起来更像一个静态的普通站点。测试本机是否正常返回:

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-ui
bash

验证时用浏览器或 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 status
bash

如果暂时不确定自己的公网 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/null
bash

证书里的域名应该是 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 端口在被随手访问时不会露出异常,防火墙收紧保证除了这一条链路之外,服务器不再有多余的暴露面。三者分别解决的是”能不能用”“像不像正常网站”和”还剩多少攻击面”这三个不同层面的问题,缺一层,前面做的另外两层都会打折扣。

3X-UI 部署 Trojan over TLS 443:证书续期、Fallback 伪装与端口收紧
https://bolaxious.cn/blog/2026/tech/3xui-trojan-tls-443
Author Bolaxious
Published at July 2, 2026