Skip to content

refine winroute#6478

Open
LjhAUMEM wants to merge 14 commits into
XTLS:mainfrom
LjhAUMEM:refine-winroute
Open

refine winroute#6478
LjhAUMEM wants to merge 14 commits into
XTLS:mainfrom
LjhAUMEM:refine-winroute

Conversation

@LjhAUMEM

Copy link
Copy Markdown
Collaborator

closes #6454

  • 处理 ERROR_NOT_FOUND 与重试,CreateAdapter 返回的 luid 可能因为 ip interface 还没准备好导致不可用,添加重试逻辑
  • findOutboundInterface 改为寻找除 tun iface 外的全局默认路由,同时添加 Route Change 回调
  • 添加 wfp 规则,默认启用以下五条
    • 允许自身进程
    • 允许 loopback
    • 允许 tun iface
    • 黑洞 53 udp
    • 黑洞 53 tcp

@LjhAUMEM

Copy link
Copy Markdown
Collaborator Author

wfp 应该在路由查找之前,这个 pr 需要测试

@LjhAUMEM

Copy link
Copy Markdown
Collaborator Author

似乎没有原生的 gui 能查看完整的 wfp 规则,只能 netsh wfp show filters 自己翻 xml

@LjhAUMEM

Copy link
Copy Markdown
Collaborator Author

翻了下 fwpmu.h 准备补 layer ippacket,但是发现能用的 condition 有点少,那还是算了,ale layer 阻止多宿主解析应该够用了

@LjhAUMEM

LjhAUMEM commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator Author

应该只能这样了

其他进程发起系统查询要正常 xray 需要配置 dns

FwpV4AddrAndMask 需要 host order,FwpV6AddrAndMask 则不需要,晚点我再检查下,应该没什么要改的了

@serkanarisoy

Copy link
Copy Markdown

@Fangliding

Copy link
Copy Markdown
Member

这东西重启后保留吗 别给人整不带xray重启下系统网就断了 之前tun动防火墙也是怕这些

@LjhAUMEM

Copy link
Copy Markdown
Collaborator Author

FWPM_SESSION_FLAG_DYNAMIC,创建的是 dynamic session,会在进程退出后清理 filter,即使没主动清理

@RPRX

RPRX commented Jul 16, 2026

Copy link
Copy Markdown
Member

现在系统 DNS 都能设置 DoH 了,WFP block 掉 53 怕是不能彻底解决问题

TUN 的 DNS 泄露只是系统层面的而非应用层面的(应用指定 DNS 地址的话会被 TUN 劫持),你测过我说的那两个方法了吗

@LjhAUMEM

Copy link
Copy Markdown
Collaborator Author

发起doh就不算泄露了吧,哪个方法,修改默认网卡dns?如果是改tun网卡dns应该和普通设置8.8.8.8没区别,系统解析不会因为某个网卡设置了内网地址就改变行为

@LjhAUMEM

Copy link
Copy Markdown
Collaborator Author

真要测的话内容还有点多,有点懒,有六种情况,分别是开关多宿主下,默认路由的tun网卡 未配置dns,配置有效dns,配置内网dns 三种情况下是否泄露

其实就是对于win系统解析行为的猜测,测试是假设系统解析会用默认路由网卡的dns进行多宿主查询,但也没测试的必要,即使可以成功,那么要实现不泄露tun就得与dns模块绑定一起,想要简单一个tun in一个out做到不泄露就不行

网卡配置dns搭配开启wfp可以完美覆盖上面的六种测试情况,配置也可以做到一个简单tun in+一个proxy out

@RPRX

RPRX commented Jul 18, 2026

Copy link
Copy Markdown
Member

比如 Win11 能在 WLAN 详情里开 DoH,这种的话 block 53 就没用

“多宿主解析”就直接关掉吧,不然我说的那两个方法应该也还是会漏,默认它是关的情况下你再测

@LjhAUMEM

LjhAUMEM commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

前置准备:禁用多宿主,清空 http(s)_proxy env

image

配置 8.8.8.8 作为 dns,有一半

Snipaste_2026-07-18_23-11-01

配置 gateway ipv4 作为 dns,全是

Snipaste_2026-07-18_23-12-00

从 v2n 搬的绕过大陆配置

@LjhAUMEM

Copy link
Copy Markdown
Collaborator Author

wlan 那个设置我也找了一下,默认 dhcp 分配的吧,先把 53 解决了 853 以后再说

@RPRX

RPRX commented Jul 18, 2026

Copy link
Copy Markdown
Member

