Mihomo 流量路径分析报告

基于本机(Arch Linux, Mihomo Clash.Meta 内核)实际配置,分析三类流量在 nftables + ip rule + TUN 三层拦截机制下的完整路径。


1. 环境概况

1.1 系统信息

项目
OSArch Linux
内核Linux 6.x
Mihomo 版本Clash.Meta 内核
运行命令/opt/mihomo/bin/mihomo -d /opt/mihomo/etc
PID1057
运行方式systemd service(enabled)
配置文件/opt/mihomo/etc/config.yaml

1.2 Mihomo 监听端口

端口类型绑定地址用途
10810mixed (HTTP+SOCKS)127.0.0.1通用代理入站
11000socks5127.0.0.1socks5-in-1
11001mixed127.0.0.1direct 入站(强制 DIRECT)
9090HTTP127.0.0.1external-controller (API)
46569TCP0.0.0.0透明代理(nftables redirect 目标)

1.3 Docker 容器

容器网络IP端口映射
axonhubaxonhub_default (bridge)172.18.0.2127.0.0.1:8090 → 8090/tcp

Docker 网络设备:

  • br-700ced5fc520(axonhub_default 桥接):172.18.0.1/16
  • docker0(默认桥接):172.17.0.1/16(当前 linkdown)

1.4 物理网络

接口IP用途
wlp0s20f3192.168.1.5/24Wi-Fi 物理网卡
Meta198.18.0.1/30Mihomo TUN 虚拟网卡(fake-ip 范围)

2. 核心配置

2.1 Mihomo TUN 配置

tun:
  enable: true
  stack: mixed
  dns-hijack:
    - "any:53"
    - "tcp://any:53"
  auto-route: true
  strict-route: true
  auto-redirect: true
  auto-detect-interface: true

dns:
  enable: true
  enhanced-mode: fake-ip
  fake-ip-filter:
    - geosite:cn
    - geosite:category-ads-all
```text

### 2.2 TUN 模式详解

`auto-route: true` + `strict-route: true` 使 Mihomo 在启动时自动添加:
- **ip rule**(策略路由规则)
- **nftables**(`table inet mihomo`,prerouting + output 链)

两层拦截覆盖不同的协议和路径。

---

## 3. 拦截机制详解

### 3.1 nftables 规则集

```nft
table inet mihomo {
    set inet4_local_address_set {
        type ipv4_addr
        flags interval
        elements = { 127.0.0.0/8, 198.18.0.0/30 }
    }

    chain output {
        type nat hook output priority mangle; policy accept;
        oifname "Meta" meta nfproto ipv4 meta l4proto tcp
            counter redirect to :46569 return
    }

    chain prerouting {
        type nat hook prerouting priority dstnat + 1; policy accept;

        # 从 TUN 回来的包跳过(防循环)
        iifname "Meta" counter return

        # 本地 fake-ip 源发出的 DNS 查询 → DNAT 到 TUN 设备
        ip saddr @inet4_local_address_set meta l4proto { tcp, udp } th dport 53
            counter dnat ip to 198.18.0.2

        # 目标为本地地址的包跳过
        ip daddr @inet4_local_address_set counter return

        # MPTCP 选项丢弃
        tcp option mptcp exists counter drop

        # IPv6 拒绝
        meta nfproto ipv6 counter reject with icmpv6 no-route

        # ★ 核心规则:所有 IPv4 TCP → redirect 到透明代理端口 :46569
        meta nfproto ipv4 meta l4proto tcp
            counter redirect to :46569 return
    }
}
```text

### 3.2 Docker 相关 nftables 表

```nft
# 由 iptables-nft 管理,非 Mihomo 创建

table ip nat {
    chain PREROUTING { type nat hook prerouting priority dstnat; ... jump DOCKER }
    chain OUTPUT     { type nat hook output priority dstnat; ... jump DOCKER }
    chain POSTROUTING {
        type nat hook postrouting priority srcnat; policy accept;
        ip saddr 172.17.0.0/16 oifname != "docker0"   MASQUERADE
        ip saddr 172.18.0.0/16 oifname != "br-700ced5fc520" MASQUERADE
    }
}

