我一直在想一件事:一份攒了好几年的防火墙策略表,里面到底有几条是真的在起作用。

策略表这东西有个特点——只增不减。有人报"访问不了",就加一条 allow;出了安全通报,就加一条 deny。加的时候没人会回头看前面有没有一条更宽的已经把这件事定完了。几年下来,表里躺着一堆"写了但永远轮不到它执行"的规则。

我手上没有能随便导出策略表给我跑的设备,所以按一套常见的网段结构自己造了一份 42 条的表(办公/车间/财务/服务器/DMZ/访客/VPN/监控/研发/打印 + 外网),分了 2022 基线、2023 临时放通、2024 隔离补漏、2025 随手加的四批,然后写了个引擎把每条规则是死是活算了一遍。下面的数字全是这台机器上零依赖 Node 跑出来的。

先把策略表变成能算的东西

规则就是五元组 + 动作,匹配逻辑没什么花头:

function match(r, p) {
  if (!inCidrs(r.src, p.sip)) return false;
  if (!inCidrs(r.dst, p.dip)) return false;
  if (!(r.proto.includes('any') || r.proto.includes(p.proto))) return false;
  if (p.proto === 'icmp') return true;
  if (r.dport === 'any') return true;
  return r.dport.includes(p.dport);
}

难的是"算尽"。我不能靠抽样猜,得把整个空间走一遍。我的做法是把地址空间离散成 11 个区段的代表地址,配 12 个代表端口、tcp/udp 各一遍,再加 icmp,得到 3025 个域内点。在这个域里我可以逐点求值,于是"这条规则有没有可能被执行"就变成了一个能算尽的判定,而不是靠拍脑袋。

42 条里,15 条永远轮不到

我给每条规则数两件事:它在域内匹配多少个点,以及其中有多少点是首次命中它(即前面没有任何规则先匹配)。后者为 0,说明这条规则永远不会被执行到。

规则数 42 | 域点数 3025
有效(live)       27
危险影子(danger)  4
无害冗余(redun)  10
域内无匹配        1

真正要命的是那 4 条 danger。它们不是重复写,是策略意图根本没生效:

#20 访客禁业务系统 80/443 [deny]  <- 被 #3 内网访问业务系统 [allow] 抢先命中
#21 研发禁外传 8080      [deny]  <- 被 #14 临时:研发放通(未回收) [allow] 抢先命中
#22 办公禁访问 DMZ 后台  [deny]  <- 被 #4 办公上网与 DNS [allow] 抢先命中
#38 VPN 禁数据库 3306    [deny]  <- 被 #12 临时:MES 库联调 3306 [allow] 抢先命中

这四条的模式一模一样:2022 年写了一条宽 allow(10.0.0.0/8 整个内网段),2023 年又为联调开了 any 的临时口子;等到 2024 年想补一条精细 deny 时,它被排在前面那些宽 allow 后面,一个包都接不到。防火墙那边不会报错,配置界面上也显示"已启用",看上去一切正常。

我第一版脚本漏了一条

我最初的实现是标准做法:对每条规则,找有没有更早的一条在集合上完全覆盖它。

v1 逐条覆盖比对 报出: 14 条
v2 全域枚举     报出: 15 条
v1 漏掉的: #17(联合影子)

漏的是 #17「财务访问报表(补) 8080+8443」。它前面有 #15 只放 8080、#16 只放 8443,两条都不是它的超集,逐条比对全落空——但两条合起来正好把它吃干净。这种联合影子在真表里面只会更常见,因为人补规则就是这么补的:先加 A,再加 B,第三次懒得看了直接写"上面那两个"。

我还试过更省事的写法——直接比对网段字符串是不是相等或者是不是 0.0.0.0/0。结果报出 28 条,其中 14 条是误报,把 #14、#29、#37 这些活得好好的规则全判成死的。掩码不做算术、只比文本,结论就废了。

删掉它们,行为一点没变——但这不等于该删

我把 15 条不可达规则全删了,拿剩下的 27 条在 3025 个点上逐点重算一遍:

删除 15 条,剩 27 条
域内逐点判定差异: 0 处

零差异。这说明静态分析说"不可达"是硬结论,不是概率。

但那 4 条危险影子我不能删,删了等于承认"这条策略不要了"。正确做法是把它们挪到吞掉它们的规则前面。我这么干了一次:

域内放通点数 703 -> 688,判定变化 15 点
前移后仍不可达: 11 条;其中危险影子: 0 条
  office->dmz tcp/443: allow => deny
  guestwifi->server tcp/443: allow => deny
  vpn->server tcp/3306: allow => deny
  rd->wan tcp/8080: allow => deny

判据就这么一句:动作相同的影子 → 删;动作相反的影子 → 往前挪。前者是写重了,后者是策略被吃掉了。

拿命中率找死规则,会误杀两条活的

我按一套带权重的分布造了 2 万个包(办公和上网类流量占大头),跑了一遍命中统计:

