原理系列第四篇。前三篇讲了数据包怎么找到服务器(DNS)、怎么建立连接(TCP)、怎么加密传输(TLS)。这篇讲数据包到了服务器之后——在真正进入你的应用程序之前,Linux 内核还对它做了什么。理解了这一层,你才会明白防火墙为什么能拦截数据,以及 Docker 的"端口映射"到底是怎么回事。
零、从一个让人困惑的现象开始
服务明明在跑,端口也在监听,但从外面就是连不上——这是网络排障那篇讲过的场景。
当时说:"检查防火墙是否拦截了端口。"
但防火墙是怎么拦截的?它在哪里拦?拦截的规则是怎么写的?为什么有时候放行了端口,流量还是进不来?
更神奇的是:你在 Linux 服务器上跑 Docker,执行 docker run -p 8080:80,外部请求打到 8080 端口,神奇地出现在容器里的 80 端口——这个"魔法",是怎么实现的?
答案都在 Linux 内核里的一个叫做 Netfilter 的框架里。
一、Netfilter:内核里的"数据包检查站"系统
Netfilter 是 Linux 内核里负责网络包过滤的框架,从 Linux 2.4 开始内置。
它的设计思路非常优雅:在数据包流经内核网络栈的关键位置,设置若干个"钩子"(Hook),任何数据包经过这些位置时,都会被挂在钩子上"检查一遍",由你预先写好的规则决定这个包的命运。
这些钩子一共有五个,对应数据包在内核里旅行的五个关键节点:
网卡收到数据包 │ ▼① PREROUTING ← 数据包刚进来,还没决定路由 │ ├─ 目标是本机? ─────────────────────▶ ② INPUT ──▶ 本机进程(你的应用) │ │ │ ▼ │ ④ OUTPUT(本机发出的包) │ │ └─ 目标是其他主机? ──▶ ③ FORWARD ──────────────────────▶ ⑤ POSTROUTING ──▶ 网卡发出 ▲ │ ④ OUTPUT ──┘
五个钩子:
| |
|---|
PREROUTING | |
INPUT | |
FORWARD | |
OUTPUT | |
POSTROUTING | |
iptables 就是用来在这些钩子上添加、修改、删除规则的命令行工具。内核里的 Netfilter 是真正干活的,iptables 只是你和它沟通的接口。
二、表和链:规则是怎么组织的
iptables 里的规则,按用途分成四张"表",每张表在特定的钩子上工作:
| | |
|---|
filter | | |
nat | | PREROUTING、OUTPUT、POSTROUTING |
mangle | | |
raw | | |
每张表里,钩子对应的规则集合叫做"链"(Chain)。
filter 表里有三条链:INPUT、FORWARD、OUTPUT,分别对应三个钩子。
一个数据包的完整旅程(以进入本机的 HTTP 请求为例):
数据包到达网卡 │ ▼raw 表 PREROUTING 链 → mangle 表 PREROUTING 链 → nat 表 PREROUTING 链 │ ▼(路由决策:目标是本机)mangle 表 INPUT 链 → filter 表 INPUT 链 │ ▼本机进程(你的 Web 服务)
三、规则长什么样:匹配条件 + 动作
每条 iptables 规则,由两部分组成:匹配条件 和 目标动作。
iptables -A INPUT -p tcp --dport 80 -j ACCEPT# │ │ │ │# 追加 协议是TCP 目标端口80 动作:接受# 到INPUT链
匹配条件可以组合:
# 只允许来自特定 IP 的 SSH 连接iptables -A INPUT -s 192.168.1.100 -p tcp --dport 22 -j ACCEPT# 拒绝所有到 8080 端口的流量iptables -A INPUT -p tcp --dport 8080 -j DROP# 允许已建立的连接的数据包通过(状态匹配)iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
常见的目标动作:
| |
|---|
ACCEPT | |
DROP | |
REJECT | 拒绝,并发回一个错误响应(发送方立刻知道被拒绝了) |
DNAT | 修改目标地址(Destination NAT,用于端口映射) |
SNAT | |
MASQUERADE | |
LOG | |
RETURN | |
规则是按顺序匹配的:数据包进入一条链,从第一条规则开始逐条匹配,匹配到了就执行对应动作,ACCEPT 或 DROP 之后不再往下匹配。如果所有规则都不匹配,执行链的默认策略(通常是 ACCEPT 或 DROP)。
四、常用操作:怎么写防火墙规则
# 查看当前 filter 表的所有规则(带行号)iptables -L -n -v --line-numbers# 查看 nat 表iptables -t nat -L -n -v# 放行 443 端口(HTTPS)iptables -A INPUT -p tcp --dport 443 -j ACCEPT# 放行来自特定 IP 段的所有流量iptables -A INPUT -s 10.0.0.0/8 -j ACCEPT# 阻止某个 IP 的所有访问iptables -A INPUT -s 1.2.3.4 -j DROP# 允许已建立的连接(防止阻断现有会话)iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT# 设置 INPUT 链的默认策略为丢弃(白名单模式:只有明确放行的才能进)iptables -P INPUT DROP# 删除 INPUT 链第 3 条规则iptables -D INPUT 3# 保存规则(Debian/Ubuntu,防止重启后丢失)iptables-save > /etc/iptables/rules.v4
白名单模式(默认 DROP,只放行明确允许的)是生产服务器的推荐配置,比黑名单模式安全得多。配置时记得先放行 SSH(22 端口),否则会把自己锁在外面。
五、NAT:数据包的"地址变装"
NAT(Network Address Translation,网络地址转换) 是 iptables 里最"魔法"的功能,也是 Docker 端口映射的核心。
SNAT(源地址转换):让局域网机器上网
家里路由器的本质,就是一台做 SNAT 的 Linux 机器:
局域网机器(192.168.1.100) │ 发出请求:源IP=192.168.1.100 ▼路由器的 POSTROUTING 链 │ SNAT:把源IP改成公网IP(比如 1.2.3.4) ▼互联网(目标服务器看到的请求来源是 1.2.3.4) │ 响应发回 1.2.3.4 ▼路由器收到响应,查连接追踪表,知道这个包属于 192.168.1.100 │ 把目标IP改回 192.168.1.100 发给局域网机器 ▼局域网机器收到响应
# 让所有从 eth0 出去的流量做 SNAT(用 eth0 的 IP 作为源地址)iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
DNAT(目标地址转换):端口映射和负载均衡
这是 Docker -p 8080:80 的实现原理:
# 把到达本机 8080 端口的请求,转发到容器的 172.17.0.2:80iptables -t nat -A PREROUTING -p tcp --dport 8080 \ -j DNAT --to-destination 172.17.0.2:80
数据包到达本机 8080 端口,在 PREROUTING 阶段被 DNAT 规则"变装"——目标地址从本机 IP:8080 改成 172.17.0.2:80,然后路由决策发现目标是容器 IP,把包转发给容器。
这就是 Docker 端口映射的全部真相:没有任何特殊的"Docker 魔法",只是内核里的一条 iptables DNAT 规则。
验证一下:
# 查看 Docker 自动添加的 nat 表规则iptables -t nat -L -n -v | grep 8080
你会看到 Docker 在 PREROUTING 里加了 DNAT 规则,在 POSTROUTING 里加了 MASQUERADE 规则——这些规则是 Docker 守护进程在你执行 docker run -p 时自动创建的。
六、连接追踪:让"有状态"防火墙成为可能
iptables 有一个非常重要的模块:conntrack(连接追踪)。
它记录了所有经过内核的连接状态,让 iptables 可以做"有状态的"过滤:
# 放行所有属于已建立连接的数据包iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
没有连接追踪,你必须为每个方向分别写规则——放行了 80 端口的入站请求,还要放行服务端的出站响应。有了连接追踪,只需要放行初始的入站,响应包因为属于"已建立的连接",自动放行。
# 查看当前连接追踪表cat /proc/net/nf_conntrack# 或者conntrack -L
七、iptables 的现代替代者:nftables
iptables 设计于 1998 年,虽然功能强大,但有一些历史包袱:四张表、五条链,语法复杂,不同表之间的交互难以直观理解,性能在大规模规则集下也不够理想。
nftables 是 Linux 内核 3.13(2014年)引入的替代方案,在 Debian 10、RHEL 8、Ubuntu 20.04 之后已成为默认:
# nftables 的等价防火墙规则,语法更直观nft add rule ip filter input tcp dport 80 acceptnft add rule ip filter input tcp dport 443 accept# 查看规则nft list ruleset
nftables 把"表"的概念简化了,语法更接近自然语言,同一条规则里可以同时匹配多个条件,性能也更好。
但现实是:很多工具(包括较老版本的 Docker、各种运维脚本)依然在用 iptables 命令。大多数现代 Linux 发行版的 iptables 实际上是 iptables-nft——底层用 nftables 引擎,但提供 iptables 兼容的命令行接口,两套工具可以共存。
八、一张图:数据包在内核里的完整旅程
外部数据包到达网卡(比如 TCP:80 的请求) │ ▼ ┌─────────────────────┐ │ PREROUTING │ ← nat 表在这里做 DNAT │ (Docker 端口映射) │ 例:8080→容器172.17.0.2:80 └─────────┬───────────┘ │ ▼ 路由决策 / \ 目标是本机 目标是其他主机 │ │ ▼ ▼ ┌──────────┐ ┌──────────┐ │ INPUT │ │ FORWARD │ ← filter 表:是否允许转发 │ filter表 │ │ filter表 │ (Docker 容器间通信走这里) └────┬─────┘ └────┬─────┘ │ │ ▼ ▼ 本机进程 ┌──────────────┐ (你的应用) │ POSTROUTING │ ← nat 表在这里做 SNAT/MASQUERADE └──────┬───────┘ │ ▼ 发出网卡
写在最后
iptables/Netfilter 的设计,是 Linux 内核里最优雅的架构之一:
把网络包的旅程拆成五个关键节点,在每个节点上挂上规则,让数据包自己"走"过这些关卡,每个关卡决定它的命运。
理解了这个模型,原来很多"神奇"的事情就都有了解释:
- 防火墙为什么能拦截数据包:INPUT 链的 DROP 规则
- Docker 端口映射是怎么实现的:PREROUTING 的 DNAT 规则
- 家里路由器是怎么让局域网上网的:POSTROUTING 的 MASQUERADE 规则
- Kubernetes 的 Service 是怎么工作的:也是 iptables/ipvs 的 DNAT 规则
从防火墙到容器网络,从家用路由器到云上的负载均衡,很多看起来截然不同的功能,底层全是同一套 Netfilter 钩子机制在运作。
你有没有因为搞不清楚 iptables 规则顺序、或者 Docker 和 iptables 冲突,被防火墙坑过?评论区聊聊。