Files
tamabox/internal/mail/mail_alert_test.go
T
tamakyi 818fa74160 fix(honeypot): 取证记精确地址、封禁用 /64 键,并补建封与拦截日志
线上实测:IPv6 访问蜜罐后收到「你的 IP 已被封禁」,邮件里却是
`2408:…::/64` —— 既不像自己的地址,也说不清封了多大范围;同时用户自己
仍能正常访问,看日志也无从判断到底有没有封。

根因是「取证地址」与「封禁键」混用同一个变量(都取 LimitKey 的折叠值):
- 取证丢掉精确 IPv6 后 64 位,事后无法定位到具体主机、没法和其他日志对账;
- 邮件只给聚合键,运维第一反应是「不是我」,排查方向直接被带偏。

改动:
- Serve 拆 clientIP(ResolveClientIP,精确)与 banKey(LimitKey,/64):
  DB / 凭据文件 / 告警邮件用前者,封禁 / 探测采样 / 告警去重用后者;
- autoban.Options 新增 OnBan 回调(保持纯标准库、不引入 logrus),
  route.newBanner 注入后每次建封打 WARN:ban_key / reason / duration / expires_at;
  中间件 403 分支加 Debug 日志(被封者会持续请求,Info 会被刷屏);
- 告警邮件新增「封禁范围」行,仅 ip != ban_key 时渲染(IPv4 不重复显示),
  顺带把过时的「已入库」改为「写入取证文件」——凭据早就外置了;
- honeypot stats 的来源排行改按 LimitKey 合并,否则 IPv6 轮换地址会把
  排行碎成几十条各 1 次的记录;合并口径与封禁完全一致。

测试(每项都做了变异验证:改坏→确认变红→恢复):
- TestHoneypotRecordsExactIPv6ButBansByPrefix:精确地址进 DB 与凭据文件、
  /64 才是封禁键、同 /64 换地址仍 403;
- TestOnBanCallback:建封回调带键/时长/原因,封禁期内重复命中不重复回调;
- TestHoneypotAlertTemplateRenders:IPv6 渲染封禁范围、IPv4 不重复;
- TestMergeSourcesByKey:同 /64 合并、IPv4 原样、limit 与空输入。
2026-10-23 10:53:03 +08:00

118 lines
3.6 KiB
Go
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.
package mail
import (
"bytes"
"html/template"
"strings"
"testing"
"github.com/tamakyi/TamaBox/templates"
)
// TestSecurityAlertTemplateRenders 保证 security-alert.html 模板可解析,
// 且新增的「网络归属 / 代理网络提示」字段在参数齐全时正确渲染、
// proxy_hint 为空时不渲染提示块。
func TestSecurityAlertTemplateRenders(t *testing.T) {
tmpl, err := template.ParseFS(templates.FS, "mail/security-alert.html")
if err != nil {
t.Fatalf("parse template: %v", err)
}
base := map[string]string{
"ip": "203.0.113.7",
"provider": "Cloudflare(CDN)",
"paths": "/admin、/.env",
"duration": "15",
"time": "2026-09-08 12:00:00",
"year": "2026",
}
// 1. 代理网络命中:应有归属行 + 代理提示
var withHint bytes.Buffer
params := cloneParams(base)
params["proxy_hint"] = "1"
if err := tmpl.Execute(&withHint, params); err != nil {
t.Fatalf("execute with proxy_hint: %v", err)
}
out := withHint.String()
for _, want := range []string{"203.0.113.7", "网络归属", "Cloudflare(CDN)", "代理网络"} {
if !bytes.Contains([]byte(out), []byte(want)) {
t.Errorf("rendered mail missing %q", want)
}
}
// 2. 普通网络:不应出现代理提示块
var noHint bytes.Buffer
if err := tmpl.Execute(&noHint, cloneParams(base)); err != nil {
t.Fatalf("execute without proxy_hint: %v", err)
}
if bytes.Contains(noHint.Bytes(), []byte("代理网络")) {
t.Errorf("proxy hint rendered without proxy_hint param")
}
}
func cloneParams(src map[string]string) map[string]string {
dst := make(map[string]string, len(src))
for k, v := range src {
dst[k] = v
}
return dst
}
// TestHoneypotAlertTemplateRenders 保证蜜罐告警模板可解析,并且:
// - IPv6(ip ≠ ban_key)时额外渲染「封禁范围」,说明封的是整个 /64 前缀;
// - IPv4(ip == ban_key)时不渲染那一行,避免同一串地址出现两次;
// - 正文说明的是「写入取证文件」而不是早已过时的「已入库」(凭据不入库)。
func TestHoneypotAlertTemplateRenders(t *testing.T) {
tmpl, err := template.ParseFS(templates.FS, "mail/honeypot-alert.html")
if err != nil {
t.Fatalf("parse template: %v", err)
}
base := map[string]string{
"ip": "2408:8207:1234:5678::abcd",
"ban_key": "2408:8207:1234:5678::/64",
"provider": "中国联通(住宅宽带)",
"path": "/wp-login.php",
"username": "admin",
"password": "hunter2",
"disposition": "已记录并自动临时封禁 1440 分钟",
"time": "2026-10-23 10:30:00",
"year": "2026",
}
var v6 bytes.Buffer
if err := tmpl.Execute(&v6, base); err != nil {
t.Fatalf("execute ipv6: %v", err)
}
out := v6.String()
for _, want := range []string{
"2408:8207:1234:5678::abcd",
"封禁范围",
"2408:8207:1234:5678::/64",
"取证文件",
} {
if !strings.Contains(out, want) {
t.Errorf("IPv6 邮件缺少 %q", want)
}
}
if strings.Contains(out, "已入库") {
t.Error("邮件仍在说「已入库」:凭据已改为写入文件,不在数据库里")
}
// IPv4:精确地址与封禁键相同,不该重复展示「封禁范围」。
v4 := cloneParams(base)
v4["ip"] = "203.0.113.7"
v4["ban_key"] = "203.0.113.7"
var buf4 bytes.Buffer
if err := tmpl.Execute(&buf4, v4); err != nil {
t.Fatalf("execute ipv4: %v", err)
}
if strings.Contains(buf4.String(), "封禁范围") {
t.Error("IPv4 下 ip == ban_key,不应渲染「封禁范围」行")
}
if !strings.Contains(buf4.String(), "203.0.113.7") {
t.Error("IPv4 邮件缺少来源 IP")
}
}