流量 0 命中的规则: 17 条
  其中静态判定不可达: 15 条
  其中静态判定有效(误杀候选): 2 条  #16 #28
静态不可达 15 条,其中流量真的 0 命中: 15 条

两个方向都值得看。反过来的方向很干净:静态说不可达的 15 条,流量里一条都没打到,两种方法互相印证。正方向就危险了——17 条零命中里有 2 条其实是活规则(#16 财务报表 8443、#28 DMZ 应用访问数据库),只是这两条路径平时没流量。

所以我现在的看法是:命中率只能证明"有",不能证明"无"。报表里 0 命中的规则,直接拿去删是会出事的。能证明"无"的只有静态分析。

顺带说一句 #24「旧网段残留」:它在我的 11 个区段域里匹配 0 个点,被标成"域内无匹配"。我把 192.168.100.0/24 加进域重跑,它立刻变成 live,首次命中 24 点。这条不是规则的问题,是我枚举域的边界问题——审计结论只在选定的域内成立,域里没有的网段,工具根本看不见。

同一份表换套系统,4.6% 的流量判定变了

这部分是文档结论,不是我实测的。微软《Windows 防火墙规则》写得很直白(核对日期 2026-10-24):

1. Explicitly defined allow rules take precedence over the default block setting.
2. Explicit block rules take precedence over any conflicting allow rules.
3. More specific rules take precedence over less specific rules...

Windows Firewall doesn't support weighted, administrator-assigned rule ordering.

而 iptables 那边是另一种世界观,Netfilter 的 HOWTO 里把链描述成一张清单(核对日期 2026-10-24):

A chain is a checklist of rules... If the rule doesn't match the packet, then the next rule in the chain is consulted. Finally, if there are no more rules to consult, then the kernel looks at the chain policy.

一个是"顺序遍历,先到先得",一个是"阻止优先,不看顺序,也不给你排序的权"。我把同一份 42 条表分别按两种语义算了一遍:

域内 3025 点,判定不同 140 点 (4.6%)
去掉兜底 deny 后,判定不同 70 点 (2.3%)

样本 guestwifi->server tcp/443:
  顺序语义 -> 命中 #3 内网访问业务系统 [allow]
  Windows   -> deny

同一条 访客 -> 服务器 443,在顺序语义下被 2022 年那条 10.0.0.0/8 的宽 allow 放行;在 Windows 语义下被"访客隔离内网"那条 deny 一票否决。规则一条没改,结果相反。

这解释了为什么我从一台设备往另一台"原样复刻"策略是件高风险的事:那份表里的顺序信息,在 Windows 防火墙上根本不存在。反过来说,Windows 上的宽 deny 是一枚否决票,写下去就会压掉所有 allow,包括你明天要开的那个口子。

规则条数不是免费的

顺序遍历的代价是逐条试。我用那 2 万个包量了一下平均要试多少条才命中:

原始顺序(42 条)  : 24.15 步
按命中频次重排    : 16.81 步
规则数  92 条    : 40.54 步
规则数 192 条    : 73.34 步
规则数 392 条    : 138.93 步

把高频规则挪到前面,平均步数降了 30%,一分钱不花。规则数从 42 涨到 392(我往后追加了一些不会改变语义、只拖慢遍历的规则),步数近似线性地涨到 139。这是纯 CPU 成本,硬件上还有自己的开销,但趋势一致:不清理的表会一直变慢,而且慢是线性的。

我的判断

第一,"写了但不生效"比"没写"更危险。没写你心里有数,写了不生效会让你以为自己已经堵上了——#20 到 #38 那四条就是这样,审计的时候翻到它们只会觉得"这条我们做了"。

第二,清理策略表这件事不该靠人肉读。42 条就已经有 15 条是死的了,真实现网的表动辄几百条。方法本身不复杂:把规则建模、离散化出有限域、逐点算首次命中、按"动作是否相同"分删还是移。这套东西我用零依赖 Node 就写完了,不需要接任何设备。

第三,也是我做完这轮最想记住的一条:任何"按流量命中率清理规则"的流程,都必须配一层静态可达性证明。命中率会告诉你哪些规则冷,但它分不清"冷"和"死",误杀的那两条可是会真出事的。

下次我自己要往表里加一条策略,我会先算一遍它在当前顺序下到底能不能被轮到,而不是加完就走。


信息源(核对日期 2026-10-24):

上面三条是官方文档结论。文中其余所有数字(42 条规则、3025 域点、15 条不可达、4 条危险影子、703→688、17 条零命中、140 点语义差异、24.15 步)都是我在这台机器上用零依赖 Node 脚本跑出来的,脚本在 daily/fwcore.js、fwaudit.js、fw-sem.js、fw-v1.js。策略表演示集是我按通用网段结构造的,不是从任何现网设备导出的。

最后修改:2026 年 10 月 24 日
如果觉得我的文章对你有用,请随意赞赏