Skip to content

IPv4透明代理+IPv6 Passthrough——树莓派单臂软路由折腾记

Published: at 00:12

大家好,好久不见,我是某昨。

国庆前总算是下定决心要好好整治一下现在的路由了。以笔记本为核心的代理网络虽然携带方便,但对各种设备的支持实在是算不上完全。笔记本的无线网卡限制太大(信道必须为当前连接 WiFi 的信道,并且不在限制范围内),并且因为笔记本占据了有线接口,因此其他设备就没有使用有线网络的可能了。

新的网络配置使用了一台 TPLinkTL-SG2008D 交换机实现单臂路由,树莓派 4B 作软路由,systemd-networkd 管理网络接口,chinadns-ng 配合 dnsmasq 解析 DNSipt2socks 配合 v2ray 实现透明代理,nftables 配置透明代理的规则并劫持局域网的 DNS 请求至路由器的本地 DNS 服务器。

ToC

环境分析

待接入的网络环境为 IPv4+IPv6 双栈。其中 IPv4MAC 地址白名单,且需要经过 Drcom 认证才能访问公网;IPv6SLAAC,自动下发一段 /64IPv6 地址。IPv4 有出口限速 100MIPv6 不限制,能够跑到宿舍交换机的上限千兆。

IPv4 这边没什么好说的,肯定是要 NAT 了,问题在 IPv6。由于下发的地址是 /64,因此简单的 SLAAC 配置就不可能了;而 Android 设备又明确表示不支持 DHCPv6。因此留给我们的只有两条路:要么透传(Passthrough),要么中继。这里我选择了 Passthrough

配置:交换机

由于树莓派只有一个网口,因此我们需要通过 VLAN 的方式使这个网口同时作为 WAN 口和 LAN 口工作。因此我们划分两个 VLANVLAN1WANVLAN2LAN。端口 1 接外部网线,端口 2 接树莓派,端口 3-8 接局域网内的设备,配置如下图所示:

VLAN 划分
VLAN 划分

PVID 配置
PVID 配置

先看 VLAN1。没有 Tag 的外部流量从端口 1 进入,被打上端口1的 PVID=1,发送到 Tagged 的端口 2;端口 2 的流量发出后流向端口 1,由于端口 1 是 Untagged 因此抹除 Tag 发送出去。进出流量都可以正常运转。

再看 VLAN2。端口 2 发出的报文需要带有对应的 VLAN ID,因此端口 2 是 Tagged;其他端口都不需要让网络使用者知道其对应的 VLAN,因此是 Untagged。当流量从端口 3-8 进入时,自动打上 PVID=2,并被发送到端口 2;当流量从端口 2 流出时,同样地会被正常转发到实际的端口。

至此,交换机部分的配置就基本完成了。

安装:树莓派

由于是全新的树莓派,因此需要安装系统。我这里选择的是 ArchLinux ARM,官网对树莓派的安装有详细教程,这里就不展开了。

配置:网络接口

由于划分了两个 VLAN,因此对应的就需要两个 vlan 的网口。同时为了实现 IPv6Passthrough,我们需要将它们桥接在一起。最后形成的配置如下:

eth0

1
[Match]
2
Name=eth0
3
4
[Network]
5
VLAN=eth0.1
6
VLAN=eth0.2

eth0 分出两个 VLAN 接口,分别为 eth0.1eth0.2

eth0.1

1
[NetDev]
2
Name=eth0.1
3
Kind=vlan
4
5
[VLAN]
6
Id=1

第一个 VLAN 网络设备,VLAN ID=1

1
[Match]
2
Name=eth0.1
3
4
[Network]
5
Bridge=br-lan
6
Address=49.140.123.234/24
7
Gateway=49.140.123.254
8
DNS=127.0.0.1

eth0.1 的网络配置。可以看到它是桥接入 br-lan 的,并且分配了静态的 IP 地址、网关和 DNS

eth0.2