@LjhAUMEM 禁用“多宿主”后先重启一下电脑,以及你所说的“配置 dns”设置的是哪里?系统的还是 TUN?若假设为系统的:

配置 gateway ipv4 作为 dns,全是

配置了这个之后还要把 UDP/53 导到 DNS 出站,也有可能 Win 不知道这个 IP 该往哪发所以 fallback 到内置 DNS IP 了?

总之怪怪的,我记得系统 DNS 无效的话会“没网”来着,另外你试试设 Win DNS 为 127.0.0.1 然后 Xray 开个 Tunnel 入站来接收

@LjhAUMEM

Copy link
Copy Markdown
Collaborator Author

上述步骤加入重启后对于设置 gateway 作为 dns 结果还是一样

系统的还是 TUN?

tun 的,设置里其实也是动以太网那张网卡,动默认网卡感觉就没意思了

下面是配置

Details

{
  "log": {
    "loglevel": "warning"
  },
  "dns": {
    "hosts": {
      "dns.google": [
        "8.8.8.8",
        "8.8.4.4",
        "2001:4860:4860::8888",
        "2001:4860:4860::8844"
      ],
      "dns.alidns.com": [
        "223.5.5.5",
        "223.6.6.6",
        "2400:3200::1",
        "2400:3200:baba::1"
      ],
      "one.one.one.one": [
        "1.1.1.1",
        "1.0.0.1",
        "2606:4700:4700::1111",
        "2606:4700:4700::1001"
      ],
      "1dot1dot1dot1.cloudflare-dns.com": [
        "1.1.1.1",
        "1.0.0.1",
        "2606:4700:4700::1111",
        "2606:4700:4700::1001"
      ],
      "cloudflare-dns.com": [
        "104.16.249.249",
        "104.16.248.249",
        "2606:4700::6810:f8f9",
        "2606:4700::6810:f9f9"
      ],
      "dns.cloudflare.com": [
        "104.16.132.229",
        "104.16.133.229",
        "2606:4700::6810:84e5",
        "2606:4700::6810:85e5"
      ],
      "dot.pub": [
        "1.12.12.12",
        "120.53.53.53"
      ],
      "doh.pub": [
        "1.12.12.12",
        "120.53.53.53"
      ],
      "dns.quad9.net": [
        "9.9.9.9",
        "149.112.112.112",
        "2620:fe::fe",
        "2620:fe::9"
      ],
      "dns.yandex.net": [
        "77.88.8.8",
        "77.88.8.1",
        "2a02:6b8::feed:0ff",
        "2a02:6b8:0:1::feed:0ff"
      ],
      "dns.sb": [
        "185.222.222.222",
        "2a09::"
      ],
      "dns.umbrella.com": [
        "208.67.220.220",
        "208.67.222.222",
        "2620:119:35::35",
        "2620:119:53::53"
      ],
      "dns.sse.cisco.com": [
        "208.67.220.220",
        "208.67.222.222",
        "2620:119:35::35",
        "2620:119:53::53"
      ],
      "engage.cloudflareclient.com": [
        "162.159.192.1"
      ]
    },
    "servers": [
      {
        "address": "119.29.29.29",
        "domains": [
          "domain:alidns.com",
          "domain:doh.pub",
          "domain:dot.pub",
          "domain:360.cn",
          "domain:onedns.net"
        ],
        "skipFallback": true,
        "tag": "direct-dns-1"
      },
      {
        "address": "https://cloudflare-dns.com/dns-query",
        "domains": [
          "geosite:google"
        ],
        "skipFallback": true
      },
      {
        "address": "119.29.29.29",
        "domains": [
          "geosite:private",
          "geosite:cn"
        ],
        "skipFallback": true,
        "tag": "direct-dns-2"
      },
      {
        "address": "119.29.29.29",
        "domains": [
          "full:cloudflare-dns.com"
        ],
        "skipFallback": true
      },
      "https://cloudflare-dns.com/dns-query"
    ],
    "tag": "dns-module"
  },
  "inbounds": [
    {
      "tag": "tun",
      "protocol": "tun",
      "sniffing": {
        "enabled": false,
        "destOverride": [
          "http",
          "tls"
        ],
        "routeOnly": false
      },
      "settings": {
        "name": "xray_tun",
        "MTU": 9000,
        "gateway": [
          "172.18.0.1/30"
        ],
        "autoSystemRoutingTable": [
          "0.0.0.0/0"
          ,"::/0"
        ],
        "dns": [
          "172.18.0.1"
        ],
        "autoOutboundsInterface": "auto"
      }
    }
  ],
  "outbounds": [
    {
      "tag": "proxy",

    },
    {
      "tag": "direct",
      "protocol": "freedom"
    },
    {
      "tag": "block",
      "protocol": "blackhole"
    },
    {
      "tag": "dns",
      "protocol": "dns"
    }
  ],
  "routing": {
    "domainStrategy": "AsIs",
    "rules": [
      {
        "type": "field",
        "inboundTag": [
          "api"
        ],
        "outboundTag": "api"
      },
      {
        "port": "135,137-139,5353",
        "network": "udp",
        "outboundTag": "block"
      },
      {
        "outboundTag": "block",
        "ip": [
          "224.0.0.0/3",
          "ff00::/8"
        ]
      },
      {
        "port": "53",
        "outboundTag": "dns",
        "process": [
          "v2ray.exe",
          "mihomo-windows-amd64-v1.exe",
          "mihomo-windows-amd64-compatible.exe",
          "mihomo-windows-amd64.exe",
          "mihomo-windows-arm64.exe",
          "clash.exe",
          "mihomo.exe",
          "hysteria.exe",
          "naive.exe",
          "naiveproxy.exe",
          "tuic-client.exe",
          "tuic.exe",
          "sing-box-client.exe",
          "sing-box.exe",
          "juicity-client.exe",
          "juicity.exe",
          "hysteria-windows-amd64.exe",
          "hysteria-linux-amd64.exe",
          "brook_windows_amd64.exe",
          "brook_linux_amd64.exe",
          "brook.exe",
          "overtls-bin.exe",
          "overtls.exe",
          "shadowquic.exe",
          "mieru.exe"
        ]
      },
      {
        "outboundTag": "direct",
        "process": [
          "v2ray.exe",
          "xray.exe",
          "mihomo-windows-amd64-v1.exe",
          "mihomo-windows-amd64-compatible.exe",
          "mihomo-windows-amd64.exe",
          "mihomo-windows-arm64.exe",
          "clash.exe",
          "mihomo.exe",
          "hysteria.exe",
          "naive.exe",
          "naiveproxy.exe",
          "tuic-client.exe",
          "tuic.exe",
          "sing-box-client.exe",
          "sing-box.exe",
          "juicity-client.exe",
          "juicity.exe",
          "hysteria-windows-amd64.exe",
          "hysteria-linux-amd64.exe",
          "brook_windows_amd64.exe",
          "brook_linux_amd64.exe",
          "brook.exe",
          "overtls-bin.exe",
          "overtls.exe",
          "shadowquic.exe",
          "mieru.exe",
          "xray/",
          "self/"
        ]
      },
      {
        "port": "53",
        "outboundTag": "dns"
      },
      {
        "type": "field",
        "port": "443",
        "network": "udp",
        "outboundTag": "block"
      },
      {
        "type": "field",
        "outboundTag": "proxy",
        "domain": [
          "geosite:google"
        ]
      },
      {
        "type": "field",
        "outboundTag": "direct",
        "ip": [
          "geoip:private"
        ]
      },
      {
        "type": "field",
        "outboundTag": "direct",
        "domain": [
          "geosite:private"
        ]
      },
      {
        "type": "field",
        "outboundTag": "direct",
        "ip": [
          "223.5.5.5",
          "223.6.6.6",
          "2400:3200::1",
          "2400:3200:baba::1",
          "119.29.29.29",
          "1.12.12.12",
          "120.53.53.53",
          "2402:4e00::",
          "2402:4e00:1::",
          "180.76.76.76",
          "2400:da00::6666",
          "114.114.114.114",
          "114.114.115.115",
          "114.114.114.119",
          "114.114.115.119",
          "114.114.114.110",
          "114.114.115.110",
          "180.184.1.1",
          "180.184.2.2",
          "101.226.4.6",
          "218.30.118.6",
          "123.125.81.6",
          "140.207.198.6",
          "1.2.4.8",
          "210.2.4.8",
          "52.80.66.66",
          "117.50.22.22",
          "2400:7fc0:849e:200::4",
          "2404:c2c0:85d8:901::4",
          "117.50.10.10",
          "52.80.52.52",
          "2400:7fc0:849e:200::8",
          "2404:c2c0:85d8:901::8",
          "117.50.60.30",
          "52.80.60.30"
        ]
      },
      {
        "type": "field",
        "outboundTag": "direct",
        "domain": [
          "domain:alidns.com",
          "domain:doh.pub",
          "domain:dot.pub",
          "domain:360.cn",
          "domain:onedns.net"
        ]
      },
      {
        "type": "field",
        "outboundTag": "direct",
        "ip": [
          "geoip:cn"
        ]
      },
      {
        "type": "field",
        "outboundTag": "direct",
        "domain": [
          "geosite:cn"
        ]
      },
      {
        "type": "field",
        "inboundTag": [
          "direct-dns-1",
          "direct-dns-2"
        ],
        "outboundTag": "direct"
      },
      {
        "type": "field",
        "inboundTag": [
          "dns-module"
        ],
        "outboundTag": "proxy"
      }
    ]
  }
}

