Skip to content

fix: 双栈监听下识别 IPv4 映射的本地反代 - #7

Merged
stqfdyr merged 1 commit into
mainfrom
fix/ipv4-mapped-peer
Sep 15, 2026
Merged

stqfdyr merged 1 commit into
mainfrom
fix/ipv4-mapped-peer

Conversation

@stqfdyr

@stqfdyr stqfdyr commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

问题

hub 默认监听双栈 [::]:28080,IPv4 连接的 peer 地址是 ::ffff:a.b.c.d。behind_local_proxy 只认 IPv6 的回环、ULA 与 link-local,不认 IPv4 映射地址,于是本地反代发来的 X-Forwarded-For 被整个忽略,client_ip 返回反代自己的地址。

受影响的部署:

后果有三处,都经 client_ip:

  1. 节点的连接地址变成反代地址,日志为 connected from ::ffff:172.18.0.4
  2. 国家由 hub 按连接地址查询,私有地址查不到,国家标记不显示
  3. 登录与注册限流所有访客共用一个桶:任何人连输 5 次,管理员 15 分钟内无法用密码登录

修改

client_ip 先对 peer 与 X-Forwarded-For 取到的地址做 IpAddr::to_canonical,再判断。三个调用点(登录、agent 连接、agent 注册)一并修复。

没有改为读取 CF-Connecting-IP:在 nginx/caddy 后面这个头会被原样转发,调用方可以自行伪造。

测试

单元测试补了 IPv4 映射 peer 的断言,修改前失败,输出与 issue 中的地址一致。

端到端对比 1.0.0 与本分支,步骤相同:

compose(monitor + cloudflared,enable_ipv6: true,真实 Cloudflare Tunnel),客户端一台走 IPv6、一台走 IPv4

1.0.0 本分支
节点连接地址 两个都是 ::ffff:172.18.0.3 各自的公网 IPv6 / IPv4
国家 空 JP / US
A 登录失败 5 次后 B 登录 429 401
A 注册失败 5 次后 B 注册 429 403

裸二进制 [::] + nginx($proxy_add_x_forwarded_for),攻击者在 XFF 里伪造管理员地址

1.0.0 本分支
攻击者失败 5 次后管理员登录 429 401
攻击者每次轮换伪造的 XFF 429 429

直接暴露公网、无反代(回归检查):两版结果一致,伪造的 XFF 既不能锁定他人,也不能绕过自身的锁定。

Fixes #6

hub 默认监听 [::],IPv4 peer 以 ::ffff:a.b.c.d 的形式出现,behind_local_proxy
不认这种写法,于是 X-Forwarded-For 被整个忽略。容器里经 cloudflared、本机 nginx
连 127.0.0.1 都会命中:节点地址变成反代自己的地址,国家查询落空,登录与注册限流
所有访客共用一个桶。

client_ip 先对 peer 与转发地址做 to_canonical,再判断。

Fixes #6
@stqfdyr
stqfdyr merged commit ba63589 into main Sep 15, 2026
1 check passed
CarlJia referenced this pull request in CarlJia/monitor Sep 15, 2026
- #2/P1: housekeeping 首轮跳过离线判定,消除重启后假离线告警
- #3/P1: 宿主函数加墙钟 deadline,堵住 host 调用循环绕过 fuel/timeout 的沙箱缺口
- #4: host_http_post 有界读取响应体(64 KiB 硬上限)
- #5: db_restore 后重建插件 Registry
- #7: 插件删除行+kv 清理单事务化
- #8: enable 失败回滚 enabled 并修面板徽标优先级
- #10: delete_node 级联清理 notification_log
- #12: 解压炸弹防线补齐 4 类测试
- #13: settings GET 回显通知阈值默认值,读改写回环可闭环
- #16: http 响应写回抽纯函数并补 5 分支测试
- #17: 新增 DELETE kv 路由,前端 KvDialog 去除清空临时方案
- #18: DispatchEntry.result 枚举与文档对齐
- #19: 日志卡片选中插件被删后不再弹假 404
@stqfdyr
stqfdyr deleted the fix/ipv4-mapped-peer branch September 16, 2026 03:55
CarlJia referenced this pull request in CarlJia/monitor Sep 16, 2026
独立评审(7 路本地 + codex 对抗尝试)后的修复。P0/P1 全部经独立校验器确认。

阻断项:
- #1/#4 wasm 在运行时上下文里被同步调用,宿主函数里的嵌套 block_on 会
  panic;release 档 panic=abort 直接终止进程,启用财务插件即崩。
  call_hook/call_json_hook 补 block_in_place(与 run_one 同一形态),
  call_plugin_json 去掉多余的外层 block_on。新增 run_blocking:只在多线程
  运行时上包一层,纯同步/单线程路径直接跑。
- #2 SSRF 预检只覆盖首个 URL,而共享 client 默认跟随重定向(含
  https->http 降级),任一公网开放重定向即可绕过。新增 App::plugin_http
  (Policy::none()),插件出网与宿主隔离。
- #7 注册表读锁横跨插件执行,插件回头发事件重入同一把锁会死锁。
  dispatch_ticks/call_json/dispatch_one 改为只在取快照时借锁。

迁移与数据边界:
- #5 删列闸门按 plugin_id 限定(不再被第三方插件的 node: 前缀打开)。
- #15 四条 DROP COLUMN 收进一个事务,完成判据与待删清单同源。
- #16 Db::open 补 version > SCHEMA_VERSION 守卫(与 check_backup 一致)。
- #17 GATED_VERSION 用绝对常量,不再随 SCHEMA_VERSION 漂移。
- #6 删列前记一行非默认值行数,运维在日志里看得到这次丢了什么。
- #18/#23 plugin_data 配额统一按字节,检查与写入同一事务(BEGIN IMMEDIATE)。
- #20 无到期字段的插件事件按 payload 内容哈希(FNV-1a)定键,不再永久去重。
- #21/#22 SSRF 预检的 DNS 解析与请求超时都按剩余预算收缩。
- #24 data_list/nodes_query 用各自的上限,放不下报 -6 而不是静默截断。

契约与文档:
- #13/#14 插件端点先判加载状态(400)再判能力声明(404);错误体统一纯文本。
- #8/#9/#29 README 的 nodes_query 字段、data_list 格式、tick 周期按实现改写。
- #26 表单控件按字段名推断的规则写进协议(#27 同步修掉清空价格存成 0)。
- #25 破坏性 ABI/API 变更按 SemVer 升到 2.0.0,README 补升级说明。
- #28 tg-notify 错误码表补 -9 对应的 19。

测试与 CI:
- #10 删除空测试体;#11 nodes_query 测试播种节点并断言在线状态。
- #19 插件 crate 接进 CI(独立 crate,根 cargo test 覆盖不到)。
- #12 插件区分「宿主查询失败」与「确实没有节点」,清理拿不到存活集合拒删。
- #3 抽出 plugin_http_fetch,http_post/http_get 不再逐行重复。

验证:hub 166 passed;finance-stats 8;tg-notify 3;web-admin build+lint。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

演示站点的国家/地区,是在哪里设置的

1 participant