Skip to content

修复机房中继的错误响应、镜像链路与开放代理问题 - #2

Merged
LoveLH merged 2 commits into
mainfrom
cursor/fix-audit-findings-4234
Oct 20, 2026
Merged

LoveLH merged 2 commits into
mainfrom
cursor/fix-audit-findings-4234

Conversation

@LoveLH

@LoveLH LoveLH commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

针对 2026-10-20 审计(main@4f55a8f,并对照未合并分支 agent/forge/c9caff7f73dd@0014acd)做了一轮修复。运行方式仍是单文件、仅标准库、python classrelay.py。认证默认关闭;若开启,使用 http://用户:密码@教师机:端口,不再使用会弄坏 pip 的 ?token=。

测试在本地假上游上跑,不访问外网:python -m pytest。其中包含真实的 pip install 和 npm install。GitHub Actions 在 Linux / Windows × Python 3.9 / 3.13 上跑同一套测试。

Windows CI(87d8d4c 失败,本提交修复)

87d8d4c 上 Ubuntu 的 Python 3.9 / 3.13 通过。windows-latest 的 3.9 和 3.13 在两次运行(39414192779、39414148125)里都是 11 failed / 37 passed / 1 skipped。失败的测试:

  • test_cache_ttl_size_and_partial_file
  • test_chunked_upstream_and_cache
  • test_singleflight_and_no_partial_cache
  • test_rewritten_index_is_not_shared_across_addresses
  • test_pip_install_through_relay
  • test_npm_install_through_relay
  • test_docker_registry_flow
  • test_connection_limit
  • test_decimal_and_odd_literals_resolve_to_loopback
  • test_connect_policy_and_malformed_targets
  • test_cli_help_stays_compatible

原因:

  • 缓存写不进去。 commit 用只读方式重新打开临时文件再 os.fsync。Windows 对只读句柄返回 [Errno 9] Bad file descriptor,提交失败,盘上没有缓存。上面前 7 个测试因此一直是 MISS;singleflight 的跟随者在两次领头都没写入后得到 502。现改为以读写方式打开再 fsync。
  • 503 被 RST 吃掉。 连接数超限时没读请求就 close()。Windows 对仍有未读数据的套接字发 RST,客户端是 ConnectionAbortedError(WinError 10053),读不到 503。现先读完请求头,写出 503,shutdown(SHUT_WR) 后再关闭。
  • 怪异 IPv4 字面量。 2130706433、0x7f000001、127.1 在 glibc 上是 127.0.0.1,Windows 的 getaddrinfo 不认,DNS 失败变成 502 而不是 403。现按 inet_aton 规则先规范成点分地址,再做内网判断。
  • --help 在 cp1252 控制台崩溃。 帮助和横幅是中文,UnicodeEncodeError 导致退出码 1。启动时把 stdout/stderr 设为 UTF-8(无法编码的字符替换掉)。

没有为了让 Windows 变绿而跳过这些测试。

唯一仍跳过的是 test_bash_setup_persists_and_restore_removes_it:它要真的执行 bash,并往 HOME 的 .bashrc 写入标记。Windows 学生机用的是 PowerShell 脚本,不跑这段 bash。脚本正文(不含 strict-ssl false、含还原命令)在所有平台都有断言。本机没有 bash 时同样跳过执行,只保留正文检查。

每个 job 两条注解:

  • actions/checkout@v4、actions/setup-python@v5、actions/setup-node@v4 运行在已弃用的 Node.js 20。工作流已改到这三个 action 的 v6(Node 24)。setup-node 关闭了自动缓存,本仓库没有 package.json。
  • Ubuntu job 的另一条是 GitHub 通知:ubuntu-latest 将切到 Ubuntu 26。这是托管镜像的说明,不是失败。Windows job 的另一条是 Process completed with exit code 1,来自上面的测试失败。