table ip filter {
    chain FORWARD { type filter hook forward priority filter; policy drop;
        jump DOCKER-USER
        jump DOCKER-FORWARD
    }
    chain DOCKER-FORWARD {
        jump DOCKER-CT
        jump DOCKER-INTERNAL
        jump DOCKER-BRIDGE
        iifname "br-700ced5fc520" accept
        iifname "docker0" accept
    }
}

table ip raw {
    chain PREROUTING {
        type filter hook prerouting priority raw; policy accept;
        ip daddr 172.18.0.2 iifname != "br-700ced5fc520" drop
        ip daddr 127.0.0.1 iifname != "lo" tcp dport 8090 drop
    }
}
```text

### 3.3 ip rule(策略路由)

PRI 规则 → 路由表 ──────────────────────────────────────────────────────────────── 0 from all lookup local → local 表 9000 from all to 198.18.0.0/30 lookup 2022 → 表 2022(fake-IP 回包) 9001 not from all dport 53 lookup main suppress 0 → main(DNS 绕过) 9001 from all iif Meta goto 9010 → 跳过(TUN 回包防循环) 9002 ★ not from all iif lo lookup 2022 → 表 2022(非 lo → TUN) 9002 from 0.0.0.0 iif lo lookup 2022 → 表 2022(lo → TUN) 9002 from 198.18.0.0/30 iif lo lookup 2022 → 表 2022(fake-IP → TUN) 32766 from all lookup main → main 表 32767 from all lookup default → default 表


### 3.4 路由表 2022

default via 198.18.0.2 dev Meta


### 3.5 路由表 main(摘要)

default via 192.168.1.1 dev wlp0s20f3 proto dhcp 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


---

## 4. 流量路径分析

### 4.1 本机程序发出的 TCP 流量

**场景:** `firefox` 等本地进程访问 `https://www.google.com:443`(IP: 142.250.80.68)

本机 socket(src=192.168.1.5, dst=142.250.80.68:443) │ ▼ 路由决策(ip rule 逐条检查) ├── rule 0(local): 目标不是本地地址 → 不匹配 ├── rule 9000(fake-IP): 目标不在 198.18.0.0/30 → 不匹配 ├── rule 9001(DNS, port 53): 不匹配 ├── rule 9001(iif Meta): iif ≠ Meta → 不匹配 ├── rule 9002(not iif lo): 本机出站,iif=lo → 不匹配 └── rule 9002(from 0.0.0.0 iif lo): ★ 匹配! → 查路由表 2022 → default via 198.18.0.2 dev Meta → 此时 oifname = “Meta” │ ▼ nftables OUTPUT hook(type nat hook output priority mangle) │ ├── 其他规则不匹配 └── ★ oifname “Meta” meta nfproto ipv4 meta l4proto tcp → redirect to :46569 → 目标改为 127.0.0.1:46569,源维持 192.168.1.5 │ ▼ 路由决策(第二次) → 目标 127.0.0.1 → 本地投递 → 经 INPUT → Mihomo socket (:46569) │ ▼ Mihomo 核心引擎 → [TCP] 192.168.1.5:xxxxx –> 142.250.80.68:443 match GeoSite(geolocation-!cn) using SELECT[🇺🇸US-DMIT] → 经代理节点(VLESS XTLS)发出


**关键点:**
- 路由决策决定了 oif = Meta,但包并未实际到达 TUN 设备
- OUTPUT hook 在路由决策之后执行,利用了已知的 `oifname`
- redirect 到 `:46569` 后重新路由为本地投递

---

### 4.2 本机程序发出的 ICMP/UDP 流量

**场景:** `ping 1.1.1.1`(ICMP echo)