1
[NetDev]
2
Name=eth0.2
3
Kind=vlan
4
MACAddress=2a-62-d5-f6-e8-7f
5
6
[VLAN]
7
Id=2

第二个网络设备,VLAN ID=2

这里给它手动分配了一个 MAC 地址,否则 VLAN 设备的 MAC 地址会和其 Parent 设备,即 eth0 保持一致。MAC 地址的配置理论上不是必须的。但我之前配了就懒得动了(

MACAddress 支持 : 分隔、- 分隔和点分隔三种形式。注意这个地址必须是有效的,否则端口是起不来的,同时 journalctl -xe 里会有 Failed to set MAC address, ignoring: Cannot assign requested address 的报错。

1
[Match]
2
Name=eth0.2
3
4
[Network]
5
Bridge=br-lan

eth0.2 的网络配置,相当简单,不需要有网络地址,只作交换用。

br-lan

1
[NetDev]
2
Name=br-lan
3
Kind=bridge

网络设备 br-lan,负责桥接起所有的网口。

1
[Match]
2
Name=br-lan
3
4
[Network]
5
IPMasquerade=ipv4
6
7
[Address]
8
Address=10.245.0.1/16

br-lan 的网络配置,Address 部分填写内网的网段。我这里选用的内网网段是 10.245.0.1/16

ebtables

eth0.1 加入 br-lan 后,局域网内的设备就可以直接获取到 IPv6 的公网地址了。但 IPv4 的访问也因此中断。因此我们需要让 IPv4 不要被桥接出去,走正常的路由;仅 IPv6 桥接就可以了。

由于 nftables 暂时不支持 ebtablesbroute并不是太行的样子,因此我们还是需要用到 ebtables。但 archlinux-arm 并没有提供对应的包,因此这里就需要我们手动构建了。首先是安装 base-devel

Terminal window
1
pacman -S base-devel

然后从 AUR 上找到对应的包:https://aur.archlinux.org/packages/ebtables:

Terminal window
1
git clone https://aur.archlinux.org/ebtables.git
2
cd ebtables

下载完之后需要修改 PKGBUILD,在 arch 中增加 armv7h,然后就可以构建了:

Terminal window
1
makepkg -si .

如此就安装完成了。

我们可以把 ebtables 的配置写成简单的 systemd 服务,如下所示:

1
[Unit]
2
After=network.target
3
Wants=network.target
4
5
[Service]
6
Type=oneshot
7
RemainAfterExit=yes
8
ExecStart=/usr/bin/ebtables -t broute -A BROUTING -p ! ipv6 -j DROP -i eth0.1
9
ExecStop=/usr/bin/ebtables -t broute -D BROUTING -p ! ipv6 -j DROP -i eth0.1
10
11
[Install]
12
WantedBy=multi-user.target

然后运行并开机启动就可以了。

Terminal window
1
systemctl start route-ipv4
2
systemctl enable route-ipv4

效果

1
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
2
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
3
inet 127.0.0.1/8 scope host lo
4
valid_lft forever preferred_lft forever
5
inet6 ::1/128 scope host
6
valid_lft forever preferred_lft forever
7
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
8
link/ether [redacted] brd ff:ff:ff:ff:ff:ff
9
inet6 fe80::[redacted]/64 scope link
10
valid_lft forever preferred_lft forever
11
3: br-lan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
12
link/ether [redacted] brd ff:ff:ff:ff:ff:ff
13
inet 10.245.0.1/16 brd 10.245.255.255 scope global br-lan
14
valid_lft forever preferred_lft forever
15
inet6 2001:[redacted]/64 scope global dynamic mngtmpaddr noprefixroute
16
valid_lft 2591981sec preferred_lft 604781sec
17
inet6 fe80::[redacted]/64 scope link
18
valid_lft forever preferred_lft forever
19
4: eth0.1@eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-lan state UP group default qlen 1000
20
link/ether [redacted] brd ff:ff:ff:ff:ff:ff
21
inet 49.[redacted]/24 brd 49.[redacted] scope global eth0.1
22
valid_lft forever preferred_lft forever
23
5: eth0.2@eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-lan state UP group default qlen 1000
24
link/ether [redacted] brd ff:ff:ff:ff:ff:ff
25
6: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master br-lan state UP group default qlen 1000
26
link/ether [redacted] brd ff:ff:ff:ff:ff:ff
27
inet6 fe80::[redacted]/64 scope link
28
valid_lft forever preferred_lft forever

从没有被 [redacted] 的部分可以看到,br-lan 拥有内网的 IPv4 地址和公网 2001 开头的 IPv6 地址,eth0.1 拥有公网 IPv4 地址。双栈都可以正常访问。

安装:MariaDB

首先是安装:

Terminal window
1
pacman -S mariadb

然后是初始化:

Terminal window
1
mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql

初始化完成后会自动生成两个账户:rootmysql。这两个账户都没有密码,但需要登录时是对应的用户才可以正常访问。随后,我们配置一些必要的安全选项:

Terminal window
1
mysql_secure_installation

最后启动即可:

Terminal window
1
systemctl start mariadb
2
systemctl enable mariadb

配置:DHCP

我们选择通过 kea 配置 DHCP。首先是安装:

Terminal window
1
pacman -S kea

配置数据库(可选)

如果我们想使用 mariadb 替代 memfile,就需要在数据库里新建一张表和一个用户供 kea 使用:

Terminal window
1
CREATE DATABASE kea;
2
CREATE USER 'kea'@'localhost' IDENTIFIED BY 'kea';
3
GRANT ALL ON kea.* TO 'kea'@'localhost';
4
quit

然后通过 kea-admin 初始化:

Terminal window
1
kea-admin db-init mysql -u kea -p kea -n kea

配置文件

然后编辑 kea-dhcp4 的配置文件:

1
{
2
"Dhcp4": {
3
"interfaces-config": {
4
// 需要监听的端口是 br-lan
5
"interfaces": ["br-lan"],
6
"dhcp-socket-type": "raw"
7
},
8
9
"valid-lifetime": 3600,
10
"renew-timer": 900,
11
"rebind-timer": 1800,
12
13
"subnet4": [
14
{
15
// 分配网段
16
"subnet": "10.245.0.0/24",
17
// IP 池
18
"pools": [{ "pool": "10.245.0.2 - 10.245.0.254" }],
19
20
"option-data": [
21
{
22
// 默认网关
23
"name": "routers",
24
"data": "10.245.0.1"
25
},
26
{
27
// 默认 DNS
28
"name": "domain-name-servers",
29
"data": "10.245.0.1"
30
}
31
]
32
}
33
],
34
35
"control-socket": {
36
"socket-type": "unix",
37
"socket-name": "/tmp/kea4-ctrl-socket"
38
},
39
40
"lease-database": {
41
"type": "memfile",
42
"lfc-interval": 3600
43
},
44
45
// 如果上面使用了数据库,这里可以取消注释
46
//"hosts-database": {
47
// "type": "mysql",
48
// "name": "kea",
49
// "user": "kea",
50
// "password": "kea",
51
// "host": "localhost",
52
// "port": 3306
53
//},
54
55
"option-data": [
56
{
57
"name": "domain-search",
58
"data": "mmf.lan, lan.mmf.moe"
59
}
60
],
61
62
"loggers": [
63
{
64
"name": "kea-dhcp4",
65
"output_options": [
66
{
67
"output": "/var/log/kea-dhcp4.log"
68
}
69
],
70
"severity": "INFO",
71
"debuglevel": 0
72
}
73
]
74
}
75
}

最后启动服务:

Terminal window
1
systemctl start kea-dhcp4
2
systemctl enable kea-dhcp4

配置:无线网络

我们选用 hostapd,使用树莓派的板载无线网卡开启 AP。首先是安装,直接通过 pacman 安装即可:

Terminal window
1
pacman -S hostapd

配置如下:

1
interface=wlan0
2
bridge=br-lan
3
driver=nl80211
4
own_ip_addr=127.0.0.1
5
ctrl_interface=/run/hostapd
6
ctrl_interface_group=0
7
8
9
#
10
# 基本信息
11
#
12
ssid=SSID Of Your WiFi
13
wpa_passphrase=Your password
14
15
16
# 国家代码
17
country_code=CN
18
# 信道为 40,5200 MHz,在中国和日本的信道重合范围内
19
channel=40
20
21
22
# IEEE 802.11a
23
hw_mode=a
24
ieee80211n=1
25
ht_capab=[HT40+][SHORT-GI-40][DSSS_CCK-40]
26
ieee80211ac=1
27
28
29
#
30
# WPA2 认证配置
31
#
32
wpa=2
33
wpa_key_mgmt=WPA-PSK
34
wpa_pairwise=CCMP
35
# 1=WPA
36
auth_algs=1
37
38
39
#
40
# 日志
41
#
42
logger_syslog=-1
43
logger_syslog_level=2
44
logger_stdout=-1
45
logger_stdout_level=2
46
47
48
#
49
# MAC 地址黑名单
50
#
51
macaddr_acl=0
52
deny_mac_file=/etc/hostapd/hostapd.deny
53
54
55
#
56
# WMM 相关配置
57
#
58
wmm_enabled=1
59
wmm_ac_bk_cwmin=4
60
wmm_ac_bk_cwmax=10
61
wmm_ac_bk_aifs=7
62
wmm_ac_bk_txop_limit=0
63
wmm_ac_bk_acm=0
64
wmm_ac_be_aifs=3
65
wmm_ac_be_cwmin=4
66
wmm_ac_be_cwmax=10
67
wmm_ac_be_txop_limit=0
68
wmm_ac_be_acm=0
69
wmm_ac_vi_aifs=2
70
wmm_ac_vi_cwmin=3
71
wmm_ac_vi_cwmax=4
72
wmm_ac_vi_txop_limit=94
73
wmm_ac_vi_acm=0
74
wmm_ac_vo_aifs=2
75
wmm_ac_vo_cwmin=2
76
wmm_ac_vo_cwmax=3
77
wmm_ac_vo_txop_limit=47
78
wmm_ac_vo_acm=0

那堆 wmm 相关的配置在安装自带的配置文件里没有注释掉,因此这里我也保留了。这里有个坑点是树莓派的板载网卡不支持 ACS,即自动信道探测。这也是我试了好几遍才得出的结论(

可以看到,AP 接入的方式是 bridge 进了 br-lan,因此可以直接从 br-lanDHCP 获取到 IPv4 地址;能够直接通过 SLAAC 获取到公网 IPv6。配置完成后直接启动 hostapd 就可以了:

Terminal window
1
systemctl start hostapd
2
systemctl enable hostapd

配置:透明代理

至此,有线网络和无线网络都可以正常工作了,于是问题就只剩下透明代理了。透明代理的实现有两个最重要的问题:DNS 和代理方式。

DNS 我这里选择的是 chinadns-ng+dnsmasq 的方案。黑白名单的设计与我理想中的 DNS 获取方式完全契合,dnsmasq 则在缓存的同时能够给自定义解析域名,对内网服务的部署是一大助力。

剩下的问题就是代理方式了。我选择的方案是 ipt2socks 配合 v2 的本地 socks5 服务器。选用 ipt2socks 是为了和 v2 的实现解耦,v2 则是简单地作代理用,外加给自己的流量打上 SO_MARK

另一个相对次要的问题就是 IPv6IPv6 的透明代理看上去就很奇怪,并且也不是所有的代理服务器都支持 IPv6,因此我这里选择仅透明代理 IPv4。仅透明代理 IPv4 就意味着我们需要尽可能地让应用不走 IPv6,因此我选择在 DNS 上下手,拦截所有的 AAAA 返回,这样就只有 IP 直连的情况下才能使用 IPv6 网络了。

设置了这么多限制,那当初配置 IPv6 网络的意义又在哪里呢?意义当然还是有的。首先是 PTPT 是直接走 IP 的,因此不会受到 DNS 的影响,可以正常运作,这也就意味着 BT 客户端能够正确获取并上报自己的 IPv6 公网 IP,实现点对点的传输;此外,针对实际需要 IPv6 的服务,我们也可以在 dnsmasq 中指定对应的解析。

chinadns-ng

将仓库 clone 到本地,并通过 makemake install 构建安装:

Terminal window
1
git clone https://github.com/zfl9/chinadns-ng
2
cd chinadns-ng
3
make
4
sudo make install

然后新建一个 systemdservice

1
[Unit]
2
After=network-online.target
3
4
[Service]
5
Type=simple
6
User=nobody
7
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
8
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
9
ExecStart=/usr/local/bin/chinadns-ng -c 10.10.10.10,223.5.5.5 -t 1.0.0.1,8.8.4.4 -g /srv/proxy/chinadns-ng/gfwlist.txt -m /srv/proxy/chinadns-ng/chnlist.txt -N
10
11
[Install]
12
WantedBy=multi-user.target

这里可以看到我们的命令参数。其中 -c 对应的是国内 DNS 服务器,-t 对应的是国外(可信)DNS 服务器,-g 对应的是黑名单,-m 是白名单,-N 则是不返回 IPv6 的解析结果。

chinadns-ng 还需要用到 ipset,因此需要提前导入:

Terminal window
1
ipset -R <chnroute.ipset
2
ipset -R <chnroute6.ipset

为了能够持久化对应的 ipset,我们需要将 ipset 写入 /etc/ipset.conf,然后启动 ipset.service

Terminal window
1
ipset save > /etc/ipset.conf
2
systemctl enable ipset

注意这里不要 start 这个 ipset.service,因为对应的 set 已经存在了,start 是必定会失败的。只需要 enable 即可,下次启动时就正常了。

一切部署完毕,启动 chinadns-ng

Terminal window
1
systemctl start chinadns-ng
2
systemctl enable chinadns-ng

它会默认监听 127.0.0.165353 端口。

systemd-resolved

在配置 dnsmasq 之前,我们需要先把搅局的 resolved 解决。编辑 /etc/systemd/resolved.conf

1
[Resolve]
2
DNSStubListener=no

然后重启 systemd-resolved.service,它就不会监听 127.0.0.53:53 了:

Terminal window
1
systemctl restart systemd-resolved

dnsmasq

解决了 resolved,接下来就是 dnsmasq 了。dnsmasq 的存在是为了实现 DNS 缓存和自定义解析,可以直接通过 pacman 直接安装:

Terminal window
1
pacman -S dnsmasq

安装完后修改配置文件:

1
# 监听 53 端口
2
port=53
3
4
# 禁用 /etc/resolv.conf
5
no-resolv
6
no-poll
7
8
# 监听内网
9
listen-address=127.0.0.1,10.245.0.1
10
11
# 指定额外配置文件夹
12
conf-dir=/etc/dnsmasq.d/

然后建立 /etc/dnsmasq.d 目录:

Terminal window
1
mkdir /etc/dnsmasq.d

最后将 chinadns-ng 的服务器写入额外配置:

1
server=127.0.0.1#65353

并启动即可:

Terminal window
1
systemctl start dnsmasq
2
systemctl enable dnsmasq

v2

这里给出一个最简单的配置,监听的是 127.0.0.1:1080。所有传入流量都会走代理,除了 BT 流量会直连。所有的传出流量都带有值为 0xffSO_MARK

1
{
2
"log": {
3
"loglevel": "error"
4
},
5
"inbounds": [
6
{
7
"port": 1080,
8
"listen": "127.0.0.1",
9
"protocol": "socks",
10
"sniffing": {
11
"enabled": true,
12
"destOverride": ["http", "tls"]
13
},
14
"settings": {
15
"auth": "noauth",
16
"udp": true
17
}
18
}
19
],
20
"outbounds": [
21
{
22
"tag": "proxy",
23
"protocol": "vmess",
24
"settings": {
25
"vnext": [
26
{
27
"address": "your-domain",
28
"port": 443,
29
"users": [
30
{
31
"id": "your-uuid",
32
"alterId": 64
33
}
34
]
35
}
36
]
37
},
38
"streamSettings": {
39
"network": "ws",
40
"security": "tls",
41
"wsSettings": {
42
"path": "/your-path"
43
},
44
"sockopt": {
45
"mark": 255
46
}
47
}
48
},
49
{
50
"tag": "direct",
51
"protocol": "freedom",
52
"settings": {
53
"domainStrategy": "UseIP"
54
},
55
"streamSettings": {
56
"sockopt": {
57
"mark": 255
58
}
59
}
60
}
61
],
62
"routing": {
63
"domainStrategy": "IPOnDemand",
64
"rules": [
65
{
66
"type": "field",
67
"protocol": ["bittorrent"],
68
"outboundTag": "direct"
69
}
70
]
71
}
72
}

ipt2socks

这里就直接参考 KAAAsS配置了,详细的解释在对应文章里有说明。

1
[Unit]
2
Description=utility for converting iptables(redirect/tproxy) to socks5
3
After=network.target
4
5
[Service]
6
User=nobody
7
EnvironmentFile=/usr/local/etc/ipt2socks/ipt2socks.conf
8
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
9
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
10
NoNewPrivileges=true
11
ExecStart=/usr/local/bin/ipt2socks -s $server_addr -p $server_port -l $listen_port -j $thread_nums $extra_args
12
Restart=on-failure
13
RestartSec=5
14
LimitNOFILE=20480
15
16
[Install]
17
WantedBy=multi-user.target
1
# ipt2socks configure file
2
#
3
# detailed helps could be found at: https://github.com/zfl9/ipt2socks
4
# Socks5 server ip
5
server_addr=127.0.0.1
6
# Socks5 server port
7
server_port=1080
8
# Listen port number
9
listen_port=60080
10
# Number of the worker threads
11
thread_nums=1
12
# Extra arguments
13
extra_args=

nftables

最后就是 nftables 的配置了。不过在此之前,还需要执行 iproute 的命令:

Terminal window
1
ip -4 rule add fwmark 1 table 100
2
ip -4 route add local default dev lo table 100

这两条已经是我们的老朋友了,不用多说。下面就是正片:

Terminal window
1
define lo_fwmark = 1
2
define tproxy_port = 60080
3
define dns_port = 53
4
define direct_fwmark = 0xff
5
6
#
7
# 新建表
8
#
9
add table proxy
10
11
#
12
# 国内路由
13
#
14
include "/srv/proxy/config/chnroute.ruleset"
15
16
#
17
# 私有网段
18
#
19
include "/srv/proxy/config/private.ruleset"
20
21
#
22
# 代理规则
23
#
24
add chain proxy doproxy
25
# 从 connmark 中恢复 mark
26
add rule proxy doproxy meta mark set ct mark
27
# 避免回环
28
add rule proxy doproxy mark $lo_fwmark return
29
# 私有地址直连
30
add rule proxy doproxy ip daddr @private_addr return
31
# 国内 IP 直连
32
add rule proxy doproxy ip daddr @chnroute counter return
33
# 重路由 TCP(SYN)
34
add rule proxy doproxy tcp flags syn counter meta mark set $lo_fwmark
35
# 重路由 UDP
36
add rule proxy doproxy meta l4proto udp ct state new counter meta mark set $lo_fwmark
37
# 将 mark 存储到 connmark 中
38
add rule proxy doproxy ct mark set mark
39
40
#
41
# 局域网代理
42
#
43
add chain proxy prerouting { type filter hook prerouting priority 0 ; }
44
# 放行本地不带 mark 的包
45
add rule proxy prerouting iifname "lo" mark != $lo_fwmark return
46
# 放行非本地发出的 DNS 包,供后续劫持
47
add rule proxy prerouting fib saddr type != local udp dport $dns_port return
48
# 对非本机发出、非本机接收的包进行规则路由
49
add rule proxy prerouting meta l4proto {tcp, udp} fib saddr type != local fib daddr type != local jump doproxy
50
# 对带有 $lo_fwmark 的包 转发至 ipt2socks 端口
51
add rule proxy prerouting meta l4proto {tcp, udp} mark $lo_fwmark tproxy to 127.0.0.1:$tproxy_port meta mark set $lo_fwmark
52
53
#
54
# 劫持局域网 DNS
55
#
56
add chain proxy dns { type nat hook prerouting priority 0 ; }
57
add rule proxy dns fib saddr type != local udp dport 53 counter redirect to :$dns_port
58
59
#
60
# 本机代理
61
#
62
add chain proxy output { type route hook output priority 0 ; }
63
# 直连 direct_fwmark 流量
64
add rule proxy output mark $direct_fwmark return
65
# 对本机发出的剩余包进行规则路由
66
add rule proxy output meta l4proto {tcp, udp} fib saddr type local fib daddr type != local jump doproxy

这个配置参照了 KAAAsS 的配置以及新白话文教程中对应的 nftables 部分。核心思想和 iptables 版本的一样,都是在 Prerouting 部分下手。对于本机发出的包,首先经过 $direct_fwmark 的判断,放行这部分直连;对于没有 $direct_fwmarksaddr 为本地、daddr 不为本地的 TCPUDP 包执行规则路由(跳转到 doproxy 链)。在 doproxy 链中对需要代理的包会进行一次 mark,使其来到 Prerouting 链。

局域网的包而言,我们首先放行了本地发出且不带 $lo_fwmark 的包,然后放行了局域网传来的所有 DNS 包(saddr != localudp dport $dns_port 包),接下来对局域网传来的非本机发出非本机目的地的包进行规则路由,最后将带有 $lo_fwmark 的包送到了 TProxy 的端口。

对规则路由而言,其判断的本质就是 chnrootprivateip_addr 两个 set。当 daddr 在这两个 set 中时,是无条件走直连的。此外,为了减少匹配次数,这里前后还使用了 conntrackmeta mark set ct mark 中的 ct 就是 conntrack 的缩写,这句等效于 skb->mark = nf_conn->mark,意思是说给数据包标记上 conntrack 中的 mark。如果这个包是之前连接的一部分,那么在这一步中就能从之前的连接中把 mark 恢复,后面的步骤就不需要对其进行标记了,减少了标记的次数。配合这条规则的是最后的 ct mark set mark,等效 nf_conn->mark = skb->mark,将当前包的 mark 存储到 nf_conn 中供之后复用。打上 mark 的时机也有讲究,是对 flagSYNTCP 包和 state newUDP 包进行。

最后就是拦截局域网 DNS 了。在 Prerouting 里我们选择放行局域网的 DNS 包,而在此之后我们把这个包 redirect:$dns_port,即本地的 DNS 服务,这样就可以确保所有去往 :53 的包都由本机的 dnsmasq 处理了。

上述的规则中引用了两个文件,一个是 chnroute.ruleset,这个文件是通过 chinadns-ngchnroute.ipset 转换过来的,由于原文过长,这里就不贴出来了,内容格式如下:

1
#!/usr/sbin/nft -f
2
3
add set v2ray chnroute { type ipv4_addr; flags interval; }
4
add element v2ray chnroute { 1.0.1.0/24 }
5
add element v2ray chnroute { 1.0.2.0/23 }
6
add element v2ray chnroute { 1.0.8.0/21 }
7
add element v2ray chnroute { 1.0.32.0/19 }
8
add element v2ray chnroute { 1.1.0.0/24 }
9
add element v2ray chnroute { 1.1.2.0/23 }
10
add element v2ray chnroute { 1.1.4.0/22 }

然后是 private.ruleset,记录了 IPv4 中的所有私有地址:

1
#!/usr/sbin/nft -f
2
3
add set v2ray private_addr { type ipv4_addr; flags interval; }
4
add element v2ray private_addr { 0.0.0.0/8 }
5
add element v2ray private_addr { 10.0.0.0/8 }
6
add element v2ray private_addr { 100.64.0.0/10 }
7
add element v2ray private_addr { 127.0.0.0/8 }
8
add element v2ray private_addr { 169.254.0.0/16 }
9
add element v2ray private_addr { 172.16.0.0/12 }
10
add element v2ray private_addr { 192.0.0.0/24 }
11
add element v2ray private_addr { 192.0.2.0/24 }
12
add element v2ray private_addr { 192.88.99.0/24 }
13
add element v2ray private_addr { 192.168.0.0/16 }
14
add element v2ray private_addr { 198.18.0.0/15 }
15
add element v2ray private_addr { 198.51.100.0/24 }
16
add element v2ray private_addr { 203.0.113.0/24 }
17
add element v2ray private_addr { 224.0.0.0/4 }
18
add element v2ray private_addr { 240.0.0.0/4 }
19
add element v2ray private_addr { 255.255.255.255/32 }

最后,为了能够快捷地开关透明代理,我们将其部署成 systemdservice。需要准备的文件如下:

1
#!/bin/bash
2
3
ip -4 rule add fwmark 1 table 100
4
ip -4 route add local default dev lo table 100
5
6
/srv/proxy/config/proxy.nft
1
#!/bin/bash
2
3
ip -4 rule delete fwmark 1 table 100
4
ip -4 route delete local default dev lo table 100
5
6
nft delete table proxy

nftables 的配置所赐,在结束的时候我们只需要直接对我们创建的 table 进行一个删除就可以了。

1
[Unit]
2
Description=iproute2 and nftables rules for transparent proxy
3
PartOf=systemd-networkd.service
4
After=network.target systemd-networkd.service
5
Wants=network.target
6
7
[Service]
8
Type=oneshot
9
RemainAfterExit=yes
10
ExecStart=/srv/proxy/transparent_proxy.sh
11
ExecStop=/srv/proxy/transparent_proxy_off.sh
12
13
[Install]
14
WantedBy=multi-user.target

这里需要注意的是 transparent-proxy.service 必须在 systemd-networkd.service 之后启动,并且要跟随 systemd-networkd 一起开启/关闭(PartOf)。配置完后启动即可:

Terminal window
1
systemctl start transparent-proxy
2
systemctl enable transparent-proxy

至此,最后一片拼图也完成了。

结语

终于,我们得到了一个能用的软路由。它可以完美地完成原本由笔记本提供的代理功能,自带的无线网卡虽然表现一般但也能有百兆的速度,一切都正常地运转起来了。

但现在的配置还是有一些令人不大满意的地方,比如:

这些问题或许会在后续的使用中慢慢解决,或许不会(懒)

总之,能用万岁!(笑)

参考

  1. https://archlinuxarm.org/platforms/armv8/broadcom/raspberry-pi-4
  2. https://wiki.archlinux.org/title/MariaDB\_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)
  3. https://github.com/zfl9/chinadns-ng
  4. https://wiki.archlinux.org/title/Ipset\_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)#%E4%BD%BFipset%E6%8C%81%E4%B9%85%E5%8C%96
  5. https://blog.kaaass.net/archives/1446
  6. https://github.com/kaaass/manjaro-settings/blob/master/home/kaaass/shell/transparent\_proxy.sh