@LjhAUMEM

Copy link
Copy Markdown
Collaborator Author

才发现我没有测试未配置 dns 的情况,未配置 dns 如下

image

反而配置成 gateway 后全是

@LjhAUMEM

Copy link
Copy Markdown
Collaborator Author

update: 将组策略里的禁用改到未配置或者已启用都会出现(不用重启),即使未配置dns

而从未配置/已启用改到已禁用可能有缓存测试还是全是,执行 ipconfig /flushdns + 重启浏览器后则恢复正常

@LjhAUMEM

Copy link
Copy Markdown
Collaborator Author

更新:去掉dns模块去掉route只留一个proxy out

未配置dns 全是
8.8.8.8 一半
gateway 全是

Details

{
  "log": {
    "loglevel": "warning"
  },
  "inbounds": [
    {
      "tag": "tun",
      "protocol": "tun",
      "settings": {
        "name": "xray_tun",
        "MTU": 9000,
        "gateway": [
          "172.18.0.1/30"
        ],
        "autoSystemRoutingTable": [
          "0.0.0.0/0"
          ,"::/0"
        ],
        // "dns": [
        //   // "8.8.8.8"
        //   "172.18.0.1"
        // ],
        "autoOutboundsInterface": "auto"
      }
    }
  ],
  "outbounds": [
    {
      "tag": "proxy",

    }
  ]
}