本机 socket(src=192.168.1.5, dst=1.1.1.1, ICMP) │ ▼ 路由决策 ├── rule 9002(from 0.0.0.0 iif lo) → ★ 匹配 └── table 2022 → default via 198.18.0.2 dev Meta │ ▼ nftables OUTPUT hook └── oifname “Meta” … tcp → 不匹配(ICMP ≠ tcp) → 放行 │ ▼ 到达 TUN 设备 Meta │ ▼ Mihomo TUN 栈读取原始 ICMP 包 │ ├── ICMP 不在代理核心引擎处理范围 │ (VLESS/XTLS 隧道无法承载 ICMP) │ └── TUN 栈执行 NAT: src = 192.168.1.5 → 198.18.0.1(TUN 接口地址) → 写回 TUN 设备 │ ▼ ip rule(再次,此时 iif=Meta) ├── rule 9001(iif Meta goto 9010) → ★ 匹配 └── 跳转到 9010(nop) │ ▼ 查找 main 路由表 └── default via 192.168.1.1 dev wlp0s20f3 │ ▼ 物理网卡发出(src=198.18.0.1 → 主机再次 SNAT 为 192.168.1.5)


**关键点:**
- ICMP 和 UDP 不触发 nftables OUTPUT 的 `tcp redirect`,实际到达 TUN 设备
- TUN 栈直接 NAT 转发,不经过核心引擎的规则匹配
- 日志中表现为 `write ICMPv4 echo request` / `read ICMPv4 echo reply`,无 `[TCP]` 条目

---

### 4.3 Docker 容器发出的 TCP 流量

**场景:** `docker exec axonhub wget http://1.1.1.1`

容器 axonhub(172.18.0.2:xxxxx → 1.1.1.1:80, TCP SYN) │ ▼ veth pair → br-700ced5fc520 │ iif = br-700ced5fc520 ▼ ip raw PREROUTING(priority raw) └── 172.18.0.2 是容器地址,检查通过 │ ▼ nftables prerouting(priority dstnat + 1) │ ├── iifname “Meta” return ❌ iif=br-700ced5fc520 ├── dport 53 dnat ❌ 非 DNS ├── daddr local set return ❌ 1.1.1.1 不在本地 ├── tcp mptcp drop ❌ 无 MPTCP ├── ipv6 reject ❌ IPv4 └── ★ meta nfproto ipv4 meta l4proto tcp → redirect to :46569 → 目标改为 127.0.0.1:46569 → 源保持 172.18.0.2(容器原始 IP) │ ▼ 路由决策(因目标变为 127.0.0.1 → 本地投递) └── 经 INPUT → Mihomo socket (:46569) │ ▼ Mihomo 核心引擎 ├── 从 socket 读取包,识别 src=172.18.0.2 │ ├── 容器 DNS(前置步骤): │ 容器 → 127.0.0.11(Docker 内置 DNS) │ → 转发到 192.168.1.1:53(宿主机 DNS) │ → 包经 br-700ced5fc520 → prerouting │ → iif=br-700ced5fc520, src=172.18.0.2 │ → 不匹配 dport 53 DNAT 规则(源不在 local set 内) │ → tcp redirect 匹配 → DNS (TCP 53) 也被 redirect │ → Mihomo dns-hijack 劫持 → fake-ip 响应 │ └── 日志输出: [TCP] 172.18.0.2:56690 –> 1.1.1.1:443 match Match using SELECT[🇺🇸US-DMIT] │ ▼ 经代理节点发出 └── 实际物理网卡上看到的是: 192.168.1.5:xxxxx → 154.17.224.88:50025(代理节点)


**关键点:**
- **容器源 IP(172.18.0.2)完整保留**,未经 SNAT
- 原因:`redirect` 在 `prerouting`(路由决策前)发生,将目标改为 `127.0.0.1`
- 重新路由判定为本地投递,不经过 `POSTROUTING`(Docker SNAT 在 POSTROUTING)
- `ip rule 9002` 也**不参与**,因为包在 nftables 阶段已被截获

**实测验证:**

