Skip to content

v0.5.8 - Go Security Update, Sign-in Lockout Warnings, and No More Idle HTTP/2 Connection Pile-ups

Choose a tag to compare

@github-actions github-actions released this 09 Oct 16:20
· 24 commits to main since this release
1463252

v0.5.8 - Go Security Update, Sign-in Lockout Warnings, and No More Idle HTTP/2 Connection Pile-ups

This release fixes eleven Go standard library and golang.org/x/net vulnerabilities; upgrade promptly. Pair it with codeProxy v0.6.1, whose sign-in forms use the fields added here to show the attempts left and to count a lock down. Either upgrade order works; see Upgrade Notes.

Highlights

1. Go 1.26.9 security update

  • What was wrong: v0.5.7 was built with Go 1.26.6 and golang.org/x/net v0.57.0. govulncheck now reports eleven vulnerabilities, GO-2026-6603 to GO-2026-6617, that CliRelay's code can reach:
    • HTTP/2 server: a crash from an HPACK encoder race, memory exhaustion from trailer headers, excessive CPU from repeated window changes, and a flow-control accounting error;
    • HTTP/2 client: malformed framing-related headers were accepted;
    • HTTP/1: connections could fall out of sync after a CONNECT response, on the server and on the client;
    • Header parsing: Range headers had no size limit, and MIME header parsing could bypass its memory limit;
    • crypto/tls: malformed ECH extension references were not rejected;
    • Windows only: os.Root directory creation could follow junctions out of the root.
  • The fix:
    • Builds use Go 1.26.9 and golang.org/x/net v0.60.0, and govulncheck reports no vulnerabilities that the code reaches.
    • The Docker image built its updater binary with Go 1.26.1; it now uses 1.26.9 as well.
  • Upgrade promptly: CliRelay is both an HTTP server and an HTTP/2 client.

2. Sign-in warns before it locks, and says how long a lock lasts

  • What was wrong: users were locked out of sign-in without warning and then given the wrong explanation, so they kept guessing.
    • The admin sign-in locked on the third wrong password, without saying so.
    • During that lock, wrong passwords still got "invalid credentials". Only the correct password got account_locked, which the panel shows as "This account is disabled or locked. Contact your tenant administrator." In production, an administrator typed the correct password during a lock, read that message, and gave up for an hour and a half.
    • Neither the admin nor the portal sign-in said how many attempts were left. The failure that tripped a lock looked like one more wrong password.
    • Throttle rejections carried the wait only in the Retry-After header, which the panel does not read.
  • The fix:
    • The admin panel and the portal now share one cooldown ladder:

      Failures Lock
      5 1 minute
      10 5 minutes
      15 15 minutes
      20 30 minutes
      25, and every 5th after 60 minutes

      The count restarts after 15 minutes without a failure. There is no permanent lock. The admin panel's first lock moves from 3 failures to 5, matching the portal.

    • The admin sign-in checks the lock before it checks the password. A sign-in during a lock gets 429 login_cooldown, whatever password is typed. account_locked now means only a lock set by an administrator.

    • A wrong password that does not trip a lock gets 401 invalid_credentials with details.remaining_attempts. For an ordinary run of mistypes the number is the same whether or not the username exists, so it does not reveal which accounts exist.

    • The wrong password that trips a lock gets 429, with the longest of the per-IP, per-account and cooldown waits.

    • login_rate_limited and login_cooldown responses carry details.retry_after_seconds in the body, as well as the Retry-After header.

3. Idle HTTP/2 connections to chatgpt.com no longer pile up

  • What was wrong: a user reported 373 connections from one relay to chatgpt.com:443. They were hours old, and each was exchanging only about 4.7 kB an hour in each direction. Production showed the same: 218 such connections 3.5 hours after a deploy. Two problems combined:
    • The Codex quota probe built a new fingerprinted transport on every call, and with it a new HTTP/2 connection, which it then abandoned. account-status-refresh is on by default and probes every Codex account every 15 minutes. Each round therefore leaked one connection per account, and two for accounts with reset credits.
    • The health-check pings added in v0.5.7 had no idle timeout. Because pings kept the abandoned connections busy, nothing ever closed them.
  • The fix:
    • Both self-managed HTTP/2 transports, for Anthropic and for the TLS-fingerprinted providers, close a connection that has carried no requests for 90 seconds. This applies however the connection was orphaned. The ping health check is unchanged.
    • The management fingerprint transport is now cached per egress, so probes through the same egress share one connection. The cache key includes the CA bundle, so a rotated bundle is still picked up.
    • Connections are cached by host:port instead of by host, so a request for one port can never reuse a connection to another.

4. api-call no longer sends a literal $TOKEN$ upstream

  • What was wrong: POST /v0/management/api-call replaces $TOKEN$ in request headers with the token of the credential chosen by auth_index. If the index was missing, stale or from another tenant, the header was sent upstream unchanged as Authorization: Bearer $TOKEN$. The upstream's 401 came back wrapped in a 200, and the panel showed the credential as expired. Operators then tried a re-login that could not help.

  • The fix: a request with a $TOKEN$ placeholder is now rejected before anything is sent upstream:

    • with no auth_index: 400 missing auth index;
    • with an index that matches no credential in the tenant: 400 auth not found for index.

    Requests without the placeholder are unaffected.


