防火墙不是「判断对错」,是「按顺序匹配、第一条命中就执行」。 所以规则的顺序比条数更重要:顺序错了,写了等于没写。
上一节说「关掉不用的端口」。 这一节讲关不掉的时候怎么管:总有一些端口必须开,那就得有一条条规矩。 Demo 用一个五条规则的引擎跑八个包,再演示「只调顺序、不改内容」会怎样。
一、三代防火墙,看的越来越深
防火墙这个词被用得很泛,但它的核心动作只有一个:看一个包,决定放还是丢。 区别在于「看多深」。
| 类型 | 看什么 | 记不记状态 |
|---|---|---|
| 包过滤 | 五元组:源、目的、端口、协议、方向 | 不记 |
| 状态型 | 五元组加连接表 | 记,回包自动放行 |
| 应用网关 | 拆到应用层看内容 | 记,还能改内容 |
第一行的问题很明显:它不记状态,所以「允许出去」就必须同时写「允许回来」, 而「允许回来」这条一旦写宽,外面的包也能顺着进来。 这是早期防火墙最常见的配置错误。 第二行解决了它:先记下「谁先发起的」,回来的包对得上表就放行, 于是规则只需要写「允许内网出去」,不用写「允许外面回来」。 今天说「防火墙」,默认就是指这种。 第三行是代理:它把连接拆成两段,中间自己看内容, 所以能拦「HTTP 里夹带的命令」这种应用层的东西。 代价是它要理解每一个协议, 协议一升级就得跟着改,所以现在多用「下一代防火墙」把这事做得更泛。
二、规则是一张有顺序的清单
判断一个包的过程不是「找出所有匹配的规则」,而是「从上往下找,找到第一条就走」。 这个细节决定了防火墙的一切行为。 所以有两条铁律: 一是具体的写在笼统的前面,二是拒绝的写在允许的前面。 违反第一条,具体规则会被笼统规则吃掉;违反第二条,一个小的允许会把一大片拒绝捅穿。 由此产生一个运维术语:被架空的规则,也就是命中计数永远是零的规则。 它看着在,其实永远轮不到,而写它的人还以为策略生效了。 所以巡检防火墙的第一件事,就是把计数为零的规则全部翻出来看一遍。 还有一条容易被忘的:最后那条「默认策略」必须是拒绝。 如果默认是允许,那前面所有拒绝规则只是在「补漏」,而漏是补不完的。 「默认拒绝、逐个放行」这八个字,是所有访问控制的共同起点。
三、NAT 与 DMZ:两个顺带的好处
NAT 本来是为了省 IP 地址,但它顺手成了一个最原始的防火墙: 内网地址在外面根本不存在,外面的包不知道往哪儿发, 于是「内网不可直接访问」这件事自动成立了。 但这个保护是「顺带」的,不是「设计」的:一旦做了端口映射,那道墙就开了一个洞, 而很多人的映射是「临时开的、后来忘了关」。 所以别把 NAT 当安全措施用,它只是恰好挡住了而已。 DMZ 是另一个思路:把对外提供服务的机器单独放一个网段, 外网能访问它,它却很难主动进内网。 这样一台 Web 服务器被攻破,损失就停在 DMZ 里。 设计 DMZ 的时候有一个判断标准很好用: 「这台机器被拿走了,我最怕它接触到什么」——答案就是它不该能访问的东西。 最后说一个方向性的问题:大多数人只关心「谁能进来」,而真实的入侵往往是「从里面出去的」。 一台中了招的机器会主动向外连一个奇怪的地址,如果出口策略是「随便出」,这条连接畅通无阻。 所以成熟的策略一定管出口,至少要能回答「为什么这台服务器要连那个国家的 443 端口」。
四、动手 Demo:包过滤是一张「从上往下第一条命中就算」的清单
五条规则、八个包,看每条规则各被命中几次;然后把其中一条挪个位置,行为立刻不一样。
import ipaddress
RULES = [
("入", "*", "10.0.0.5", "80", "允许"),
("入", "*", "10.0.0.5", "443", "允许"),
("出", "10.0.0.0/24", "*", "53", "允许"),
("入", "10.0.0.0/24", "10.0.0.5", "*", "允许"),
("入", "*", "10.0.0.5", "3389", "拒绝"),
]
def hit(rule, pkt):
for i in (0, 1, 2, 3):
rv, pv = rule[i], pkt[i]
if rv == "*":
continue
if i in (1, 2) and "/" in rv:
net = ipaddress.ip_network(rv, strict=False)
if ipaddress.ip_address(pv) not in net:
return False
continue
if rv != pv:
return False
return True
def judge(rules, pkt):
for n, r in enumerate(rules, 1):
if hit(r, pkt):
return n, r[4]
return 0, "拒绝(默认丢弃)"
实跑输出:
--------------------------------------------------------------------
包过滤是一张「从上往下第一条命中就算」的清单
--------------------------------------------------------------------
**防火墙的规则不是「判断对错」,是「按顺序匹配,第一条命中就执行」。**
**这个顺序决定了一切,下面用一个五条规则的表跑八个包。**
序 方向 源 目的 端口 动作
---------------------------------------------------------------
1 入 * 10.0.0.5 80 **允许**
2 入 * 10.0.0.5 443 **允许**
3 出 10.0.0.0/24 * 53 **允许**
4 入 10.0.0.0/24 10.0.0.5 * **允许**
5 入 * 10.0.0.5 3389 **拒绝**
包 方向 源 目的 端口 命中规则 结果
----------------------------------------------------------------------------------------
1 入 203.0.113.9 10.0.0.5 80 **1** **允许**
2 入 203.0.113.9 10.0.0.5 443 **2** **允许**
3 入 203.0.113.9 10.0.0.5 22 **无** **拒绝(默认丢弃)**
4 出 10.0.0.7 8.8.8.8 53 **3** **允许**
5 入 10.0.0.8 10.0.0.5 3389 **4** **允许**
6 入 203.0.113.9 10.0.0.5 3389 **5** **拒绝**
7 出 10.0.0.7 8.8.8.8 80 **无** **拒绝(默认丢弃)**
8 入 10.0.0.8 10.0.0.5 22 **4** **允许**
**8 个包,放行 5 个、拒绝 3 个。**
**先看第 5 个包:它是内网发往 10.0.0.5 的远程桌面**,**按第 5 条本该被拒绝,结果却被放行了。**
**原因是它先撞上了第 4 条**:**「内网到 10.0.0.5 的任何端口都允许」写得太宽**,
**把所有内网流量一口气放了过去**,**后面那条「拒绝远程桌面」就永远轮不到。**
**这种规则叫「被架空的规则」,它的计数永远是零**——
**所以运维看防火墙的第一件事,就是找计数为零的规则。**
**接着做一次实验:只把第 4 条挪到最后。**
**同一个第 5 个包,现在命中第 4 条,结果 拒绝。**
**而第 8 个包(内网 SSH)照样是 允许**,**因为拒绝远程桌面那条只管 3389,不管 22。**
**规则一条没增没减,行为全变了。**
**这就是为什么防火墙规则要遵守两条铁律**:**具体的写在笼统的前面**,**拒绝的写在允许的前面**。
**违反第一条会让规则失效,违反第二条会让整个策略漏成筛子。**
先看第 5 个包:它是内网发往 10.0.0.5 的远程桌面,按第 5 条本该被拒绝,结果却被放行了。 原因很简单:它先撞上第 4 条「内网到 10.0.0.5 的任何端口都允许」。 第 4 条就是那条「被架空的规则」的反面:它太宽了,把后面的规则全吃掉了。 而第 5 条才是那条永远命不中的规则——它的计数会是零。 真实巡检里,一条计数为零的拒绝规则等于没写,而它给人的安全感是假的。 把第 4 条挪到最后再跑一次:同一个包立刻被第 4 条(此时是那条拒绝规则)拦下, 而内网的 SSH 照样放行——因为拒绝只管 3389,不管 22。 规则一条没增没减,行为全变了。 这就是为什么改防火墙要先看顺序,再看内容。
30 秒实验
- 算一下:如果把第 4 条写成
10.0.0.0/8,会多放行多少地址? (一千六百万个,等于把整个大内网都放了) - 想一下:为什么「默认策略」必须是拒绝?
- 想一下:状态型防火墙凭什么可以不写「允许回来」?
常见误区
- 以为「规则写得越多越安全」。 顺序错了等于没写,被架空的规则只会给人假的安全感。
- 以为「NAT 就是防火墙」。 它只是恰好挡住了,做一条端口映射就开一个洞。
- 以为「防火墙只管进来的方向」。 真实的入侵多半是往外连的,出口策略同样重要。
自测三问(点开对答案)
1. 包过滤和状态型防火墙的关键差别? —— **后者记连接表**,所以回包能自动放行,不用写反向规则。 2. 什么叫「被架空的规则」? —— **命中计数永远为零**:被上面更宽的规则挡住了,写了也不生效。 3. DMZ 的设计目的是什么? —— **让对外服务的机器被攻破时,损失停在隔离区里**。下一节看第二道门:IDS。防火墙管「能不能过」,IDS 管「过去的东西是不是坏的」。
- ☐ (本节完,下一节 9.2.2 IDS(入侵检测系统))