在 FreeBSD 上跑 mihomo:TUN 的三条路与八个坑
把一套 Linux 上的 clash/mihomo 管理脚本移植到 FreeBSD,最麻烦的不是 shell 语法(虽然也够麻烦),而是大家默认「Linux 能跑的 TUN,FreeBSD 也能跑」。实际测下来,mihomo 在 FreeBSD 上的两条原生路径都是死的,最后只能换后端。
这篇按实际排查顺序记一遍:先解决虚拟机出网,再试 TUN 的三条路,最后是八个 FreeBSD 特有的适配缺陷。
一、先解决出网:SSH 反向隧道
测试环境是 KVM 里的 FreeBSD 15.1(4C/4G),GitHub 直连不通,但 pkg.freebsd.org 与 gh-proxy.com 可达。安装脚本要从 GitHub 拉 yq、geoip、dashboard,所以先得让 guest 能出网。
做法是用 SSH 反向隧道,把宿主机的代理端口映射进虚拟机:
1 | # 在宿主机执行,把 guest 的 127.0.0.1:17890 转发到宿主机 127.0.0.1:7890 |
guest 内把代理指向 127.0.0.1:17890 即可。这里有个自找的坑:第一版隧道直接监听 guest 的 7890,而 mihomo 的 mixed-port 也是 7890——脚本的端口冲突检测生效,把 mixed-port 自动改成了 7891。看起来”能跑”,但后续所有验证都对着错误的端口。把隧道改到 17890 并重装后,mixed-port 才恢复 7890。
结论:隧道端口不要和被测服务的默认端口重合,否则「自动避让」会悄悄改掉你的配置。
二、TUN 的三条路
方案 A:mihomo 自带 TUN —— 必然失败
第一反应是开 mihomo 的 tun: 配置。结果设备起不来,日志也不明显。查 sing-tun 源码才看到根因:
1 | // tun_other.go |
FreeBSD 直接落进 tun_other.go,返回 os.ErrInvalid。这不是内核不支持 /dev/tun,而是依赖库根本没实现这个平台。所以无论怎么调 mihomo 配置都不会成功。
方案 B:pf rdr + redir —— 后端是坏的
第二条路是 FreeBSD 传统的透明代理:pf 的 rdr 把流量重定向到本地端口,mihomo 用 redir 后端接管。理论上可行,实际也死了。
mihomo 的 tcp_freebsd.go 照搬了 Linux 的 SO_ORIGINAL_DST:
1 | const SO_ORIGINAL_DST = 80 |
这个 socket option 在 FreeBSD 上不存在。实测调用直接返回 errno 42 Protocol not available;更糟的是 handleRedir 解析失败后直接 conn.Close(),没有任何日志——表现就是”连上了但立刻断”,非常难查。
方案 C:tun2socks —— 可行
最后走的是「mihomo 只做 SOCKS 代理,TUN 交给 tun2socks」:
- mihomo 监听 SOCKS(本地回环即可);
tun2socks创建 TUN 设备,把流量转给 mihomo 的 SOCKS 端口;- 路由把默认流量指到
tun0。
实测 tun on 后默认路由从 vtnet0 切到 tun0(198.18.0.1/15),pkg.freebsd.org 与 github.com 都返回 200,UDP 日志能看到 1.1.1.1:53 的 DNS 查询;tun off 后路由恢复、设备销毁。TCP + UDP 都是真 TUN,不需要改 mihomo 内核。
代价:多一个进程、多一层转发,但这是 FreeBSD 上唯一能用的方案。
三、八个 FreeBSD 适配缺陷
脚本从 Linux 搬过来后,按最小权限在 VM 里回归,修了这些:
- autostart 关闭时
service start会被拒绝:FreeBSD 的 rc 系统在rc.conf里服务为NO时不允许start,必须用onestart;stop/restart同理改成onestop/onerestart; - rc.d 启动环境缺
/usr/local/bin:导致 mihomo 与控制器探测都找不到命令,日志报「控制器未在预期时间内可访问」——rc.d 模板里显式设置PATH; clashctl select <组> <节点>非交互调用崩溃:缺proxy_select_direct函数,补上并复用proxy_group_select;- doctor 缺
freebsd-rc分支:诊断脚本的case没有 FreeBSD 分支,直接落到 unknown; - status 后端显示错误:非 root 用户看不到 root 进程,改用 pidfile +
ps -p检测; - Dashboard 白名单漏了
zashboard-release:新版 dashboard 的路径没进白名单,访问 404; - 卸载不清理 root shell rc:system scope 卸载后 root 的
.profile/.cshrc里还留着环境变量块; tun off误删回环路由:排除网段里包含127.0.0.0/8,关闭 TUN 时把回环网段的路由一起删了——关闭逻辑要显式跳过回环。
另外两个不是”缺陷”但必须处理:
- TUN 切换默认路由会断公网 SSH:SSH 对端流量被吸进 TUN,连接立刻断。解法是从
SSH_CONNECTION、netstat、who里识别对端 IP,加一条直连路由,tun off时清理; - bash → sh 全量迁移成本过高:FreeBSD 的 sh 不支持数组、
[[ ]]、(( ))、<()、<<<、${var:1:2},全量迁移涉及 1.6 万行、300+ 语法点。最后决定不迁移,只修缺陷。
四、验证清单
FreeBSD 上验证代理是否真的工作,按这个顺序:
1 | # 1. 服务与端口 |
总结
FreeBSD 上跑 mihomo 的核心结论只有一句:不要假设 Linux 的 TUN 实现可以平移。
sing-tun对 FreeBSD 是显式返回os.ErrInvalid,不是配置问题;- mihomo 的
redir后端照搬了 Linux 的 socket option,在 FreeBSD 上必然失败且无日志; - 唯一可行的是 tun2socks + SOCKS,多一层转发换真 TUN;
- 移植脚本时,rc.d 的
onestart、PATH、pidfile 检测、回环路由这些都是 FreeBSD 特有的窄路径,要逐项过一遍。
最后一条经验:测试环境的网络要先打通再谈功能。这次如果没先把 SSH 反向隧道和端口冲突理清楚,后面所有”TUN 不通”的现象都会指向错误的方向。