云原生家庭网络(十七):使用 nftables 透明拦截流量

2024-05-30 09:57:13 浏览数 (1)

概述

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

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

思路

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

代码语言:yaml复制
        lifecycle:
          postStart:
            exec:
              command:
              - /scripts/set-rules.sh
          preStop:
            exec:
              command:
              - /scripts/clean.sh

脚本和 nftables 通过 configmap 挂载。

volumeMounts 部分:

代码语言:yaml复制
        volumeMounts:
        - mountPath: /scripts
          name: script
        - mountPath: /etc/nftables
          name: nftables-config

volumes 部分:

代码语言:yaml复制
      volumes:
      - configMap:
          defaultMode: 420
          name: nftables-config
        name: nftables-config
      - configMap:
          defaultMode: 511
          name: script
        name: script

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

代码语言:yaml复制
      initContainers:
      - command:
        - /scripts/clean.sh
        image: your-proxy-image
        imagePullPolicy: IfNotPresent
        name: clean
        securityContext:
          privileged: true
        volumeMounts:
        - mountPath: /scripts
          name: script

nftables 规则

nftables.conf:

代码语言:conf复制
#!/usr/sbin/nft -f

table inet proxy
delete table inet proxy

table inet proxy {

    # 保留网段,参考:https://zh.wikipedia.org/zh-sg/保留IP地址
    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-255.255.255.255
        }
    }
    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;

        meta l4proto != { tcp, udp } counter return

        # 避免拦截公网回包(如果没用主路由方案,可以删除)
        iifname ppp0 
          ct state new 
          ct mark set 11 
          log prefix "Public Inbound:" 
          counter 
          return 
          comment "Mark conntrack for connection from internet, avoid intercept the return packets"

        # 不拦截从公网主动进来的包(如果没用主路由方案,可以删除)
        iifname ppp0 counter return

        # 避免源和目的都是本机公网 IP 时造成代理 UDP 无限回环
        fib daddr type local counter return

        # 对保留 IP(包括内网) 放行。
        ip daddr @byp4 counter return
        ip6 daddr @byp6 counter return
        goto proxy
    }
    chain proxy {
        # 对其它数据包(其它内网机器访问公网的包)进行拦截,全部转发给代理。
        # 踩坑:一定 tproxy 到回环 ip (127.0.0.1和::1),如果 tproxy 到 0.0.0.0和::,运行一段时间后,可能会导致 UDP 疯狂不停回环打满 CPU(本机外部IP到本机外部IP一直在lo网卡打转)
        meta l4proto { tcp, udp } tproxy ip to 127.0.0.1:12345 meta mark set 1 counter
        meta l4proto { tcp, udp } tproxy ip6 to [::1]:12345 meta mark set 1 counter
    }
    chain output {
        type route hook output priority filter; policy accept;

        # 不拦截代理自身发出的包。
        meta skgid eq 23333 counter return

        # 不拦截公网回包
        ct mark 11 counter return

        # 对保留 IP(包括内网) 放行。
        ip daddr @byp4 counter return
        ip6 daddr @byp6 counter return

        # 避免源和目的都是本机公网 IP 时造成代理 UDP 无限回环
        fib daddr type local counter return

        # 主机内进程发出的包打上 mark,让报文走策略路由重新进入 PREROUTING,以便被代理。
        meta l4proto { tcp, udp } meta mark set 1 counter
    }
}

设置规则的脚本

set-rules.sh:

代码语言:bash复制
#!/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:

代码语言:bash复制
#!/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

定向拦截

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

这种情况可以考虑只拦截特定设备的流量,比如只拦截 10.10.10.2~10.10.10.10 这个小网段的流量,该网段的 IP 可以静态分配,在 dnsmasq 的 dhcp 里配一下,示例:

代码语言:conf复制
log-queries=extra
no-resolv
no-poll
server=61.139.2.69
strict-order
log-dhcp
cache-size=2000
dhcp-range=10.10.10.11,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=a0:78:17:87:0e:51,10.10.10.5 # M1
dhcp-host=fc:18:3c:37:6a:1c,10.10.10.6 # iPhone - roc
  • dhcp-host 配置项里填入设备的 MAC 地址及其对应固定分配的 IP 地址。

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

0 人点赞