使用 nftables 按需路由流量至虚拟网卡

Author Avatar
Axell Oct 09, 2025
  • Read this article on other devices

这篇文章记录了在服务器上部署 Mihomo 时遇到的一个常见痛点:默认的 auto-route 机制会把全局流量都拉到 TUN 设备里,影响内网访问和部分运营商定向流量。最终的解决方案是手动关闭 Mihomo 的自动路由功能,改用 nftables 标记需要走代理的流量,再配合 ip rule/ip route 把这些带标记的数据包导向 Mihomo 的虚拟网卡。为了让规则在网卡创建后自动生效,还需要用 udev 监听设备事件并触发 Systemd 服务。

1. 关闭 Mihomo 的自动路由

Mihomo 的 auto-route 会自动在内核里写入策略路由规则,把大部分流量转发到 TUN。虽然简单,但:

  1. 规则不可控,无法过滤内网网段;
  2. 和宿主机上的其他路由策略可能冲突;
  3. auto-route 配置隐藏在 Mihomo 内部,排错成本高。

因此第一步就是显式关闭它,交还控制权给系统: 编辑 Mihomo 配置文件: nano /etc/mihomo/config.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tun:
enable: true
stack: system
dns-hijack:
- any:53
- tcp://any:53
auto-route: false
auto-redirect: false
device: mtun
mtu: 1350
strict-route: false
gso: false
udp-timeout: 300
endpoint-independent-nat: false

修改后重启 Mihomo:systemctl restart mihomo

2. 用 nftables 标记需要走 Mihomo 的流量

2.1 了解 nftables

nftablesiptables 的继任者,核心优势是统一的表达式语法、更高性能和更好的状态查询能力。我们用它来实现「只标记特定目标地址」的需求:当数据包符合条件时,把 fwmark(防火墙标记)设为特定十六进制值,后面策略路由会读取这个标记。

2.2 清理旧防火墙并确认后端

Ubuntu 默认带有 ufw(一个 iptables 封装),为了避免规则冲突,先把 ufw 关闭并禁用启动:

1
2
sudo systemctl stop ufw
sudo systemctl disable ufw

确认当前 Netfilter 后端已经切换到 nftables(输出应为 nf_tables):

1
sudo update-alternatives --display iptables

如果结果不是 nf_tables,可通过 update-alternatives --config iptables 手动选择,同时为 ip6tables 做同样切换。

2.3 编写 nftables 规则集

编辑 /etc/nftables.conf(这是 Systemd 默认加载的规则文件): nano /etc/nftables.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
chain input {
type filter hook input priority 0; policy drop;
iif "lo" accept
meta l4proto { icmp, ipv6-icmp } accept
tcp dport {22} accept
meta mark {0x135} accept
ip6 daddr fe80::/64 udp dport dhcpv6-client accept
ct state vmap { invalid : drop, established : accept, related : accept }
ct state new limit rate over 1/second burst 10 packets drop
}

chain forward {
type filter hook forward priority 0; policy drop;
iifname {"mtun"} accept
ct state established,related accept
}

chain output {
type filter hook output priority 0; policy accept;
}

# 标记命中 Mihomo 目标的流量
chain prerouting-tun {
type filter hook prerouting priority raw; policy accept;
ip daddr {10.7.0.1/24} return # 跳过本地局域网
ip daddr {10.210.0.1/16, 10.220.0.1/16} return # 跳过特定内网
meta mark set 0x135 return # 打上 fwmark 0x135
}
}

几点说明:

  • prerouting 链在数据包路由前执行,适合做标记;
  • priority raw 确保在连接跟踪之前执行;
  • meta mark 设置的 fwmark 会让后续策略路由捕获;
  • return 表示不用继续执行当前链,以免重复标记。

保存后立即加载规则:

1
sudo nft -f /etc/nftables.conf

启用系统服务,保证开机自动应用:

1
2
sudo systemctl enable nftables
sudo systemctl restart nftables

排错技巧:sudo nft list ruleset 可以查看当前生效的完整规则,确认标记链是否存在;sudo nft monitor trace 能实时观察匹配情况。

3. 用策略路由把标记流量导向 Mihomo

3.1 简述 ip rule / ip route

