RRPP 服务端部署规范
一套统一的跨境代理节点部署流程,适用于 Debian/Ubuntu 系 Linux。 每台节点运行相同的服务栈:BBR 拥塞控制 + nginx CONNECT 隧道 + sing-box(VLESS+REALITY / Shadowsocks) + mtg(MTProto)。
本文档为通用规范,所有 IP / 域名 / 端口 / 密钥均以占位符表示,部署时替换为实际值。 凡标注
<...>的均为占位符,不要原样照抄。
目录
- 0. 架构总览
- 1. 端口与服务规划
- 2. 系统基础优化(BBR 跨境丢包优化)
- 3. 伪装站点页面
- 4. HTTPS 证书(acme.sh DNS-01)
- 5. nginx 源码编译 + CONNECT 隧道
- 6. sing-box(VLESS+REALITY / Shadowsocks)
- 7. mtg(MTProto 代理)
- 8. 本机公网 IP 自动识别(防自代理回环)
- 9. systemd 服务文件
- 10. 启动顺序与验证清单
- 11. 目录结构与文件索引
0. 架构总览
客户端 ──► nginx(443/<nginx端口>) ──┬─► CONNECT 隧道(任意端口, 全端口放行)
│ └─► 伪装页面(直接访问/IP访问)
│
├─► sing-box(<vless端口>) ──► VLESS+REALITY(vision)
├─► sing-box(<ss端口>) ──► Shadowsocks(2022-blake3-aes-256-gcm)
└─► mtg(<mtproto端口>) ──► MTProto(Fake TLS, Telegram 内置代理)
四层代理各司其职:
| 层 | 组件 | 协议 | 适用场景 |
|---|---|---|---|
| HTTP 隧道 | nginx | CONNECT + Basic Auth | 浏览器/curl/系统代理,全端口 WebSocket 透传 |
| 通用代理 | sing-box | VLESS+REALITY | Shadowrocket/Clash/v2rayN 客户端 |
| 兼容旧客户端 | sing-box | Shadowsocks | Shadowrocket 2.x 等不支持 REALITY 的客户端 |
核心密钥体系(2026-09 起全部每节点独立):
- VLESS+REALITY:每节点独立 UUID、REALITY 密钥对、short_id;SNI 仍全节点统一(用
<伪装SNI域名>,选一个高流量、TLS 指纹稳定的知名站点域名) - Shadowsocks:每节点独立 PSK,方法
2022-blake3-aes-256-gcm(SIP022,抗重放探测) - nginx CONNECT:每节点独立 Basic Auth 密码(客户端按节点携带凭据,无兼容性问题)
- MTProto:每台独立 secret(Fake TLS 类型,fronting 到
<伪装SNI域名>)
独立化的目的:单节点密钥泄漏/被污染时不波及其余节点,且换钥粒度更小。 代价是订阅/nodes.json 必须逐节点维护,改任一项后必须做端到端验证(见 6.3、第 10 节)。
1. 端口与服务规划
端口全部落在同一区段便于防火墙管理(示例用 1200–1220,可按需调整):
| 端口 | 服务 | 说明 |
|---|---|---|
<nginx端口> |
nginx | HTTPS 站点 + CONNECT 隧道(示例 1205) |
<vless端口> |
sing-box | VLESS+REALITY(示例 1206) |
<ss端口> |
sing-box | Shadowsocks(示例 1207) |
<mtproto端口> |
mtg | MTProto(示例 1210) |
节点 A 用
80xx区段、节点 B 用120xx区段皆可,只要同节点内不冲突、且运营商/防火墙放行。 防火墙放行:iptables -A INPUT -p tcp -m multiport --dports <nginx端口>,<vless端口>,<ss端口>,<mtproto端口> -j ACCEPT
域名规划: 每个节点一个独立域名 <节点域名>(如 node1.example.com),A 记录指向节点公网 IP。acme.sh 用 DNS-01 签证书,无需 80 端口可达。
2. 系统基础优化(BBR 跨境丢包优化)
BBR(Bottleneck Bandwidth and RTT)+ fq 队列,显著改善跨境 TCP 丢包/高延迟下的吞吐。
2.1 启用 BBR
写入 /etc/sysctl.d/99-bbr.conf:
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr
应用并验证:
modprobe tcp_bbr 2>/dev/null || true # 部分内核需手动加载模块
sysctl --system
sysctl net.ipv4.tcp_congestion_control net.core.default_qdisc
# 期望输出: bbr / fq
内核要求:BBR 需 Linux 4.9+。
default_qdisc=fq在部分新内核(6.12+ cloud 内核)可能无此参数,仅保留tcp_congestion_control=bbr即可,不影响 BBR 生效。
2.2 文件描述符
nginx worker 高并发需要提高 limit,在 /etc/security/limits.conf 追加:
* soft nofile 65535
* hard nofile 65535
2.3 时区与 NTP
代理证书校验、日志排障都依赖准确时间:
timedatectl set-timezone Asia/Shanghai
apt install -y chrony && systemctl enable --now chrony
2.4 PMTUD 黑洞缓解(tcp_mtu_probing)
症状:部分客户端网络上出现"TCP 能连上、但 TLS 握手稳定超时"
(如 _ssl.c:1011 handshake operation timed out),而服务器从其他网络探测一切正常。
原因是客户端↔服务器路径上某跳 MTU 变小且 ICMP Fragmentation Needed 被过滤
(PMTUD 黑洞),服务器发出的满尺寸大包永远到不了客户端。
开启 RFC 4821 PLPMTUD,让服务器在探测失败后自动降低 MSS 恢复通信。
写入 /etc/sysctl.d/99-mtu-probing.conf:
net.ipv4.tcp_mtu_probing = 1
sysctl --system
sysctl net.ipv4.tcp_mtu_probing # 期望输出: 1
只影响命中黑洞的连接,正常连接无感知,可安全全节点开启。
2.5 fail2ban(sshd 爆破防护)
节点 SSH 暴露在公网,爆破尝试是常态(实测单节点累计 3 万+ 次)。装 fail2ban 护 sshd:
apt install -y fail2ban
cat > /etc/fail2ban/jail.d/sshd.local << 'EOF'
[sshd]
enabled = true
backend = systemd
EOF
systemctl enable --now fail2ban
fail2ban-client status sshd # 确认 jail 在跑
backend = systemd:无/var/log/auth.log的最小化系统(无 rsyslog)必须加这行, 否则 fail2ban 启动报 "Have not found any log file for sshd jail"。- LXC 容器也能正常封禁(实测 ban/unban 链路可用)。
- 只加 fail2ban、不动 sshd 认证方式(root 登录等现状保留)。
- 如需同时护 nginx CONNECT 的 401 爆破,可再加 nginx jail(凭据够强时可不做)。
3. 伪装站点页面
直接访问节点/IP 时返回一个普通网页(而非代理错误),降低被探测的特征。
放置一个静态 index.html 到 /var/www/html/index.html:
- 任意抓取的合法 HTML 页面(博客/门户首页快照均可)
- 所有节点用同一份(md5 一致),避免逐节点特征差异
mkdir -p /var/www/html
# index.html 为本仓库 fake-site/index.html 的拷贝
cp fake-site/index.html /var/www/html/index.html
4. HTTPS 证书(acme.sh DNS-01)
nginx 站点与 CONNECT 隧道共用一张证书。用 acme.sh + DNS-01(以阿里云 DNS 为例,其他 DNS 商同理):
curl https://get.acme.sh | sh -s email=<你的邮箱>
export Ali_Key="<阿里云AccessKey>"
export Ali_Secret="<阿里云Secret>"
~/.acme.sh/acme.sh --issue --dns dns_ali -d <节点域名> --keylength ec-256
# 安装到 nginx 读取路径
mkdir -p /etc/nginx/ssl/<节点域名>
~/.acme.sh/acme.sh --install-cert -d <节点域名> --ecc \
--fullchain-file /etc/nginx/ssl/<节点域名>/fullchain.pem \
--key-file /etc/nginx/ssl/<节点域名>/privkey.pem
acme.sh 自带 cron 自动续期。证书路径固定为 /etc/nginx/ssl/<节点域名>/{fullchain,privkey}.pem。
5. nginx 源码编译 + CONNECT 隧道
标准 nginx 不自带 CONNECT 方法支持,需打 ngx_http_proxy_connect_module 补丁后源码编译。
5.1 编译
apt install -y build-essential libssl-dev libpcre3-dev zlib1g-dev curl wget
NGINX_VER=1.26.3
cd /usr/local/src
curl -LO http://nginx.org/download/nginx-$NGINX_VER.tar.gz
tar xzf nginx-$NGINX_VER.tar.gz
git clone https://github.com/chobits/ngx_http_proxy_connect_module.git
cd nginx-$NGINX_VER
patch -p1 < ../ngx_http_proxy_connect_module/patch/proxy_connect_rewrite_102101.patch
./configure \
--prefix=/usr/local/nginx \
--conf-path=/etc/nginx/nginx.conf \
--pid-path=/var/run/nginx.pid \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_realip_module \
--with-http_stub_status_module \
--with-http_gzip_static_module \
--add-module=../ngx_http_proxy_connect_module
make -j$(nproc) && make install
若发行版 nginx 已自带 proxy_connect 补丁(极少见),可用
apt install nginx跳过编译,但源码编译是通用做法。
5.2 Basic Auth 凭据
CONNECT 代理需要 Basic Auth(用户名/密码),每节点独立密码(客户端配置按节点携带凭据,无兼容性问题)。生成 htpasswd:
apt install -y apache2-utils
htpasswd -bc /etc/nginx/.htpasswd <代理用户名> '<本节点独立密码>'
chmod 644 /etc/nginx/.htpasswd # worker 以 nobody 运行,必须可读
无 apache2-utils 的机器可用 openssl 生成(sha512-crypt,nginx 同样支持):
echo "<用户名>:$(openssl passwd -6 '<密码>')" > /etc/nginx/.htpasswdhtpasswd 是每次请求实时读取的,改密码无需 reload nginx。
5.3 nginx.conf
完整配置见 nginx/nginx.conf。关键设计:
- 两个 server 块共用
<nginx端口>:- 主块
server_name <节点域名>:证书站点 +location /返回伪装页;非本域名一律 rewrite 到/__proxy转发 - fallback 块
default_server:其他 SNI 也接受 CONNECT 代理;本机/私网/公网 IP 直连返回伪装页,其余走/__proxy
- 主块
proxy_connect_allow all;:全端口 CONNECT 放行(codex/WebSocket 走任意端口均通)。若需限制,改回proxy_connect_allow 443 563;auth_basic:仅 CONNECT/正向代理需要认证,location /伪装页auth_basic off- 自代理回环保护:
/__proxy转发时带X-Proxy-Loop-Guard头,二次进入返回 508 防死循环 /__local:本机 IP 直连时剥前缀返回伪装页map $host $is_local:私网/回环段判定;公网 IP 由 cron 脚本动态写入local_ip_rules.conf
5.4 自代理回环保护说明
正向代理转发目标若指向本机自己(如域名公网 IP 变更、或代理目标就是本节点),会无限循环。两层防护:
map判定私网/回环段 + cron 维护的本机公网 IP 规则 → 直连伪装页/__proxy的 Loop-Guard 头兜底 → 二次进入返回 508
5.5 路线 B 裸 UoT relay
TUN 客户端的 UDP 会先通过认证 CONNECT 请求
sp.v2.udp-over-tcp.arpa:443。当前 hk2 使用的
ngx_http_proxy_connect_module 没有注册 $connect_host,因此在 fallback
server 实际接收的 CONNECT 请求上用 $request 正则匹配,再把连接转到
127.0.0.1:15201;nginx 只负责 TLS、Basic Auth 和字节流转发,不解析 UoT。relay 使用仓库中的
[uot_relay.py](uot_relay.py),只监听回环地址,不需要 sing-box:
install -m 755 uot_relay.py /opt/rrpp-uot-relay/uot_relay.py
install -m 644 systemd/uot-relay.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now uot-relay
ss -lntp | grep 127.0.0.1:15201
nginx -t && systemctl reload nginx
客户端的 CONNECT 端口必须大于 0,当前统一使用 443;15201 是服务器回环
relay 的内部端口,不写入客户端 CONNECT 目标。nginx fallback 块使用
proxy_connect_read_timeout 10m 和 proxy_connect_send_timeout 10m,不要再同时
配置会产生 duplicate 的 proxy_connect_data_timeout。
5.5 UoT(UDP over TCP)支持
HTTP CONNECT 只隧道 TCP。要让 UDP 应用(DNS/游戏/QUIC 等)走代理,需 UoT v2
(SagerNet 专有协议,见 sing-box 文档 configuration/shared/udp-over-tcp):
客户端把 UDP 数据报封装进 TCP 流,经 CONNECT 隧道发到 magic 地址
sp.v2.udp-over-tcp.arpa,由本机 UoT relay 解封装后转发真实 UDP 目标。
客户端 TUN/UDP ──UoT v2──▶ TLS HTTP CONNECT ──▶ nginx proxy_connect
──▶ 127.0.0.1:15201(uot-relay)──▶ 真实 UDP 目标
relay 部署(Python 标准库实现,见 uot-relay/uot_relay.py):
install -d /opt/rrpp-uot-relay
install -m 755 uot-relay/uot_relay.py /opt/rrpp-uot-relay/uot_relay.py
install -m 644 uot-relay/uot-relay.service /etc/systemd/system/uot-relay.service
systemctl daemon-reload && systemctl enable --now uot-relay
relay 只监听 127.0.0.1:15201,空闲 9 分钟断开(须小于 nginx 隧道超时 10m)。
支持 UoT v2 两种模式:isConnect=1 固定目标、isConnect=0 每帧自带目标。
nginx 侧(模板已含,nginx/nginx.conf):map $request 识别
magic 地址 → 127.0.0.1:15201,其余 CONNECT 走 $connect_addr 默认解析;
proxy_connect_address $rrpp_uot_connect_address; 放在 default_server 块。
四个实测坑(2026-09 全节点部署验证):
- 该模块版本未注册
$connect_host变量(README 有、源码无)→ 必须用$request正则匹配(任何阶段可用); proxy_connect_data_timeout与proxy_connect_read_timeout写同一配置槽, 同时写报 duplicate → 长连接超时用 read/send timeout 设 10m;- 所有 CONNECT 请求都落在
default_server(fallback)块——nginx 按请求 Host 头选虚拟主机,CONNECT 的 Host 是目标域名,永远匹配不到server_name; - 模块拒绝 CONNECT 端口 0(
port < 1返回 400)→ 客户端连 magic 地址时 端口必须非 0(用 443 即可,relay 不读 CONNECT 端口,目标取自 UoT 帧头)。
客户端协议:认证与 TCP CONNECT 相同(TLS + Basic Auth),然后
CONNECT sp.v2.udp-over-tcp.arpa:443 HTTP/1.1,200 之后收发 UoT v2 帧:
请求头 | isConnect u8 | ATYP u8 | address | port u16be |,
connect 模式帧 | length u16be | data |,non-connect 帧每帧自带目标地址。
5.6 CONNECT 超时口径(避免客户端"卡死")
三个超时要按"客户端最没耐心"来配:
| 项 | 值 | 说明 |
|---|---|---|
proxy_connect_connect_timeout |
10s | 出站连接目标;须 < 客户端超时(lite 客户端 15s) |
proxy_connect_read_timeout / send_timeout |
10m | 隧道空闲超时(UoT 长连接需要;注意与 data_timeout 同槽位) |
典型故障:目标 IP 是黑洞(不发 RST)时,若 nginx 出站超时设 60s 而客户端 15s 超时,
客户端会表现为 The read operation timed out 而不是收到 502 —— 看起来像"代理卡死",
实际是超时错配。2026-09 实测:改为 10s 后,黑洞目标从 >25s 降至 10.2s 返回,
正常目标(0.4~0.7s)不受影响。
排查"CONNECT 502"的正确顺序(重要):
- 先看目标是否本身就不可达——从多个网络(国内/日本/美国/香港)实测该 IP:端口, 若都失败则是目标问题(服务下线/过期 IP/黑洞),不是代理问题;
- 再看 nginx error.log:
Connection refused/timed out/Network is unreachable分别对应目标拒绝、目标黑洞、路由不可达; - 最后才怀疑 CONNECT 模块本身。
客户端日志只记 IP 不记域名时,无法判断"是不是 DNS 过期解析"。 建议客户端日志同时记录原始域名(实测案例:某 Dropbox 旧 IP 已失效, 域名现已解析到其它地址,客户端仍在拨旧 IP)。
6. sing-box(VLESS+REALITY / Shadowsocks)
6.1 安装
SINGBOX_VER=1.13.19
curl -LO https://github.com/SagerNet/sing-box/releases/download/v$SINGBOX_VER/sing-box-$SINGBOX_VER-linux-amd64.tar.gz
tar xzf sing-box-*.tar.gz
install -m 755 sing-box-$SINGBOX_VER-linux-amd64/sing-box /usr/local/bin/sing-box
6.2 配置
完整配置见 sing-box/config.json。要点:
- VLESS+REALITY:每节点独立 UUID/密钥对/short_id,SNI 全节点统一,flow
xtls-rprx-vision;REALITY handshake server 用<伪装SNI域名>:443 - Shadowsocks:
2022-blake3-aes-256-gcm(SIP022:会话子密钥 + 抗重放探测,显著优于老 AEADaes-256-gcm)+ 每节点独立 PSK,独立端口(兼容不支持 REALITY 的老客户端)- PSK 格式要求:密码必须是 base64 编码的 32 字节密钥(
openssl rand -base64 32),不是任意字符串;改错格式 sing-box 启动即报错 - sing-box / Shadowrocket / mihomo 均支持 2022-blake3 系列
- PSK 格式要求:密码必须是 base64 编码的 32 字节密钥(
- 无 SOCKS5 inbound(已按需移除;如需可在 inbounds 追加
type: socks)
6.3 REALITY 密钥生成
每台节点独立生成一套(互不共用):
# 生成私钥/公钥(sing-box 1.13+ 内置 reality 密钥生成)
sing-box generate reality-keypair
# 输出 private_key / public_key,记录备用
# short_id 用 8 字节随机 hex:
openssl rand -hex 8
# UUID:
sing-box generate uuid
私钥只放服务端配置;公钥给客户端订阅。UUID/密钥对/short_id 每节点独立。
公钥必须推导验证,禁止手抄。 订阅/节点配置里的 public_key 与服务端
private_key 必须严格配对(X25519 推导),曾发生手抄错一个字符导致全节点
REALITY 握手失败(客户端报 reality verification failed)的事故。改配置后验证:
python3 - << 'EOF'
import base64
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
from cryptography.hazmat.primitives import serialization
priv = base64.urlsafe_b64decode('<REALITY_PRIVATE_KEY>' + '==')
pub = X25519PrivateKey.from_private_bytes(priv).public_key().public_bytes(
serialization.Encoding.Raw, serialization.PublicFormat.Raw)
print(base64.urlsafe_b64encode(pub).decode().rstrip('=')) # 应与下发的 public_key 逐字符一致
EOF
7. mtg(MTProto 代理)
Telegram 内置代理,Fake TLS 模式抗封锁。用 mtg(Go 单二进制):
7.1 安装
MTG_VER=2.2.8
curl -LO https://github.com/9seconds/mtg/releases/download/v$MTG_VER/mtg-$MTG_VER-linux-amd64.tar.gz
tar xzf mtg-*.tar.gz
install -m 755 mtg-$MTG_VER-linux-amd64/mtg /usr/local/bin/mtg
7.2 生成 secret(每台独立)
mtg generate-secret --hex <伪装SNI域名>
# 输出 ee<32hex><域名hex>,以 ee 开头 = Fake TLS 类型
7.3 运行
mtg simple-run --prefer-ip=prefer-ipv4 0.0.0.0:<mtproto端口> <上面生成的secret>
--prefer-ip=prefer-ipv4:强制 IPv4 出站,避免容器/双栈环境下 IPv6 不可达导致回退失败。
7.4 Telegram 客户端参数
- 服务器:
<节点域名> - 端口:
<mtproto端口> - Secret:上面生成的 hex(Telegram 自动识别 Fake TLS)
8. 本机公网 IP 自动识别(防自代理回环)
正向代理转发时,若目标 Host 是本机公网 IP,会被识别为"本机直连"返回伪装页(而非经代理转发本机,造成回环)。公网 IP 可能变动,用 cron 脚本每 5 分钟自动维护。
脚本见 scripts/sync-nginx-local-ips.sh,cron 见 cron/sync-nginx-local-ips:
*/5 * * * * root /usr/local/sbin/sync-nginx-local-ips.sh >/dev/null 2>&1
脚本逻辑:取 api.ipify.org 出站 IP + 节点域名 A 记录,比对重写 /etc/nginx/local_ip_rules.conf,仅变更时 nginx -t && systemctl reload nginx。
9. systemd 服务文件
| 服务 | 文件 |
|---|---|
| nginx | systemd/nginx.service |
| sing-box | systemd/sing-box.service |
| mtg | systemd/mtg.service |
| uot-relay | uot-relay/uot-relay.service |
cp systemd/*.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now nginx sing-box mtg
10. 启动顺序与验证清单
启动顺序
sysctl --system # 1. BBR
systemctl start sing-box # 2. VLESS/SS
systemctl start mtg # 3. MTProto
systemctl start nginx # 4. nginx(最后,依赖证书/伪装页/规则文件)
systemctl start uot-relay # 5. UoT relay(须在 nginx 前可用)
验证清单
# BBR
sysctl net.ipv4.tcp_congestion_control # => bbr
sysctl net.ipv4.tcp_mtu_probing # => 1(PMTUD 黑洞缓解,见 2.4)
# fail2ban
fail2ban-client status sshd # => jail 存在(见 2.5)
# sing-box
sing-box check -c /etc/sing-box/config.json
systemctl is-active sing-box
# mtg(Fake TLS:非法 ClientHello 应返回 TLS alert 0x15)
timeout 8 python3 -c "
import socket,os,struct
s=socket.connect(('<节点域名>',<mtproto端口>)) if False else socket.create_connection(('<节点域名>',<mtproto端口>),timeout=6)
s.sendall(b'\x16\x03\x01'+struct.pack('>H',100)+os.urandom(100))
print('resp:',s.recv(8).hex())" # 期望首字节 15 (TLS alert)
# UoT relay(127.0.0.1:15201 监听 + CONNECT magic 地址)
ss -tlnp | grep 15201 # => uot_relay.py 监听 127.0.0.1:15201
# nginx CONNECT(带 Basic Auth,B64 = base64('<代理用户名>:<代理密码>'))
B64=$(printf '<代理用户名>:<代理密码>' | base64 -w0)
curl -sk --proxy-insecure -x https://<节点域名>:<nginx端口> \
--proxy-header "Authorization: Basic $B64" \
-o /dev/null -w '%{http_code}\n' http://example.com/ # => 200
# 伪装页(直连 IP/域名,无认证)
curl -sk https://<节点域名>:<nginx端口>/ -o /dev/null -w '%{http_code}\n' # => 200
11. 目录结构与文件索引
server_config/
├── README.md ← 本文档
├── nginx/
│ ├── nginx.conf ← 完整 nginx 配置(占位符版)
│ ├── local_ip_rules.conf.example ← 本机 IP 规则文件示例(cron 自动生成,勿手改)
│ └── conf.d/ ← 预留:额外站点配置
├── uot-relay/
│ ├── uot_relay.py ← UoT v2 relay(UDP over TCP 解封装)
│ └── uot-relay.service ← systemd unit
├── sing-box/
│ └── config.json ← sing-box 配置(VLESS+REALITY + SS,占位符版)
├── systemd/
│ ├── nginx.service
│ ├── sing-box.service
│ └── mtg.service
├── scripts/
│ └── sync-nginx-local-ips.sh ← 本机公网 IP 规则自动维护
├── cron/
│ └── sync-nginx-local-ips ← cron 定义(每 5 分钟)
└── fake-site/
└── index.html ← 伪装站点页面(所有节点共用同一份)
旧的
traffic-monitor/、pac/、conf.d/*.conf已下线或迁移,不再维护,已从本目录移除。