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
- volumes
volumeMounts:
- mountPath: /scripts
name: script
- mountPath: /etc/nftables
name: nftables-config
volumes:
- configMap:
defaultMode: 420
name: nftables-config
name: nftables-config
- configMap:
defaultMode: 511
name: script
name: script
另外,保险起见,可以加个 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 也是类似的: