diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index af7b46c..abbafbb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -57,57 +57,55 @@ jobs: $fv = (Get-Item .\WarpGameBoost.exe).VersionInfo "file version $($fv.FileVersion), product $($fv.ProductName) $($fv.ProductVersion)" if (-not $fv.FileVersion.StartsWith($env:VERSION_NUM)) { throw "file version $($fv.FileVersion)" } - - name: Self-test with IPv4 exit (the runner has no IPv6, so IPv4 entries are allowed) + - name: Self-test -v6only with system DNS (the runner has no IPv6, so IPv4 entries) shell: pwsh run: | - .\WarpGameBoost.exe -selftest -ipv4 -v6only=false -count 80 - if ($LASTEXITCODE -ne 0) { throw "selftest failed" } + .\WarpGameBoost.exe -selftest -v6only -dns system -diag=false -count 80 + if ($LASTEXITCODE -ne 0) { throw "selftest -v6only failed" } Start-Sleep 2 if (Get-NetAdapter -Name WarpGameBoost -ErrorAction SilentlyContinue) { throw "adapter left behind" } - - name: Self-test with defaults (IPv6-only exit; no IPv6 here, so IPv4 entries) + - name: Self-test with defaults (diagnosis, both exits, malware-blocking DNS) shell: pwsh run: | .\WarpGameBoost.exe -selftest -count 80 - if ($LASTEXITCODE -ne 0) { throw "selftest without IPv6 failed" } - - name: Whole-system traffic leaves over IPv6 only, and a killed process leaves no adapter + if ($LASTEXITCODE -ne 0) { throw "selftest failed" } + - name: Whole-system with defaults, and a killed process leaves no adapter shell: pwsh run: | '' | Set-Content empty.txt - $p = Start-Process .\WarpGameBoost.exe -ArgumentList '-ipv4','-count','80' -RedirectStandardInput empty.txt -RedirectStandardOutput out.txt -PassThru - $ok = $false - foreach ($i in 1..30) { - Start-Sleep 3 - $t = try { (Invoke-WebRequest https://www.cloudflare.com/cdn-cgi/trace -UseBasicParsing -TimeoutSec 5).Content } catch { '' } - if ($t -match 'warp=on') { $ok = $true; break } - } - "trace ip: $(($t -split "`n" | Where-Object { $_ -like 'ip=*' }))" - if ($t -notmatch 'ip=[0-9a-f]*:') { Stop-Process -Id $p.Id -Force; throw "exit is not IPv6" } - $c = New-Object Net.Sockets.TcpClient - $sw = [Diagnostics.Stopwatch]::StartNew() - $v4 = try { $c.ConnectAsync('1.1.1.1', 443).Wait(8000) -and $c.Connected } catch { $false } - $c.Dispose() - "IPv4 1.1.1.1:443 reachable: $v4 (answered in $($sw.ElapsedMilliseconds) ms)" - if ($v4) { Stop-Process -Id $p.Id -Force; throw "IPv4 not blocked" } - if ($sw.ElapsedMilliseconds -gt 6000) { Stop-Process -Id $p.Id -Force; throw "IPv4 is dropped, not refused" } - $addrs = [Net.Dns]::GetHostAddresses('www.cloudflare.com') -join ' ' - "getaddrinfo: $addrs" - if ($addrs -notmatch ':') { Stop-Process -Id $p.Id -Force; throw "Windows does not hand out IPv6 addresses" } + $p = Start-Process .\WarpGameBoost.exe -ArgumentList '-diag=false','-count','80' -RedirectStandardInput empty.txt -RedirectStandardOutput out.txt -PassThru + function Fail($m) { Get-Content out.txt -Encoding utf8; Stop-Process -Id $p.Id -Force; throw $m } + foreach ($i in 1..40) { Start-Sleep 2; if ((Get-Content out.txt -Raw -Encoding utf8 -ErrorAction SilentlyContinue) -match '加速已开启|失败') { break } } Get-Content out.txt -Encoding utf8 - Get-NetRoute -DestinationPrefix 0.0.0.0/1, 128.0.0.0/1 -ErrorAction SilentlyContinue | Format-Table -AutoSize - # With -v6only the adapter carries Cloudflare's IPv6 resolver and - # nothing else (fec0:0:0:ffff::1-3 are Windows' placeholders). - $dns = (Get-DnsClientServerAddress -InterfaceAlias WarpGameBoost -ErrorAction SilentlyContinue).ServerAddresses | Where-Object { $_ -notlike 'fec0:0:0:ffff::*' } + $t4 = curl.exe -4 -s -m 10 https://www.cloudflare.com/cdn-cgi/trace + $t6 = curl.exe -6 -s -m 10 https://www.cloudflare.com/cdn-cgi/trace + "IPv4 exit: $($t4 | Select-String 'ip=|warp=')" + "IPv6 exit: $($t6 | Select-String 'ip=|warp=')" + if (-not ($t4 -match 'warp=on')) { Fail "IPv4 exit is not WARP" } + if (-not ($t6 -match 'warp=on')) { Fail "IPv6 exit is not WARP" } + # fec0:0:0:ffff::1-3 are the placeholders Windows shows on IPv6 + # interfaces without DNS servers. + $dns = (Get-DnsClientServerAddress -InterfaceAlias WarpGameBoost).ServerAddresses | Where-Object { $_ -notlike 'fec0:0:0:ffff::*' } "adapter DNS servers: [$($dns -join ', ')]" - if (($dns -join ',') -ne '2606:4700:4700::1111,2606:4700:4700::1001') { Stop-Process -Id $p.Id -Force; throw "unexpected adapter DNS: $dns" } + if (($dns -join ',') -ne '1.1.1.2,1.0.0.2,2606:4700:4700::1112,2606:4700:4700::1002') { Fail "unexpected adapter DNS: $dns" } + # The system resolver (no -Server) must be the malware-blocking one: + # Cloudflare's test name resolves to 0.0.0.0 there and normally elsewhere. Clear-DnsClientCache - $sw = [Diagnostics.Stopwatch]::StartNew() - $resolved = Resolve-DnsName www.blizzard.com -ErrorAction SilentlyContinue | Select-Object -First 1 - "system DNS still resolves: $($resolved.Name) in $($sw.ElapsedMilliseconds) ms" - if (-not $resolved) { Stop-Process -Id $p.Id -Force; throw "name resolution broken" } + $blocked = (Resolve-DnsName malware.testcategory.com -Type A -ErrorAction SilentlyContinue | Where-Object Type -eq 'A').IPAddress -join ',' + $normal = (Resolve-DnsName www.blizzard.com -Type A -ErrorAction SilentlyContinue | Where-Object Type -eq 'A').IPAddress -join ',' + "malware.testcategory.com -> $blocked; www.blizzard.com -> $normal" + if ($blocked -ne '0.0.0.0') { Fail "malware name not blocked" } + if (-not $normal) { Fail "name resolution broken" } + $addrs = [Net.Dns]::GetHostAddresses('www.cloudflare.com') -join ' ' + "getaddrinfo: $addrs" + if ($addrs -notmatch ':') { Fail "Windows does not hand out IPv6 addresses" } Stop-Process -Id $p.Id -Force Start-Sleep 3 - if (-not $ok) { throw "system traffic did not go through WARP" } if (Get-NetAdapter -Name WarpGameBoost -ErrorAction SilentlyContinue) { throw "adapter left behind after kill" } + Clear-DnsClientCache + $after = (Resolve-DnsName malware.testcategory.com -Type A -ErrorAction SilentlyContinue | Where-Object Type -eq 'A').IPAddress -join ',' + "after exit: malware.testcategory.com -> $after" + if ($after -eq '0.0.0.0') { throw "DNS not restored" } $t = (Invoke-WebRequest https://www.cloudflare.com/cdn-cgi/trace -UseBasicParsing -TimeoutSec 10).Content if ($t -notmatch 'warp=off') { throw "network not restored: $t" } "network restored: warp=off" diff --git a/README.md b/README.md index cc1745c..517a404 100644 --- a/README.md +++ b/README.md @@ -2,38 +2,30 @@ **Windows 一键 Cloudflare WARP 游戏加速:自动挑出最稳的入口,游戏的 UDP 流量也走加速,关掉窗口就恢复原网络。** -## 出口仅 IPv6(默认开启) +## 默认模式:阻止恶意软件 DNS + IPv4/IPv6 双栈 -从 v0.5.0 起,默认**只用 IPv6 访问网络,IPv4 全部禁用**(`-v6only`)。入口用 IPv4 还是 IPv6 跟着你的网络走;出口一律是 Cloudflare 的 IPv6 地址。家里 Wi-Fi 只有 IPv4 也一样可以用。 +从 v0.7.0 起,双击运行就是这个模式,不用加任何参数: -| | 默认(`-v6only`) | 加 `-v6only=false` | -| :--- | :--- | :--- | -| 网站看到的地址 | Cloudflare 的 IPv6 | 支持 IPv6 的网站看到 IPv6,其余看到 Cloudflare 的 IPv4 | -| 只有 IPv4 的网站、游戏 | **打不开**(立即被拒,不会卡住) | 正常 | -| DNS | Cloudflare `2606:4700:4700::1111`(经隧道) | 系统设置(仅流量模式) | - -> **玩守望先锋等只有 IPv4 服务器的游戏,必须加 `-v6only=false`**,否则游戏连不上。做法:右键 exe →“创建快捷方式”→ 右键快捷方式 →“属性”→ 在“目标”最后加一个空格和 `-v6only=false`,以后打游戏双击这个快捷方式。 - -为什么 v6only 下 DNS 改用 Cloudflare:只有 IPv4 的网络上,Windows 只在“发出 DNS 查询的网卡有 IPv6”时才给程序 IPv6 地址,否则所有程序都只拿到 IPv4 地址、全部被拒。所以 v6only 给虚拟网卡配上 Cloudflare 的 IPv6 DNS,查询经隧道发出。 +| | 默认 | +| :--- | :--- | +| 入口(连 WARP) | **IPv4 和 IPv6 入口一起优选**,选综合最稳的那个;本机没有 IPv6 时只用 IPv4 入口 | +| 出口(访问网站、游戏) | **IPv4 和 IPv6 都走 WARP**:只有 IPv4 的游戏(如守望先锋)照常能玩,支持 IPv6 的网站走 IPv6 | +| DNS | **Cloudflare 阻止恶意软件**(`1.1.1.2`、`1.0.0.2`、`2606:4700:4700::1112`、`2606:4700:4700::1002`),经隧道查询 | +| 加密设备到 Cloudflare 的连接 | 是 | +| 隐藏网站看到的原始公网 IP | 通常可以 | +| 提供匿名性 / 绕过所有封锁 | 不保证 | -## 仅流量模式(`-v6only=false`) +**阻止恶意软件**:Cloudflare 已知的恶意软件、钓鱼网站的域名会被解析成 `0.0.0.0`,直接打不开;其他网站不受影响,不过滤成人内容和广告。想确认生效,开着程序访问 Cloudflare 的测试地址 `malware.testcategory.com`,应该打不开。 -加 `-v6only=false` 时,本程序工作在**仅流量模式(Traffic Only)**:网络流量经 Cloudflare WARP 加密隧道传输,但不接管 DNS。 +### 其他模式 -| 功能 | 仅流量模式 | +| 参数 | 效果 | | :--- | :--- | -| 通过 Cloudflare WARP 隧道上网 | 是 | -| 加密设备到 Cloudflare 的网络连接 | 是 | -| 隐藏网站看到的原始公网 IP | 通常可以 | -| 使用 Cloudflare 的 DNS 过滤 | 不启用 | -| 改变 DNS 解析行为 | 不改变,保留系统 DNS 设置 | -| 提供匿名性 | 不保证 | -| 绕过所有网络封锁 | 不保证 | +| `-dns system` | **仅流量模式**:虚拟网卡不设 DNS,保持系统设置。路由器 DNS(如 `192.168.1.1`)的查询留在局域网;公网 DNS(如 `8.8.8.8`)的查询经隧道发出 | +| `-ipv4=false` | 只用 IPv6 入口(本机没有 IPv6 时无效,仍用 IPv4) | +| `-v6only` | 出口只走 IPv6,IPv4 全部禁用并立即拒绝。**只有 IPv4 的游戏(如守望先锋)会连不上**,不适合打游戏 | -DNS 怎么走: -- 系统 DNS 是路由器(如 `192.168.1.1`)时,查询留在局域网内,再由路由器交给运营商解析,**不经过隧道**。 -- 系统 DNS 是公网服务器(如 `8.8.8.8`)时,查询经隧道到 Cloudflare 后再发往该服务器。 -- 公司内网域名、路由器自带的本地域名都照常解析。 +想固定用某个模式:右键 exe →“创建快捷方式”→ 右键快捷方式 →“属性”→ 在“目标”最后加一个空格和参数,以后双击这个快捷方式。 ## 解决什么问题 @@ -63,7 +55,7 @@ DNS 怎么走: | → 运营商 | 先 traceroute 找到第一个公网路由器,再 ping 它 | 运营商接入有问题,WARP 也要经过这一段,**帮不上** | | → 暴雪(美东) | 对 `overwatch.blizzard.com`(AWS 弗吉尼亚)做 TLS 握手计时 | 前两段正常而这段不稳 = 运营商到暴雪的线路问题,**这种情况 WARP 可能有帮助** | -用 `-v6only=false` 运行时,连上 WARP 后还会**经 WARP 再测一次暴雪**,和直连并排对比,直接告诉你开加速更稳还是直连更好。 +连上 WARP 后还会**经 WARP 再测一次暴雪**,和直连并排对比,直接告诉你开加速更稳还是直连更好。 说明: - 测的是暴雪美东的网站服务器,不是对局服务器(对局服务器地址不公开),所以延迟数字只代表到暴雪美东的线路,和游戏里显示的不完全一样。 @@ -95,8 +87,9 @@ DNS 怎么走: | 参数 | 作用 | | :--- | :--- | | `-diag=false` | 跳过启动时的网络诊断 | -| `-v6only=false` | 恢复 IPv4 出口(仅流量模式,DNS 用系统设置)。**玩只有 IPv4 的游戏(如守望先锋)时必须加** | -| `-ipv4` | 有 IPv6 时也同时测试 IPv4 入口(默认有 IPv6 就只用 IPv6 入口) | +| `-dns system` | DNS 保持系统设置(仅流量模式);默认 `malware` = Cloudflare 阻止恶意软件 | +| `-ipv4=false` | 只用 IPv6 入口(默认 IPv4、IPv6 入口一起优选) | +| `-v6only` | 出口只走 IPv6,禁用 IPv4(守望先锋会连不上) | | `-count 200` | 每次优选测试的入口数量 | | `-v` | 显示 WireGuard 调试日志(排查连不上时用) | | `-version` | 显示版本号后退出 | @@ -112,18 +105,19 @@ DNS 怎么走: - **整台电脑都走 WARP**:开着时网站和游戏看到的是 Cloudflare 的地址,游戏可能按这个地址判断地区。在美国打美服,出口也在美国,影响不大。 - **Cloudflare 能看到你连了哪些服务器**,看不到加密的内容。 -- **域名解析**:默认(v6only)DNS 经隧道发给 Cloudflare;加 `-v6only=false` 时用系统设置,不加密,运营商或路由器能看到你查询了哪些域名。这个程序是游戏加速,不是隐私工具。 +- **域名解析**:默认经隧道发给 Cloudflare 的阻止恶意软件 DNS,运营商看不到;Cloudflare 能看到你查询了哪些域名。加 `-dns system` 时用系统设置,不加密,运营商或路由器能看到。这个程序是游戏加速,不是隐私工具。 - **不要同时开其他 VPN 或官方 WARP 客户端**,它们会争抢路由。 - 免费账号注册使用 WARP 官方 App 的同一接口(非公开 API),Cloudflare 可能随时更改或限制。 - 部分杀毒软件可能对虚拟网卡驱动或网络程序误报。 ## 工作原理 -1. **优选**:从 Cloudflare WARP 的入口地址段(IPv6 `2606:4700:d0::/48`、`d1::/48`,必要时 IPv4 `162.159.192–195.x`、`188.114.96–99.x`)和 50 多个 UDP 端口中随机抽约 200 个,每个发 5 次 WireGuard 握手,按“平均延迟 + 2×抖动 + 丢包”打分。 +1. **优选**:从 Cloudflare WARP 的入口地址段(IPv6 `2606:4700:d0::/48`、`d1::/48` 和 IPv4 `162.159.192–195.x`、`188.114.96–99.x`)和 50 多个 UDP 端口中随机抽约 200 个,每个发 5 次 WireGuard 握手,按“平均延迟 + 2×抖动 + 丢包”打分。 - 所有探测包用同一把密钥,而 WARP 服务器会丢弃时间戳不比上一次新的握手。程序保证每个探测包的时间戳都比之前的新,否则会把服务器的丢弃误判成入口丢包(v0.2.0 修复前约每 5 次误判 1 次)。 2. **隧道**:用 [wireguard-go](https://git.zx2c4.com/wireguard-go) 和 WireGuard 官方的 [Wintun](https://www.wintun.net/) 虚拟网卡建立隧道,MTU 1280。 3. **路由**:加 `0.0.0.0/1`、`128.0.0.0/1`、`::/1`、`8000::/1` 四条路由,盖过默认路由但不修改它;隧道自己的 UDP 包绑定在实体网卡上发出。关闭虚拟网卡,这些路由随之消失。 -4. **出口仅 IPv6**(默认):IPv4 仍然全部进虚拟网卡(不会绕过隧道直连),但在交给 WireGuard 之前就被拦下:TCP 回一个 RST、其他回 ICMP 不可达,程序立刻知道“连不上”并改用 IPv6,而不是干等超时。系统里原本的 IPv4 DNS 服务器例外放行。虚拟网卡另加一条 `::/0` 路由,让没有 IPv6 的网络上 Windows 也把 IPv6 当作可用。 +4. **DNS**:虚拟网卡的接口优先级设为最高(metric 0)并配上 Cloudflare 阻止恶意软件的 DNS,Windows 优先问它,查询经隧道发出。程序退出、虚拟网卡删除后,DNS 自动回到系统原来的设置。 +5. **出口仅 IPv6**(`-v6only`):IPv4 仍然全部进虚拟网卡(不会绕过隧道直连),但在交给 WireGuard 之前就被拦下:TCP 回一个 RST、其他回 ICMP 不可达,程序立刻知道“连不上”并改用 IPv6,而不是干等超时。系统里原本的 IPv4 DNS 服务器例外放行。虚拟网卡另加一条 `::/0` 路由,让没有 IPv6 的网络上 Windows 也把 IPv6 当作可用。 ## 文件 @@ -159,6 +153,7 @@ go build -trimpath -ldflags "-s -w" -o WarpGameBoost.exe . 版本号显示在窗口标题和第一行,也可以右键 exe →“属性”→“详细信息”查看。 +- **v0.7.0**:默认改为“阻止恶意软件 DNS(Cloudflare `1.1.1.2`)+ IPv4/IPv6 入口一起优选 + IPv4/IPv6 双出口”,守望先锋不用再加参数。`-v6only` 改为可选(默认关);原来的仅流量模式用 `-dns system`。 - **v0.6.0**:新增网络诊断(默认开启,`-diag=false` 跳过):启动时测“电脑→路由器”“→运营商”“→暴雪美东”三段的延迟、抖动、丢包,指出问题在哪一段并给出做法;`-v6only=false` 时连上后再经 WARP 测一次暴雪,与直连对比,告诉你开不开加速更好。 - **v0.5.0**:新增出口仅 IPv6(`-v6only`,默认开启):IPv4 全部禁用且立即拒绝,DNS 改用经隧道的 Cloudflare IPv6 解析;只有 IPv4 的游戏(如守望先锋)要加 `-v6only=false`。修复:本机没有 IPv6 时,以前经隧道的 IPv6 实际用不上(Windows 只给程序 IPv4 地址),现在可以正常使用。 - **v0.4.0**:改为仅流量模式:虚拟网卡不再设置 Cloudflare DNS,DNS 保持系统设置。 diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 90988f1..ea8e59e 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,8 +1,8 @@ -新增**网络诊断**(默认开启,加 `-diag=false` 跳过)。 +默认模式改为:**阻止恶意软件 DNS + IPv4/IPv6 入口一起优选 + IPv4/IPv6 双出口**。双击运行即可,守望先锋不用再加参数。 -启动时先花约 20 秒测三段的延迟、抖动、丢包:电脑→路由器、→运营商(traceroute 找到的第一个公网路由器)、→暴雪美东(`overwatch.blizzard.com` 的 TLS 握手计时)。然后告诉你问题在哪一段: -- 家里 Wi-Fi/路由器不稳:WARP 帮不上,给出插网线、5GHz、关定位、关网卡节能、开 SQM 等做法; -- 运营商接入不稳:WARP 也帮不上; -- 运营商到暴雪的线路不稳:WARP 可能有帮助。 +- **DNS**:Cloudflare 阻止恶意软件(`1.1.1.2`、`1.0.0.2`、`2606:4700:4700::1112`、`2606:4700:4700::1002`),经隧道查询。已知的恶意软件、钓鱼域名解析为 `0.0.0.0`。程序退出后 DNS 回到系统设置。 +- **入口**:IPv4 和 IPv6 入口一起优选(本机没有 IPv6 时只用 IPv4)。 +- **出口**:IPv4 和 IPv6 都经 WARP,启动后显示两个出口地址。 +- 连上后经 WARP 再测一次暴雪,与直连对比。 -用 `-v6only=false` 运行(守望先锋需要)时,连上 WARP 后会**经 WARP 再测一次暴雪**,与直连并排对比,直接告诉你开加速更稳还是直连更好。 +旧模式仍可用:`-dns system`(仅流量,DNS 保持系统设置)、`-v6only`(出口仅 IPv6,守望先锋会连不上)、`-ipv4=false`(只用 IPv6 入口)。 diff --git a/diag.go b/diag.go index 3386867..a62d21b 100644 --- a/diag.go +++ b/diag.go @@ -105,7 +105,7 @@ func verdict(gw, isp, target *series, wifi bool) []string { case bad(target, false): return []string{ "问题在运营商到暴雪的线路:家里和运营商接入都正常,往后才不稳。", - " · 这种情况 WARP 加速可能有帮助:守望先锋只有 IPv4,请用 -v6only=false 运行,开和不开各打几局对比", + " · 这种情况 WARP 加速可能有帮助:开和不开各打几局,用游戏里的网络图(Ctrl+Shift+N)对比", } case !measured(target): return []string{"家里网络和运营商接入正常;暴雪服务器没测到,无法判断后面的线路。"} @@ -113,7 +113,7 @@ func verdict(gw, isp, target *series, wifi bool) []string { return []string{ "三段都正常,网络本身没有明显问题。", " · 游戏里仍然卡,可能是游戏服务器或本机(后台下载、同步、杀毒扫描)", - " · 不一定需要加速;想试试 WARP,守望先锋请用 -v6only=false", + " · 不一定需要加速;下面会经 WARP 再测一次,对比后再决定", } } } diff --git a/main_windows.go b/main_windows.go index bff4d80..098d250 100644 --- a/main_windows.go +++ b/main_windows.go @@ -41,13 +41,14 @@ const adapterName = "WarpGameBoost" var version = "dev" var ( - allowV4 = flag.Bool("ipv4", false, "也使用 IPv4 入口(默认只用 IPv6 入口)") + allowV4 = flag.Bool("ipv4", true, "同时优选 IPv4 入口(默认开启;-ipv4=false 只用 IPv6 入口)") count = flag.Int("count", 200, "每次优选测试的入口数量") selftest = flag.Bool("selftest", false, "连接、验证后立即退出(用于自动测试)") verbose = flag.Bool("v", false, "显示 WireGuard 调试日志") showVer = flag.Bool("version", false, "显示版本号后退出") diag = flag.Bool("diag", true, "启动时先诊断网络(路由器/运营商/暴雪三段)") - v6only = flag.Bool("v6only", true, "出口只走 IPv6,禁用 IPv4;只有 IPv4 的游戏(如守望先锋)要加 -v6only=false") + v6only = flag.Bool("v6only", false, "出口只走 IPv6,禁用 IPv4(只有 IPv4 的游戏,如守望先锋,会连不上)") + dnsMode = flag.String("dns", "malware", "DNS:malware = Cloudflare 阻止恶意软件(1.1.1.2);system = 保持系统设置(仅流量)") ) // physIndex is the interface the tunnel's own UDP packets leave through; probes @@ -115,11 +116,15 @@ func run() int { saveAccount(acctPath, acct) } + if *dnsMode != "malware" && *dnsMode != "system" { + fmt.Println("-dns 只能是 malware 或 system") + return 1 + } physIndex6 = defaultInterface(winipcfg.AddressFamily(windows.AF_INET6), 0) - if physIndex6 == 0 && !*allowV4 { + if physIndex6 == 0 { // Without IPv6 the only way in is IPv4; say so and carry on rather than // make the user restart with a flag. - fmt.Println("本机没有 IPv6 网络,改用 IPv4 入口。") + fmt.Println("本机没有 IPv6 网络,只用 IPv4 入口。") fmt.Println() *allowV4 = true } @@ -166,11 +171,23 @@ func run() int { if *verbose { fmt.Println(time.Now().Format("15:04:05.000"), "开始验证") } - trace := warpTrace() + // Verify over the exit that must work: IPv6 with -v6only, else IPv4 (the + // game's). IPv6 is then checked once more, but its absence only warns. + primary := "tcp4" + if *v6only { + primary = "tcp6" + } + trace := warpTrace(primary, 3) if !strings.Contains(trace, "warp=on") { fmt.Println("隧道已建立,但验证失败:", strings.TrimSpace(trace)) return 1 } + exit4, exit6 := traceField(trace, "ip"), "" + if *v6only { + exit4, exit6 = "", exit4 + } else if t6 := warpTrace("tcp6", 2); strings.Contains(t6, "warp=on") { + exit6 = traceField(t6, "ip") + } if *v6only { if _, ok := tcpPing("1.1.1.1:443"); ok { fmt.Println("IPv4 仍然可以访问,禁用失败。") @@ -182,11 +199,22 @@ func run() int { } } fmt.Printf("\n✅ 加速已开启:本机全部网络(包括游戏)经 WARP 入口 %s\n", t.endpoint()) - if *v6only { - fmt.Printf(" 出口仅 IPv6(%s),IPv4 已禁用,DNS 经隧道用 Cloudflare。\n", traceField(trace, "ip")) - fmt.Println(" 只有 IPv4 的网站和游戏会连不上;玩守望先锋请加参数 -v6only=false。") + switch { + case *v6only: + fmt.Printf(" 出口:仅 IPv6 %s(IPv4 已禁用,只有 IPv4 的网站和游戏会连不上)\n", exit6) + case exit6 == "": + fmt.Printf(" 出口:IPv4 %s;IPv6 没有通,只能访问 IPv4\n", exit4) + default: + fmt.Printf(" 出口:IPv4 %s,IPv6 %s\n", exit4, exit6) + } + if *dnsMode == "malware" { + fmt.Println(" DNS:Cloudflare 阻止恶意软件(1.1.1.2),经隧道查询") + } else if *v6only { + fmt.Println(" DNS:Cloudflare(2606:4700:4700::1111),经隧道查询") } else { - fmt.Println(" 仅流量模式:DNS 保持系统设置。") + fmt.Println(" DNS:保持系统设置(仅流量)") + } + if !*v6only { fmt.Println() compareWarp(blizzard) } @@ -333,19 +361,26 @@ func (t *tunnel) configure(acct *Account) error { iface.Set() } } - // Windows only returns IPv6 addresses for a name when the interface the - // query went out on has IPv6. On an IPv4-only network that is never the - // real adapter, so with -v6only the tunnel gets Cloudflare's resolver at - // its IPv6 addresses, asked through the tunnel. - if *v6only { - if err := t.luid.SetDNS(windows.AF_INET6, v6DNS, nil); err != nil { + // With -dns malware the adapter gets Cloudflare's malware-blocking + // resolver; the adapter's metric of 0 makes Windows ask it first, and the + // queries travel through the tunnel. With -dns system it gets none and + // Windows keeps its own settings (traffic only): a resolver on the local + // network stays local, one on the internet is reached through the tunnel. + // + // -v6only always needs a resolver on the adapter, IPv6 only: Windows hands + // out IPv6 addresses for a name only when the interface the query went out + // on has IPv6, which on an IPv4-only network is never the real adapter. + v4dns, v6dns := tunnelDNS() + if len(v4dns) > 0 { + if err := t.luid.SetDNS(windows.AF_INET, v4dns, nil); err != nil { + return fmt.Errorf("设置 DNS:%w", err) + } + } + if len(v6dns) > 0 { + if err := t.luid.SetDNS(windows.AF_INET6, v6dns, nil); err != nil { return fmt.Errorf("设置 DNS:%w", err) } } - // Traffic only: the adapter gets no DNS servers, so Windows keeps resolving - // with the system's own DNS settings. Queries to a resolver on the local - // network stay local (its on-link route is more specific than the halves - // below); queries to a resolver on the internet travel through the tunnel. routes := []*winipcfg.RouteData{ {Destination: netip.MustParsePrefix("0.0.0.0/1"), NextHop: netip.IPv4Unspecified()}, {Destination: netip.MustParsePrefix("128.0.0.0/1"), NextHop: netip.IPv4Unspecified()}, @@ -506,7 +541,26 @@ func (t *tunnel) reoptimize(acct *Account, acctPath string) { saveAccount(acctPath, acct) } -var v6DNS = []netip.Addr{netip.MustParseAddr("2606:4700:4700::1111"), netip.MustParseAddr("2606:4700:4700::1001")} +// Cloudflare's resolvers: 1.1.1.2 and friends refuse names Cloudflare knows to +// serve malware; 1.1.1.1 and friends answer everything. +var ( + malwareDNS4 = []netip.Addr{netip.MustParseAddr("1.1.1.2"), netip.MustParseAddr("1.0.0.2")} + malwareDNS6 = []netip.Addr{netip.MustParseAddr("2606:4700:4700::1112"), netip.MustParseAddr("2606:4700:4700::1002")} + plainDNS6 = []netip.Addr{netip.MustParseAddr("2606:4700:4700::1111"), netip.MustParseAddr("2606:4700:4700::1001")} +) + +// tunnelDNS is what the tunnel adapter's DNS servers should be. +func tunnelDNS() (v4, v6 []netip.Addr) { + switch { + case *dnsMode == "malware" && *v6only: + return nil, malwareDNS6 + case *dnsMode == "malware": + return malwareDNS4, malwareDNS6 + case *v6only: + return nil, plainDNS6 + } + return nil, nil +} func pingTarget() string { if *v6only { @@ -563,19 +617,17 @@ func tcpPing(addr string) (time.Duration, bool) { return time.Since(start), true } -func warpTrace() string { - client := &http.Client{Timeout: 10 * time.Second} - if *v6only { - // Go's resolver may hand back only IPv4 addresses here; ask for IPv6. - d := &net.Dialer{} - client.Transport = &http.Transport{ - DialContext: func(ctx context.Context, _, addr string) (net.Conn, error) { - return d.DialContext(ctx, "tcp6", addr) - }, - ForceAttemptHTTP2: true, - } - } - for i := 0; i < 3; i++ { +// warpTrace asks Cloudflare how this connection looks from outside, over +// "tcp4" or "tcp6" (Go's resolver may otherwise hand back one family only). +func warpTrace(network string, tries int) string { + d := &net.Dialer{} + client := &http.Client{Timeout: 10 * time.Second, Transport: &http.Transport{ + DialContext: func(ctx context.Context, _, addr string) (net.Conn, error) { + return d.DialContext(ctx, network, addr) + }, + ForceAttemptHTTP2: true, + }} + for i := 0; i < tries; i++ { resp, err := client.Get("https://www.cloudflare.com/cdn-cgi/trace") if err == nil { b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))