Detailed Changes

Backend CliRelay

  • chore(deps): Go 1.26.9 and golang.org/x/net v0.60.0 for GO-2026-6603 to GO-2026-6617. The Docker build image moves to golang:1.26.9-alpine (#1141).
  • fix(auth): warn before sign-in lockouts and report how long they last, with one cooldown ladder for the admin panel and the portal (#1136).
  • fix(http2): stop idle connections that pings keep alive from piling up on chatgpt.com. Adds an idle timeout to the self-managed HTTP/2 transports, caches the management fingerprint transport, and keys the connection cache by host:port (#1137).
  • fix(api-call): refuse $TOKEN$ headers when the auth index is missing or matches nothing (#1138).
  • feat(menus): an Appearance page under System settings (#1134), retired before release when the panel moved the settings to a header button (#1135). Startup removes the menu row if a development build added it. Upgrading from v0.5.7 leaves the menus unchanged.

Upgrade Notes

  • Security: this release fixes eleven Go and golang.org/x/net vulnerabilities. Upgrade even if you do not need the other changes.
  • Either upgrade order works:
    • Panels older than codeProxy v0.6.1 show neither the attempts left nor a countdown. Their admin sign-in shows a lock as "Too many login attempts, please try again later".
    • Panel v0.6.1 on a backend older than v0.5.8 falls back to its previous sign-in messages.
    • CliRelay pulls the latest codeProxy release on its own, roughly every three hours.
  • Admin sign-in behaviour changes:
    • The first lock now comes on the 5th wrong password, not the 3rd.
    • During a lock, even the correct password gets 429 login_cooldown, not 401 account_locked. Scripts that sign in to the management API and treat account_locked as a temporary lock should handle 429 instead, and wait details.retry_after_seconds (or Retry-After).
  • api-call: a $TOKEN$ request must now carry a valid auth_index. Without one it gets 400, instead of being forwarded with the literal placeholder.
  • No changes required: there are no configuration or database schema changes.
中文说明(点击展开)

v0.5.8 - Go 安全更新、登录锁定前预警、HTTP/2 空闲连接不再堆积

本版修复了 Go 标准库和 golang.org/x/net 中的 11 个漏洞,请尽快升级。 请与 codeProxy v0.6.1 搭配,新面板的登录表单会用这里新增的字段显示剩余尝试次数和锁定倒计时。前后端谁先升级都可以,详见下方升级提示。

核心更新

1. Go 1.26.9 安全更新

  • 原来的问题:v0.5.7 使用 Go 1.26.6 和 golang.org/x/net v0.57.0 构建。govulncheck 现在报告了 11 个 CliRelay 代码能触达的漏洞(GO-2026-6603 至 GO-2026-6617):
    • HTTP/2 服务端:HPACK 编码器竞争导致崩溃、Trailer 头导致内存耗尽、反复修改窗口导致 CPU 占用过高,以及流量控制记账错误;
    • HTTP/2 客户端:会接受畸形的分帧相关头;
    • HTTP/1:CONNECT 响应之后,服务端和客户端的连接都可能失去同步;
    • 头部解析:Range 头没有大小限制,解析 MIME 头时可以绕过内存限制;
    • crypto/tls:没有拒绝畸形的 ECH 扩展引用;
    • 仅 Windows:os.Root 创建目录时可能沿着 junction 跳出根目录。
  • 修复:
    • 构建改用 Go 1.26.9 和 golang.org/x/net v0.60.0,govulncheck 不再报告代码能触达的漏洞。
    • Docker 镜像中的 updater 原来用 Go 1.26.1 编译,现在同样改用 1.26.9。
  • 请尽快升级:CliRelay 既是 HTTP 服务端,也是 HTTP/2 客户端。

2. 登录在锁定前给出预警,锁定后说明还要多久

  • 原来的问题:用户在没有任何预警的情况下被锁在登录之外,之后看到的提示还是错的,只能一直猜。
    • 管理端登录在第 3 次输错时就锁定了,界面上没有任何提示。
    • 锁定期间,输错的密码仍然返回「用户名或密码错误」;只有输对的密码才返回 account_locked,面板把它显示为「当前账号已被禁用或锁定,请联系租户管理员」。生产环境里有管理员在锁定期间输入了正确密码,看到这条提示后放弃了一个半小时。
    • 管理端和门户登录都不提示还剩几次机会,触发锁定的那一次失败看起来也只是又一次密码错误。
    • 限流拒绝只把等待时间放在 Retry-After 响应头里,而面板不读这个头。
  • 修复:
    • 管理端和门户共用同一套冷却阶梯:

      失败次数 锁定时长
      5 1 分钟
      10 5 分钟
      15 15 分钟
      20 30 分钟
      25 次及之后每 5 次 60 分钟

      连续 15 分钟没有失败,计数就重新开始;不存在永久锁定。管理端的首次锁定从第 3 次失败改为第 5 次,与门户一致。

    • 管理端登录先检查锁定再校验密码。锁定期间无论输入什么密码,都返回 429 login_cooldown;account_locked 现在只表示管理员手动设置的锁定。

    • 没有触发锁定的密码错误返回 401 invalid_credentials,并附 details.remaining_attempts。在普通的连续输错场景下,用户名存在与否看到的次数都一样,不会暴露哪些账号存在。

    • 触发锁定的那次密码错误返回 429,等待时间取按 IP 限流、按账号限流和冷却阶梯三者中最长的一个。

    • login_rate_limited 和 login_cooldown 响应除了 Retry-After 头,响应体里也带 details.retry_after_seconds。

3. 到 chatgpt.com 的 HTTP/2 空闲连接不再堆积

  • 原来的问题:有用户反馈,一个 relay 到 chatgpt.com:443 有 373 条连接,都已存在好几个小时,每条每小时单向只收发约 4.7 kB。生产环境也一样:部署 3.5 小时后已有 218 条这样的连接。原因是两个问题叠加:
    • Codex 额度探测每次调用都新建一个带指纹的传输层,也就新建一条 HTTP/2 连接,用完就丢下不管。account-status-refresh 默认开启,每 15 分钟探测一遍所有 Codex 账号,所以每轮每个账号泄漏一条连接,有重置额度的账号泄漏两条。
    • v0.5.7 加入的健康检查 PING 没有配空闲超时。PING 让这些被丢弃的连接一直有流量,所以永远没有人去关闭它们。
  • 修复:
    • 两个自管的 HTTP/2 传输层(Anthropic 用的和 TLS 指纹提供商用的)都会关闭 90 秒内没有承载请求的连接,不管它是怎么被丢下的。PING 健康检查保持不变。
    • 管理端的指纹传输层现在按出口缓存,同一出口的探测共用一条连接。缓存键包含 CA 证书包,证书包轮换后仍会生效。
    • 连接缓存的键从主机改为 host:port,发往一个端口的请求不会再复用到另一个端口的连接。

4. api-call 不再把字面量 $TOKEN$ 发给上游

  • 原来的问题:POST /v0/management/api-call 会把请求头里的 $TOKEN$ 替换成 auth_index 所选凭据的令牌。如果没有传索引、索引已失效或属于其他租户,请求头会原样发给上游,变成 Authorization: Bearer $TOKEN$。上游返回的 401 被包在 200 里,面板因此把凭据显示为已过期,运维随后去重新登录,而这根本解决不了问题。

  • 修复:带 $TOKEN$ 占位符的请求现在在发往上游之前就会被拒绝:

    • 没有 auth_index:返回 400 missing auth index;
    • 索引在当前租户下匹配不到任何凭据:返回 400 auth not found for index。

    不带占位符的请求不受影响。

详细改动

  • chore(deps):升级到 Go 1.26.9 和 golang.org/x/net v0.60.0,修复 GO-2026-6603 至 GO-2026-6617;Docker 构建镜像改为 golang:1.26.9-alpine(#1141)。
  • fix(auth):登录锁定前给出预警并说明锁定时长,管理端与门户共用同一套冷却阶梯(#1136)。
  • fix(http2):修复被 PING 保活的空闲连接在 chatgpt.com 上堆积:为自管 HTTP/2 传输层加空闲超时,缓存管理端指纹传输层,连接缓存改按 host:port 区分(#1137)。
  • fix(api-call):缺少 auth_index 或匹配不到凭据时拒绝 $TOKEN$ 请求头(#1138)。
  • feat(menus):在「系统设置」下新增外观页(#1134),发布前随面板把设置移到顶栏按钮而撤回(#1135)。如果开发版本写入过该菜单项,启动时会删除;从 v0.5.7 升级不会改变菜单。

升级提示

  • 安全:本版修复了 Go 和 golang.org/x/net 中的 11 个漏洞,即使用不到其他改动也建议升级。
  • 前后端谁先升级都可以:
    • 早于 codeProxy v0.6.1 的面板不显示剩余次数,也没有倒计时;其管理端登录在锁定时显示「登录尝试过于频繁,请稍后再试」。
    • v0.6.1 面板搭配早于 v0.5.8 的后端时,登录提示回退到原来的文案。
    • CliRelay 大约每三小时会自动拉取最新的 codeProxy release。
  • 管理端登录的行为变化:
    • 首次锁定改为在第 5 次输错时触发,不再是第 3 次。
    • 锁定期间即使输入正确密码,也返回 429 login_cooldown,不再是 401 account_locked。如果有脚本登录管理 API,并把 account_locked 当作临时锁定处理,请改为处理 429,并按 details.retry_after_seconds(或 Retry-After)等待。
  • api-call:带 $TOKEN$ 的请求现在必须提供有效的 auth_index;缺少时会返回 400,不再带着字面量占位符转发出去。
  • 无需改动:配置和数据库结构都没有变化。

Multi-arch binaries and the Docker image are published to GitHub Releases and GHCR. Released together with codeProxy v0.6.1.