Skip to content

[Bug] tun模式下,Mac无法获取正确的系统DNS #2544

@NimbuSHh

Description

@NimbuSHh

Verify steps

  • I have read the documentation and understand the meaning of all the configuration items I have written, rather than just piling up seemingly useful options or default values.
  • I have carefully reviewed the documentation and have not resolved the issue.
  • I have searched the Issue Tracker for the issue I want to raise and did not find it.
  • I am a non-Chinese user.
  • I have tested with the latest Alpha branch version, and the issue still persists.
  • I have provided the server and client configuration files and processes that can reproduce the issue locally, rather than a sanitized complex client configuration file.
  • I provided the simplest configuration that can be used to reproduce the errors in my report, rather than relying on remote servers or piling on a lot of unnecessary configurations for reproduction.
  • I have provided complete logs, rather than just the parts I think are useful out of confidence in my own intelligence.
  • I have directly reproduced the error using the Mihomo command-line program, rather than using other tools or scripts.

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)上几乎注定会失败。

核心问题分析

  1. 端口冲突 (Port Conflict):
  • DHCP 协议规定客户端必须监听 UDP 68 端口。
  • 问题: macOS 的内核进程(configd / ipconfig时刻占用着 UDP 68 端口以维护系统的网络连接。
  • 这段代码中的 ListenDHCPClient 试图去绑定这个端口。在没有特殊 Socket 选项(如 SO_REUSEPORT 且需要内核支持)或 Root 权限的情况下,它要么绑定失败,要么绑定成功了也抢不过内核,根本收不到路由器回复的 OFFER 包。
  1. 权限拦截 (Local Network Privacy):
  • 代码执行了 conn.WriteTo(..., net.IPv4bcast, ...),即向 255.255.255.255 发送广播。
  • 问题: macOS 15/16 引入了极其严格的本地网络隐私权限。普通的 App(如 Clash)如果没有经过用户明确授权,发送广播包会被系统直接静默拦截。路由器根本收不到你的请求,自然不会回复。
  1. 逻辑冗余与竞态 (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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions