101 lines
6.6 KiB
Markdown
101 lines
6.6 KiB
Markdown
# 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`。
|