Skip to content

企业邮箱总收垃圾邮件?按来源 IP 归属地自动拦截(Go 实现) ​

公司用的 Postfix 邮件服务器,每天进几百封垃圾邮件:境外赌场广告、伪造的「发票」「律师函」、钓鱼链接。SPF、DKIM、DMARC 都配了,能防「冒用自家域名」,但防不住陌生域名从陌生 IP 发进来的垃圾——它们根本不伪造你,就是拿低信誉 IP 池硬塞。反垃圾网关有,可 RBL 黑名单有滞后,新 IP 出来一两天内照样漏进来。这篇讲一个补丁方案:把来信来源 IP 打上归属地标签,统计各地区的「垃圾命中率」,对高垃圾比例的来源地区做拦截,Go 实现,能直接跑。

一、垃圾邮件来源有什么规律 ​

拿企业邮局的日志统计一个月,规律一般很明显:

  • 来源国家高度集中:垃圾邮件的大头来自少数几个国家/地区的低信誉 IP 段,正规客户的国家来源却相对固定;
  • 动态 IP 和机房 IP 混着来:很多是境外动态住宅 IP 和中转机房轮番试投,HELO 名随机、发信频率脉冲式;
  • 正规客户来源稳定:你的海外客户、分公司、供应商,来源国家基本一年不变。

所以「按来源 IP 归属地」做拦截的逻辑是:先统计、再拦截,而不是拍脑袋封国家。业务上只有 5 个国家/地区的邮件往来,突然每天有 40 个国家的 IP 来投递,其中 30 个国家的命中率接近 100%——这些地区直接拒收,几乎不误伤。

二、方案:日志打属地标签 → 统计命中率 → 生成拦截策略 ​

三步走:

  1. 打标签:解析 Postfix maillog,把每条来信的 client=xxx.xxx.xxx.xxx 抽出来,调 IP 归属地接口补上国家/省市/运营商;
  2. 统计:按国家聚合「来信总数 vs 被判垃圾数」,算出各地区垃圾命中率;
  3. 落策略:对「来信量不小 + 垃圾命中率超过 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 。