```bash
# nftables prerouting 计数器
# 容器 wget 前:counter packets 29 bytes 1740
# 容器 wget 后:counter packets 32 bytes 1920  ← 增量 3(SYN+请求+ACK)

# Mihomo 日志
[TCP] 172.18.0.2:56690 --> 1.1.1.1:443 match Match using SELECT[🇺🇸US-DMIT]
                           ↑ 容器原始 IP,未 SNAT
```text

---

### 4.4 Docker 容器发出的 ICMP/UDP 流量

**场景:** `docker exec axonhub ping 1.1.1.1`

容器 axonhub(172.18.0.2 → 1.1.1.1, ICMP echo) │ ▼ br-700ced5fc520(iif = br-700ced5fc520) │ ▼ nftables prerouting └── meta l4proto tcp redirect → 不匹配(ICMP ≠ tcp) → 放行 │ ▼ ip rule ├── rule 0(local): 1.1.1.1 不在本地 → 不匹配 ├── rule 9000(fake-IP): 不匹配 ├── rule 9001(port 53): 不匹配 ├── rule 9001(iif Meta): iif=br-700ced5fc520 → 不匹配 └── ★ rule 9002(not iif lo): iif=br-700ced5fc520 ≠ lo → 匹配 → table 2022 → default via 198.18.0.2 dev Meta │ ▼ TUN 设备 Meta │ ▼ Mihomo TUN 栈 └── ICMP 不进核心引擎 → TUN 栈 NAT:src = 172.18.0.2 → 198.18.0.1 → 写回 TUN 设备 │ ▼ ip rule(iif=Meta) └── 9001(iif Meta goto 9010)→ 跳过 │ ▼ main 路由表 └── default via 192.168.1.1 dev wlp0s20f3 │ ▼ 物理网卡发出(src=198.18.0.1 → SNAT 为 192.168.1.5)


**实测验证:**

```bash
# 容器 ping 1.1.1.1 → 192ms(经代理转发的典型延迟)
# 容器 ping 192.168.1.1 → 1.2ms(直连延迟)

# Mihomo debug 日志
write ICMPv4 echo request from 198.18.0.1 to 1.1.1.1 id 19022 seq 166
read  ICMPv4 echo reply   from 1.1.1.1 to 198.18.0.1 id 19022 seq 166
# 注意 src 是 198.18.0.1(TUN 接口),不是 172.18.0.2(容器源 IP)
# 且无 [TCP] 规则匹配记录
```text

**TCP vs ICMP 路径差异总结:**

| 维度 | 容器 TCP | 容器 ICMP/UDP |
|------|---------|--------------|
| 拦截点 | nftables prerouting redirect :46569 | ip rule 9002 → TUN |
| 源 IP | 172.18.0.2(原始,未变) | 198.18.0.1(TUN 栈 NAT) |
| 经过核心引擎 | ✅ 是(规则匹配、代理选择) | ❌ 否(TUN 栈直接转发) |
| 规则引擎日志 | `[TCP] 172.18.0.2:xxx → ... match ...` | 无 `[TCP]` 日志 |

---

### 4.5 外部入站流量

**场景:** 外部机器 `203.0.113.5` 访问本机 `192.168.1.5`

#### 4.5.1 外部 TCP 入站(理论分析)

外部 → wlp0s20f3(iif=wlp0s20f3) │ ▼ nftables prerouting └── ★ meta l4proto tcp redirect to :46569 → 匹配! → 目标改为 127.0.0.1:46569 → 源保持 203.0.113.5 │ ▼ 路由决策:本地投递 → INPUT → Mihomo socket │ ▼ Mihomo 核心引擎 └── 将此 TCP 连接视为透明代理请求处理


**问题:** 所有外部入站 TCP(如 SSH、HTTP 服务等)会被强制吸入 Mihomo 透明代理端口,可能导致:
- 外部 SSH 到本机无法正常建立
- 需在 nftables 中添加豁免规则

**验证盲区:** 本机 SSH `192.168.1.5` 实际走 OUTPUT hook(`oif=lo`),不经过 PREROUTING,无法验证。需另一台物理机或虚拟机从外部发起连接。

#### 4.5.2 外部 ICMP/UDP 入站(经实验验证)

外部 → wlp0s20f3(iif=wlp0s20f3) │ ▼ nftables prerouting └── tcp redirect → 不匹配(ICMP/UDP ≠ tcp)→ 放行 │ ▼ ip rule ├── ★ rule 0(from all lookup local) │ → 目标 192.168.1.5 在本地(local 表有对应路由) │ → 最高优先级匹配 → 本地投递 │ ├── rule 9000/9001/9002 → 永远不会检查到 │ (因为 rule 0 已匹配) │ ▼ 正常交付给本地对应 UDP socket / ICMP handler


**实测验证:**

```bash
# 从本机 ping 自己的外部 IP
ping 192.168.1.5

# 在 Meta 设备上 tcpdump 监听
sudo tcpdump -ni Meta icmp  # → 没有抓到包
```text

得出结论:外部入站 ICMP/UDP **不被吸入 TUN**,因为 `ip rule 0`(`lookup local`)优先级最高,目的地为本机的包直接被 local 表捕获,正常交付。

> **注意:** 此实验使用本机 ping 自己的外部 IP。严格来说数据包在本机内部经由路由/lo 处理,与真正物理网卡上外部流量仍有差异。但基于 rule 0 的优先级分析,结论应是可靠的。

---

## 5. 关键机制总结表

| 协议 | 流量来源 | 拦截点 | 规则/链 | 是否进核心引擎 | 源 IP 变化 |
|------|---------|--------|---------|--------------|-----------|
| TCP | 本机 | OUTPUT hook | `oif=Meta tcp redirect :46569` | ✅ | 保持(192.168.1.5) |
| TCP | 容器 | PREROUTING hook | `meta l4proto tcp redirect :46569` | ✅ | 保持(172.18.0.2) |
| TCP | 外部入站 | PREROUTING hook | `meta l4proto tcp redirect :46569` | ✅(有风险) | 保持(外部 IP) |
| ICMP/UDP | 本机 | ip rule 9002 | `not iif lo → table 2022 → Meta` | ❌ | 改为 198.18.0.1 |
| ICMP/UDP | 容器 | ip rule 9002 | `not iif lo → table 2022 → Meta` | ❌ | 改为 198.18.0.1 |
| ICMP/UDP | 外部入站 | ip rule 0(local) | `from all lookup local` | ❌ | 不变(正常交付) |

### 拦截优先级链

ip rule 0(local) → 最高优先级,目的为本机的包被拦截 ↓ nftables prerouting(redirect) → TCP 在此处被截获改写目标 ↓ ip rule 9000/9001/9002 → ICMP/UDP 在此处被转向 TUN ↓ nftables output(redirect) → 本机 TCP 在路由决策后 oif=Meta 时被截


---

## 6. TUN 栈与核心引擎的区分

Mihomo 内部有两个不同的处理层:

| 层 | 处理对象 | 行为 |
|----|---------|------|
| **TUN 栈** | 从 TUN 设备读取的原始 IP 包 | 对 TCP 送核心引擎;对 ICMP/UDP 做 NAT 后直接转发 |
| **核心引擎** | 从 socket 收到的流(代理端口 / 透明代理端口) | 规则匹配 → 代理选择 → 协议封装 → 经代理节点发出 |

日志中可清晰区分:
- `[TCP]` 开头 → 核心引擎处理
- `[TUN]`、`write/read ICMPv4` 开头 → TUN 栈处理

---

## 7. 盲点与待验证项

### 7.1 外部 TCP 入站测试

**待验证:** 从另一台物理机/虚拟机 SSH 到本机 `192.168.1.5:22`,观察是否被 `prerouting redirect :46569` 拦截破坏。

**修复方案(如需):**

```bash
nft add rule inet mihomo prerouting ip daddr 192.168.1.5 tcp dport {22, 9090} return
```text

### 7.2 可能的 `bypass` 规则

当前 Mihomo 规则中有 `GEOIP,lan,DIRECT,no-resolve`,但这条规则在**核心引擎**内生效,不阻止 TCP 被 redirect 截获。入站 TCP 在 nftables 阶段就被拦截,核心引擎的规则无法阻止这一过程。

### 7.3 宿主机/容器出站延迟差异

容器 TCP 和本机 TCP 都经核心引擎代理,但路径不同:
- 本机:OUTPUT redirect → socket
- 容器:PREROUTING redirect → socket

两者理论上延迟一致,可通过实际测试验证是否存在差异。

---

## 验证与自查

1. `ip route get 1.1.1.1` 与 `ip route get 1.1.1.1 from 172.18.0.2 iif br-700ced5fc520` 分别模拟本机/容器流量,均落入表 2022(dev Meta)
2. `sudo nft list chain inet mihomo prerouting | grep "redirect to :46569"` 观察计数器:容器一次 wget 后增量约为 3(SYN+请求+ACK)
3. 容器 `ping 1.1.1.1` 延迟约 192ms(经代理转发),`ping 192.168.1.1` 约 1.2ms(直连),对比确认转发路径
4. Mihomo debug 日志可区分两层处理:`[TCP]` 开头为核心引擎,`write/read ICMPv4` 开头为 TUN 栈 NAT 转发

## 附录

### A. 常用诊断命令

```bash
# 查看 nftables 规则集
sudo nft list ruleset

# 查看策略路由规则
ip rule show

# 查看所有路由表
ip route show table all

# 查看特定路由表
ip route show table 2022
ip route show table main

# 模拟路由决策(查看包会走哪个路由表)
ip route get 1.1.1.1 from 172.18.0.2 iif br-700ced5fc520

# 查看 nftables 计数器
sudo nft list chain inet mihomo prerouting | grep "redirect to :46569"

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

# 抓包
sudo tcpdump -ni Meta icmp
sudo tcpdump -ni lo port 46569
sudo tcpdump -ni wlp0s20f3 'tcp[tcpflags] & (tcp-syn) != 0 and tcp[tcpflags] & (tcp-ack) == 0'

# conntrack 连接跟踪(需 root)
sudo conntrack -E -p tcp --dport 46569
```text

### B. 相关文件路径

| 文件 | 路径 |
|------|------|
| Mihomo 配置文件 | `/opt/mihomo/etc/config.yaml` |
| Mihomo 二进制 | `/opt/mihomo/bin/mihomo` |
| Systemd unit | `/etc/systemd/system/mihomo.service` |
| 旧配置备份 | `/etc/mihomo/config.yaml` |

### C. 对话中关键日志输出

容器 TCP → 核心引擎

[TCP] 172.18.0.2:56690 –> 1.1.1.1:443 match Match using SELECT[🇺🇸US-DMIT] [TCP] 172.18.0.2:46184 –> 1.1.1.1:80 match Match using SELECT[🇺🇸US-DMIT]

容器 ICMP → TUN 栈 NAT 转发

write ICMPv4 echo request from 198.18.0.1 to 1.1.1.1 id 19022 seq 166 read ICMPv4 echo reply from 1.1.1.1 to 198.18.0.1 id 19022 seq 166

本机 TCP → 核心引擎 + 代理节点

192.168.1.5:57870 > 154.17.224.88:50025: Flags [S] # 代理节点出站

nftables prerouting redirect 计数器增量

29 → 32(一次容器 wget 请求)


## 参考

- [Mihomo 官方文档](https://wiki.metacubex.one/)
- [Mihomo GitHub 仓库](https://github.com/MetaCubeX/mihomo)
- [nftables 官方 wiki](https://wiki.nftables.org/)
- [Arch Wiki: nftables](https://wiki.archlinux.org/title/Nftables)
- [ip-rule 手册页](https://man7.org/linux/man-pages/man8/ip-rule.8.html)