P0

  • P0-1 错误响应把连接直接断开:log_message 改为 format % args,不再固定取 args[0..2]。send_error 能正常返回 404 / 501 / 502,并带响应体。请求处理外包了一层,未发出响应头时的异常会落到 500。
  • P0-2 pip 镜像不可用:simple 页面里的 ../../packages/... 以及 files.pythonhosted.org 等包地址,会按上游页面解析后改写成 http://<本次连接地址>/pypi/packages/...(保留 #sha256=)。/pypi/<name>/、/pypi/simple/<name>/、/pypi/packages/... 分别落到 simple 根和文件根。测试里 pip install --index-url http://127.0.0.1/pypi/ classrelay-local-demo 能装上本地造的 wheel。
  • P0-3 开放代理 / SSRF:CONNECT 默认只允许 443,明文代理默认只允许 80/443。回环、链路本地、RFC1918、100.64/10、组播、文档网段、云元数据地址在 DNS 之后拒绝;任一解析结果落在这些地址上就整次拒绝,并连到解析出的地址,避免检查和连接之间被重新绑定。十进制、十六进制和缩写 IPv4 会先规范成点分地址,避免 Windows 把它们当成 DNS 失败。可用 --allow-host 再限制域名。--allow-private 默认关闭。可选 --auth user:password(HTTP Basic / Proxy-Authorization),覆盖 CONNECT、正向代理和镜像路由,比较使用 hmac.compare_digest。默认仍不强制认证,方便教室零配置。公网 80/443 仍然放行,否则 PlatformIO 和 git clone 无法使用;需要收紧时用 --allow-host。
  • P0-4 缓存串用户:正向代理不再缓存。镜像请求带 Cookie、响应带 Set-Cookie,或 Cache-Control: private/no-store/no-cache、Vary: Cookie/* 时不缓存。落盘头里去掉 Set-Cookie。转发到上游的 Authorization(例如 Docker Bearer)会计入缓存键,避免不同身份共用一份响应。

P1

  • P1-1 CONNECT 大文件中断、60 秒空闲断开:隧道改为阻塞套接字、双向线程、sendall。200 已经发出后不再补 502。空闲超时默认 600 秒(--tunnel-idle,0 为不限制)。测试在对端暂停读取时传完 2MB。
  • P1-2 Docker 基本不可用:补了 HEAD、透传 Accept 与 Bearer/非本代理的 Basic,并把 WWW-Authenticate 的 realm 封成带 HMAC 的 /_classrelay/docker-token/...,token 请求经教师机代拉,且不能拿这个入口去任意网址。manifest 按 Accept 分缓存键。README 仍标为实验性:没有 push、跨仓库 blob mount、Range 断点续传,也没有对真实 Docker daemon 做联调。开启 --auth 时,Docker 自己的 Bearer 和本代理的 Basic 共用 Authorization,这一组合不承诺可用。
  • P1-3 npm tarball 不走缓存:dist.tarball 改写成教师机上的绝对 http://<本次连接地址>/npm/...。测试包含协议级下载,以及本机 npm install(不设置 strict-ssl false)。
  • P1-4 整包进内存、缓存非原子、并发重复拉取:安装包按块转发并写入临时文件,完整读完后 os.replace 再写 meta;读者用 meta 里的大小核对,对不上就当未命中。同一缓存键 singleflight,只有一个上游请求。容量统计在锁内按差额更新,不再每次全盘扫描后重复累加。索引和包文件按「学生连到的地址」分开缓存,避免一块网卡的绝对链接被另一块网卡复用。
  • P1-5 一键脚本:去掉 npm config set strict-ssl false。bash 把代理写进 ~/.bashrc 和 ~/.zshrc 的标记块,并说明 curl | bash 改不了当前 shell。unbash / unwindows 会 npm config delete strict-ssl 并删掉标记块。控制台展示脚本 SHA256,并提示先下载核对。没有做非对称签名(没有密钥分发),用 SHA256 加明文 HTTP 警告代替。
  • P1-6 显示的 IP 可能不是学生能访问的那块网卡:脚本和控制台的主地址改为 connection.getsockname()。启动时探测到的其他网卡只作参考,且 gethostbyname_ex 有超时,不再每个请求都查。

P2

  • P2-1 畸形 CONNECT 直接崩:host:abc、缺端口、IPv6 [::1]:443 都在 try 里解析,返回 400 或 403,而不是空连接。
  • P2-2 Host / IPv6:生成脚本和页面时不再信任 Host 头,避免把不可信 Host 拼进 HTML 或 PowerShell。
  • P2-3 缓存无 TTL、无上限:索引默认 10 分钟,安装包默认 7 天,总容量默认 1 GB(--max-cache-mb,0 表示不落盘),按最旧淘汰。
  • P2-4 重复响应头被合并、逐跳头被转发:上游头按列表保留;去掉 Proxy-Connection、Proxy-Authorization、Transfer-Encoding 等逐跳头。镜像请求不把 Cookie 和本代理的 Basic 认证转给清华 / npmmirror。客户端查询串里的 token 等参数会丢掉,不会拼进上游 URL,也不会因为 token 不同而拆成多份缓存。
  • P2-5 HTTP/1.0、每请求都查本机 IP:响应改为 HTTP/1.1,带 Content-Length 或分块编码,正常 GET 可以保持连接。没有做 HTTP/2。本机 IP 只在启动时查一次。
  • P2-6 日志不刷新:改用 logging,StreamHandler 每条都 flush。URL 里的 user:pass@ 会打成 ***:***@。start.sh / start.bat 使用 python -u。
  • P2-7 测试要联网、路径写死 /tmp、没有 CI:旧的联网 test_relay.py 已换成 tests/test_relay.py。假上游、临时目录、pytest。工作流见 .github/workflows/test.yml。
  • P2-8 没有并发和请求体限制:--max-connections 默认 64,超出直接 503。只实现 GET / HEAD / CONNECT;其他方法返回 501,不读取请求体。索引类响应有 16 MB 上限。没有再做全局带宽配额。

未合并分支 0014acd 里采纳和没有继承的部分

采纳了:安全的 log_message、PyPI href 重写和 /pypi/packages/ 路由、npm tarball 重写、缓存 TTL / 容量、IPv6 CONNECT 解析、避免重复 Content-Length。

没有继承的问题:

  • B-1 token 不管 CONNECT:现改为 Basic,CONNECT 也要 Proxy-Authorization。
  • B-2 ?token= 把 pip 索引拼坏,改写链接也不带 token:改为 http://user:pass@host,并用带认证的 pip install 验证。
  • B-3 查询串(含 token)原样拼到上游:会剥离 token / password 等参数。
  • B-4 /pypi/simple/... 把子路径当成上游基址:改为拼到 simple 根上。
  • B-5 明文 == 比较、token 进日志:改为摘要后 compare_digest,日志打码。
  • B-6 每次写入都全量扫描、cached_bytes 先加再重算:改为内存索引和差额更新。

仍刻意留着的行为

学生通过教师机访问公网 443(以及明文 80)是教学场景本身的能力,默认不按域名封死。Docker 只保证测试里覆盖的 Registry v2 pull 流程,README 里写明实验性。

Open in Web Open in Cursor 

Error responses now return real status codes, pip and npm package links stay on the teacher machine, and the proxy refuses private targets and disallowed ports. Cached replies are no longer shared across users, CONNECT tunnels use blocking bidirectional copies, and offline tests cover the classroom flows.

Co-authored-by: LoveLH <LoveLH@users.noreply.github.com>
@LoveLH
LoveLH marked this pull request as ready for review October 20, 2026 11:20
Windows fsync rejects a read-only handle, so cache commits never landed
and later fetches stayed MISS. Closing a full socket also aborted the
503, and getaddrinfo does not parse inet_aton literals. Help text now
uses UTF-8 so --help survives a cp1252 console.

Co-authored-by: LoveLH <LoveLH@users.noreply.github.com>
@LoveLH
LoveLH merged commit 2b530d6 into main Oct 20, 2026
8 checks passed
@cursor
cursor Bot deleted the cursor/fix-audit-findings-4234 branch October 20, 2026 11:42
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.

2 participants