@LjhAUMEM

Copy link
Copy Markdown
Collaborator Author

简单来说,现在的防漏依赖于 禁用多宿主,未配置dns,dns模块 缺一不可,且不治本,在查询失败下仍可能fallback到其他网卡,这解释了为什么单in out+未配置dns也会泄露的情况

而且目前单in out想要不泄露只能wfp

@RPRX

RPRX commented Jul 20, 2026

Copy link
Copy Markdown
Member

我说的当然是改系统(默认网卡?)DNS,你只改 TUN 的 dns 有啥用,系统要是全遵循 Xray TUN 那么一开始就不会有泄露了

@LjhAUMEM

Copy link
Copy Markdown
Collaborator Author

禁用多宿主,未配置dns,dns模块 缺一不可

所以继续往三条件上堆条件有意义吗

改默认网卡还有个问题,1. 后续如何恢复,2. 如果有其他网卡也设置了dns是否还会泄露

假设把所有网卡dns都设置成了tun gateway成功不漏了,那么就是四条件,其中两个还是要手动设置的

@RPRX

RPRX commented Jul 20, 2026

Copy link
Copy Markdown
Member

主要是不太想把 WFP 弄进来,此外就像我说的,如果系统 DNS 配置成 DoH,你那规则也拦不住吧,且不是 853 而是 443

还有并不是堆条件,比如如果自动把所有网卡的 DNS 全改了,似乎也不需要“禁用多宿主,未配置 dns”了,只需要 dns 模块

除非台式机瞬间断电,其它情况 Xray-core 都能收到“退出”信号,可以自动恢复,还可以加个“修复 DNS”的脚本打包进 release

@RPRX

RPRX commented Jul 20, 2026

Copy link
Copy Markdown
Member

总之你先测测“改所有 DNS”,我说的两种都测测,不过都别的网卡了可能只能 127 了,改成 gateway 可能没用

另外“任务管理器”直接杀掉 Xray 进程可能也会导致它收不到“退出”信号,这个也没测过

@LjhAUMEM

Copy link
Copy Markdown
Collaborator Author

改其他网卡dns这侵入比wfp动态session还大,我不支持这种,我也不会pr这种...

所以是放弃单in out的防漏了吗,不是很想配置dns模块,至于改网卡dns的修复谁pr谁测吧...

@Kc2353

Kc2353 commented Jul 20, 2026

Copy link
Copy Markdown

不过都别的网卡了可能只能 127 了,改成 gateway 可能没用

呃,修改物理网卡的DNS为tun gateway 其实计算机里的程序能接收到,即使这个地址在这个网卡不可达(至少AdGuardHome可以接收到),见
#6454 (comment)
#6454 (comment)

@LjhAUMEM

Copy link
Copy Markdown
Collaborator Author

