Files
Private-Proxy/server_config/README.md
T
RoyirinRosmontis 1cc4da8318 server_config: 如实记录 UDP 接入现状(服务端已部署、订阅不下发、无 fallback 组)
- §6.4 新增「接入现状」表:订阅无 hy2/tuic、代理组仅 select、lite 客户端仅 HTTP CONNECT+UoT、UDP 目前只能手工导入
- 记录日后接入订阅所需三步(nodes.json 字段 / app.py 生成 / fallback 组 REALITY 优先)及 fallback 非瞬时切换的注意点
2026-09-20 22:10:20 +08:00

833 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RRPP 服务端部署规范
> 一套统一的跨境代理节点部署流程,适用于 Debian/Ubuntu 系 Linux。
> 每台节点运行相同的服务栈:**BBR 拥塞控制 + nginx CONNECT 隧道 + sing-box(VLESS+REALITY / Shadowsocks) + mtg(MTProto)**。
>
> 本文档为**通用规范**,所有 IP / 域名 / 端口 / 密钥均以**占位符**表示,部署时替换为实际值。
> 凡标注 `<...>` 的均为占位符,不要原样照抄。
---
## 目录
- [0. 架构总览](#0-架构总览)
- [1. 端口与服务规划](#1-端口与服务规划)
- [2. 系统基础优化(BBR 跨境丢包优化)](#2-系统基础优化bbr-跨境丢包优化)
- [3. 伪装站点页面](#3-伪装站点页面)
- [4. HTTPS 证书(acme.sh DNS-01)](#4-https-证书acmesh-dns-01)
- [5. nginx 源码编译 + CONNECT 隧道](#5-nginx-源码编译--connect-隧道)
- [6. sing-box(VLESS+REALITY / Shadowsocks)](#6-sing-boxvlessreality--shadowsocks)
- [7. mtg(MTProto 代理)](#7-mtgmtproto-代理)
- [8. 本机公网 IP 自动识别(防自代理回环)](#8-本机公网-ip-自动识别防自代理回环)
- [9. systemd 服务文件](#9-systemd-服务文件)
- [10. 启动顺序与验证清单](#10-启动顺序与验证清单)
- [11. 目录结构与文件索引](#11-目录结构与文件索引)
- [12. RRPP 订阅后端(PAC → 规则集下发)](#12-rrpp-订阅后端pac--规则集下发)
---
## 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 的客户端 || Telegram 专用 | mtg | MTProto Fake TLS | Telegram 内置代理,抗封锁 |
**核心密钥体系(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`:
```conf
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr
```
应用并验证:
```bash
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
代理证书校验、日志排障都依赖准确时间:
```bash
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`:
```conf
net.ipv4.tcp_mtu_probing = 1
```
```bash
sysctl --system
sysctl net.ipv4.tcp_mtu_probing # 期望输出: 1
```
> 只影响命中黑洞的连接,正常连接无感知,可安全全节点开启。
### 2.5 fail2ban(sshd 爆破防护)
节点 SSH 暴露在公网,爆破尝试是常态(实测单节点累计 3 万+ 次)。装 fail2ban 护 sshd:
```bash
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(凭据够强时可不做)。
### 2.6 系统级网络优化(KVM / LXC 差异)与 qdisc 持久化
> 模板:[sysctl/99-zz-rrpp-network.kvm.conf](sysctl/99-zz-rrpp-network.kvm.conf)(完整 VM,含 Azure 等)、
> [sysctl/99-zz-rrpp-network.lxc.conf](sysctl/99-zz-rrpp-network.lxc.conf)(容器)
> 落地位置统一为 `/etc/sysctl.d/99-zz-rrpp-network.conf`;
> qdisc 伴侣:[systemd/rrpp-qdisc.service](systemd/rrpp-qdisc.service) + [scripts/rrpp-qdisc.sh](scripts/rrpp-qdisc.sh)。
```bash
# KVM / 完整 VM
install -m 644 sysctl/99-zz-rrpp-network.kvm.conf /etc/sysctl.d/99-zz-rrpp-network.conf
# LXC / OpenVZ 容器
install -m 644 sysctl/99-zz-rrpp-network.lxc.conf /etc/sysctl.d/99-zz-rrpp-network.conf
# qdisc 持久化(仅完整 VM 有意义;容器内网卡是 noqueue,脚本会自动跳过)
install -m 755 scripts/rrpp-qdisc.sh /usr/local/sbin/rrpp-qdisc.sh
install -m 644 systemd/rrpp-qdisc.service /etc/systemd/system/
systemctl daemon-reload && systemctl enable --now rrpp-qdisc
sysctl --system
```
**为什么文件名是 `99-zz-`**:sysctl 按文件名字典序加载、**后者覆盖前者**。`99-zz-` 保证本文件排在最后,
避免被其它 `99-*.conf` 覆盖。
**⚠ 最容易踩的坑:`/etc/sysctl.conf`**。`sysctl --system` 会在**所有 sysctl.d 之后**再加载 `/etc/sysctl.conf`。
2026-09 实测:某节点(aq)存在 2022 年的一键优化版 `/etc/sysctl.conf`,把 `tcp_mtu_probing` 覆盖回 `0`,
导致 sysctl.d 里的设置完全不生效(现象:`sysctl -n` 与文件内容不一致)。
**处理**:该文件里凡是与本规范重叠的键,逐行注释掉(保留其非冲突的调优,如 conntrack、rps、swappiness),
或直接把本规范内容追加到该文件末尾。排查方法:
```bash
sysctl -p /etc/sysctl.d/99-zz-rrpp-network.conf # 显式加载后应看到期望值
sysctl --system 2>/dev/null | grep -n tcp_mtu_probing | tail -5 # 看谁在最后覆盖
```
**KVM / 完整 VM 可写(本规范设置项)**
| 项 | 值 | 作用 |
|---|---|---|
| `tcp_congestion_control` / `core.default_qdisc` | `bbr` / `fq` | BBR 需 fq 搭配,否则 bufferbloat |
| `tcp_mtu_probing` | 1 | PMTUD 黑洞缓解(见 2.4) |
| `tcp_fastopen` | 3 | 客户端+服务端 TFO,省一次 RTT |
| `tcp_slow_start_after_idle` | 0 | 空闲后不回退慢启动(长连接代理有益) |
| `tcp_tw_reuse` / `tcp_fin_timeout` | 2 / 15 | TIME-WAIT 复用与回收 |
| `tcp_keepalive_time/intvl/probes` | 300 / 30 / 5 | 及时清理死连接(默认 7200 过长) |
| `ip_local_port_range` | 10240 65000 | 出站端口范围,防高并发端口耗尽 |
| `core.somaxconn` / `tcp_max_syn_backlog` | 32768 | accept / 半连接队列 |
| `core.netdev_max_backlog` | 32768 | 网卡收包队列(默认 1000 易突发丢包) |
| `core.rmem_max` / `wmem_max` | 33554432 | socket 缓冲上限(默认 208KB 偏小) |
| `tcp_rmem` / `tcp_wmem` | 4K/128K/32M、4K/16K/32M | 自动调优范围 |
**LXC / OpenVZ 容器不可用(由宿主机控制,实测 2026-09)**
| 项 | 现象 |
|---|---|
| `core.rmem_max` / `core.wmem_max` | **不存在**或 `-r--r--r--` 只读 |
| `core.default_qdisc` | **不存在**(`sysctl` 报 cannot stat) |
| `core.netdev_max_backlog` | 缺失 |
| 默认路由网卡根 qdisc | `noqueue`,**无法设置 fq** |
| `/usr/lib/sysctl.d` 的 `kernel.pid_max`、`fs.protected_*` | `permission denied`——容器内属**正常现象**,与本文档无关 |
> 结论:容器节点只能调 **netns 级**项(拥塞算法、MTU 探测、TFO、TW 复用、keepalive、
> somaxconn、syn backlog、本地端口范围);socket 缓冲上限与 qdisc 需服务商在**宿主机**设置。
> 因此容器节点想进一步提升,应优先在**应用层/传输层**做(例如走 UoT 而不是直连 UDP)。
**qdisc 为什么必须单独用 systemd 单元**:`net.core.default_qdisc=fq` **只对新建网卡生效**;
已存在的网卡即使重启也可能仍是 `pfifo_fast` / `fq_codel`(实测:aq 为 `pfifo_fast`、us 为 `fq_codel`)。
`rrpp-qdisc.service` 在开机后显式执行 `tc qdisc replace dev <默认路由网卡> root fq` 补齐;
脚本对容器(`noqueue`)会自动跳过。
**UDP 策略(2026-09-20 修订)**:节点**已启用 UDP 传输**(sing-box 内建 **Hysteria2** 与 **TUIC v5** 入站,见 §6.4),
用于对抗跨境链路的丢包/高 RTT;**同时保留 UoT v2** 路径(见 §5.5)——客户端 TUN 模式的 UDP
仍可封装进 TCP CONNECT 隧道,两条路并存、互不影响。
节点侧 UDP 相关系统调优(与 6.4 配套):
| 项 | 值 / 说明 |
|---|---|
| `net.ipv4.udp_rmem_min` / `udp_wmem_min` | `8192`(容器内可写,实测 LXC 同样生效) |
| `net.core.rmem_max` / `wmem_max` | `33554432`(KVM 可写;**LXC 缺失/只读**) |
| `rx-udp-gro-forwarding` / `rx-gro-list`(ethtool) | 由 `rrpp-qdisc.sh` 开机开启;**实测支持情况**:us/jp ✅ 两项;aq(内核 5.10)仅 `rx-gro-list`;**LXC 容器无 netdev 权限,无法设置** |
| QUIC/UDP 吞吐 | 受宿主机 socket 缓冲上限约束——**LXC 节点上这是硬约束**,需服务商在宿主调 `rmem_max` |
**明确不采用的手段**:锐速/LotServer(闭源、绑定旧内核)、net-speeder(重复包、已废弃)、
`tcp_tw_recycle`(4.12 起内核已移除,开启会破坏 NAT 后客户端)、`tcp_notsent_lowat` 激进值
(可能压制高 BDP 吞吐)、`busy_poll`/`busy_read`(1 vCPU 机器上代价大于收益)、各类"一键优化脚本"
(多为本文档子集 + 心理安慰,且常与 sysctl.d 冲突)。
**落地状态(2026-09-20)**
| 节点 | 虚拟化 | 落地情况 |
|---|---|---|
| hk2 | LXC(Debian 12) | LXC 模板;`bbr` + `mtu_probing=1` + `fastopen=3` + 队列/端口项;缓冲与 qdisc 受宿主限制 |
| ru | LXC(Debian 13 cloud 内核) | 同上(`rmem_max`/`wmem_max` 只读) |
| us | KVM(Debian 12) | KVM 模板 + fq(原 `fq_codel`) |
| aq | KVM(Debian 11 cloud) | KVM 模板 + fq(原 `pfifo_fast`);并注释其 `/etc/sysctl.conf` 中 17 行冲突键(原 `tcp_mtu_probing=0`) |
| jp | Azure VM(Ubuntu 6.17) | KVM 模板(经 `sudo` 落地;原有 `99-tune.conf` 保留) |
---
## 3. 伪装站点页面
直接访问节点/IP 时返回一个普通网页(而非代理错误),降低被探测的特征。
放置一个静态 `index.html` 到 `/var/www/html/index.html`:
- 任意抓取的合法 HTML 页面(博客/门户首页快照均可)
- **所有节点用同一份**(md5 一致),避免逐节点特征差异
```bash
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](https://github.com/acmesh-official/acme.sh) + DNS-01(以阿里云 DNS 为例,其他 DNS 商同理):
```bash
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](https://github.com/chobits/ngx_http_proxy_connect_module) 补丁后**源码编译**。
### 5.1 编译
```bash
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:
```bash
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/.htpasswd`
> htpasswd 是**每次请求实时读取**的,改密码无需 reload nginx。
### 5.3 nginx.conf
完整配置见 [nginx/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 变更、或代理目标就是本节点),会无限循环。两层防护:
1. `map` 判定私网/回环段 + cron 维护的本机公网 IP 规则 → 直连伪装页
2. `/__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:
```bash
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](uot-relay/uot_relay.py)):
```bash
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](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 全节点部署验证)**:
1. 该模块版本**未注册 `$connect_host` 变量**(README 有、源码无)→ 必须用
`$request` 正则匹配(任何阶段可用);
2. `proxy_connect_data_timeout` 与 `proxy_connect_read_timeout` **写同一配置槽**,
同时写报 duplicate → 长连接超时用 read/send timeout 设 10m;
3. **所有 CONNECT 请求都落在 `default_server`(fallback)块**——nginx 按请求
Host 头选虚拟主机,CONNECT 的 Host 是目标域名,永远匹配不到 `server_name`;
4. 模块**拒绝 CONNECT 端口 0**(`port < 1` 返回 400)→ 客户端连 magic 地址时
端口必须非 0(用 443 即可,relay 不读 CONNECT 端口,目标取自 UoT 帧头)。
**健壮性要求(2026-09 实测修复,别踩)**:
- relay 必须用**未连接**的 UDP socket(`sendto`/`recvfrom`)。用连接式 socket 时,
目标侧 ICMP(port unreachable 等)会变成 `ConnectionRefusedError` 在 send/recv 上抛出,
若被兜底 `except` 接住并 `stop.set()`,**一条数据报出错就杀掉整条 UoT 隧道**
(客户端表现为 "UoT TCP 流已关闭"、多条隧道同秒集体死亡)。
- 数据报级错误(`BlockingIOError`/EAGAIN、`ENETUNREACH`、`EHOSTUNREACH`、`EMSGSIZE`)
**只丢当前包**,EAGAIN 先等可写再重试;只有 TCP EOF、协议错误、空闲超时才关隧道。
- 按地址族惰性建 socket,non-connect 模式可混用 IPv4/IPv6 目标;IPv6 `recvfrom`
返回 4 元组需归一化。
- connect 模式要**预建** socket(否则主循环首轮无可 select 对象,会立刻关连接)。
**节点出口差异(实测)**:RU 节点到境外 DNS(8.8.8.8:53)UDP 成功率仅约 4/10
(俄 ISP 限制),表现为 UoT DNS 偶发超时——属该节点出口特性,非 relay 故障。
此类节点的 UDP 业务建议换用当地可达目标,或走其它节点。
**客户端协议**:认证与 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"的正确顺序**(重要):
1. 先看目标是否**本身就不可达**——从多个网络(国内/日本/美国/香港)实测该 IP:端口,
若都失败则是目标问题(服务下线/过期 IP/黑洞),不是代理问题;
2. 再看 nginx error.log:`Connection refused` / `timed out` / `Network is unreachable`
分别对应目标拒绝、目标黑洞、路由不可达;
3. 最后才怀疑 CONNECT 模块本身。
> 客户端日志只记 IP 不记域名时,无法判断"是不是 DNS 过期解析"。
> 建议客户端日志同时记录原始域名(实测案例:某 Dropbox 旧 IP 已失效,
> 域名现已解析到其它地址,客户端仍在拨旧 IP)。
## 6. sing-box(VLESS+REALITY / Shadowsocks)
### 6.1 安装
```bash
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](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:会话子密钥 + 抗重放探测,显著优于老 AEAD `aes-256-gcm`)+ 每节点独立 PSK,独立端口(兼容不支持 REALITY 的老客户端)
- **PSK 格式要求**:密码必须是 base64 编码的 32 字节密钥(`openssl rand -base64 32`),不是任意字符串;改错格式 sing-box 启动即报错
- sing-box / Shadowrocket / mihomo 均支持 2022-blake3 系列
- 无 SOCKS5 inbound(已按需移除;如需可在 inbounds 追加 `type: socks`)
### 6.3 REALITY 密钥生成
**每台节点独立生成一套**(互不共用):
```bash
# 生成私钥/公钥(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`)的事故。改配置后验证:
```bash
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
```
---
### 6.4 Hysteria2 / TUIC(UDP 传输,2026-09-20 起全节点启用)
TCP 系协议在跨境丢包链路上受拥塞控制限制(BBR 已缓解但仍有天花板);QUIC 系
(**Hysteria2** 的 Brutal CC、**TUIC v5**)能显著改善高丢包下的吞吐与时延。
两者都是 **sing-box 内建**,无需额外二进制;UDP 需求仍可走 UoT(§5.5),两条路并存。
```json
// Hysteria2(sing-box inbound)
{ "type": "hysteria2", "tag": "hy2-in-<端口>", "listen": "0.0.0.0", "listen_port": "<端口>",
"up_mbps": 100, "down_mbps": 100,
"obfs": { "type": "salamander", "password": "<obfs密码>" },
"users": [ { "name": "<节点>-hy2", "password": "<hy2密码>" } ],
"tls": { "enabled": true, "server_name": "<节点域名>",
"certificate_path": "/etc/nginx/ssl/<节点域名>/fullchain.pem",
"key_path": "/etc/nginx/ssl/<节点域名>/privkey.pem", "alpn": ["h3"] },
"masquerade": "file:///var/www/html" }
// TUIC v5
{ "type": "tuic", "tag": "tuic-in-<端口>", "listen": "0.0.0.0", "listen_port": "<端口>",
"users": [ { "name": "<节点>-tuic", "uuid": "<UUID>", "password": "<密码>" } ],
"congestion_control": "bbr",
"tls": { /* 同上 */ } }
```
**要点**
- **带宽声明决定 Brutal**:`up_mbps`/`down_mbps` 不写时,服务端会命令客户端改用 BBR;
写了才允许 Hysteria CC(Brutal)。默认 100 Mbps,计量流量的节点给 50。
Brutal **不遵守拥塞控制**,设错会占满链路甚至被服务商判定异常流量——务必按实际带宽设。
- `obfs: salamander` 给 QUIC 加一层混淆(对抗 QUIC 特征识别);`masquerade` 指向伪装页,
未认证访问返回与 nginx 一致的页面。
- TUIC 保持 `zero_rtt_handshake` 关闭(默认,0-RTT 可被重放)。
- 凭据**每节点独立**,与 VLESS/SS/CONNECT 同策略。
**各节点端口与部署差异(2026-09-20 实测,务必照抄结论)**
| 节点 | 虚拟化 | Hysteria2 (UDP) | TUIC (UDP) | 差异 / 坑 |
|---|---|---|---|---|
| hk2 | LXC(NAT) | 4305 | 4306 | NAT 段 4301-4320 UDP 1:1(探针 + 外部回扫实测) |
| us | KVM | 1208 | 1209 | 公网直连,UDP 任意端口可用 |
| ru | LXC(NAT) | 23333 | 23334 | **外部只有宿主已映射的 UDP 端口可达**:容器内 bind 任意端口都"正常",但 23336+ 实测直接 `connection refused`(ICMP port unreachable);最终复用 **23333/23334**——与 nginx/vless 占用的 **TCP 同号**,UDP 与 TCP 互不冲突 |
| aq | KVM(IPv6-only) | 8444 | 8445 | 节点为 IPv6 出口,`listen` 用 `"::"`;客户端须走 IPv6 |
| jp | Azure VM | 未部署 | 8445 | **Azure NSG 只放行 UDP 8445**(443/80/8080/8088/18080/8443/9444 的 UDP 面实测均不可达)→ 单节点只能承载一个 UDP 协议;要加 Hysteria2 须先在 NSG 放行第二个 UDP 端口 |
**客户端链接格式(手工导入用)**
```
hysteria2://<password>@<域名>:<端口>/?sni=<域名>&alpn=h3&obfs=salamander&obfs-password=<obfs密码>#备注
tuic://<uuid>:<password>@<域名>:<端口>/?congestion_control=bbr&alpn=h3&sni=<域名>#备注
```
**接入现状(2026-09-20,务必先读)**
| 层 | 状态 |
|---|---|
| 服务端 | ✅ 五节点已部署并端到端验证(见上表) |
| **订阅下发** | ❌ **不下发 UDP**——`app.py` 无 hysteria2/tuic 生成逻辑,`nodes.json` 无对应字段;订阅里只有 http / vless / ss |
| **代理组** | ❌ 订阅只生成一个 `RRPP`(`type: select`,手动选节点),**没有 fallback / url-test**,因此**不存在"UDP 挂了自动回落 TCP"** |
| lite 客户端(Windows) | 仅 HTTP CONNECT + UoT v2(UDP 走 magic 地址),**不使用 UDP 入站**,且节点为手动切换、无自动故障转移 |
| 结论 | UDP 通道目前**只能手工导入**上面的链接使用;抗封主路径仍是 **REALITY+Vision(TCP)**,UDP 属可选加速,未接入客户端链路 |
> 若日后要接入订阅,需要三步:① `nodes.json` 每节点补 `methods.hysteria2` / `methods.tuic`;
> ② `app.py` 增加 mihomo `type: hysteria2` / `type: tuic` 生成与分享链接;
> ③ 代理组改为**每节点一个 `fallback` 组、REALITY 排第一**(否则"UDP 挂了回落 TCP"不成立)。
> 注意 mihomo 的 fallback 依赖健康检查周期(默认 300s),切换不是瞬时的。
**验证方法(端到端,不能只看监听)**
```bash
# 在任意机器用 sing-box 作客户端:socks 入站 + 上述 outbound,然后
curl -s -m 20 -x socks5h://127.0.0.1:11080 -o /dev/null -w '%{http_code}\n' http://example.com/ # 期望 200
```
`ss -lunp` 看到端口在听**不足以**证明外部可达:ru(NAT 未映射)与 jp(NSG 未放行)
都是"监听正常但外部不可达"的真实案例。
⚠️ 代价与限制:UDP 在部分运营商会被 QoS/封禁;Brutal 对同链路其他流量不友好;
计量流量的节点(如 jp,15GB/月)注意消耗;LXC 容器内 socket 缓冲上限受宿主机约束。
---
## 7. mtg(MTProto 代理)
Telegram 内置代理,Fake TLS 模式抗封锁。用 [mtg](https://github.com/9seconds/mtg)(Go 单二进制):
### 7.1 安装
```bash
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(每台独立)
```bash
mtg generate-secret --hex <伪装SNI域名>
# 输出 ee<32hex><域名hex>,以 ee 开头 = Fake TLS 类型
```
### 7.3 运行
```bash
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](scripts/sync-nginx-local-ips.sh),cron 见 [cron/sync-nginx-local-ips](cron/sync-nginx-local-ips):
```cron
*/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](systemd/nginx.service) |
| sing-box | [systemd/sing-box.service](systemd/sing-box.service) |
| mtg | [systemd/mtg.service](systemd/mtg.service) |
| uot-relay | [uot-relay/uot-relay.service](uot-relay/uot-relay.service) |
| rrpp-qdisc(fq 队列,见 2.6) | [systemd/rrpp-qdisc.service](systemd/rrpp-qdisc.service) |
```bash
cp systemd/*.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now nginx sing-box mtg
```
---
## 10. 启动顺序与验证清单
### 启动顺序
```bash
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 前可用)
```
### 验证清单
```bash
# BBR
sysctl net.ipv4.tcp_congestion_control # => bbr
sysctl net.ipv4.tcp_mtu_probing # => 1(PMTUD 黑洞缓解,见 2.4)
# 系统级网络优化(见 2.6):确认 sysctl.d 的值真的生效(注意 /etc/sysctl.conf 覆盖坑)
sysctl -n net.core.somaxconn net.ipv4.tcp_max_syn_backlog net.ipv4.tcp_fastopen \
net.ipv4.tcp_slow_start_after_idle net.ipv4.ip_local_port_range
# KVM/完整 VM 追加:net.core.netdev_max_backlog / rmem_max / wmem_max
tc qdisc show | grep -v noqueue # 期望默认路由网卡为 fq(容器内为 noqueue,属正常)
systemctl is-active rrpp-qdisc # 期望 active(容器内该单元会自行跳过)
# 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
# UDP 传输(Hysteria2/TUIC,见 6.4)——必须端到端验证,不能只看监听
ss -lunp | grep -E ':<hy2端口>|:<tuic端口>' # 仅确认服务在听
# 再用 sing-box 客户端 + curl 经由 socks 实测(见 6.4「验证方法」)
ethtool -k <网卡> | grep -E 'rx-udp-gro-forwarding|rx-gro-list' # KVM 期望 on;LXC 容器不可设
# 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 ← 本文档
├── sysctl/
│ ├── 99-zz-rrpp-network.kvm.conf ← 系统级网络优化(KVM/完整 VM,见 §2.6)
│ └── 99-zz-rrpp-network.lxc.conf ← 系统级网络优化(LXC/OpenVZ 容器,见 §2.6)
├── 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
│ └── rrpp-qdisc.service ← 开机把默认网卡 qdisc 设为 fq(见 §2.6)
├── scripts/
│ ├── sync-nginx-local-ips.sh ← 本机公网 IP 规则自动维护
│ └── rrpp-qdisc.sh ← fq 队列设置脚本(rrpp-qdisc.service 调用)
├── cron/
│ └── sync-nginx-local-ips ← cron 定义(每 5 分钟)
├── rrpp-subscription/ ← RRPP 订阅后端(bj,见 §12)
│ ├── API.md ← 订阅端点 / method / 分层 / 规则集文档
│ └── gen_rulesets.py ← PAC → 客户端规则集转换脚本
└── fake-site/
└── index.html ← 伪装站点页面(所有节点共用同一份)
```
> 旧的 `traffic-monitor/`、`pac/`、`conf.d/*.conf` 已下线或迁移,不再维护,已从本目录移除。
---
## 12. RRPP 订阅后端(PAC → 规则集下发)
订阅服务运行在 **bj**(`C:/BtSoft/nginx/others/backEnd/api-rrrosmontis-icu/RRPP/`,nssm 服务 `RRPPSubBackend`,监听 `127.0.0.1:5010`,由 nginx `api.rrrosmontis.icu` 反代)。
完整端点说明见 `rrpp-subscription/API.md`(bj 上同名文件为主副本,两处内容保持一致)。
### 12.1 PAC 为什么不能直接下发
PAC 是浏览器/系统用的 JS 自动配置,**mihomo/Clash 与 Shadowrocket 都读不了**。两者唯一共同能吃的格式是
「每行自带规则类型的规则行文本」(Clash 的 `behavior: classical`):
- mihomo → `rule-providers`(`behavior: classical` + `format: text`)
- Shadowrocket → `RULE-SET,<url>,<策略>`(官方手册:规则集内容需包含规则类型)
因此由后端 `gen_rulesets.py` 把 PAC 数据转成三个 `.list`,两种客户端共用同一份文件。
### 12.2 规则集产物
| 规则集 | 文件(`https://cdn.rrrosmontis.icu/RRPP/PAC/`) | 规则数 | 策略 |
|---|---|---|---|
| `rrpp-proxy` | `rrpp-proxy.list` | 3943(`DOMAIN` 539 + `DOMAIN-SUFFIX` 3404) | `RRPP` / `PROXY` |
| `rrpp-direct` | `rrpp-direct.list` | 16(`DOMAIN-SUFFIX`) | `DIRECT` |
| `rrpp-cn-ip` | `rrpp-cn-ip.list` | 7456(`IP-CIDR`) | `DIRECT` |
顺序(与 PAC 语义一致):强制代理 → 直连种子 → 中国 IP 直连 → `MATCH` 全代理。
### 12.3 维护:PAC 更新后重新生成
```bash
# bj 上执行(Windows:C:/BtSoft/nginx/others/htmlResources/cdn/RRPP/PAC/ 为生成目录,同时即 CDN 根)
python gen_rulesets.py # 幂等:内容无变化则不写盘;原子替换
python gen_rulesets.py --check # 只预览差异,不写盘
```
产物文件名与 URL 固定,**订阅端与客户端均无需改动**:mihomo 按 `interval: 86400` 自动更新,
Shadowrocket 在「配置 → 规则集URL」中自动更新。
### 12.4 客户端接入
```bash
# mihomo / Clash Verge:规则走 rule-providers(YAML 约 2.4KB)
...&method=clash
# 不认 rule-providers,或要「导入一次到位」:规则内联(约 440KB)
...&method=clash&rules=inline
# Shadowrocket:只下发规则([General] + 远程 RULE-SET + FINAL,不含节点)
...&method=shadowrocket
# 兜底:全流量走代理(旧行为)
...&method=clash&rules=global
```
> `method=shadowrocket` 的 `.conf` 不写 `[Proxy]` 段——Shadowrocket 的 REALITY 参数写法官方未文档化,不猜测;
> 策略 `PROXY` 指向首页当前选中的节点,节点用 `clash`/`v2ray` 订阅导入。
> 想在小火箭里「一次导入节点+规则」,用 `method=clash&rules=inline`。
### 12.5 验证口径
- 生成:11415 条规则(`regexp:` 跳过 7、非法 1)
- CDN:三个 URL HTTP 200,md5 与本地一致(`806808b8` / `8f89397e` / `3e75093d`)
- mihomo v1.19.31 真实内核:`-t` 通过;运行时 `GET /providers/rules` 报 `3943 / 16 / 7456`(`behavior=Classical`),
落地文件字节数与生成一致;`rules=inline` 亦通过 `-t`
- Shadowrocket 为 iOS 应用,未做实机验证(依官方手册 `RULE-SET` 与 Clash YAML 兼容说明实现)