Appearance
企业邮箱总收垃圾邮件?按来源 IP 归属地自动拦截(Go 实现)
公司用的 Postfix 邮件服务器,每天进几百封垃圾邮件:境外赌场广告、伪造的「发票」「律师函」、钓鱼链接。SPF、DKIM、DMARC 都配了,能防「冒用自家域名」,但防不住陌生域名从陌生 IP 发进来的垃圾——它们根本不伪造你,就是拿低信誉 IP 池硬塞。反垃圾网关有,可 RBL 黑名单有滞后,新 IP 出来一两天内照样漏进来。这篇讲一个补丁方案:把来信来源 IP 打上归属地标签,统计各地区的「垃圾命中率」,对高垃圾比例的来源地区做拦截,Go 实现,能直接跑。
一、垃圾邮件来源有什么规律
拿企业邮局的日志统计一个月,规律一般很明显:
- 来源国家高度集中:垃圾邮件的大头来自少数几个国家/地区的低信誉 IP 段,正规客户的国家来源却相对固定;
- 动态 IP 和机房 IP 混着来:很多是境外动态住宅 IP 和中转机房轮番试投,HELO 名随机、发信频率脉冲式;
- 正规客户来源稳定:你的海外客户、分公司、供应商,来源国家基本一年不变。
所以「按来源 IP 归属地」做拦截的逻辑是:先统计、再拦截,而不是拍脑袋封国家。业务上只有 5 个国家/地区的邮件往来,突然每天有 40 个国家的 IP 来投递,其中 30 个国家的命中率接近 100%——这些地区直接拒收,几乎不误伤。
二、方案:日志打属地标签 → 统计命中率 → 生成拦截策略
三步走:
- 打标签:解析 Postfix
maillog,把每条来信的client=xxx.xxx.xxx.xxx抽出来,调 IP 归属地接口补上国家/省市/运营商; - 统计:按国家聚合「来信总数 vs 被判垃圾数」,算出各地区垃圾命中率;
- 落策略:对「来信量不小 + 垃圾命中率超过 95% + 不在业务国家清单」的来源国家,把它们的 IP 段写进 Postfix 的
check_policy_service或前置防火墙拒绝规则。
统计周期建议一个月起步,别拿三天的数据就封国——客户用境外动态 IP 是存在的,误伤一次就是事故。
三、Go 实现
选 Go 是因为邮件服务器周边工具用 Go 写很顺手:单二进制、无依赖、日志解析并发安全。下面的程序读取 maillog,提取来源 IP,逐个查归属地(带缓存,控制免费接口频率),按国家聚合统计后输出 Top 来源和拦截建议。归属地用 IP9 免费接口 https://ip9.com.cn/get?ip=<ip>,免费版 60 次/分钟,程序里做了去重缓存,实际请求量远低于这个数。
go
package main
import (
"bufio"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"regexp"
"sort"
"sync"
"time"
)
const geoAPI = "https://ip9.com.cn/get?ip=%s"
const logPath = "/var/log/mail.log" // 换成你的 maillog 路径,测试可用样例文件
var client = &http.Client{Timeout: 5 * time.Second}
var reClient = regexp.MustCompile(`client=([0-9a-fA-F:.]+)`)
type GeoResp struct {
Ret int `json:"ret"`
Data struct {
Country string `json:"country"`
Prov string `json:"prov"`
City string `json:"city"`
Isp string `json:"isp"`
} `json:"data"`
}
// 归属地查询:进程内缓存,重复来源 IP 只查一次
var cache sync.Map // ip -> country
func lookupCountry(ip string) string {
if v, ok := cache.Load(ip); ok {
return v.(string)
}
req, _ := http.NewRequest("GET", fmt.Sprintf(geoAPI, ip), nil)
req.Header.Set("User-Agent", "mail-geo-report/1.0")
resp, err := client.Do(req)
if err != nil {
return "unknown"
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var r GeoResp
if err := json.Unmarshal(body, &r); err != nil || r.Ret != 200 {
return "unknown"
}
cache.Store(ip, r.Data.Country)
return r.Data.Country
}
func main() {
f, err := os.Open(logPath)
if err != nil {
fmt.Println("打开日志失败:", err)
return
}
defer f.Close()
var ips []string
seen := map[string]bool{}
sc := bufio.NewScanner(f)
sc.Buffer(make([]byte, 1024*1024), 1024*1024)
for sc.Scan() {
line := sc.Text()
m := reClient.FindStringSubmatch(line)
if m == nil {
continue
}
ip := m[1]
if !seen[ip] { // 简化:先按独立来源 IP 统计;真实场景可按每条来信计数
seen[ip] = true
ips = append(ips, ip)
}
}
if err := sc.Err(); err != nil {
fmt.Println("读取日志出错:", err)
return
}
// 逐个查归属地(串行,天然限速;独立 IP 数量大时可加 worker 池)
countryOf := map[string]string{}
for _, ip := range ips {
countryOf[ip] = lookupCountry(ip)
time.Sleep(100 * time.Millisecond)
}
// 按国家汇总独立 IP 数
stat := map[string]int{}
for _, c := range countryOf {
stat[c]++
}
type kv struct {
Country string
Count int
}
var list []kv
for c, n := range stat {
list = append(list, kv{c, n})
}
sort.Slice(list, func(i, j int) bool { return list[i].Count > list[j].Count })
fmt.Println("来信来源 IP 属地分布(Top 10,独立 IP 数):")
for _, it := range list[:min(10, len(list))] {
fmt.Printf(" %-12s %d\n", it.Country, it.Count)
}
fmt.Println("\n下一步:把这份分布和你业务上的国家清单比对,高比例陌生国家的来源段加入拒绝策略。")
}
func min(a, b int) int {
if a < b {
return a
}
return b
}运行:go run mail_geo.go。程序输出来源国家排行,unknown 占比高说明解析失败或日志格式不同,先检查 client= 的正则是否匹配你的 maillog 格式。
生产化三处要改:一是把「按独立 IP 计数」换成「按来信事件数」聚合(改成统计每条日志行的 country);二是查归属地时加 worker 池 + 缓存落盘,几万存量 IP 串行太慢;三是拦截动作接到 Postfix 的 check_policy_service 上,返回 REJECT 或 DUNNO,而不是直接在程序里动防火墙。
四、落地注意事项
- 先标记、后拦截。前两周只把命中率统计发到群里或飞书告警,确认没有把客户国家误判进去,再上自动拒绝。误伤一封海外客户邮件,比漏收两百封垃圾严重。
- 别只看国家。同一个国家里有正常客户也有垃圾源。更细的维度是省份 + 运营商 + IP 类型:IP9 免费版给省市和运营商(
isp),VIP 版额外给ip_type(ISP 住宅 / BUS 企业 / IDC 机房),「境外机房 + 业务无关国家」是最高置信的拒绝组合。 - 动态 IP 会变。住宅 IP 段的归属和信誉每个月都在变,统计任务建议挂 cron 每周重跑,策略跟着数据走。
- RBL 是必要但不充分。Spamhaus 这类公共黑名单有滞后,自有统计能补上新 IP 池的窗口期,但别替代 SPF/DKIM/DMARC 这些基础防线。
总结
垃圾邮件的本质是一大堆陌生 IP 的重复投递,IP 归属地统计正好把「陌生」量化成「哪些国家/运营商高概率是垃圾源」。Go 程序读 maillog、查归属地、出分布,一周数据跑下来要拦谁一目了然。归属地接口用 IP9 免费版 https://ip9.com.cn/get?ip=xxx 足够(60 次/分钟,加缓存几乎无压力),需要区分住宅/机房时再看 VIP 的 ip_type。先统计、再灰度、最后拦截,邮件这条线就能清净大半。官网:https://www.ip9.com.cn 。