Files
tamabox/.workbuddy/memory/2026-10-23.md
T
2026-10-23 11:14:27 +08:00

101 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 2026-10-23
## 单题分享卡二维码提到 5px/模块(`310f851`)
用户决定「单题一起做」,同时否掉方案 5(结果落盘/R2)——理由:功能使用率不高,不值得大动干戈。
- 单题卡卡片 210×255 放不下 5px 模块(37 模块要 185px):`QQRCardW` 236、`QQRCardH` 300、
`QQRSize` 190。X 由 `QContentX+QContentW-QQRCardW` 派生 → 679,Y 居中 → 120。
- `QTextMaxW` / `qLeftClip` 都从 `QQRCardX` 派生,左栏自动收窄 26px,无需另改。
- 抽了 `drawQuestionQRCard(canvas, user, question, logo, fontCtx)`(handler 只剩调用),
这样测试能直接渲染整卡:`TestQuestionQRCardLayout` 断言正方形 / 模块≥5 / 四边留白≥20 /
居中≤1px,`TEST_PREVIEW=1` 出 `outputs/sharecard-preview/question-card.png`。
- 实测:二维码 185px、居中偏差 0.5px、上下留白 34px;gozxing 在 960/720/480/320 全通过。
变异验证:`QQRSize` 改回 150 → "QR module is 4px, want >=5" FAIL。
- 未做(用户明确不要):分享卡结果落盘/上 R2。进程内 LRU 仍是 16 条 + TTL 1h,
冷渲染成本接受现状。
## 部署后复验(箱子卡 + 单题卡,`310f851`)
- 箱子卡:QR 185×185 @ y120-304 / x702-886,卡中心 795 → 着墨中心 794.0(−1.0px);
提示语 794.0、分隔线 795.0、链接两行 795.0 / 794.5;上下留白各 18px。
- 单题卡:QR 185×185 @ y154-338 / x704-888,卡中心 797 → 着墨中心 796.0(−1.0px);
提示语 797.0;上下留白 34 / 33px。
- 端到端解码(部署图,含真实背景与内嵌头像):两张卡在 960 / 720 / 480 / 320 宽全部 OK,
内容正确(单题卡 `/114`)。
- 顺带确认 **不是** bug:删除提问后分享卡立刻 302(`GetByID` 在缓存查询之前),
不存在「删了还能靠缓存取到图」的窗口。
## 缓存 key 排查结论(`675cecb`)
用户要求「有影响就修」。查下来**当前没有实际影响**,只有一条理论缺口:
| 可能场景 | 是否真有影响 | 原因 |
|---|---|---|
| 用户改域名 → 二维码指向旧域名 | ❌ 当前无 | Domain 只在注册时写(`register.go` / `CreateUser`),无改域名入口 |
| 改 ExternalURL / ShareBackground | ❌ 无 | 配置需重启,缓存是进程内的,重启即清 |
| 换头像/背景后仍是旧图 | ❌ 无 | R2 key = `picture/日期/随机 hex`,内容不可变,换图必换 URL |
| 域名将来可改 | ⚠️ 理论 | 单题卡 key 未含 `user.Domain` |
处理:给 `questionShareCardCacheKey` 补 `user.Domain`(箱子卡本来就有);
两处「TTL 30 分钟」注释改成 1 小时(代码一直是 `time.Hour`)。
新增 `TestShareCardCacheKeysCoverDrawnFields`——逐字段变异断言 key 必须变,
去掉 Domain 立刻 FAIL。**以后改分享卡要加新字段时,先看这张测试。**
## 正式环境「蜜罐封了但还能访问」排查(IPv6)
本地用 IPv6 全程复现了一遍 autoban 链路(CF-Connecting-IP 注入 + trust_proxy=true):
封禁前 200 → POST 凭据后 `IsBanned`=true → 同地址 403 → 同 /64 换地址仍 403 →
不同 /64 放行 200。**判定逻辑与中间件都正确,不是代码 bug。**
封禁键 = `shared.LimitKey(ResolveClientIP(req))`:
- IPv4 原样
- IPv6 折成 `xxxx:xxxx:xxxx:xxxx::/64`
写入(honeypot.Serve / route.NotFound)与检查(autobanMiddleware)用的是同一个函数,一致。
⇒ 「邮件说封了但能访问」只能是**键不一致**或**进程重启(封禁纯内存态)**。
三种可能:① 部署后重启过;② trust_proxy 未配 → 键是反代地址(`127.0.0.1` / `::/64`);
③ 前后请求地址族或 /64 不同(双栈、IPv6 前缀轮换、多实例)。
判定方法:`honeypot hits` / `data/honeypot-creds/creds-YYYY-MM-DD.jsonl` 里的 `ip` 字段
就是**被封的键**,与本机 `curl -6 ifconfig.co` 比对即可。
**发现的真实取证缺陷**:`db.HoneypotHit.IP` 与 creds 文件存的是折叠后的 `/64` 键,
IPv6 丢掉了精确地址(IPv4 无损);邮件里也只给键,运维会误以为「这不是我的 IP」。
建议:取证记精确 IP,封禁仍用 /64;建封与 403 命中各打一条日志。
### 据此动手改的三处(精确 IP / 封禁键分离 + 可观测性)
1. `honeypotHandler.Serve` 拆两个变量:`clientIP = ResolveClientIP`(精确,取证/邮件用)、
`banKey = LimitKey(clientIP)`(/64,封禁/采样/告警去重用)。DB 与凭据文件都写 clientIP。
2. `autoban.Options.OnBan(key, duration, reason)` 回调(新增 `ReasonProbe` / `ReasonHoneypot`),
保持 autoban 纯标准库、不再依赖 logrus。route.newBanner 注入 → 建封打 WARN
(ban_key / reason / duration / expires_at)。中间件 403 分支打 Debug。
3. 邮件同时给「来源 IP」(精确)与「封禁范围」(ban_key,仅 `ne .ip .ban_key` 时渲染);
顺手把模板里过时的「已入库」改成「写入取证文件」(上一轮凭据外置后没同步)。
副作用修掉:`honeypot stats` 的来源排行改按 `LimitKey` 合并(否则 IPv6 轮换地址会把排行碎成
几十条各 1 次)。SQL 取 `limit*4` 再合并,属近似。
测试:`TestHoneypotRecordsExactIPv6ButBansByPrefix`(精确 IP + /64 键 + store 断言)、
`TestOnBanCallback`、`TestHoneypotAlertTemplateRenders`、`TestMergeSourcesByKey`。
四处变异验证有效(改回存聚合键 / 封禁用精确地址 / 去掉 notifyBan / 不合并排行)。
**变异验证的坑**:把 `shared.LimitKey(r.IP)` 改成 `r.IP` 会因 `shared` 未使用而
**编译失败**(不是断言失败);变异要保证仍能编译(改成 `LimitKey(r.IP) + r.IP` 才测到逻辑)。
### 测试环境验收(部署 29ddee0 后)
伪造 `CF-Connecting-IP: 2001:db8:…`(RFC 文档保留段,封了不影响任何人)直连源站端口:
- 基线 200 → POST 凭据 200(假页面)→ **同 /64 换地址 403**(聚合封禁生效);
- 凭据文件 `ip` = `2001:db8:dead:beef::1234`(精确,无 /64 后缀),`banned:true`;
- 换另一个 /64(`2001:db8:cafe::1`)→ 200,说明封的是 /64 不是全局;
- `od -c` 复核 `"path":"/wp-login.php"` **无前导空格** → cleanPath 生效。
**教训**:上一次那条 `" /wp-login.php"` 是**那次请求的 URI 本身带空格**(本地实测
`Match(" /wp-login.php")=true` 靠 TrimSpace 命中、`Match("/ wp-login.php")=false`),
不是代码问题;判断这类"看不见的字符"一律用 `od -c`,别靠终端渲染。
测试环境服务是**前台跑在 /dev/pts/1**,日志不在文件里 —— 拿日志要 `tmux capture-pane` 或
下次启动加 `>> logs/tamabox.log 2>&1`。