不是,还真有人愿意被动默认网卡啊,反正我是不会用

至于 doh 那个问题也简单,改成 blockAll 就行了,就是允许项要增多

@Kc2353

Kc2353 commented Jul 20, 2026

Copy link
Copy Markdown

不是,还真有人愿意被动默认网卡啊,反正我是不会用

CF warp client在Windows的默认做法就是这个 你看人家大厂都在用 反正我对修改系统设置没什么看法,只要明确告知、方便改回去就好
大部分(有一点但不多经验的)小白在未关闭代理软件重启之后的第一做法就是重新开一次代理,关机前要关代理软件差不多是个常识了 (虽然其实直接系统设置关闭代理就行) 所以异常退出之类对小白问题也不太大
(不过可能导致问题不典型(懂一些的人看不出是这个问题),因为代理未正确设置在浏览器的提示很明确,代理服务器未连接,如果这么做乍一看是DNS出问题了不过异常退出之后手动改系统DNS也是能解决问题的)

@LjhAUMEM

LjhAUMEM commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

这位应该是gui用户,不会自己手写json,不过无所谓了

那蹲一个改所有网卡dns的pr,然后这个可以close了,我也想看看那个怎么实现

@Kc2353

Kc2353 commented Jul 21, 2026

Copy link
Copy Markdown

反正我懒,客户端最多用GUI生成默认模板进行修改,不过服务器端或者反向代理之类特殊需求还是手搓

问了AI,得到以下slop,我不会go, 不知道怎么调用这些powershell功能, 同蹲pr:

Details

开发一款需要接管系统 DNS 的网络工具,在 Windows 上修改 DNS 并不只是简单地改个 IP 地址。由于 Windows 网络栈的复杂性, 最容易导致接管失败的坑通常是 IPv6 回退和虚拟网卡干扰

以下我将为你提供从脚本到代码层面的多种实现方案,并在最后附上开发此类工具必须注意的核心避坑指南


方案一:使用 PowerShell(最推荐,易于代码集成)

在现代 Windows (Win 10/11) 开发中,直接在代码中调用 PowerShell 是最稳妥、兼容性最好的方式,因为它能同时完美处理 IPv4 和 IPv6。

1. 修改所有物理网卡(推荐)

# 获取所有状态为Up且非虚拟的网卡,将DNS设置为 127.0.0.1 和 ::1
Get-NetAdapter | Where-Object { $_.Status -eq 'Up' -and $_.Virtual -eq $false } | Set-DnsClientServerAddress -ServerAddresses @("127.0.0.1", "::1")

2. 修改指定网卡

# 通过网卡名称(InterfaceAlias)修改
Set-DnsClientServerAddress -InterfaceAlias "以太网" -ServerAddresses @("127.0.0.1", "::1")

# 或者通过网卡索引(InterfaceIndex)修改
Set-DnsClientServerAddress -InterfaceIndex 12 -ServerAddresses @("127.0.0.1", "::1")

C# / Python 等语言调用示例 (C#)

using System.Management.Automation;

public void SetSystemDnsToLocalhost()
{
    using (PowerShell ps = PowerShell.Create())
    {
        string script = @"
            Get-NetAdapter | Where-Object { $_.Status -eq 'Up' -and $_.Virtual -eq $false } | 
            Set-DnsClientServerAddress -ServerAddresses @('127.0.0.1', '::1');
            Clear-DnsClientCache"; // 顺便清理DNS缓存
        ps.AddScript(script);
        ps.Invoke();
    }
}

