Docker、K8S 和 CNI(flannel、calico 等等) 都使用了 iptables:
iptables 知识点非常广,全部讲解到的话是可以单独出一本书的,所以只介绍基础知识。
数据中心中有硬件防火墙,Linux 也有自己的防火墙,它就是 netfilter 安全框架,字面意思 “网络过滤” ,netfilter 处于内核空间,iptables 是命令行工具,我们用这个工具来增删改查 netfilter 提供的 hook 点上的规则(rule)。
对于 iptables 基础教程,推荐先去看完 朱双印 iptables 详解系列,看完后接着看下面的。
很多介绍 iptables 的文章都画了类似的图,虽然风格和位置不一样,但是核心都是是五链(netfilter 提供的 hook 点)五表(存储链的规则,对于匹配的包执行什么行为):
五链总是有人记不住,这东西不能死记硬背,最简单的一个记忆方法根据网络情况记忆:
五表:
很多教程教查看 iptables 规则是:
iptables -nvL iptables -nvL INPUT |
这个非常不适合 iptables 新手阅读使用,而是应该 iptables -S:
$ iptables -S -P INPUT ACCEPT -P FORWARD DROP #<- 有些应用启动后,会把默认策略设置为 DROP,导致手动做转发的时候匹配到最后被DROP掉 -P OUTPUT ACCEPT -N BASE-RULE -A INPUT -p icmp -j ACCEPT -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT -A INPUT -j BASE-RULE -A FORWARD -j ACCEPT -A BASE-RULE -i eth0 -m set ! --match-set whiteiplist src -m set --match-set whiteportlist dst -j DROP -A BASE-RULE -j RETURN |
该命令会输出 iptables 命令行风格的规则。假如外部无法访问上面机器的 80 端口,只能 ping 通,你会隐约感觉是 match-set 这条规则导致的,而假设该规则你又看不懂具体是干啥的,可以两个思路:
外面一台机器 192.168.1.2 频繁触发访问 80 端口 |
1、计数器
# 清空计数器 iptables --wait --zero INPUT iptables --wait --zero BASE-RULE # 观察计数器增加,看匹配到哪条链下的哪个规则 ipatbles --wait -nvL INPUT ipatbles --wait -nvL BASE-RULE |
2、二分法,增加放行,例如
# 明细的来源 IP 和目标端口放行,特别是线上的时候 # insert 是最前面插入 iptables --wait --insert INPUT --source 192.168.1.2/32 -p tcp --dport 80 -j ACCEPT # 外面能通了后删除该规则,也能 iptables --wait --delete INPUT 1 删除该规则 iptables --wait --delete INPUT --source 192.168.1.2/32 -p tcp --dport 80 -j ACCEPT iptables --wait --insert INPUT 2 --source 192.168.1.2/32 -p tcp --dport 80 -j ACCEPT iptables --wait --delete INPUT 2 iptables --wait --insert BASE-RULE --source 192.168.1.2/32 -p tcp --dport 80 -j ACCEPT iptables --wait --delete BASE-RULE 1 |
最后以此内推,定位到是 BASE-RULE 链里的那个看不懂的规则,生产环境特别是部署了 K8S 后,iptables -nvL 的计数器会因为流量大而不容易观察,更多是后面说的二分法定位大概规则范围。
iptables 的规则就是 匹配条件 行为,下面列举一些规则:
# 使用 --insert 短选项 -I 和省略 --wait 举例了,实践的话最好找个能 tty 登录的机器以免失联,每个规则记得自行删除 # 放行 icmp 协议,让外面能ping 通本机 iptables -I INPUT --protocol icmp -j ACCEPT # 从网卡 eth0 进来,来源 IP 是 192.168.1.0/24 的直接放行 iptables -I INPUT --in-interface eth0 --source 192.168.1.0/24 -j ACCEPT # DNS 劫持,常见用于路由器上 # 例如路由器上运行了 5300 DNS server,这个 dns server 上游用 dns over http iptables -t nat -I PREROUTING -p udp -m udp --dport 53 -j REDIRECT --to-ports 5300 iptables -t nat -I PREROUTING -p tcp -m tcp --dport 53 -j REDIRECT --to-ports 5300 # 劫持所有 1.1.1.1:53 的 DNS 请求并重定向到 223.5.5.5:53 iptables -t nat -A PREROUTING -d 1.1.1.1 -p udp --dport 53 -j DNAT --to-destination 223.5.5.5:53 iptables -t nat -A PREROUTING -d 1.1.1.1 -p tcp --dport 53 -j DNAT --to-destination 223.5.5.5:53 # 扔掉来源端口是 8082 的出去的 tcp 包 iptables -I OUTPUT -p tcp --sport 8082 -j DROP # 下列是组合使用的 # 已经建立链接的直接放行 -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT # INPUT 链加了个BASE-RULE 链 -A INPUT -j BASE-RULE # 表示从网卡 eth0 进来,来源 IP 不在 whiteiplist 的 ipset 里就 DROP -A BASE-RULE -i eth0 -m set ! --match-set whiteiplist src -j DROP # 符合了就继续往下一跳链走,会走到 RETURN,回到 INPUT 链 -A BASE-RULE -j RETURN # 然后最后 INPUT 链默认策略是 ACCEPT,也就是 -P INPUT ACCEPT 上 |
最起码要先要学会看得懂 iptables 规则,熟悉了后就可以自己写规则了。
iptables 重启规则就会清空,所以如果不是程序添加的,人为手动添加的需要使用 iptables.service 之类的服务,例如下面的
yum install ipset ipset-service -y sed -ri '/^IPSET_SAVE_ON_STOP=/s#no#yes#' /etc/sysconfig/ipset-config systemctl enable --now ipset vi /etc/sysconfig/iptables systemctl enable iptables # apt 系列 os apt update && apt install -y ipset-persistent iptables-persistent vi /etc/iptables/rules.v4 systemctl enable netfilter-persistent.service |
iptables 有两种模式:
iptables-legacy
iptables-nft
模式可以通过 iptables -V 查看:
iptables v1.8.9 (nf_tables) iptables v1.8.9 (legacy) |
如果老版本的 Linux 发行版上面命令输出没有结尾的模式,都是 legacy 模式。 如果你不知道机器上两种模式规则都存在,在添加规则后会出现非预期的结果,是因为 nf_tables 优先级比 legacy 高,你如果执行 iptables -S 出现下面的:
$ iptables -S # Warning: iptables-legacy tables presetn. use iptables-legacy to see them # 可以单独查看模式的规则 iptables-legacy -S iptables-nft -S |
特别是容器里使用 iptables 避免和宿主机 iptables 模式不一致,可以像 flannel 那样使用下列仓库的编译整进去:https://github.com/kubernetes-sigs/iptables-wrappers
这个仓库主干分支是 golang 写的,之前的 v2 版本是脚本形式的的,例如可以下面这样:
FROM alpine:3.18 RUN set -eux; \ # sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories; \ apk add -u iptables ipset bash \ --no-cache; \ apk add --no-cache --virtual .curl \ curl; \ # 避免容器内和宿主机使用 iptables-nft iptables-legacy 模式不一致造成网络问题 curl -L https://raw.githubusercontent.com/kubernetes-sigs/iptables-wrappers/v2/iptables-wrapper-installer.sh > /iptables-wrapper-installer.sh; \ bash /iptables-wrapper-installer.sh --no-sanity-check; \ apk del .curl; \ rm -rf /var/cache/apk/* /tmp/* |
另外如果容器纯粹操作 iptables 规则是不需要特权的,uid 为 0 且有对应 CAP 就可以了,例如下面的:
services: ipset: image: 'reg.xxx.lan:5000/xxx/ipset:v1.1' container_name: ipset network_mode: host hostname: ipset restart: always # unless-stopped working_dir: '/data/kube/' cap_drop: ["ALL"] cap_add: ["NET_ADMIN", "NET_RAW", "FOWNER"] volumes: # 需要挂载 `/run/` 目录,或者直接 `/run/xtables.lock` 文件 - /run/xtables.lock:/run/xtables.lock:rw - '/data/kube/rule/:/data/kube/' |