Linux 的策略路由(Policy Routing)通过 Routing Policy Database (RPDB) 与多路由表配合工作:

  • ip rule 控制匹配条件(如源地址、接口、fwmark)以及命中后使用的路由表;
  • ip route 为每个路由表配置指向的网关、设备或黑洞行为。

我们需要做的是:当数据包携带 fwmark 0x135 时,改用自定义的 table 100,把它的默认路由指到 Mihomo TUN 接口。

3.2 创建 Systemd oneshot 单元

这样做的好处是将所有规则写在一个受管服务里,方便启动、停止和排错: nano /etc/systemd/system/mihomo-rules.service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[Unit]
Description=Policy routing for Mihomo marked traffic
After=network.target

[Service]
Type=oneshot

# START
ExecStart=-/usr/sbin/ip rule add fwmark 0x135 table 100
ExecStart=-/usr/sbin/ip route replace default via 198.18.0.1 dev mtun table 100 # 把地址替换为 Mihomo 分配的 IPv4 网关
# IPv6
ExecStart=-/usr/sbin/ip -6 rule add fwmark 0x135 table 100
ExecStart=-/usr/sbin/ip -6 route replace default dev mtun table 100

# STOP
ExecStop=-/usr/sbin/ip rule del fwmark 0x135 table 100
# IPv6
ExecStop=-/usr/sbin/ip -6 rule del fwmark 0x135 table 100

RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

执行以下命令让服务立即生效并设置为开机启动:

1
2
sudo systemctl daemon-reload
sudo systemctl enable --now mihomo-rules.service

验证:

1
2
3
ip rule show | grep 0x135
ip route show table 100
ip -6 route show table 100

若输出中出现 lookup 100 以及指向 mtun 的默认路由,说明策略路由已加载成功。

4. 使用 udev 在 TUN 设备出现时刷新规则

4.1 为什么需要 udev

当 Mihomo 重启或系统在网络服务启动后才加载 mtun 设备时,策略路由可能在接口准备好之前就执行,导致路由表中缺少正确的 Nexthop。udev 是 Linux 设备管理层,用来监听内核发出的硬件/虚拟设备事件。我们可以写一条规则:一旦名为 mtun 的网络接口被创建,就重新执行策略路由服务。

4.2 编写 udev 规则

nano /etc/udev/rules.d/99-mihomo.rules

1
ACTION=="add", SUBSYSTEM=="net", NAME=="mtun", RUN+="/bin/systemctl restart mihomo-rules.service"

让规则立即生效:

1
2
sudo udevadm control --reload
sudo udevadm trigger --subsystem-match=net --attr-match=name=mtun

如果 udevadm trigger 成功触发,可在 journalctl -u mihomo-rules.service -f 中看到服务被重启。

5. 验证流程

  1. 确认 nftables 标记: 在客户端对需要代理的目标发起连接,然后使用 sudo nft monitor trace 验证 prerouting-tun 链是否被命中。
  2. 确认策略路由: ip rule showip route show table 100 应该分别显示 fwmark 规则和指向 mtun 的默认路由。
  3. 抓包验证: 使用 sudo tcpdump -i mtun 观察是否有代理流量经过 Mihomo 的虚拟网卡。
  4. 回退机制: 如果需要临时停用策略,可运行 sudo systemctl stop mihomo-rules.service 并执行 sudo nft flush ruleset

6. 常见问题与排查

  • 流量无法访问外网: 检查 /etc/mihomo/config.yaml 中的网关地址,确认 198.18.0.1 等 placeholder 是否替换成真实地址。
  • nft 命令报错: 可能是语法不兼容旧版本内核,可用 nft --check -f /etc/nftables.conf 先验证。
  • mtun 设备不存在: 确保 Mihomo 配置 tun.enable=true,并检查 journalctl -u mihomo 日志。
  • 策略路由未生效: 查看 journalctl -u mihomo-rules.service,确认服务运行是否报错;必要时在服务单元里加入 ExecStartPre=/usr/bin/sleep 2 等延时。

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.

Link to this article: https://blog.axell.top/archives/%E4%BD%BF%E7%94%A8-nftables-%E6%8C%89%E9%9C%80%E8%B7%AF%E7%94%B1%E6%B5%81%E9%87%8F%E8%87%B3%E8%99%9A%E6%8B%9F%E7%BD%91%E5%8D%A1/