跳到主要内容

nftables 透明拦截流量

概述

iptables 和 nftables 都能用于透明代理的流量拦截,区别就是 nftables 是后起之秀,未来取代 iptables。可读性更高、性能更好。

本文介绍如何在路由器利用 nftables 透明拦截流量。

思路

可以利用 Pod 的 postStart 和 preStop 两个生命周期的 hook 来设置和清理 nftables 相关规则:

        lifecycle:
postStart:
exec:
command:
- /scripts/set-rules.sh
preStop:
exec:
command:
- /scripts/clean.sh
说明

当前本人已将在 postStart 中设置规则改成了用单独的 sidecar 来设置规则,因为 postStart 中的日志输出无法通过 kubectl logs 查看,在排障时会比较麻烦(只能看到 Pod 一直在启动中,无法看代理本身和设置规则的脚本的输出)。

脚本和 nftables 通过 configmap 挂载:

        volumeMounts:
- mountPath: /scripts
name: script
- mountPath: /etc/nftables
name: nftables-config

另外,保险起见,可以加个 initContainer,保证在启动的时候也执行下清理规则的脚本(避免代理 Pod 异常挂掉导致 preStop 里的清理规则脚本没执行到):

      initContainers:
- command:
- /scripts/clean.sh
image: your-proxy-image
imagePullPolicy: IfNotPresent
name: clean
securityContext:
privileged: true
volumeMounts:
- mountPath: /scripts
name: script

nftables 规则

nftables.conf
#!/usr/sbin/nft -f

table inet proxy
delete table inet proxy

table inet proxy {

# 保留网段,参考:https://zh.wikipedia.org/zh-sg/%E4%BF%9D%E7%95%99IP%E5%9C%B0%E5%9D%80
set byp4 {
typeof ip daddr
flags interval
elements = {
0.0.0.0/8,
10.0.0.0/8,
100.64.0.0/10,
127.0.0.0/8,
169.254.0.0/16,
172.16.0.0/12,
192.0.0.0/24,
192.0.2.0/24,
192.88.99.0/24,
192.168.0.0/16,
198.18.0.0/15,
198.51.100.0/24,
203.0.113.0/24,
224.0.0.0/4,
240.0.0.0/4
}
}
set byp6 {
typeof ip6 daddr
flags interval
elements = {
::,
::1,
::ffff:0:0:0/96,
100::/64,
64:ff9b::/96,
2001::/32,
2001:10::/28,
2001:20::/28,
2001:db8::/32,
2002::/16,
fc00::/7,
fe80::/10,
ff00::/8
}
}

chain prerouting {
type filter hook prerouting priority filter; policy accept;

# 只拦截内网部分设备(10.10.10.0/28 用于拦截内网部分指定设备的流量,10.42.0.0/16 用于拦截容器网络流量)
fib saddr type != local ip saddr != { 10.10.10.0/28, 10.42.0.0/16 } counter return

# 放行发往 local 的。如果连接已被 tproxy 拦截,后续流量会被认为是 local 的(代理会在本机监听目标地址),流量可以直接转发给代理进程,无需拦截,直接 return 避免再次被 tproxy 拦截
fib daddr type local counter return

# 放行 reply 方向的
ct direction reply counter return

# 将局域网设备访问公网的流量标记为需要拦截
meta l4proto {tcp,udp} ct mark != 1 ct state new,related counter jump do_proxy

# 拦截已经标记要拦截的流量(连接的第一个包在这里被 tproxy 到代理进程,然后代理进程会监听目标地址,后续的包无需拦截就可以被转发给代理进程)
meta l4proto {tcp,udp} ct mark 1 tproxy to :12345 meta mark set 1 counter
}

chain do_proxy {

# 放行内网
ip daddr @byp4 counter return
ip6 daddr @byp6 counter return

# 其余全部打上 mark,标记流量需要拦截
ct mark set 1 counter
}

chain output {
type route hook output priority filter; policy accept;

# 放行发往 local 的
fib daddr type local counter return

# 放行 reply 方向的
ct direction reply counter return

# 放行代理发出的包。
meta skgid eq 23333 counter return

# 尝试给需要拦截的连接进行 mark
meta l4proto {tcp,udp} ct state new,related counter jump do_proxy

# 让报文走策略路由重新进入 PREROUTING,以便被代理。
ct mark 1 meta mark set 1 counter
}
}

设置规则的脚本

set-rules.sh
#!/bin/bash

set -x

while :; do
export https_proxy=http://127.0.0.1:10809 # 假设代理会启动 10809 端口的 HTTP 代理
code=$(curl -I -o /dev/null -m 5 -s -w %{http_code} https://www.google.com | xargs) # 通过代理探测目标地址是否正常响应
if [[ "${code}" == "200" ]]; then # 探测成功就退出探测循环,准备设置策略路由
break
fi
echo "bad code:${code};" >/tmp/error.log
sleep 5s
done

echo "proxy is ready, set up rules"
ip rule list | grep "from all fwmark 0x1 lookup 100"
if [ $? -ne 0 ]; then
echo "add rule and route"
ip rule add fwmark 0x1 table 100
ip route add local 0.0.0.0/0 dev lo table 100
ip -6 rule add fwmark 0x1 table 100
ip -6 route add local ::/0 dev lo table 100
fi

nft -f /etc/nftables/nftables.conf # 设置 nftables 规则
exit 0

清理规则的脚本

clean.sh
#!/bin/bash

set -x

nft delete table inet proxy # 关闭时清理 nftables,避免拉不了镜像
ip rule delete fwmark 0x1 table 100
ip route delete local 0.0.0.0/0 dev lo table 100
ip -6 rule delete fwmark 0x1 table 100
ip -6 route delete local ::/0 dev lo table 100

exit 0

定向拦截

如果家里的设备特别多,全局拦截流量的话,对代理的压力可能较大,有时可能会影响网速。

这种情况可以考虑只拦截特定设备的流量,比如 DHCP 动态分配网段为 10.10.10.15~10.10.10.254,而 10.10.10.1~10.10.10.14 这个小网段用于静态 IP 分配,且 nftables 拦截规则只拦截这个小网段的设备(10.10.10.0/28),示例:

# https://thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html
log-queries=extra
no-resolv
no-poll
server=61.139.2.69
strict-order
log-dhcp
cache-size=2000
dhcp-range=set:green,10.10.10.15,10.10.10.254,255.255.255.0,12h
dhcp-authoritative
dhcp-leasefile=/var/lib/dnsmasq/dnsmasq.leases
dhcp-option=option:router,10.10.10.2
dhcp-option=option:dns-server,61.139.2.69,218.6.200.139
dhcp-host=fc:19:3c:37:2a:1c,10.10.10.6,set:red # iPhone - me
dhcp-host=f8:f2:c2:95:7e:fa,10.10.10.7,set:red # iPhone - wife
dhcp-host=59:e6:ba:5e:6f:2e,10.10.10.8,set:red # iPad
dhcp-host=a0:78:17:87:9e:52,10.10.10.5,set:red # M1
dhcp-host=7c:f3:5d:ec:8a:3a,10.10.10.9,set:red # M3
dhcp-host=a4:c9:a9:d5:73:77,10.10.10.10,set:red # Mate70 Pro+

dhcp-host 配置项里填入设备的 MAC 地址及其对应固定分配的 IP 地址。

注意

有些设备在内网通信时可能不会使用真实的 MAC 地址,比如 iPhone,可以在无线局域网里点进连上的 WiFi 设置下,关掉 私有无线局域网地址

macOS 也是类似的: