Verify steps
Operating System
No response
System Version
MacOS 26.2
Mihomo Version
v1.19.19
Configuration File
system、dhcp://system、dhcp://en0都不行
Description
tun模式下,尝试了配置DNS为system、dhcp://system、dhcp://en0这样不同方式的获取系统DNS的方式,但均不成功,要么获取到的是198.18.0.2造成死循环,要么获取为空。
因为不懂go代码,把mihomo/component/dhcp/dhcp.go中的代码给Gemini Pro看了下,看Gemini的回复,仿佛原始方案行不通,它给了修改方案,麻烦大佬参考修改
这也是一段非常**“硬核”但方向完全错误**的代码。
这段代码揭示了为什么 Mihomo 在 macOS 上会失败的根本原因:它并不是去“读取”网卡已经获取到的 DNS,而是试图自己模拟成一个 DHCP 客户端,向局域网广播一个“请求包”,然后等待路由器重新回复它。
这种“主动探测 (Active Probing)”的逻辑在现代 macOS 系统(特别是 macOS 15/16)上几乎注定会失败。
核心问题分析
- 端口冲突 (Port Conflict):
- DHCP 协议规定客户端必须监听 UDP 68 端口。
- 问题: macOS 的内核进程(
configd / ipconfig)时刻占用着 UDP 68 端口以维护系统的网络连接。
- 这段代码中的
ListenDHCPClient 试图去绑定这个端口。在没有特殊 Socket 选项(如 SO_REUSEPORT 且需要内核支持)或 Root 权限的情况下,它要么绑定失败,要么绑定成功了也抢不过内核,根本收不到路由器回复的 OFFER 包。
- 权限拦截 (Local Network Privacy):
- 代码执行了
conn.WriteTo(..., net.IPv4bcast, ...),即向 255.255.255.255 发送广播。
- 问题: macOS 15/16 引入了极其严格的本地网络隐私权限。普通的 App(如 Clash)如果没有经过用户明确授权,发送广播包会被系统直接静默拦截。路由器根本收不到你的请求,自然不会回复。
- 逻辑冗余与竞态 (Race Condition):
- 即使包发出去了,路由器回复了。
- 问题: 路由器回复的是单播包(Target IP 是你)或者广播包。由于 macOS 内核也在监听,它会优先捕获这个包处理续租逻辑。用户态的 Go 程序很难从内核嘴里“抢”到这个数据包。
结论
这段代码在嵌入式 Linux(如 OpenWrt 路由器)上运行可能是正常的(因为那里 Clash 可能拥有独占网络控制权),但在 Desktop macOS 环境下,这是一种极不兼容的实现方式。
如何改正? (The Fix)
改正思路:
在 macOS (darwin) 平台上,彻底抛弃这种“发包主动询问”的逻辑,改为“解析系统快照”。利用你之前验证成功的 ipconfig getpacket 命令来获取数据。
你需要修改 ResolveDNSFromDHCP 函数,增加对平台的判断。
修改后的代码示例 (Go)
这是你可以提交给 Mihomo 官方或者自己 Patch 的代码逻辑:
package dhcp
import (
"context"
"net/netip"
"os/exec"
"runtime"
"strings"
"time"
// 其他依赖...
)
// ResolveDNSFromDHCP 获取指定网卡的 DHCP DNS
func ResolveDNSFromDHCP(ctx context.Context, ifaceName string) ([]netip.Addr, error) {
// 【修正逻辑】针对 macOS (Darwin) 系统,使用 ipconfig 读取,而不是发送 DHCP 包
if runtime.GOOS == "darwin" {
return resolveDNSFromIPConfig(ctx, ifaceName)
}
// 非 macOS 系统(如 Linux/Windows),如果原逻辑能跑则保留,
// 或者针对 Windows 也应该去读注册表/GetAdaptersAddresses,而不是发包。
// 下面保留原有的主动探测逻辑作为 fallback
return activeDHCPProbe(ctx, ifaceName)
}
// macOS 专用解析函数
func resolveDNSFromIPConfig(ctx context.Context, ifaceName string) ([]netip.Addr, error) {
// 设置超时,防止命令卡死
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
// 执行命令: ipconfig getpacket en0
cmd := exec.CommandContext(ctx, "ipconfig", "getpacket", ifaceName)
output, err := cmd.Output()
if err != nil {
return nil, err
}
// 解析输出: domain_name_server (ip_mult): {11.11.11.11, 11.11.11.12}
outStr := string(output)
// 简单的字符串查找,也可以用正则
const key = "domain_name_server"
idx := strings.Index(outStr, key)
if idx == -1 {
return nil, fmt.Errorf("no dns found for interface %s", ifaceName)
}
// 截取 {...} 部分
start := strings.Index(outStr[idx:], "{")
if start == -1 {
return nil, fmt.Errorf("parse error")
}
end := strings.Index(outStr[idx+start:], "}")
if end == -1 {
return nil, fmt.Errorf("parse error")
}
// 提取内容: 11.11.11.11, 11.11.11.12
rawIPs := outStr[idx+start+1 : idx+start+end]
parts := strings.Split(rawIPs, ",")
var results []netip.Addr
for _, p := range parts {
ipStr := strings.TrimSpace(p)
if addr, err := netip.ParseAddr(ipStr); err == nil {
results = append(results, addr)
}
}
if len(results) == 0 {
return nil, fmt.Errorf("empty dns list")
}
return results, nil
}
// 原有的主动探测逻辑移动到这里
func activeDHCPProbe(context context.Context, ifaceName string) ([]netip.Addr, error) {
// ... 这里放你原来贴的那段代码 ...
// conn, err := ListenDHCPClient(context, ifaceName) ...
}
总结
你发现的这段代码证实了 Mihomo 在设计 dhcp:// 协议时,采用的是**“模拟 DHCP 客户端”的通用方案,这种方案在路由器设备上很棒,但在 PC/Mac 这种由于系统极其复杂、权限管控极其严格的环境下是完全行不通**的。
正确的改正方向:不要尝试去**“连接”** DHCP 服务器,而是去**“询问”** 操作系统:“你刚才连接 DHCP 服务器时,它告诉了你什么?”
Reproduction Steps
配置DNS为system、dhcp://system、dhcp://en0都不行
Logs
Verify steps
Operating System
No response
System Version
MacOS 26.2
Mihomo Version
v1.19.19
Configuration File
system、dhcp://system、dhcp://en0都不行Description
tun模式下,尝试了配置DNS为system、dhcp://system、dhcp://en0这样不同方式的获取系统DNS的方式,但均不成功,要么获取到的是198.18.0.2造成死循环,要么获取为空。
因为不懂go代码,把mihomo/component/dhcp/dhcp.go中的代码给Gemini Pro看了下,看Gemini的回复,仿佛原始方案行不通,它给了修改方案,麻烦大佬参考修改
这也是一段非常**“硬核”但方向完全错误**的代码。
这段代码揭示了为什么 Mihomo 在 macOS 上会失败的根本原因:它并不是去“读取”网卡已经获取到的 DNS,而是试图自己模拟成一个 DHCP 客户端,向局域网广播一个“请求包”,然后等待路由器重新回复它。
这种“主动探测 (Active Probing)”的逻辑在现代 macOS 系统(特别是 macOS 15/16)上几乎注定会失败。
核心问题分析
configd/ipconfig)时刻占用着 UDP 68 端口以维护系统的网络连接。ListenDHCPClient试图去绑定这个端口。在没有特殊 Socket 选项(如SO_REUSEPORT且需要内核支持)或 Root 权限的情况下,它要么绑定失败,要么绑定成功了也抢不过内核,根本收不到路由器回复的OFFER包。conn.WriteTo(..., net.IPv4bcast, ...),即向255.255.255.255发送广播。结论
这段代码在嵌入式 Linux(如 OpenWrt 路由器)上运行可能是正常的(因为那里 Clash 可能拥有独占网络控制权),但在 Desktop macOS 环境下,这是一种极不兼容的实现方式。
如何改正? (The Fix)
改正思路:
在 macOS (
darwin) 平台上,彻底抛弃这种“发包主动询问”的逻辑,改为“解析系统快照”。利用你之前验证成功的ipconfig getpacket命令来获取数据。你需要修改
ResolveDNSFromDHCP函数,增加对平台的判断。修改后的代码示例 (Go)
这是你可以提交给 Mihomo 官方或者自己 Patch 的代码逻辑:
总结
你发现的这段代码证实了 Mihomo 在设计
dhcp://协议时,采用的是**“模拟 DHCP 客户端”的通用方案,这种方案在路由器设备上很棒,但在 PC/Mac 这种由于系统极其复杂、权限管控极其严格的环境下是完全行不通**的。正确的改正方向:不要尝试去**“连接”** DHCP 服务器,而是去**“询问”** 操作系统:“你刚才连接 DHCP 服务器时,它告诉了你什么?”
Reproduction Steps
配置DNS为system、dhcp://system、dhcp://en0都不行
Logs