路由表、ip rule 与 nftables:Linux 三层流量劫持机制解析

以本机 Mihomo(Clash.Meta)TUN 模式 + Docker 容器网络为实例,逐层拆解

本文适合同时想理解概念和看真实案例的读者。



1. 路由表(Routing Table)

1.1 什么是路由表

路由表是内核中用于决定数据包下一跳的规则集合。最简单的形式:

目标网络         → 从哪个接口出去 → 经过哪个网关

传统 Linux 只有一张路由表(main),所有路由决策都查它。但现代 Linux 支持多路由表,允许根据不同条件选择不同路由表。

1.2 标准路由表

Linux 预定义了三张路由表:

表名ID用途
local255本地地址和广播地址,由内核自动维护
main254主路由表,ip route 不加 table 参数时操作的就是它
default253默认路由表(通常为空)

自定义表可以使用 1~252 之间的任意 ID。本机 Mihomo 创建了一张 2022 表。

1.3 查看所有路由表

ip route show table all
```text

本机输出(仅 IPv4,已过滤 IPv6):

─── 表 2022(Mihomo 创建)───

default via 198.18.0.2 dev Meta table 2022

─── 表 main(ID 254)───

default via 192.168.1.1 dev wlp0s20f3 proto dhcp src 192.168.1.5 metric 20600 172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown 172.18.0.0/16 dev br-700ced5fc520 proto kernel scope link src 172.18.0.1 192.168.1.0/24 dev wlp0s20f3 proto kernel scope link src 192.168.1.5 metric 600 198.18.0.0/30 dev Meta proto kernel scope link src 198.18.0.1

─── 表 local(ID 255)───

local 127.0.0.0/8 dev lo table local proto kernel scope host src 127.0.0.1 local 127.0.0.1 dev lo … local 172.17.0.1 dev docker0 … local 172.18.0.1 dev br-700ced5fc520 … local 192.168.1.5 dev wlp0s20f3 … local 198.18.0.1 dev Meta … broadcast …(各种广播地址)


### 1.4 拆解 main 表

```bash
default via 192.168.1.1 dev wlp0s20f3 proto dhcp src 192.168.1.5 metric 20600
```text

- `default` — 目标 0.0.0.0/0(所有未匹配更具体路由的流量)
- `via 192.168.1.1` — 下一跳网关(路由器)
- `dev wlp0s20f3` — 从 Wi-Fi 网卡发出
- `proto dhcp` — 由 DHCP 自动添加
- `src 192.168.1.5` — 使用本机 IP 作为源地址
- `metric 20600` — 路由优先级(数值越小优先级越高)

172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown 172.18.0.0/16 dev br-700ced5fc520 proto kernel scope link src 172.18.0.1


这两条是 Docker 创建的桥接网络路由:
- `172.17.0.0/16` → `docker0`(当前未连接,显示 `linkdown`)
- `172.18.0.0/16` → `br-700ced5fc520`(Docker Compose 创建的 axonhub 网络)
- `scope link` — 直连网络,不经过网关

192.168.1.0/24 dev wlp0s20f3 proto kernel scope link src 192.168.1.5 metric 600


物理局域网路由:到 `192.168.1.0/24` 的流量从 `wlp0s20f3` 直连发出。

198.18.0.0/30 dev Meta proto kernel scope link src 198.18.0.1


Mihomo TUN 设备 `Meta` 的直连路由。`198.18.0.0/30` 只有两个可用 IP:`198.18.0.1`(本机 TUN 接口)和 `198.18.0.2`(TUN 对端,Mihomo 进程侧)。

### 1.5 拆解表 2022

```bash
default via 198.18.0.2 dev Meta table 2022
```text

这张表只有**一条默认路由**:所有匹配这条表的流量都指向 `Meta` TUN 设备,下一跳是 `198.18.0.2`(Mihomo 进程端)。

关键问题是:**什么流量会进入这张表?** 这取决于 `ip rule`——下一章的主题。

---

## 2. 策略路由(ip rule)

### 2.1 什么是策略路由

传统路由只用**目标 IP** 查路由表。策略路由允许在查表之前附加条件:

if(数据包满足条件 X)→ 查路由表 A else → 查路由表 B


条件可以是:源 IP、入站接口、目标端口、防火墙标记等。

### 2.2 查看规则

```bash
ip rule show
```text

本机输出:

0: from all lookup local 9000: from all to 198.18.0.0/30 lookup 2022 9001: not from all dport 53 lookup main suppress_prefixlength 0 9001: from all iif Meta goto 9010 9002: not from all iif lo lookup 2022 9002: from 0.0.0.0 iif lo lookup 2022 9002: from 198.18.0.0/30 iif lo lookup 2022 9010: from all nop 32766: from all lookup main 32767: from all lookup default


> **重要:** 规则按优先级从小到大依次检查,第一个匹配的生效。

### 2.3 逐条解析

#### 规则 `0: from all lookup local`

- **含义:** 所有流量先查 `local` 表
- **作用:** 目标为本机 IP 的包在这里被匹配并本地投递
- **效果:** 防止本机地址被后续规则劫持。ICMP ping 本机外部 IP 时,rule 0 直接匹配,不会落入 TUN

#### 规则 `9000: from all to 198.18.0.0/30 lookup 2022`

- **含义:** 目标地址是 `198.18.0.0/30`(fake-IP 范围)→ 查表 2022 → 走 TUN
- **作用:** 当 Mihomo 的 fake-IP DNS 返回 198.18.x.x 地址后,对这个地址的访问被路由到 TUN,Mihomo 才能从 TUN 读取到真实请求
- **为什么只有 `/30`:** Mihomo 只用了 `198.18.0.1` 和 `198.18.0.2` 两个地址

#### 规则 `9001: not from all dport 53 lookup main suppress_prefixlength 0`

**这是第一个需要仔细理解的难点。**

- **解析语法:** `not` 否定 `from all dport 53` 这个整体条件
- **实际含义:** 如果数据包**不是**(来自任意源且目标端口为 53)→ 查 main 表,但抑制前缀长度为 0 的条目
- **更直白的理解:** DNS 查询(目标端口 53)**不匹配**此规则,会继续检查后续规则;**非 DNS** 流量匹配此规则,被送到 main 表且屏蔽默认路由

**等一下,这似乎不合理——非 DNS 流量去 main 表且抑制默认路由,那不就路由失败了吗?** 实际上这正是 Mihomo 的意图:

> 这条规则起到一个**旁路标记**的作用:非 DNS 流量在被送到 main 表后因默认路由被抑制而路由失败,于是内核继续检查下一优先级规则(9002),最终落入 TUN。

#### 规则 `9001: from all iif Meta goto 9010`

- **含义:** 从 `Meta` 接口(TUN 设备)进入的包跳转到优先级 9010
- **作用:** 防止路由循环

**`goto` 与 `jump` 的区别:**
- `jump` 会记住当前位置,子链执行完后返回继续检查下一条规则
- `goto` 直接跳转到指定优先级,不会返回
- 这里用 `goto` 是因为跳转到 `9010: from all nop`(空操作),然后规则会落到 `32766`(main 表)正常路由。这确保 TUN 发回的包不再被 9002 吸入 TUN 形成死循环

#### 规则 `9002: not from all iif lo lookup 2022`

**核心难点。这条规则的语义是关键。**

- **语法:** `not` 否定 `from all iif lo`
- **实际含义:** 如果数据包**不是**(来自任意源且入站接口为 lo)→ 查表 2022 → 走 TUN
- **也就是:** **所有不入 lo 的流量 → TUN**
- **为什么不是 `from all iif lo` 而是 `not iif lo`:** 因为需要覆盖两种情况:
  - 本机发出的流量(`iif = lo`)→ 需要其他规则处理
  - 非本机发出的流量(`iif = 网卡/桥接`)→ 入 TUN

**这条规则是 Docker 容器流量被拦截的关键:** 容器流量从 `br-700ced5fc520` 进入内核,`iif` 不是 `lo`,因此匹配此规则 → 路由到 TUN。

#### 规则 `9002: from 0.0.0.0 iif lo lookup 2022`

- **含义:** 从 lo 接口进入的任意源 IP 流量 → 查表 2022 → 走 TUN
- **作用:** 覆盖本机进程发出的流量(它们都从 lo 发出)
- **`from 0.0.0.0`:** 匹配所有源地址,等同于 `from all`

#### 规则 `9002: from 198.18.0.0/30 iif lo lookup 2022`

- **含义:** 源地址在 fake-IP 范围内且从 lo 进入 → 查表 2022
- **注意:** 这实际上是冗余规则——上一条 `from 0.0.0.0 iif lo` 已经覆盖了所有 lo 流量。这可能是 Mihomo 显式添加以保证确定性行为

#### 规则 `9010: from all nop`

- **含义:** 空操作,什么都不做
- **作用:** 作为 `goto 9010` 的跳转目标。后续规则继续从 32766 开始检查

#### 规则 `32766: from all lookup main`

- **含义:** 所有流量查 main 表(兜底)
- **这实际上是 `ip rule` 的默认规则,Mihomo 并没有添加它**

#### 规则 `32767: from all lookup default`

- **含义:** 所有流量查 default 表(通常为空,兜底的兜底)
- **也是系统默认规则**

### 2.4 难点总结

| 难点 | 关键理解 |
|------|---------|
| `not from all iif lo` 的语义 | 匹配所有**入口非 lo** 的流量,这是 Docker 流量被劫持的原因 |
| `suppress_prefixlength 0` | 抑制默认路由(前缀长度 0)。非 DNS 流量被发到 main 表后因无默认路由而失败,继续走下一优先级规则 |
| `goto 9010` vs `jump` | `goto` 不返回,用于 TUN 回包防循环 |
| 表 2022 只有一条默认路由 | 足够了——所有匹配 9000/9002 规则的流量只需要一个去处:TUN 设备 |
| `rule 0` 优先级最高 | 所有目标为本机的包在 rule 0 就被 local 表截获,不会被后续规则干扰 |

---

## 3. nftables 规则集

### 3.1 什么是 nftables

nftables 是 Linux 内核的包过滤框架,取代了早期的 iptables/ip6tables/arptables/ebtables。

核心概念:

| 概念 | 说明 |
|------|------|
| **table** | 规则集容器,指定协议族(inet/ip/ip6/arp/bridge) |
| **chain** | 规则链表,挂载到内核的 hook 点 |
| **hook** | 内核网络栈中的固定拦截点(prerouting/input/forward/output/postrouting) |
| **rule** | 由匹配条件(match)+ 动作(verdict/statement)组成 |
| **set** | 高效匹配集合,可用于批量 IP/端口匹配 |

### 3.2 数据包在内核 hook 点的流程

[PREROUTING] → [路由决策] → [FORWARD] → [POSTROUTING] ↓ [INPUT] → 本地进程 ↑ 本地进程 → [OUTPUT] → [路由决策] → [POSTROUTING]


nftables 可以挂载到这些 hook 点,在每个阶段对数据包进行检查、修改或丢弃。

本机 Mihomo 的 `table inet mihomo` 使用了两个 hook:
- `prerouting`:数据包到达网卡后、路由决策之前
- `output`:本机进程发出的数据包、路由决策之后(因为需要知道 `oifname`)

### 3.3 配置查看

```bash
sudo nft list ruleset
```text

输出内容按 table 组织。我们只看 Mihomo 创建的 `table inet mihomo`(`inet` 表示同时处理 IPv4 和 IPv6):

table inet mihomo {

# ─── 集合 ───
set inet4_local_address_set {
    type ipv4_addr
    flags interval
    elements = { 127.0.0.0/8, 198.18.0.0/30 }
}

**这是 Mihomo 定义的本地地址集合。** 包含:
- `127.0.0.0/8`(标准 loopback 地址段)
- `198.18.0.0/30`(Mihomo TUN 接口和 fake-IP 地址段)

后续规则用 `@inet4_local_address_set` 引用此集合,快速判断数据包是否与本地地址相关。
# ─── output 链 ───
chain output {
    type nat hook output priority mangle; policy accept;

**类型、hook 和优先级说明:**
- `type nat` — 此链用于网络地址转换
- `hook output` — 挂载在 OUTPUT 钩子(本机进程发出的包)
- `priority mangle` — 优先级为 `mangle`(-150),在路由决策之后执行。这意味着此时内核**已经决定了这个包从哪个接口发出**
    oifname "Meta" meta nfproto ipv4 meta l4proto tcp
        counter redirect to :46569 return

**逐字段拆解:**

| 条件/动作 | 含义 |
|-----------|------|
| `oifname "Meta"` | 出站接口是 Meta(TUN 设备) |
| `meta nfproto ipv4` | 网络协议是 IPv4 |
| `meta l4proto tcp` | 传输层协议是 TCP |
| `counter` | 统计匹配的数据包数量和字节数 |
| `redirect to :46569` | 将数据包重定向到本机 46569 端口 |
| `return` | 重定向后不再检查本链后续规则 |

**作用:** 本机进程发出的 TCP 包,在路由决策中被决定走 TUN 设备(`oif=Meta`),被此规则捕获并 REDIRECT 到 `:46569`(Mihomo 透明代理端口)。这样包不会实际进入 TUN 设备,而是被转入 Mihomo 的代理引擎处理。
# ─── prerouting 链 ───
chain prerouting {
    type nat hook prerouting priority dstnat + 1; policy accept;

- `type nat` — NAT 类型链
- `hook prerouting` — 挂载在 PREROUTING 钩子(所有入站包,路由决策前)
- `priority dstnat + 1` — 优先级是 `dstnat`(100)加 1,即 101。这意味着它在标准 DNAT 处理之后执行
- 重要:**此时还没有路由决策**,`iif`(入站接口)是确定的,但 `oif` 还不知道
    # ── 规则 1:防循环 ──
    iifname "Meta" counter return

- **条件:** 入站接口是 `Meta`(TUN 设备)
- **动作:** 计数后 `return`(跳过本链后续规则)
- **作用:** 从 TUN 发回的包不再被 PREROUTING 规则处理,防止循环
    # ── 规则 2:DNS 劫持 ──
    ip saddr @inet4_local_address_set meta l4proto { tcp, udp } th dport 53
        counter dnat ip to 198.18.0.2

- **条件:** 源 IP 属于本地地址集合(127.0.0.0/8 或 198.18.0.0/30),且是 TCP/UDP 协议,目标端口 53
- **动作:** DNAT 到 `198.18.0.2`(Mihomo TUN 对端地址)
- **作用:** 本机发出的 DNS 查询被 DNAT 到 TUN 设备对端,Mihomo 从 TUN 读取后用自己的 DNS 处理
- **注意:** 这个条件限制了只有**本机**的 DNS 才会被劫持(源 IP 在 local set 中)。Docker 容器的 DNS 查询源 IP 是 `172.18.0.2`,**不匹配**此规则
    # ── 规则 3:本地地址放行 ──
    ip daddr @inet4_local_address_set counter return

- **条件:** 目标 IP 是本地地址(127.0.0.0/8 或 198.18.0.0/30)
- **动作:** 计数后跳过
- **作用:** 目标是本机/ fake-IP 范围的包不进入后续处理(不会被 redirect 到 :46569)
    # ── 规则 4:MPTCP 丢弃 ──
    tcp option mptcp exists counter drop

- **条件:** TCP 头部包含 MPTCP(Multipath TCP)选项
- **动作:** 丢弃
- **作用:** 阻止 MPTCP 连接,避免 Mihomo 透明代理无法处理
    # ── 规则 5:IPv6 拒绝 ──
    meta nfproto ipv6 counter reject with icmpv6 no-route

- **条件:** IPv6 协议
- **动作:** 拒绝,并发送 ICMPv6 "no route to destination"
- **作用:** 当前 Mihomo 配置没有完整处理 IPv6 透明代理,直接拒绝 IPv6 流量避免意外行为
    # ── 规则 6:TCP 重定向(核心) ──
    meta nfproto ipv4 meta l4proto tcp
        counter redirect to :46569 return

**本机 Mihomo 流量劫持的核心规则。**

- **条件:** IPv4 + TCP
- **动作:** REDIRECT 到本机 `:46569`,然后跳过后续规则
- **覆盖范围:** 所有未被前几条规则放行的 IPv4 TCP 包

这包括:
- Docker 容器发出的 TCP 流量(源 IP 172.18.0.x,入站接口 br-700ced5fc520)
- 外部进入本机的 TCP 流量(源 IP 外部,入站接口 wlp0s20f3)
- **不包括本机发出的 TCP**(本机发出的 TCP 走的是 OUTPUT 链,不是 PREROUTING)

**效果:** 容器 TCP、外部入站 TCP 全部被 REDIRECT 到 `:46569`,Mihomo 在此端口接收后进入代理核心引擎处理。

### 3.4 Docker 管理的其他 nftables 表

`sudo nft list ruleset` 还会输出其他表,但它们由 Docker 维护,非 Mihomo 创建:

Warning: table ip nat is managed by iptables-nft, do not touch!

table ip nat { … } # Docker 端口映射(DNAT)、容器出站 SNAT(MASQUERADE)

Warning: table ip filter is managed by iptables-nft, do not touch!

table ip filter { … } # Docker 网络隔离规则(FORWARD 链控制容器间通信)

Warning: table ip6 nat is managed by iptables-nft, do not touch!

table ip6 nat { … } # IPv6 版本(当前 Docker 相关规则为空)

table ip6 filter { … } # IPv6 Docker filter(当前策略 accept)

table ip raw { … } # raw 表 PREROUTING:阻止外部直接访问 Docker 映射端口


**最关键的一条 Docker 规则——容器出站 SNAT:**

```nft
table ip nat {
    chain POSTROUTING {
        ip saddr 172.18.0.0/16 oifname != "br-700ced5fc520"
            counter masquerade
    }
}
```text

- **条件:** 源 IP 为 docker 容器网段,且**出站接口不是桥接网卡**(即流量要去外部网络)
- **动作:** MASQUERADE(动态 SNAT),将源 IP 改为宿主机 IP
- **但是这条规则在 TCP 路径中不会被触发:** 因为容器 TCP 在 PREROUTING 阶段就被 `redirect to :46569` 截获(目标改为 `127.0.0.1`),路由决策判定为本地投递,不经过 POSTROUTING

**验证:** Mihomo 日志中容器 TCP 连接显示 `[TCP] 172.18.0.2:xxxxx --> ...`,源 IP 是容器原始 IP,未被 SNAT。

---

## 4. 三者协同:一条数据包的完整旅程

### 4.1 容器 TCP → 外部

容器 axonhub (172.18.0.2:xxxxx → 1.1.1.1:80)

桥接网卡 br-700ced5fc520 │ iif = br-700ced5fc520 ▼ ┌─────────────────────────────────────────────────────────────┐ │ nftables prerouting (priority dstnat + 1) │ │ │ │ 规则 1: iifname “Meta” → 不匹配 │ │ 规则 2: local src + dport 53 → 不匹配 │ │ 规则 3: ip daddr local 范围 → 不匹配 │ │ 规则 4: tcp mptcp → 不匹配 │ │ 规则 5: ipv6 → 不匹配 │ │ ★ 规则 6: meta nfproto ipv4 tcp → ★ REDIRECT :46569│ └─────────────────────────────────────────────────────────────┘ │ 目标改为 127.0.0.1:46569 │ 源保持 172.18.0.2(容器原始 IP) ▼ ┌─────────────────────────────────────────────────────────────┐ │ ip rule(策略路由) │ │ │ │ rule 0: lookup local → 127.0.0.1 是本地 → 匹配 │ │ (后续规则不再检查) │ │ → 路由决策:本地投递 │ │ → 经 INPUT → Mihomo socket (:46569) │ └─────────────────────────────────────────────────────────────┘ │ ▼ Mihomo 核心引擎 └─ [TCP] 172.18.0.2:56690 –> 1.1.1.1:443 match Match using SELECT[🇺🇸US-DMIT] └─ 经代理节点 VLESS XTLS 发出


**路径总结:** PREROUTING redirect → rule 0(local 表)→ INPUT → Mihomo socket → 代理引擎

### 4.2 容器 ICMP → 外部

容器 axonhub (172.18.0.2 → 1.1.1.1, ICMP echo)

br-700ced5fc520 │ iif = br-700ced5fc520 ▼ ┌─────────────────────────────────────────────────────────────┐ │ nftables prerouting │ │ ★ 规则 6: meta nfproto ipv4 tcp → 不匹配(ICMP ≠ TCP)│ │ → 所有规则不匹配,放行 │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ ip rule │ │ │ │ rule 0: lookup local → 1.1.1.1 不本地 │ │ rule 9000: to 198.18.0.0/30 → 不匹配 │ │ rule 9001: dport 53 → 不匹配 │ │ rule 9001: iif Meta → iif=br → 不匹配 │ │ ★ rule 9002: not from all iif lo → iif≠lo → 匹配 │ │ → 查表 2022 → default via 198.18.0.2 dev Meta │ └─────────────────────────────────────────────────────────────┘ │ ▼ TUN 设备 Meta │ ▼ Mihomo TUN 栈 └─ ICMP 不进核心引擎(VLESS/XTLS 无法承载 ICMP) └─ TUN 栈 NAT:源 IP 172.18.0.2 → 198.18.0.1 └─ 写回 TUN 设备 │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ ip rule(TUN 回包,此时 iif=Meta) │ │ ★ rule 9001: iif Meta goto 9010 → 跳过 │ │ → 查 main 表 → default via 192.168.1.1 dev wlp0s20f3 │ └─────────────────────────────────────────────────────────────┘ │ ▼ 物理网卡发出(经 SNAT 后源 IP 变为 192.168.1.5)


**路径总结:** PREROUTING 放行 → ip rule 9002 → TUN → TUN 栈 NAT 直发(不进核心引擎)

### 4.3 本机 TCP → 外部

firefox (192.168.1.5 → 142.250.80.68:443)

↓ socket
▼

┌─────────────────────────────────────────────────────────────┐ │ ip rule │ │ ★ rule 9002: from 0.0.0.0 iif lo → 匹配 │ │ → 查表 2022 → default via 198.18.0.2 dev Meta │ │ → 此时 oifname = “Meta” │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ nftables output (priority mangle, 路由决策后执行) │ │ │ │ ★ oifname “Meta” tcp → REDIRECT to :46569 │ └─────────────────────────────────────────────────────────────┘ │ 目标改为 127.0.0.1:46569 ▼ 路由决策(第二次) └─ 本地投递 → INPUT → Mihomo socket └─ 进入代理核心引擎


**路径总结:** ip rule 9002 → 路由决策(oif=Meta)→ OUTPUT redirect → 重新路由本地投递

---

## 总结

### 三层拦截机制的分工

| 层 | 工具 | hook/规则 | 拦截对象 | 时机 |
|----|------|-----------|---------|------|
| **1** | nftables prerouting | `tcp redirect :46569` | 外部入站 TCP、容器 TCP | 路由决策前 |
| **2** | nftables output | `oif=Meta tcp redirect :46569` | 本机发出 TCP | 路由决策后 |
| **3** | ip rule 9002 + 表 2022 | `not iif lo → TUN` | 容器 ICMP/UDP、本机 ICMP/UDP | 路由决策(nftables 放行后) |

### TCP 与 ICMP/UDP 的路径分歧

| 维度 | TCP | ICMP/UDP |
|------|-----|----------|
| 本机发出 | 路由(→oif=Meta) → OUTPUT redirect :46569 → 核心引擎 | 路由(→TUN) → TUN 栈 NAT → 直发 |
| 容器发出 | PREROUTING redirect :46569 → 核心引擎(源 IP 保持 172.18.0.2) | PREROUTING 放行 → ip rule → TUN → TUN 栈 NAT |
| 外部入站 | PREROUTING redirect :46569 → 核心引擎(需额外测试验证影响) | PREROUTING 放行 → ip rule 0 → 正常交付 |

### 文章对应命令速查

```bash
# 路由表
ip route show table all                  # 查看所有路由表
ip route show table main                 # 查看 main 表
ip route show table 2022                 # 查看自定义表
ip route show table local                # 查看 local 表

