fix: restore reviewable migration evidence
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
# ClickHouse Migration 安全修复计划
|
||||
|
||||
> [!NOTE]
|
||||
> This document may not reflect the current implementation.
|
||||
> See the final report for up-to-date state:
|
||||
> [Final Report](../reports/reliability-security-fixes.md)
|
||||
|
||||
**变更规模:** 大型跨模块修复。现有迁移主体复用,仅重新实施受本次审查影响的任务及其集成依赖。
|
||||
|
||||
## 全局约束
|
||||
|
||||
- 保留非阻塞 best-effort 日志语义,并通过 Git 提交保留可审查的变更证据。
|
||||
- 先用失败测试固定根因,再实施最小修复。
|
||||
- `Send` 模糊失败不重试;不宣称分布式 exactly-once。
|
||||
- WebSocket 只管理连接并记录 101 元数据,不采集帧。
|
||||
|
||||
### Task 1: 严格配置与安全默认
|
||||
|
||||
**文件:** `config/config.go`、`tests/config/config_test.go`
|
||||
|
||||
- [ ] 严格解析整数、布尔值和 duration,拒绝非正 body/队列/timeout。
|
||||
- [ ] 增加 `LOG_QUEUE_BYTES`、响应总/idle timeout、`TRUSTED_PROXIES`。
|
||||
- [ ] 在配置加载阶段校验 ClickHouse DSN,并补齐边界测试。
|
||||
|
||||
### Task 2: Queue 生命周期、字节预算与确定性写入
|
||||
|
||||
**文件:** `logger/queue.go`、`tests/logger/queue_test.go`
|
||||
|
||||
- [ ] 用同步状态机消除 Submit/Stop send-close 竞态,Stop 使用单一 context 总预算。
|
||||
- [ ] 增加条数/字节双预算并在所有消费、丢弃和 shutdown 路径释放预留。
|
||||
- [ ] 区分 Prepare、Append、Send 错误;Append 失败清理 batch,Send 模糊失败不重试。
|
||||
- [ ] 增加并发关闭、预算、清理、部分失败和模糊提交测试。
|
||||
|
||||
### Task 3: ClickHouse DSN 与 schema 验证
|
||||
|
||||
**文件:** `db/clickhouse.go`、`db/migrate.go`、`db/clickhouse_test.go`
|
||||
|
||||
- [ ] 严格解析 scheme、TLS 和白名单 query 参数。
|
||||
- [ ] 创建 schema 后校验列、引擎、分区和排序键;不兼容时保持 unhealthy。
|
||||
- [ ] 增加 DSN 与 schema 元数据校验测试。
|
||||
|
||||
### Task 4: HTTP/SSE 代理完整性与可信来源
|
||||
|
||||
**文件:** `proxy/proxy.go`、`proxy/writer.go`、`proxy/capture.go`、`proxy/sse.go`、`tests/proxy/*`
|
||||
|
||||
- [ ] 请求体读取失败 fail closed;502 使用固定客户端文本。
|
||||
- [ ] 记录响应写错误/短写,响应中断不提交完整日志。
|
||||
- [ ] 仅对真正 SSE 按完整事件检测终止,并等待正常返回后提交。
|
||||
- [ ] 为普通响应总 timeout 与 SSE idle timeout 包装 upstream Body。
|
||||
- [ ] 默认忽略转发头,仅按可信代理链提取客户端 IP。
|
||||
|
||||
### Task 5: WebSocket 元数据与统一 shutdown
|
||||
|
||||
**文件:** `proxy/writer.go`、`proxy/proxy.go`、`tests/proxy/websocket_test.go`、`main.go`
|
||||
|
||||
- [ ] Hijack 成功后登记连接,记录 101 握手元数据并在结束时注销。
|
||||
- [ ] 提供 Handler shutdown,关闭受管升级连接。
|
||||
- [ ] server 启动错误和 signal 共用清理路径,所有关闭步骤受单一总预算约束。
|
||||
|
||||
### Task 6: Compose、文档与最终验证
|
||||
|
||||
**文件:** `compose.yml`、`.env.example`、`README.md`、`docs/compose/reports/*`
|
||||
|
||||
- [ ] 固定镜像、取消默认 ClickHouse 端口发布、强制显式密码/DSN并同步新配置。
|
||||
- [ ] 记录 best-effort、模糊提交、schema 和 WebSocket 取舍。
|
||||
- [ ] 运行 `gofmt`、`go test ./...`、`go vet ./...` 并生成报告。
|
||||
@@ -0,0 +1,5 @@
|
||||
# Reliability And Security Fixes Plan
|
||||
|
||||
The canonical dated plan is [2026-07-09-clickhouse-migration.md](2026-07-09-clickhouse-migration.md).
|
||||
|
||||
This stable path is the review entry point for the reliability and security fixes.
|
||||
@@ -0,0 +1,32 @@
|
||||
# TokenThief 可靠性与安全修复验证报告
|
||||
|
||||
## 结果
|
||||
|
||||
本次修订覆盖队列并发关闭和单一总 shutdown deadline、响应完整性、请求体 fail-closed、ClickHouse 部分/模糊提交、64 MiB 默认队列字节预算、严格正值配置、响应体总/idle timeout、SSE 协议边界、WebSocket 101 与 shutdown、DSN/TLS、schema 校验、Compose 安全默认、固定 502、可信代理、batch 清理和 server 启动统一清理。
|
||||
|
||||
Compose 使用固定 ClickHouse 镜像 `clickhouse/clickhouse-server:25.3.3.42-alpine`,默认不发布 ClickHouse 端口,并在配置展开阶段拒绝空的 `UPSTREAM_URL`、`CLICKHOUSE_URL` 和 `CLICKHOUSE_PASSWORD`。`.env.example` 不提供密码或 DSN 默认值。
|
||||
|
||||
## 关键取舍
|
||||
|
||||
- 日志仍是内存中、非阻塞、best-effort。队列满、字节预算不足或数据库不可用时允许丢弃并计数。
|
||||
- ClickHouse `PrepareBatch` 的确定失败保留整批;逐项 `Append` 的确定失败只保留失败项,成功项照常发送。`Send` 错误无法从单机客户端确认服务端是否已提交,因此不自动重试该批并计入 `ambiguous_send`,优先避免静默重复。跨进程 exactly-once 需要持久化 outbox 和服务端幂等协议,未在本次引入。
|
||||
- WebSocket 仅记录 101 握手元数据并关闭受管连接,不解析帧或承诺跨进程迁移。
|
||||
- schema 会自动创建缺失表,并校验现有表的列类型、MergeTree 引擎、分区键和排序键;不兼容时拒绝标记健康,不执行有数据风险的自动重建。
|
||||
- Compose 不默认发布 ClickHouse 端口,强制显式密码和独立 DSN,以同时保留原始服务端密码和 URL 编码凭据。
|
||||
|
||||
## 行为边界
|
||||
|
||||
- 仅当请求体完整可重放且 `ReverseProxy` 正常结束时提交 HTTP 日志;读取失败的请求不转发,响应中断不提交不完整日志。
|
||||
- SSE 仅由 `Content-Type: text/event-stream` 判定;看到 `[DONE]` 不会提前提交,仍等待上游正常结束。普通响应使用总 timeout,SSE 使用可重置 idle timeout。
|
||||
- WebSocket 仅记录成功 `101` 的握手元数据,不采集帧;进程关闭会关闭当前进程管理的升级连接,但不提供跨进程连接迁移或协调。
|
||||
- `502` 对客户端使用固定错误文本,内部连接错误只进入服务端日志。转发客户端 IP 仅在 TCP 对端属于 `TRUSTED_PROXIES` 时生效。
|
||||
|
||||
## 验证
|
||||
|
||||
- `gofmt -w .`:通过。
|
||||
- `go test ./...`:通过。
|
||||
- `go vet ./...`:通过。
|
||||
- 新增 `tests/deployment` 契约测试,覆盖必填部署参数、固定 ClickHouse 镜像、不发布数据库端口和示例凭据留空。
|
||||
- `docker compose config`:未执行,当前环境没有可用 Docker daemon/CLI;Compose 安全约束由 Go 契约测试覆盖。
|
||||
- `go test -race`:不属于验收命令,未执行。
|
||||
- 未连接真实 ClickHouse 做断链模糊提交和旧 schema 集成测试;相关阶段语义通过 fake batch 与纯 schema 校验单测覆盖。
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
feature: reliability-security-fixes
|
||||
status: delivered
|
||||
specs:
|
||||
- docs/compose/specs/reliability-security-fixes.md
|
||||
- docs/compose/specs/2026-07-09-clickhouse-migration.md
|
||||
plans:
|
||||
- docs/compose/plans/reliability-security-fixes.md
|
||||
- docs/compose/plans/2026-07-09-clickhouse-migration.md
|
||||
branch: main
|
||||
---
|
||||
|
||||
# 可靠性与安全修复 - 最终报告
|
||||
|
||||
## What Was Built
|
||||
|
||||
本轮完成了 token_thief 的可靠性与安全加固。异步日志队列现在安全处理并发 `Submit`/`Stop`、使用条目数和字节双预算、在统一 shutdown deadline 内排空,并区分 ClickHouse 的可安全重试、确定失败和提交结果不明三类写入结果。
|
||||
|
||||
反向代理现在对请求体读取、上游响应复制、普通响应超时、SSE idle timeout、WebSocket 101 元数据和升级连接关闭实施完整性保护。客户端错误文本不再泄露内部信息,转发来源头仅在直接对端属于 `TRUSTED_PROXIES` 时参与客户端 IP 判定。
|
||||
|
||||
配置、ClickHouse DSN/TLS、数据库 schema 和 Compose 部署均采用 fail-closed 校验与更安全默认;服务启动错误和信号关闭共用资源清理路径。
|
||||
|
||||
## Architecture
|
||||
|
||||
`config/config.go` 在启动前严格解析正值预算、duration、布尔值、可信代理和 DSN。`proxy/` 在转发前完整读取请求体,通过 capture writer 和 response body wrapper 判断响应是否完整,并管理 hijacked 连接。`logger/queue.go` 提供非阻塞有界队列,按 `PrepareBatch`、`Append`、`Send` 阶段决定重试或丢弃;`db/` 只在连接、迁移和 schema 校验均成功后标记健康。`main.go` 用一个 30 秒 shutdown context 依次关闭 HTTP、升级连接、队列和数据库。
|
||||
|
||||
### Design Decisions
|
||||
|
||||
- 选择仅重试 `PrepareBatch` 失败,因为此时可确定没有提交;`Send` 失败计为 ambiguous 且不重放,避免静默重复。
|
||||
- 选择进程内有界 best-effort 队列,因为当前项目没有持久化 outbox;该策略明确不承诺跨进程 exactly-once。
|
||||
- 选择 WebSocket 仅记录 101 握手元数据并管理连接,不采集帧,以保持代理边界和关闭行为可测试。
|
||||
- 选择拒绝不兼容 ClickHouse schema,而不是自动破坏性迁移或重建表。
|
||||
|
||||
## Usage
|
||||
|
||||
必须设置 `UPSTREAM_URL`、`CLICKHOUSE_URL`,Compose 部署还必须显式设置强 `CLICKHOUSE_PASSWORD`。关键新增配置包括 `LOG_QUEUE_BYTES`、`UPSTREAM_RESPONSE_TIMEOUT`、`UPSTREAM_STREAM_IDLE_TIMEOUT` 和 `TRUSTED_PROXIES`;所有预算和 timeout 必须大于零。`CLICKHOUSE_URL` 支持严格的 `host:port`、`clickhouse://` 和 `clickhouses://`,TLS 与压缩参数受白名单校验。
|
||||
|
||||
## Verification
|
||||
|
||||
迭代 1 验证全部通过:`gofmt -l .` 无输出,`go test -json ./...` 共 118 个测试通过、0 失败、5 个测试包通过,`go vet ./...` 无诊断,`go build ./` 成功。针对 config、queue、ClickHouse、HTTP/SSE/WebSocket 和 Compose 的失败路径均有测试覆盖。
|
||||
|
||||
## Journey Log
|
||||
|
||||
> Brief notes on what informed the final design. Not required reading.
|
||||
|
||||
- [lesson] 迭代 1:网络写入的 `Send` 错误无法证明提交与否;最小安全策略是不自动重试、显式统计 ambiguous,并将连接标记为不健康。
|
||||
- [lesson] 迭代 1:进程内队列只能提供有界 best-effort;跨进程幂等需要持久化 outbox 和下游幂等协议,不能由本地重试可靠模拟。
|
||||
- [pivot] 迭代 1:SSE 完成判定限定为真正的 `text/event-stream` 且等待代理正常返回,避免终止标记导致提前记录不完整响应。
|
||||
|
||||
## Source Materials
|
||||
|
||||
| File | Role | Notes |
|
||||
|------|------|-------|
|
||||
| `docs/compose/specs/2026-07-09-clickhouse-migration.md` | 安全修复规格 | 定义本轮行为边界与取舍 |
|
||||
| `docs/compose/plans/2026-07-09-clickhouse-migration.md` | 实施计划 | 覆盖跨模块修复和验证 |
|
||||
@@ -0,0 +1,54 @@
|
||||
# ClickHouse Migration 安全修复规格
|
||||
|
||||
> [!NOTE]
|
||||
> This document may not reflect the current implementation.
|
||||
> See the final report for up-to-date state:
|
||||
> [Final Report](../reports/reliability-security-fixes.md)
|
||||
|
||||
## 修订范围
|
||||
|
||||
本规格是现有 ClickHouse Migration 的增量修订。保留当前单表 `proxy_logs`、内存异步队列、`httputil.ReverseProxy` 和 best-effort 审计模型,不引入 PostgreSQL 兼容层、持久化 outbox 或 WebSocket 帧采集。
|
||||
|
||||
## 行为要求
|
||||
|
||||
### 队列与关闭
|
||||
|
||||
- `Submit` 始终非阻塞;与 `Stop` 并发、停止后提交和重复停止均不得 panic。
|
||||
- 队列由条数和字节双重预算约束。默认 `LOG_QUEUE_SIZE=256`、`LOG_QUEUE_BYTES=67108864`、`MAX_BODY_BYTES=1048576`;任一预算不足即丢弃并计数。
|
||||
- `Stop(ctx)` 使用调用方提供的单一总 deadline 排空;deadline 到期取消所有 worker I/O、丢弃剩余条目并返回错误。
|
||||
- worker flush 后清空 batch 指针并释放条目的字节预留。
|
||||
|
||||
### ClickHouse 写入
|
||||
|
||||
- `PrepareBatch` 错误是确定未提交错误,可有限重试整个 batch。
|
||||
- `Append` 错误是确定未提交错误,必须中止/关闭 batch,并允许逐条隔离坏记录;成功记录继续写入,失败记录明确计数。
|
||||
- `Send` 错误视为提交结果不明,不自动重试或逐条重发,避免静默重复;该批计为 ambiguous drop 并标记连接不健康。
|
||||
- 进程内 best-effort 方案不承诺跨进程 exactly-once。需要更强保证时必须另行引入持久化 outbox 与幂等协议。
|
||||
|
||||
### 代理完整性
|
||||
|
||||
- 请求体预读失败时返回固定 400,不调用 upstream,不转发已损坏请求。
|
||||
- 502 客户端响应只包含固定 `bad gateway` 和 request ID;内部网络错误仅写服务端日志和 `LogEntry.Error`。
|
||||
- 只有 ReverseProxy 正常完成的普通/SSE 响应才提交完整日志。下游写失败、短写、客户端断开或响应复制中断不得提交成完整成功日志。
|
||||
- SSE 终止检测只在响应 `Content-Type` 为 `text/event-stream` 时按完整 event 边界解析。终止事件仅用于状态判断,不触发提前提交;提交仍等待代理正常返回。
|
||||
- 普通 HTTP 响应体使用可配置总 timeout;SSE 使用可配置 idle timeout,成功读取数据后重置;timeout 必须能关闭阻塞中的上游 Body。
|
||||
- WebSocket 成功升级时记录一条 101 握手元数据日志,body 为空。Handler 跟踪已 hijack 连接,并提供受 context 限制的 shutdown 关闭能力;不采集帧。
|
||||
- 默认不信任 `X-Forwarded-For`/`X-Real-IP`。仅当直接对端命中 `TRUSTED_PROXIES` CIDR 时,从 XFF 右向左剥离可信代理并选择最近的不可信地址。
|
||||
|
||||
### 配置与数据库边界
|
||||
|
||||
- 所有整数、布尔值和 duration 环境变量格式错误时启动失败;要求正值的 body、队列、batch、worker、重连和 timeout 配置必须严格大于零。
|
||||
- `CLICKHOUSE_URL` 支持严格 `host:port`、`clickhouse://` 和 `clickhouses://`。仅允许 `secure`、`skip_verify`、`compress` 查询参数;未知参数、冲突 TLS 配置、fragment、空 host/port 均失败。
|
||||
- 建表后校验 `system.columns` 与 `system.tables` 的必需列类型、引擎、分区键和排序键;不兼容 schema 拒绝标记健康,不自动重建或改类型。
|
||||
|
||||
### 启动与部署
|
||||
|
||||
- server 启动错误与 signal 进入同一清理路径;禁止 goroutine 内 `log.Fatalf` 绕过 defer。
|
||||
- shutdown 总预算依次覆盖 HTTP、升级连接、日志队列、DB 和根 context。
|
||||
- Compose 固定 ClickHouse 明确版本,不使用 `latest`;默认不发布 ClickHouse 端口;`CLICKHOUSE_PASSWORD` 必须显式设置,应用 DSN 使用独立 `CLICKHOUSE_URL`,避免弱默认和 URL 编码冲突。
|
||||
|
||||
## 验证
|
||||
|
||||
- 针对上述失败路径增加 config、logger、proxy、db 单元测试。
|
||||
- 运行 `gofmt`、`go test ./...` 和 `go vet ./...`。
|
||||
- 使用 Git 提交保留完整变更证据;审查应以提交及其 diff 验证实现和验收项。
|
||||
@@ -0,0 +1,5 @@
|
||||
# Reliability And Security Fixes Specification
|
||||
|
||||
The canonical dated specification is [2026-07-09-clickhouse-migration.md](2026-07-09-clickhouse-migration.md).
|
||||
|
||||
This stable path is the review entry point for the reliability and security fixes.
|
||||
Reference in New Issue
Block a user