Hub 代理功能设计:从订阅节点到可故障切换的 HTTP CONNECT¶
背景¶
代理功能真正难的部分不是“启动一个代理进程”,而是把不稳定、格式不一且包含敏感信息的订阅节点,变成一个可管理、可观测、可恢复的运行时系统。
Hub 的代理插件把这条链路拆成四层:
- 订阅层:安全拉取和解析订阅内容。
- 数据层:加密保存订阅 URL 与完整节点配置,用指纹维护稳定身份。
- 运行时:为实际使用的节点按需启动 Xray。
- 转发层:对外提供 HTTP CONNECT,并在节点之间重试和切换。
一、总体架构¶
订阅 URL
→ URL 校验 / SSRF 防护 / 重定向检查
→ 拉取文本 / 大小限制 / Base64 解码
→ URI 解析 / 协议归一化 / fingerprint
→ 节点与分组持久化
→ 按需启动独立 Xray SOCKS 入站
→ GroupForwarder 接收 HTTP CONNECT
→ SOCKS5 握手 / 节点重试 / 首选节点提升
→ 目标站点
代理作为独立服务 hub-proxy 通过 manifest.json 注册接入 Hub,使用 core SDK 的统一数据库、日志和缓存能力。配置数据和运行时状态分离:数据库保存“应该如何运行”,内存对象和子进程保存“现在正在运行什么”。
二、订阅解析:先把输入变成稳定内部模型¶
订阅内容存在两种常见形态:
- 每行一个节点 URI
- 整段 Base64 编码后再传输
解析器先判断并解码订阅体,再逐行调用节点解析器。当前支持:
不支持的协议不会被假装解析成可用节点,而是保留为 unknown,便于用户知道订阅里确实存在无法处理的内容;运行时则不会把它交给 Xray。
这种“解析层保留事实、运行层过滤能力”的做法,比直接丢弃异常行更容易排查订阅问题。
三、用 fingerprint 解决节点顺序变化¶
订阅刷新时,节点顺序可能变化,名称也可能被供应商修改。如果用数组下标关联节点,刷新一次就可能把历史测速和分组关系挂到另一个节点上。
Hub 为节点敏感配置计算 SHA-256 指纹:
刷新流程变成:
稳定 ID 的价值不只在数据库关系,还包括:
- Xray 进程池复用
- 测速结果关联
- 分组成员关系
- 首选节点状态
- 故障记录和运维排查
指纹不能包含订阅 URL、密码或完整 URI 等不必要的可逆信息;它的用途是稳定比较,不是秘密存储。
四、敏感信息保护:加密保存,不把密文当作鉴权¶
代理功能至少有两类敏感数据:
- 订阅 URL
- 节点完整配置,例如 UUID、密码、传输参数
Hub 使用 AES-256-GCM 保存这些内容,存储格式为:
密钥来自部署环境中的加密密钥配置。生产环境缺少有效密钥时,应拒绝继续处理敏感数据,而不是生成临时密钥,因为临时密钥会让重启后所有历史数据无法解密。
需要区分两件事:
- 加密存储:降低数据库泄露后的直接暴露风险。
- 访问控制:限制谁能读取、刷新、启动和使用代理。
加密并不会自动解决日志泄密、进程内存暴露、错误堆栈打印完整 URI 等问题,因此日志中只能记录订阅 ID、节点 ID、协议和脱敏地址。
五、订阅拉取的 SSRF 防护¶
订阅 URL 是服务端主动发起请求的入口,不能把它当作普通的 fetch(url)。攻击者可能利用它访问本机管理接口、容器元数据、内网数据库或云平台保留地址。
Hub 的拉取函数需要完成以下检查:
- 只允许
http:和https:。 - 拒绝 URL 中的用户名和密码。
- DNS 解析后检查所有地址。
- 阻止 loopback、私网、链路本地和保留地址。
- 手动处理重定向,每一跳重新检查目标。
- 限制重定向次数、请求超时和响应体大小。
可抽象为:
当前约束包括 15 秒超时、2 MiB 响应上限和最多 3 次重定向。默认拒绝解析到 loopback、私网、链路本地和保留地址的订阅;受控测试环境可以显式设置 HUB_PROXY_ALLOW_PRIVATE_SUBSCRIPTIONS=true 放宽这一限制,但不应作为公网部署默认值。关键原则是“检查最终连接地址,而不是只检查用户输入的域名”。DNS rebinding、恶意重定向和 IPv4/IPv6 混用都必须纳入测试。
六、Xray 进程池:按节点隔离运行时¶
每个可用节点按需启动一个独立 Xray 子进程,并给它分配 loopback SOCKS 端口。
节点 fingerprint
→ ManagedXrayPool.ensureNode()
→ 生成临时 Xray 配置
→ 配置文件权限 0600
→ xray -test 校验
→ 启动子进程
→ 等待 SOCKS 入站可用
→ 删除临时配置
→ 返回 socksPort
独立进程的好处:
- 单节点崩溃不会直接拖垮转发器。
- 每个节点有清晰的端口和生命周期。
- 运行时可以单独重启失效节点。
- 节点配置不会混在一个不断膨胀的全局配置中。
临时配置必须使用 0600,启动后尽快删除。删除配置文件不等于进程不再持有敏感信息,因此仍需避免 core dump、调试日志和错误信息泄露。
进程池还需要处理:
- 启动超时
- 端口冲突
- Xray 配置校验失败
- 子进程异常退出
- 节点被删除后的回收
- 分组停止时的批量清理
6.1 Xray 运行时管理(2026-08-12 更新:已移除 Docker 控制)¶
早期版本通过 docker-control.ts 调 Docker CLI 承载 Xray 容器;重构后改为直接管理 xray-core 子进程,不再依赖 Docker:
xray-config.ts:把解析后的节点(ParsedNode)生成为 xray-core 26.x 的config.json。默认入口127.0.0.1:1111(SOCKS5)+127.0.0.1:1112(HTTP CONNECT),路由保持简单、不做复杂规则;不兼容节点(xrayCompatible=false)直接拒绝生成配置。xray-process.ts:使用spawn启动/管理单个 xray-core 子进程(关闭 shell 解释),提供状态查询(running/stopped/missing)、进程号与启动时间、500 行环形日志缓冲;assertInside(stateDir, file)用路径约束阻止把HUB_PROXY_STATE_DIR之外的任意文件作为配置挂载/加载。- 进程池(
xray-pool.ts)仍负责端口分配、启动超时、异常退出回收、节点删除与分组停止时的批量清理,以及空闲回收(HUB_PROXY_XRAY_IDLE_TTL_MS)与进程数上限(HUB_PROXY_XRAY_MAX_PROCESSES)。
Docker 控制层移除后,HUB_PROXY_STATE_DIR 仍是敏感文件与配置文件的唯一允许目录;日志与错误输出统一经过 log-sanitizer.ts 脱敏(sanitizeProxyError / sanitizeProxyTarget),避免完整订阅 URL 与节点凭据进入日志。
七、为什么外部是 HTTP CONNECT、内部是 SOCKS5¶
很多客户端和系统工具更容易配置 HTTP 代理,尤其是 HTTPS 代理实际使用的就是 HTTP CONNECT:
而 Xray 为每个节点提供的是本机 SOCKS5 入站。Hub 在中间做协议适配:
HTTP CONNECT 客户端
→ GroupForwarder
→ 127.0.0.1:<node-socks-port>
→ SOCKS5 no-auth greeting
→ SOCKS5 CONNECT target
→ 双向转发
SOCKS5 握手不能假设一次 data 事件就能收到完整响应,因此实现了定长读取、超时和多余字节回塞。网络协议解析必须按协议边界读取,不能依赖 TCP 分包恰好符合预期。
八、分组路由与故障切换¶
一个代理分组可以包含多个候选节点。候选排序综合考虑:
priority- 是否是短期首选节点
- 节点健康和测速结果
- 每个节点允许的重试次数
请求处理流程:
收到 CONNECT
→ 解析目标 host:port
→ 获取可用候选
→ 依次准备 Xray 节点
→ 建立 SOCKS5 隧道
→ 返回 200 Connection Established
→ 发送已缓存的客户端首包
→ 等待上游首个响应
→ 记录成功并提升节点
→ 开始双向转发
节点切换时,需要注意 CONNECT 响应时机。只有在至少一个候选节点已经建立可用隧道后,才应向客户端承诺 200 Connection Established。否则客户端会以为隧道已经成功,却只能收到一个已经失效的连接。
成功节点会被提升为短期首选,默认 TTL 为 10 分钟。它是优化,不是永久绑定:过期后仍需重新参与排序,失败时也必须允许回退其他节点。
节点管理还提供独立的连通性测速。测速通过节点自己的 SOCKS 隧道访问一个无凭据的 HTTPS 测试地址,默认目标为 https://api.openai.com/v1/models,也可以通过 HUB_PROXY_TEST_URL 配置。实现使用 TLS 加 HTTP HEAD 请求,默认超时 12 秒,并拒绝带用户信息、查询凭据或非 HTTPS 的测试 URL。节点测试并发默认为 4,范围限制为 1 到 12,可通过 HUB_PROXY_TEST_CONCURRENCY 调整。
测速结果与节点一起保存,至少包括:
last_test_statuslast_test_latency_mslast_test_errorlast_test_at
转发尝试则单独记录:
- 分组 ID、节点 ID
- 目标地址
- 尝试序号
- 成功或失败
- 延迟
- 错误原因
测速用于提前判断节点连通性,转发日志用于解释真实请求为什么切换;两者不能互相替代。没有这些记录,用户只会看到“代理偶尔失败”,无法知道是订阅失效、节点不可达、Xray 启动失败、SOCKS 握手失败还是目标站点不可达。
九、并发与运行时状态¶
订阅刷新、分组启动、分组停止和节点进程回收会共同修改运行时。代理插件使用串行 Promise 队列保护这些操作:
let runtimeOperationQueue: Promise<unknown> = Promise.resolve()
function withRuntimeLock<T>(operation: () => Promise<T>): Promise<T> {
const result = runtimeOperationQueue.then(operation, operation)
runtimeOperationQueue = result.then(() => undefined, () => undefined)
return result
}
这里的锁只保护单个进程内的运行时状态。如果未来部署多个后端实例,还需要把“谁负责启动某分组”和“谁拥有某节点进程”的协调放到共享控制面,或通过明确的实例粘性避免重复管理。
十、安全边界¶
代理功能的主要安全风险不只是节点配置泄露,还包括:
- SSRF 访问内网资源
- 未授权用户借代理访问外部网络
- 监听地址绑定到不应暴露的网卡
- 错误日志包含完整订阅 URL
- 临时 Xray 配置权限过宽
- 节点进程无限增长造成资源耗尽
部署时应额外确认:
- 转发监听地址是否只绑定到需要的网卡
- 是否有上层鉴权或网络 ACL
- 是否限制分组数量、节点数量和并发连接
- 是否设置连接、响应和空闲超时
- 是否对管理接口与数据转发接口分别授权
十一、故障排查¶
| 现象 | 排查方向 |
|---|---|
| 订阅刷新失败 | URL 协议、DNS、SSRF 检查、重定向、响应大小和超时 |
| 节点数量为零 | Base64 解码、URI 协议、节点格式和 unknown 数量 |
| 节点测速失败 | HUB_PROXY_TEST_URL 是否为无凭据 HTTPS、SOCKS 隧道、TLS、12 秒超时和测试并发 |
| Xray 无法启动 | 临时配置、配置校验、二进制路径(getXrayExecutablePath)、端口冲突和启动日志 |
| CONNECT 返回 503 | 分组无成员、节点均不兼容或候选全部失败 |
| 建立 CONNECT 后无数据 | 客户端首包缓存、SOCKS5 握手、上游首响应超时 |
| 同一节点反复重启 | 进程退出事件、配置错误和节点健康状态 |
| 重启后订阅无法解密 | 加密密钥未持久化或环境变量改变 |
| 内网地址被访问 | SSRF 检查是否覆盖 DNS 解析结果和每次重定向 |
十二、验证清单¶
- 使用脱敏的明文 URI 列表刷新订阅。
- 使用 Base64 订阅刷新,确认解析结果一致。
- 混入不支持协议,确认被标记为
unknown且不进入运行时。 - 刷新时改变节点顺序,确认既有节点 ID 和分组关系稳定。
- 检查数据库,确认订阅 URL 和完整节点配置不是明文。
- 使用无效加密密钥启动,确认不会静默生成新密钥覆盖历史数据。
- 使用重定向到私网地址的测试 URL,确认每一跳都被拒绝;在受控测试环境验证
HUB_PROXY_ALLOW_PRIVATE_SUBSCRIPTIONS=true的显式放宽行为。 - 执行节点测速,确认状态、延迟、错误和时间戳写入
last_test_*字段,并验证并发上限。 - 启动一个分组,确认 Xray 配置文件权限为
0600且启动后被清理,assertInside拒绝HUB_PROXY_STATE_DIR之外的配置路径。 - 让首选节点失败,确认请求会按候选顺序切换。
- 检查尝试日志和
/api/plugins/proxy/xray-logs,确认包含延迟、错误和运行时诊断信息。 - 停止分组,确认监听端口、连接和 Xray 子进程全部释放。
十三、模块化部署(2026-08-12 更新)¶
重构后代理独立为服务 hub-proxy(版本 0.2.0),是五服务架构之一:
- 默认端口
5202(HUB_PLUGIN_PORT),通过manifest.json(id: proxy,权限proxy:subscription:*/proxy:node:*/proxy:group:*/proxy:process:*/proxy:log:read)注册到门户。 service.ts是独立 Fastify 入口(数据落独立库hub_proxy),main.ts保留订阅、转发器与进程池逻辑。- 新增运行时控制类环境变量:
HUB_PROXY_CIRCUIT_FAILURE_THRESHOLD/HUB_PROXY_CIRCUIT_COOLDOWN_MS(节点级熔断)、HUB_PROXY_MAX_CONNECTIONS/HUB_PROXY_MAX_CONNECTIONS_PER_NODE/HUB_PROXY_NODE_QUEUE_LIMIT/HUB_PROXY_NODE_QUEUE_TIMEOUT_MS(并发与排队)、HUB_PROXY_XRAY_IDLE_TTL_MS/HUB_PROXY_XRAY_MAX_PROCESSES(进程池回收)、HUB_PROXY_CONNECT_USERNAME/HUB_PROXY_CONNECT_PASSWORD/HUB_PROXY_CONNECT_ALLOWED_IPS(转发端口鉴权与 IP 白名单)、HUB_PROXY_LOG_REDACT_IPS/HUB_PROXY_LOG_TARGET_MODE(日志脱敏策略)。SSRF 与测速相关开关(HUB_PROXY_ALLOW_PRIVATE_SUBSCRIPTIONS/HUB_PROXY_TEST_URL/HUB_PROXY_TEST_CONCURRENCY)保持不变。
代码索引¶
hub-proxy/backend/src/main.ts(订阅、分组转发、运行时管理入口)hub-proxy/backend/src/service.ts(独立服务入口:Fastify + 门户注册)hub-proxy/backend/src/sub-parser.ts(订阅解码与节点解析)hub-proxy/backend/src/crypto.ts(订阅 URL 与节点配置加密)hub-proxy/backend/src/xray-pool.ts(Xray 进程池)hub-proxy/backend/src/xray-config.ts(生成 xray-core 26.x 配置)hub-proxy/backend/src/xray-process.ts(单进程 spawn / 状态 / 日志 / 路径约束)hub-proxy/backend/src/group-forwarder.ts(HTTP CONNECT → SOCKS5 转发与故障切换)hub-proxy/backend/src/node-tester.ts(节点连通性测速)hub-proxy/backend/src/log-sanitizer.ts(日志脱敏)hub-proxy/manifest.json(插件清单与权限声明)