# 策略路由
ip rule show                             # 查看所有规则
ip rule add priority 100 from 10.0.0.0/8 lookup main  # 添加规则示例
ip rule del priority 9000                # 删除规则示例

# nftables
sudo nft list ruleset                    # 查看完整规则集
sudo nft list table inet mihomo          # 查看特定表
sudo nft list chain inet mihomo prerouting   # 查看特定链
sudo nft add rule inet mihomo prerouting ip daddr 192.168.1.5 tcp dport 22 return  # 添加豁免规则示例

# 模拟路由决策
ip route get 1.1.1.1                     # 本机到 1.1.1.1 走哪个路由表
ip route get 1.1.1.1 from 172.18.0.2 iif br-700ced5fc520  # 模拟容器路由

# 抓包诊断
sudo tcpdump -ni Meta icmp               # 监控 TUN 设备上的 ICMP
sudo tcpdump -ni lo port 46569           # 监控被 redirect 到透明代理端口的流量

# 日志
sudo journalctl -u mihomo --since "1 min ago"  # Mihomo 日志

验证与自查

  1. ip route get 1.1.1.1 显示本机流量走表 2022(dev Meta);ip route get 1.1.1.1 from 172.18.0.2 iif br-700ced5fc520 可模拟容器流量同样落入 TUN
  2. sudo nft list table inet mihomo 中核心规则的 counter 计数随访问流量增长
  3. sudo tcpdump -ni lo port 46569 能看到被 REDIRECT 到透明代理端口的 TCP 流量
  4. 容器内 curl -v https://1.1.1.1 正常返回,Mihomo 日志出现 [TCP] 172.18.0.2:xxxxx --> ...(源 IP 未被 SNAT)

参考