方案二:使用 WMI (适合 C++ / C# 底层原生开发)

如果你不想依赖 PowerShell 进程,可以使用 WMI 的 Win32_NetworkAdapterConfiguration 类。
注意:WMI 的 SetDNSServerSearchOrder 方法仅支持 IPv4。如果你的软件必须接管 IPv6,仍需配合 PowerShell 或 Windows 10+ 的 SetInterfaceDnsSettings API。

C# 实现示例

using System.Management;

public void SetDnsViaWmi()
{
    ManagementClass mc = new ManagementClass("Win32_NetworkAdapterConfiguration");
    ManagementObjectCollection moc = mc.GetInstances();
    
    foreach (ManagementObject mo in moc)
    {
        // 仅修改已启用 IP 的网卡
        if ((bool)mo["IPEnabled"]) 
        {
            ManagementBaseObject args = mo.GetMethodParameters("SetDNSServerSearchOrder");
            args["DNSServerSearchOrder"] = new string[] { "127.0.0.1" };
            mo.InvokeMethod("SetDNSServerSearchOrder", args, null);
        }
    }
}

方案三:使用 netsh 命令行(传统方式)

适合通过 CreateProcessShellExecute 直接调用的场景。

1. 查看网卡名称和索引

netsh interface ipv4 show interfaces

2. 修改指定网卡 DNS

:: 设置静态 DNS 为 127.0.0.1
netsh interface ipv4 set dns name="以太网" static 127.0.0.1

:: 如果是 IPv6
netsh interface ipv6 set dns name="以太网" static ::1

3. 修改所有网卡
netsh 没有直接修改“所有”网卡的命令,你需要先用代码遍历网卡名称,然后循环执行上述 set dns 命令。


⚠️ 开发 DNS 接管工具的“核心避坑指南”

作为网络工具开发者,在实现 DNS 接管时,请务必处理以下几个关键问题,否则你的软件在部分用户电脑上会“失效”或“导致断网”:

1. 必须同时处理 IPv6(致命坑)

现代 Windows 默认开启 IPv6。如果你只把 IPv4 的 DNS 改成了 127.0.0.1,当系统遇到 IPv6 请求时,会直接使用路由器下发的 IPv6 DNS,从而绕过你的本地代理。
对策:必须同时将 DNS 设置为 127.0.0.1 (IPv4) 和 ::1 (IPv6),并确保你的本地 DNS 服务同时监听这两个地址。

2. 必须过滤“虚拟网卡”(断网坑)

用户电脑上通常装有 VMware、VirtualBox、Docker、WSL、TAP-Windows (OpenVPN) 等虚拟网卡。如果你用代码无脑修改“所有网卡”的 DNS,会导致这些虚拟网络环境直接瘫痪。
对策:如方案一所示,务必通过 Virtual -eq $false 或判断 PhysicalMediaType 过滤掉虚拟网卡,只修改物理网卡

3. 权限问题 (UAC)

修改系统网络配置属于高权限操作。你的软件在启动或执行修改操作时,必须具有 Administrator (管理员) 权限。如果是 C# 开发,可以在 app.manifest 中配置 requireAdministrator,或者在需要修改时通过 ShellExecute 提权调用一个辅助进程。

4. 状态备份与异常恢复

修改系统 DNS 后,如果软件崩溃或被强制结束,用户的网络将无法解析域名。
对策

  • 在修改前,读取当前网卡的原始 DNS 配置(可通过 Get-DnsClientServerAddress 获取)。
  • 将原始配置保存到本地文件(如 JSON)或注册表中。
  • 软件正常退出时,或者在启动时检测到上次异常退出,自动执行恢复操作。
  • 恢复命令:Set-DnsClientServerAddress -InterfaceAlias "以太网" -ResetServerAddresses (恢复为 DHCP 自动获取)。

5. 清理 DNS 缓存

Windows 有强大的 DNS Client 服务 (dnscache)。修改 DNS 后,之前解析过的域名依然会命中本地缓存,导致你的工具“看起来没有生效”。
对策:修改完 DNS 后,务必执行一次清理缓存操作:Clear-DnsClientCacheipconfig /flushdns

6. DHCP 续约覆盖问题

如果用户的网卡是通过 DHCP 获取 IP 的,当 DHCP 租期到达进行续约时,路由器可能会重新下发 DNS,覆盖你设置的 127.0.0.1
对策:使用 Set-DnsClientServerAddress 设置静态 DNS 时,Windows 会在底层标记该网卡的 DNS 为“手动”,通常能抵抗 DHCP 续约的覆盖。但为了绝对安全,建议在工具运行期间,开启一个后台定时器,每隔几分钟检查一次 DNS 是否被篡改,如果被篡改则重新写回 127.0.0.1

2.DHCP/static

这是一个非常专业且切中要害的问题。在 Windows 网络编程中,“区分 DHCP 自动获取与静态手动指定” 是 DNS 恢复逻辑中最容易翻车的地方。

如果用户原来是 DHCP 自动获取,你恢复时却写入了一个静态 IP,会导致用户网络在 DHCP 租期变化或更换路由器时断网;反之,如果用户原来是手动指定的 8.8.8.8,你恢复时却执行了“重置为自动获取”,就会篡改用户的原始配置。

为了完美解决这个问题,我们需要在接管前精确备份网卡的 DNS 状态模式(DHCP/Static)及具体 IP,并在退出时按原模式恢复

以下是完整的实现逻辑和代码方案:

核心原理:如何准确判断 DNS 是 DHCP 还是 Static?

Windows 对 IPv4 和 IPv6 的 DNS 配置存储机制不同,我们需要分别判断:

  1. IPv4:通过 WMI (Win32_NetworkAdapterConfiguration) 的 DNSServerSearchOrder 属性判断。如果为 $null,说明是 DHCP 自动获取;如果有值,说明是手动静态指定。
  2. IPv6:WMI 对 IPv6 支持不佳,需要读取注册表 HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces\<GUID>\NameServer。如果为空或不存在,说明是 DHCP;如果有值,说明是静态。

完整实现方案 (PowerShell 脚本)

建议将以下逻辑封装为 PowerShell 脚本,你的主程序(无论是 C#、Python 还是 Go)只需在启动和退出时调用这两个脚本即可。

1. 备份脚本 (Backup-Dns.ps1)

在软件启动、准备接管 DNS 之前执行。

# 保存路径,建议放在用户的 AppData 目录下
$backupFile = Join-Path $env:APPDATA "YourAppName\dns_backup.json"
New-Item -ItemType Directory -Force -Path (Split-Path $backupFile) | Out-Null

$backupData = @()

# 获取所有状态为Up的物理网卡
Get-NetAdapter | Where-Object { $_.Status -eq 'Up' -and $_.Virtual -eq $false } | ForEach-Object {
    $alias = $_.Name
    $ifIndex = $_.ifIndex
    $guid = $_.InterfaceGuid
    $mac = $_.MacAddress

    # --- 判断 IPv4 DNS 状态 ---
    $wmiConfig = Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration -Filter "Index = $ifIndex" -ErrorAction SilentlyContinue
    $ipv4Mode = if ($null -eq $wmiConfig.DNSServerSearchOrder) { "DHCP" } else { "Static" }
    $ipv4Servers = if ($ipv4Mode -eq "Static") { @($wmiConfig.DNSServerSearchOrder) } else { @() }

    # --- 判断 IPv6 DNS 状态 ---
    $regPath = "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces\$guid"
    $nameServer = (Get-ItemProperty -Path $regPath -Name "NameServer" -ErrorAction SilentlyContinue).NameServer
    $ipv6Mode = if ([string]::IsNullOrWhiteSpace($nameServer)) { "DHCP" } else { "Static" }
    # IPv6 注册表中多个 DNS 是用逗号分隔的
    $ipv6Servers = if ($ipv6Mode -eq "Static") { @($nameServer -split ',') } else { @() }

    $backupData += @{
        InterfaceAlias = $alias
        InterfaceIndex = $ifIndex
        MacAddress     = $mac
        IPv4Mode       = $ipv4Mode
        IPv4Servers    = $ipv4Servers
        IPv6Mode       = $ipv6Mode
        IPv6Servers    = $ipv6Servers
    }
}

# 序列化为 JSON 保存
$backupData | ConvertTo-Json -Depth 3 | Out-File -FilePath $backupFile -Encoding UTF8
Write-Host "DNS backup saved to $backupFile"

2. 恢复脚本 (Restore-Dns.ps1)

在软件正常退出时执行。

$backupFile = Join-Path $env:APPDATA "YourAppName\dns_backup.json"

if (-not (Test-Path $backupFile)) {
    Write-Host "No DNS backup file found."
    exit
}

$backupData = Get-Content $backupFile | ConvertFrom-Json

foreach ($item in $backupData) {
    # 尝试通过 InterfaceIndex 查找当前网卡(防止用户重命名网卡)
    $currentAdapter = Get-NetAdapter -InterfaceIndex $item.InterfaceIndex -ErrorAction SilentlyContinue
    
    # 如果 Index 找不到(比如重启过或热插拔),通过 MAC 地址找回
    if ($null -eq $currentAdapter) {
        $currentAdapter = Get-NetAdapter | Where-Object { $_.MacAddress -eq $item.MacAddress }
    }

    if ($null -eq $currentAdapter) {
        Write-Warning "Could not find adapter for MAC $($item.MacAddress), skipping."
        continue
    }

    $targetAlias = $currentAdapter.Name

    # --- 恢复 IPv4 ---
    if ($item.IPv4Mode -eq "DHCP") {
        # 恢复为自动获取
        Set-DnsClientServerAddress -InterfaceAlias $targetAlias -AddressFamily IPv4 -ResetServerAddresses
    } else {
        # 恢复为手动指定的 IP
        Set-DnsClientServerAddress -InterfaceAlias $targetAlias -AddressFamily IPv4 -ServerAddresses $item.IPv4Servers
    }

    # --- 恢复 IPv6 ---
    if ($item.IPv6Mode -eq "DHCP") {
        Set-DnsClientServerAddress -InterfaceAlias $targetAlias -AddressFamily IPv6 -ResetServerAddresses
    } else {
        Set-DnsClientServerAddress -InterfaceAlias $targetAlias -AddressFamily IPv6 -ServerAddresses $item.IPv6Servers
    }
    
    Write-Host "Restored DNS for $targetAlias (IPv4: $($item.IPv4Mode), IPv6: $($item.IPv6Mode))"
}

# 恢复后必须清理缓存,否则系统仍会使用旧的 localhost 缓存
Clear-DnsClientCache
Write-Host "DNS restored and cache cleared successfully."

在 C# 中集成此逻辑

如果你使用 C# 开发,可以通过 System.Management.Automation 命名空间直接调用上述脚本,或者将逻辑内联。以下是调用脚本的优雅封装:

using System.Management.Automation;
using System.IO;

public class DnsManager
{
    private readonly string _backupPath;

    public DnsManager()
    {
        string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
        _backupPath = Path.Combine(appData, "YourAppName", "dns_backup.json");
    }

    // 接管前调用
    public void BackupAndTakeover()
    {
        ExecuteScript("Backup-Dns.ps1");
        
        // 执行接管逻辑 (设置为 localhost)
        string takeoverScript = @"
            Get-NetAdapter | Where-Object { $_.Status -eq 'Up' -and $_.Virtual -eq $false } | 
            Set-DnsClientServerAddress -ServerAddresses @('127.0.0.1', '::1');
            Clear-DnsClientCache;
        ";
        ExecuteInlineScript(takeoverScript);
    }

    // 正常退出时调用
    public void Restore()
    {
        if (File.Exists(_backupPath))
        {
            ExecuteScript("Restore-Dns.ps1");
            // 恢复完成后,可选择删除备份文件
            // File.Delete(_backupPath); 
        }
    }

    private void ExecuteScript(string scriptName)
    {
        // 假设脚本与 exe 在同一目录
        string scriptPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, scriptName);
        using (PowerShell ps = PowerShell.Create())
        {
            ps.AddCommand(scriptPath);
            ps.Invoke();
        }
    }

    private void ExecuteInlineScript(string script)
    {
        using (PowerShell ps = PowerShell.Create())
        {
            ps.AddScript(script);
            ps.Invoke();
        }
    }
}

⚠️ 恢复阶段的“核心避坑指南”

在处理“恢复”逻辑时,有几个极易被忽视的边界情况:

1. 网卡标识漂移问题 (Alias vs Index vs MAC)

用户在软件运行期间,可能会重命名网卡,或者拔插 USB 网卡,甚至重启电脑

  • :如果你只备份了 InterfaceAlias(如“以太网”),用户重命名为“网络 2”后,恢复时就会报错找不到网卡。
  • :如上述脚本所示,主键使用 InterfaceIndex,备用键使用 MacAddressInterfaceIndex 在系统重启前通常不变,而 MacAddress 是物理网卡的绝对唯一标识。恢复时先按 Index 找,找不到再按 MAC 找。

2. 多 DNS 服务器的顺序问题

用户手动设置 DNS 时,可能会设置主备两个(例如 223.5.5.5119.29.29.29)。

  • :恢复时如果顺序反了,可能会影响用户的解析速度或绕过某些限制。
  • :WMI 的 DNSServerSearchOrder 和注册表的 NameServer 都是有序的。在备份和恢复时,必须保持数组/列表的原始顺序(上述 PowerShell 脚本已处理此逻辑)。

3. 进程异常崩溃导致备份文件残留

虽然你提到“只考虑正常退出”,但作为健壮的软件,必须考虑异常。

  • 建议:在软件启动时,检查是否存在 dns_backup.json。如果存在,说明上次是异常退出,系统 DNS 可能还是 127.0.0.1。此时应自动执行一次恢复逻辑,然后再进行新的备份和接管。

4. 恢复操作同样需要管理员权限

修改和恢复 DNS 都是高权限操作。确保你的恢复逻辑(无论是独立进程还是内部方法)是在 Administrator 权限下执行的。如果主程序是普通权限,需要通过 ShellExecute (Verb="runas") 提权调用一个专门的 restore.exe 辅助程序来执行恢复。

5. 别忘了清理 DNS 缓存

在恢复脚本的最后,我加上了 Clear-DnsClientCache。这是必须的,因为在你接管期间,系统可能缓存了通过 localhost 解析的 IP。恢复原始 DNS 后,如果不flush缓存,部分域名在几分钟内依然会指向错误的 IP。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tun模式存在dns泄漏

5 participants