简介:A盾 v5.0 版本源代码面向 Windows 安全软件开发者与源码研读者,适合希望了解防护类软件整体架构、驱动模块与界面资源组织方式的中高级学习者参考。压缩包共 319 个文件,约 5.1 MB,以 133 个 .h 头文件、54 个 .c 和 52 个 .cpp 源文件为主体,并包含 .sys 驱动、.dll 动态库、.exe 可执行文件,以及 .ico 图标、.bmp 位图、.rc 资源脚本和 vcproj、vcxproj、makefile、sln 等工程配置,能够较完整地呈现源码、资源与构建文件之间的对应关系。已有 423 人学习或下载。读者可从中观察安全防护程序的模块划分、网络数据包处理相关代码、函数控制与反汇编辅助实现,也可参考多套 Visual Studio 工程和批处理构建脚本的组织方式,对照梳理驱动入口、资源编译与可执行文件生成流程,用于课程设计、源码对照阅读、工程复现或同类项目搭建时的结构借鉴,对理解传统 Windows 安全类项目的代码分层与资源组织也有一定参考价值。
1. A盾 5.0 源代码能解决什么:一套可自编译的流量防护底座
拿到 A盾 5.0 版本源代码的人,十个里有八个第一反应是直接 make,然后在一堆 find_package 报错里耗掉一整个晚上。这套 A盾 5.0 源代码给的是一套完整的主机侧与 Web 侧流量防护系统:采集层在网卡上抓包做协议还原,规则引擎做多模式串与正则匹配,控制面负责把策略下发到各节点,管理台把命中日志和告警汇成可视化的面板。它解决的问题很具体——手里有一批对外服务,想在不改动业务代码的前提下加一层访问控制、频率限制和异常请求拦截,又不愿意长期为商业授权付费,这套源码就是能自己改、自己编译、自己加规则的底子。适合有 Linux 运维基础、读得懂 C 和 Go、愿意为一次部署投两三天调试时间的团队;指望装完点两下就收工的人,用它会很难受。
2. 把 A盾 5.0 源代码拆成五层:目录职责与编译链对齐
源码包解开之后,第一眼会觉得目录多、文件杂。不要急着找 main 函数,先把目录和职责对应上,后面编译报错时才知道该去哪个目录翻代码。这套系统的分层在同类项目里算是比较克制的,五层各管一段,层与层之间靠配置文件和一个 protobuf 契约连接,没有那种跨层乱调的写法,改一处不至于牵动全身。
2.1 源码树速览与五层职责划分
我一般会先执行一遍带深度的树形列表,把两层目录看清楚再动手,这一步花三分钟,能省掉后面半小时的盲目翻找。
# 只看两层,避免被 build/ 和 vendor/ 淹没 find . -maxdepth 2 -type d -not -path "*/.git*" -not -path "*/vendor*" | sort # 顺手确认源码包里有没有预置的构建产物,有就删掉重编 find . -name "CMakeCache.txt" -o -name "*.o" | head看完目录,职责大致是这么分的:
| 目录 | 主要语言 | 职责 | 上线后改动频率 |
|---|---|---|---|
| agent/ | C | 抓包、流重组、请求初筛、本地缓存 | 低,基本不动 |
| core/ | C++ | 规则匹配引擎,AC 自动机加 PCRE2 正则 | 中,加规则时可能碰 |
| controller/ | Go | 节点管理、策略下发、REST 接口、鉴权 | 高,二开主要在这 |
| console/ | Vue3 + Vite | 管理台,规则编辑与命中看板 | 高 |
| deploy/ | Shell / SQL | 建库脚本、systemd unit、日志轮转 | 低 |
| proto/ | protobuf | 采集层与控制面的通信契约 | 极低,改动要全量重编 |
采集层用 C 写是为了贴着网卡干活,抓包和流表操作对延迟敏感,用带 GC 的语言做这一层,尾延迟会飘。控制面用 Go 是常见的取巧:并发模型简单,交叉编译方便,改完业务逻辑几十秒就能出一个新二进制,适合频繁迭代的下发逻辑。规则引擎放在 C++ 里,是因为多模式串匹配要用到自动机和内存池,这块是整套系统的性能核心,热路径上多一次内存分配都可能在十万级 QPS 下被放大。
需要提醒的是proto/这个目录。很多人二次开发时只想改控制面,结果发现字段对不上,回头才知道采集层和控制面的消息结构是从这份契约生成的,改字段必须两边重新生成代码再各自编译。常见做法是改完.proto先跑一次生成脚本,确认两边的桩代码时间戳都更新了,再开始编,否则会出现"编译通过但字段永远是零值"这种很难查的问题。
2.2 编译链与依赖版本对齐
编译这一步的坑集中在依赖版本上。源码里的CMakeLists.txt会写明最低要求的 CMake 版本和几个find_package的依赖,先读再装,比照着博客抄一堆包要靠谱得多。
# 1) 先摸清要求,别急着装一堆用不上的库 grep -rn "cmake_minimum_required" --include=CMakeLists.txt . | head grep -rhn "find_package(" --include=CMakeLists.txt . | sort -u # 2) 按上面列出来的清单装依赖(Debian/Ubuntu 系) sudo apt-get install -y build-essential cmake pkg-config \ libpcap-dev libssl-dev libpcre2-dev libjson-c-dev \ libmaxminddb-dev zlib1g-dev # 3) 出构建目录,Release 并关掉调试日志 cmake -S . -B build \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/usr/local/adun \ -DDEFAULT_CONF_DIR=/etc/adun \ -DENABLE_DEBUG_LOG=OFF \ -DWITH_MAXMIND=ON # 4) 并行度按内存给,不要无脑 nproc cmake --build build -j 4 # 5) 装到前缀目录,方便后面做升级回滚 sudo cmake --install buildCMAKE_BUILD_TYPE=Release必须显式给,默认是空串,等于不带任何优化,编出来的采集层跑满一个核也扛不住多少流量。DEFAULT_CONF_DIR决定了程序去哪里找配置文件,装到/usr/local/adun而配置放/etc/adun是常见约定,升级时替换前缀目录不会把配置冲掉。ENABLE_DEBUG_LOG关掉之后,热路径上的日志调用被编掉,性能差别在压测里能到百分之十几,但排查问题时又要临时打开,所以我的习惯是编两套,一套生产用一套排障用,别在同一套上反复改开关。-j的值看内存:规则引擎里模板用的多,单个编译单元吃内存不小,8G 的机器上-j16大概率会被 OOM killer 干掉,看到c++: fatal error: Killed signal terminated program cc1plus就是这个原因,降到-j2或者临时加 swap 就能过。
控制面和前端各自独立编译,跟上面的流程分开跑:
# 控制面:源码带 vendor 就直接用,不要去拉外网依赖 cd controller go build -mod=vendor -trimpath \ -ldflags "-s -w -X main.buildVersion=5.0.0 \ -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ -o adun-ctl ./cmd/adun-ctl # 前端:有 lock 文件就用 ci,保证依赖树和打 tag 时一致 cd ../console npm ci --no-audit --no-fund npm run build # 产物在 dist/,交给接入层做静态目录即可-trimpath去掉编译机上的绝对路径,二进制里不留构建环境信息,换台机器编也能对得上。-X main.buildVersion这类注入把版本号写死在二进制里,后面查线上到底是哪次构建、有没有漏发,靠adun-ctl version一眼就能看出来,比对着文件时间猜要靠谱。前端用npm ci而不是npm install,前者严格按 lock 文件装、有差异直接报错,后者会悄悄升级小版本,管理台白屏十个里有三个是依赖被升出了不兼容。
2.3 最小可运行拓扑:单机一体化部署
验证阶段没必要上来就搞多节点,一台机器跑全套足够了。最小拓扑是 MySQL 存配置和规则的元数据,Redis 存限速计数和会话,agent、controller、console 静态产物同机。配置文件长这样:
# /etc/adun/adun.yaml 单机验证用最小配置 agent: iface: eth0 # 采集网卡,写业务实际出入口那块 snaplen: 1514 # 截断长度,1514 覆盖标准以太帧不丢头 buffer_mb: 256 # 环形缓冲,按峰值流量两倍给 bpf: "tcp port 80 or tcp port 443 or tcp port 8080" rule: engine: hybrid # ac=纯多模式串快,hybrid=带正则更灵活 max_regex_concurrency: 4 # 正则并发上限,超了走排队别丢 rule_dir: /etc/adun/rules controller: listen: 0.0.0.0:9443 mysql_dsn: "adun:******@tcp(127.0.0.1:3306)/adun?charset=utf8mb4&parseTime=true" redis_addr: 127.0.0.1:6379snaplen设成 1514 是有讲究的,抓完整帧才能做 HTTP 头解析,设小了头部截断,规则里的 header 匹配会全部落空,而统计里的 pps 又是正常的,特别容易误判成规则写错。engine选hybrid还是ac取决于规则集:纯字符串匹配的场景用ac,吞吐能高一截;一旦规则里出现正则,就得开hybrid,因为hybrid会把正则单独走一条线程池,避免一个慢正则拖住整条匹配链路。
建库脚本在deploy/sql/下,先跑初始化再启服务:
-- 核心表就几张,节点表是排障时最常查的 CREATE DATABASE IF NOT EXISTS adun DEFAULT CHARSET utf8mb4; USE adun; CREATE TABLE IF NOT EXISTS node ( id VARCHAR(64) PRIMARY KEY, addr VARCHAR(128) NOT NULL, ver VARCHAR(32) NOT NULL, last_seen DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 1 ); CREATE TABLE IF NOT EXISTS rule_meta ( rule_id VARCHAR(64) PRIMARY KEY, stage VARCHAR(16) NOT NULL DEFAULT 'prod', version INT NOT NULL DEFAULT 1, updated_at DATETIME NOT NULL );启动顺序不能反:MySQL 和 Redis 就绪 → controller 起来并完成建表检查 → agent 起来连上 controller 拿初始规则集 → 最后把前端产物挂到接入层。顺序反了会出现 agent 启动时连不上控制面、进程直接退出,看日志只有一行连接被拒,很容易以为是防火墙问题。node.last_seen是排障时的第一现场,节点掉线、心跳停了、规则没下发,都先看这列时间有没有在动。
3. 规则引擎怎么配:从 DSL 字段到命中链路的落地
规则是这套系统里你唯一需要天天碰的东西。它用一套 JSON 描述的 DSL,字段不算多,但每个字段的取值都直接影响数据面的行为,写错一个phase就可能导致整条规则永远不命中,而控制台还显示"已生效",这类问题排查起来最费时间。
3.1 规则 DSL 结构与字段含义
一份规则由匹配条件、命中动作、风险分和生效窗口四块组成。字段含义整理如下,改规则前建议对着这张表核一遍:
| 字段 | 含义 | 取值与注意事项 |
|---|---|---|
| id | 规则唯一标识 | 建议按 业务-动作-序号 命名,如 web-api-0012 |
| phase | 匹配阶段 | header / body / response,body 阶段开销大慎用 |
| match.field | 匹配对象 | uri / header / ua / body / src_ip |
| match.op | 操作符 | prefix / contains / regex / cidr / rate |
| match.pattern | 模式串 | regex 类型必须能被 PCRE2 编译通过 |
| cond | 附加条件 | 频率、地理、指纹等,可省略 |
| action | 命中动作 | log / challenge / block / drop,drop 在数据面直接丢 |
| score | 风险分 | 0–100,多条规则可累加 |
| ttl | 生效窗口 | 秒,到期自动失效,用于临时封禁 |
| priority | 优先级 | 数字小的先匹配,同优先级按 id 字典序 |
phase是最容易搞混的一个。header阶段只看请求行和头部,开销小、命中快,绝大多数访问控制和频率限制都放在这里;body阶段要把整个请求体收全再匹配,大文件上传场景下内存会飙,除非确实要检查报文内容,否则别往这放;response阶段是看响应,适合做数据泄露类的检测,但这条链路对延迟很敏感,规则写得越复杂,业务侧感知到的响应时间就越长。
action的取值决定了后面能看到什么样的日志。log只记录不拦,是做灰度最安全的档位;challenge会返回一个校验页,适合人机区分;block返回拦截页并记录完整上下文;drop在数据面直接把连接丢掉,延迟最低,但日志是异步写的,队列一满就会丢记录,这就是后面排查章节里那条"明明被拦了却查不到日志"的由来。
3.2 自定义规则上线流程
写规则最忌讳直接改生产环境的规则文件然后重启。正确路径是先在本地校验语法,再灰度推到单台节点观察,最后全量。下面这条规则针对登录接口做源 IP 频次限制,命中后返回校验页而不是直接封,属于比较温和的写法:
{ "id": "web-api-0012", "desc": "登录接口按源 IP 做频次限制", "phase": "header", "match": { "field": "uri", "op": "prefix", "pattern": "/api/v1/login" }, "cond": { "field": "src_ip", "op": "rate", "window": 60, "threshold": 20 }, "action": "challenge", "score": 60, "ttl": 1800, "priority": 100, "enabled": true }推之前先做三件事,顺序别乱:
# 1) 语法校验,只解析不落库,错在哪个字段会直接指出来 adun-ctl rule validate -f /etc/adun/rules/web-api-0012.json # 2) 灰度推给单台采集节点,stage=canary 不影响其它节点 adun-ctl rule push -f /etc/adun/rules/web-api-0012.json \ --target agent-01 --stage canary # 3) 确认版本号和生效状态,再决定要不要全量 adun-ctl rule list --stage canary --output tablevalidate会把正则编译一遍,PCRE2 编不过的写法在这里就被拦下来了,而不是等到数据面加载时才报错。--target指定节点,配合--stage canary就形成了灰度:目标节点拉新规则,其它节点维持原样,观察半小时看误拦率再推全量。rule list输出的 version 列每次 push 自增,出问题时用--version回滚到上一版,比手工改文件快得多。这套流程看着啰嗦,但等你经历过一次把没校验的正则推到全部节点、CPU 打满的事故,就会老老实实每次都走一遍。
3.3 阈值与限速参数怎么定
阈值拍脑袋定是误拦的主要来源。我的做法是先只上log动作跑一天,把命中数据导出来,再按真实分布定阈值。下面这几个参数是最常调的:
| 参数 | 含义 | 定值方法 | 常见取值 |
|---|---|---|---|
| window | 统计窗口 | 取业务峰值周期的二分之一 | 60s |
| threshold | 窗口内阈值 | 正常流量 P99 乘以 1.5 | 20 次/窗口 |
| burst | 允许突发 | 正常并发乘以 0.3,别设 0 | 5 |
| block_ttl | 封禁时长 | 看误拦代价,宁可短 | 300s |
| score_threshold | 累计分阈值 | 按单条规则误报可接受度反推 | 100 |
threshold用 P99 而不是平均值,是因为平均会被大量低频用户拉低,按平均值定出来的阈值对正常的高频用户就是误伤。burst设成 0 是很常见的错误,看着"严格",实际会把正常的页面加载并发全部拦掉——一个页面同时请求十几个接口,在统计上就是一次突发。block_ttl宁短勿长,封五分钟和封一小时对攻击者的成本差别不大,但对被误拦的真实用户,感受完全不同。
3.4 命中日志与验证方式
规则上完必须验证,验证的入口是命中日志表。按规则维度聚合,先看命中量最大的前二十条,误拦通常藏在里面:
-- 按规则聚合看命中分布,hits 突然很大的先怀疑误拦 SELECT rule_id, action, count() AS hits, uniqExact(src_ip) AS uniq_ips, quantile(0.95)(score) AS p95_score FROM adun_hit_log WHERE ts >= now() - INTERVAL 30 MINUTE GROUP BY rule_id, action ORDER BY hits DESC LIMIT 20;看两个比例:hits / uniq_ips很高说明是少数源在反复命中,大概率是真攻击;反过来hits大而uniq_ips也大,就是大面积命中,这种要么规则写宽了,要么业务本身有这个访问模式被误伤。p95_score用来验证风险分累加是不是符合预期,如果一条规则的 P95 分数长期顶在 100,说明score_threshold定低了,正常请求也在触发累计封禁。
日志本身也有代价。命中量大的时候,一次全表扫描能把查询库拖垮,所以按小时分区或者按天分区是必须做的,ts字段上要有索引,且查询条件一定要带上时间范围,别写WHERE rule_id = ?就完事。
4. 采集与控制面联调:数据从网卡到告警的完整链路
单机跑通之后,下一步是把链路打通并确认它不会成为瓶颈。这一段的目标很明确:确认采集层抓到了包、规则引擎匹配上了、控制面收到了指标、告警按预期发出。四个环节任何一个断了,前面配的规则都是白搭。
4.1 采集模块启动参数与抓包过滤
采集层最常出的问题是"进程活着但 pps 是零"。先用同一条 BPF 表达式在 tcpdump 上验证,确认表达式本身能抓到东西,再去怀疑程序:
# 用和配置里一模一样的过滤器验证,能出包说明表达式没问题 sudo tcpdump -i eth0 -nn -c 20 \ 'tcp port 80 or tcp port 443 or tcp port 8080' # 带 VLAN 标签的环境要显式加上 vlan,否则一个包都抓不到 sudo tcpdump -i eth0 -nn -c 20 'vlan and (tcp port 443)' # 采集层自带的统计,十秒刷一次看 pps 和丢包 watch -n10 'adun-agent --stat'VLAN 这一条踩过的人不少:交换机上打了标签,报文里就多出四个字节的 VLAN 头,BPF 表达式没写vlan关键词时,内核在过滤阶段就把包判成不匹配,采集层自然一条都收不到,而ip addr看上去一切正常。另一个隐性坑是iface写错,多网卡机器上业务流量走的是eth1而配置里写了eth0,抓到的全是管理流量。
--stat输出里要盯三个数:pps是实际收到并进入处理流程的包速率,drop是环形缓冲溢出丢掉的包数,match_cost是单包匹配的平均耗时。drop长期非零说明buffer_mb给小了或者下游匹配太慢;match_cost在微秒级是正常,涨到几十微秒就要回头检查规则集里是不是混进了慢正则。
4.2 控制面接口与策略下发
控制面提供 REST 接口,鉴权走 Bearer Token。日常运维和自动化脚本都从这里下发,比手工改文件可靠:
# 换 token,密码从环境变量取,别写在脚本里 TOKEN=$(curl -sk -X POST https://127.0.0.1:9443/api/v1/auth/token \ -H 'Content-Type: application/json' \ -d "{\"username\":\"admin\",\"password\":\"${ADUN_PASS}\"}" \ | python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])') # 把规则推到指定节点,stage 决定是灰度还是全量 curl -sk -X POST https://127.0.0.1:9443/api/v1/rules/push \ -H "Authorization: Bearer ${TOKEN}" \ -H 'Content-Type: application/json' \ -d '{"rule_file":"/etc/adun/rules/web-api-0012.json", "targets":["agent-01"],"stage":"canary"}'需要批量化的时候用 Go SDK 更方便,尤其是要在发布流水线里做钩子:
// 下发客户端:超时给足,采集节点多的时候逐台推比广播可靠 cli := adun.NewClient("https://127.0.0.1:9443", adun.WithToken(os.Getenv("ADUN_TOKEN")), adun.WithTimeout(15*time.Second)) req := &adun.PushRequest{ RuleFile: "/etc/adun/rules/web-api-0012.json", Targets: []string{"agent-01", "agent-02"}, Stage: "canary", // canary 只影响目标节点,prod 才全量 DryRun: false, // true 只校验不生效,适合流水线预检 } resp, err := cli.PushRule(context.Background(), req) if err != nil { log.Fatalf("push failed: %v", err) // 失败要中断发布,别吞掉 } log.Printf("pushed=%d skipped=%d", resp.Pushed, resp.Skipped)WithTimeout别用默认值,节点多的时候逐个下发累积起来很容易超时,超时之后一半节点推了新规则一半没推,规则版本不一致,这种状态下排查问题基本靠猜。DryRun在流水线里很有用,先空跑校验一遍全量规则,通过了再真推,能把语法错误挡在发布之前。resp.Skipped非零要打出来看,通常是被下线的节点或版本不匹配的节点,静默跳过会让"规则明明推了却没生效"变成一个悬案。
4.3 告警通道配置与噪声收敛
告警配得松,一天几百条没人看;配得紧,真出事的时候没声音。收敛的核心是分级和去重,而不是把所有规则都挂到同一个通道上。
常见做法是按动作分级:drop和block走即时通道,五分钟内同一规则同一源只发一次;challenge累计到一定量再发日报;log只入库不发。去重的键用rule_id + src_ip,在 Redis 里放一个带 TTL 的标记位,TTL 就是静默窗口。这样一次持续的攻击只会在开始和结束时各响一声,中间不刷屏。
另一个要调的参数是告警的触发阈值。单条命中就告警,等于把日志搬到了聊天窗口;我的习惯是按窗口计数,比如某规则五分钟内命中超过 500 次且独立源 IP 超过 100 个才发,这两个条件同时满足基本可以排除误报。
4.4 压测与观测:确认这层不是瓶颈
上线前必须压一遍,不是为了测系统的极限,而是为了知道正常业务量下这层防护的开销。开两个终端,一个打流量一个看指标:
# 终端一:对业务接口施压,注意打的是真实业务路径 wrk -t4 -c200 -d60s --latency http://127.0.0.1:8080/api/v1/login # 终端二:盯采集层的三个核心指标 watch -n5 'adun-agent --stat | egrep "pps|drop|match_cost|queue"'观察顺序是这样的:先看drop,压测期间必须为零,非零说明缓冲不够;再看match_cost,对比压测前后有没有明显抬升,抬升超过一半说明规则集太重;最后看业务侧的 P99 延迟,这层防护引入的额外延迟应该在毫秒以内,超过就是规则里有慢正则或者日志写入阻塞了处理线程。压测结束把几条关键指标记下来,作为上线后的基线,日后指标漂移时才有对比。
5. 常见问题排查:A盾 5.0 部署里最容易翻车的五个点
下面这五条都是我在不同环境里真实撞过的,按现象、原因、解决三段写,遇到对得上的直接照着排。
5.1 编译与启动阶段的两个硬拦路虎
现象:编译进行到一半,cc1plus被 Killed,make报错退出。原因不是代码问题,是并行度给太高,单个编译单元内存占用大,机器撑不住被内核 OOM killer 干掉。解决是把-j降到 2 到 4,或者临时挂一块 swap 把这一关过掉。判断方法很简单,dmesg | tail里能看到 oom-killer 的记录,有这条就不用再怀疑编译器和依赖了。
现象:服务 systemd 启动即退出,日志只有一行连接被拒。原因是启动顺序错了,controller 起来时要连 MySQL 和 Redis,agent 起来时要连 controller,任何一个前置没就绪就直接退出。解决是把依赖写进 systemd unit 的After=和Requires=,同时在启动脚本里加一个就绪等待循环,连上再往下走。别指望"多启几次就起来了",这种不稳定会把后面的排障带偏。
5.2 抓包与规则阶段的两个静默失败
现象:agent 进程正常,--stat里 pps 恒为零。先按上一章的方法用 tcpdump 验证 BPF 表达式,如果 tcpdump 也抓不到,八成是网卡名写错或者报文带 VLAN 标签而过滤器没写vlan。解决是改iface为业务实际网卡,并在 BPF 里加上vlan and (...)。这类问题最坑的地方在于它不报错,程序状态一切正常,只有流量是零。
现象:用户反馈被拦了,控制台却查不到任何命中记录。这是异步日志队列满导致的丢弃,尤其是action为drop的规则,数据面为了低延迟不等待日志落盘,队列一满直接丢。解决分两步:先把log_queue容量调大并开启丢弃计数监控,再把关键规则临时改成block,走同步写日志的路径,等确认规则准确后再改回drop。
5.3 误拦与前端显示的两个典型坑
现象:整个办公网段被限速,投诉集中爆发。原因是出口做了地址转换,几百个终端在采集层看到的是同一个源 IP,按src_ip做频次统计自然被合并。解决是让规则优先读X-Forwarded-For里的真实地址,或者在规则维度上换成更细的指纹组合,比如源 IP 加 UA 加会话标识。只要单位里有统一出口,这条几乎必踩。
现象:管理台登录后白屏,接口请求全部失败。前端构建时的 API 基地址还指向localhost,浏览器在用户机器上自然连不到。解决是在npm run build之前把环境变量里的 API 基地址改成部署后的实际地址,改完重新构建。建议把这个变量写进部署脚本,别依赖手工改.env,忘记一次就要重来一遍。
6. 进阶技巧:用影子模式给新规则做灰度回归
规则改得越多,越不敢直接上线。我后来固定用一个办法:新规则先以影子模式跑,只匹配不动作,把"本该拦什么"和"实际拦了什么"做对比,差异在可接受范围内再切正式动作。
影子模式在配置里就是一个开关,rule.mode: shadow打开后引擎照常匹配、照常打分、照常写命中日志,但动作被强制降级成log,请求原样放行:
# /etc/adun/adun.yaml 片段 rule: mode: shadow # shadow=只记录不动作,prod=按规则动作执行 shadow_tag: shadow-v3 # 影子期日志打标,方便和正式流量区分打开之后,用一段脚本把影子日志和现有规则的日志拉下来做对比,看新增规则是不是只命中了预期的那些请求:
# diff_shadow.py:对比影子规则与现网规则的命中集合 import json, urllib.request def fetch(tag, minutes=60): url = (f"http://127.0.0.1:9443/api/v1/hits?" f"tag={tag}&window={minutes}m&limit=50000") with urllib.request.urlopen(url, timeout=30) as r: return {(h["rule_id"], h["src_ip"]) for h in json.load(r)["items"]} shadow = fetch("shadow-v3") # 新规则只记录 prod = fetch("prod") # 现网已生效规则 only_new = shadow - prod # 新规则多命中的,逐条人工确认 miss = prod - shadow # 新规则漏掉的,说明覆盖不全 print(f"shadow={len(shadow)} prod={len(prod)} " f"only_new={len(only_new)} miss={len(miss)}") for item in list(only_new)[:20]: print("check:", item) # 打到业务侧核实是否是误拦only_new是重点,全是新规则独有的命中,逐条拿去业务侧核实,能对上的放行、对不上的回头改pattern。miss用来验证覆盖度,如果新规则的目的是替代旧规则,miss必须接近零才能切。这一步做完再改mode: prod,误拦率比直接上线低一个数量级。
从那以后我每次推规则都强制走一遍影子对比,哪怕只改了一个字符。有次就是因为跳过了这步,把一条本该只匹配测试路径的前缀规则写成了匹配根路径,全站流量进了校验页,值班电话响了半小时。希望这套流程能帮你少走一遍同样的路。
本文还有配套的精品资源,点击获取