From 94039bbe7e96239e407c2183b563da26eab31eea Mon Sep 17 00:00:00 2001 From: Freecode100Year <174155046+Freecode100Year@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:16:09 -0400 Subject: [PATCH 1/3] feat: network diagnosis before connecting, on by default Measures this computer -> router (ICMP), -> the ISP's first public hop (found by traceroute), -> Blizzard (TCP handshakes to Battle.net US) and says which leg is unstable and what helps. With -v6only=false it then times Blizzard again through WARP and compares. Co-Authored-By: Claude Opus 5.5 --- diag.go | 155 ++++++++++++++++++++++++++++++++++++ diag_test.go | 75 ++++++++++++++++++ diag_windows.go | 207 ++++++++++++++++++++++++++++++++++++++++++++++++ main_windows.go | 8 ++ 4 files changed, 445 insertions(+) create mode 100644 diag.go create mode 100644 diag_test.go create mode 100644 diag_windows.go diff --git a/diag.go b/diag.go new file mode 100644 index 0000000..3386867 --- /dev/null +++ b/diag.go @@ -0,0 +1,155 @@ +package main + +import ( + "fmt" + "sort" + "strings" + "time" +) + +// series holds one diagnostic target's samples: a reply time per answered +// probe, and how many probes were sent. +type series struct { + name string + rtts []time.Duration + sent int +} + +type stats struct { + avg, jitter, max time.Duration + loss float64 // 0..1 + spikes int // replies more than 50 ms above the median +} + +func (s *series) stats() stats { + var st stats + if s.sent == 0 { + return st + } + st.loss = 1 - float64(len(s.rtts))/float64(s.sent) + if len(s.rtts) == 0 { + return st + } + var sum, dsum time.Duration + for i, r := range s.rtts { + sum += r + st.max = max(st.max, r) + if i > 0 { + d := r - s.rtts[i-1] + if d < 0 { + d = -d + } + dsum += d + } + } + st.avg = sum / time.Duration(len(s.rtts)) + if len(s.rtts) > 1 { + st.jitter = dsum / time.Duration(len(s.rtts)-1) + } + sorted := append([]time.Duration(nil), s.rtts...) + sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] }) + median := sorted[len(sorted)/2] + for _, r := range s.rtts { + if r > median+50*time.Millisecond { + st.spikes++ + } + } + return st +} + +// unstable reports whether a hop shows the kind of trouble a game feels: lost +// packets, jitter, or repeated spikes. Routers answer pings at low priority, +// so the intermediate ISP hop gets a looser bar than the endpoints. +func (st stats) unstable(lenient bool) bool { + lossBar, jitterBar := 0.03, 8*time.Millisecond + if lenient { + lossBar, jitterBar = 0.10, 15*time.Millisecond + } + return st.loss >= lossBar || st.jitter > jitterBar || st.spikes >= 2 +} + +func (st stats) String() string { + return fmt.Sprintf("延迟 %3d ms 抖动 %2d ms 最高 %3d ms 丢包 %2.0f%%", + st.avg.Milliseconds(), st.jitter.Milliseconds(), st.max.Milliseconds(), st.loss*100) +} + +// verdict turns the three hops into advice. gw is the router, isp the first +// public hop, target a Blizzard server; any may be nil when it could not be +// found or measured. +func verdict(gw, isp, target *series, wifi bool) []string { + // A hop that never answered is unmeasured, not broken: plenty of + // routers ignore pings altogether. + measured := func(s *series) bool { return s != nil && len(s.rtts) > 0 } + bad := func(s *series, lenient bool) bool { return measured(s) && s.stats().unstable(lenient) } + link := "网线/路由器" + if wifi { + link = "Wi-Fi" + } + switch { + case bad(gw, false): + lines := []string{"问题在家里:电脑到路由器这一段就不稳(" + link + ")。WARP 加速帮不上,先解决这一段:"} + if wifi { + lines = append(lines, + " · 能插网线就插网线,效果最明显;不能的话连 5GHz、靠近路由器", + " · Windows 设置 → 隐私和安全性 → 定位,关闭(定位会让网卡定期扫描 Wi-Fi,造成延迟尖峰)", + " · 设备管理器 → 无线网卡 → 电源管理,取消“允许计算机关闭此设备以节约电源”") + } else { + lines = append(lines, " · 换一根网线或换个路由器口试试;重启路由器") + } + return append(lines, " · 家里有人在下载、看视频、传文件时最明显:路由器有 SQM/QoS 就打开") + case bad(target, false) && bad(isp, true): + return []string{ + "问题在运营商接入:路由器正常,但出了家门到运营商的第一段就不稳。", + " · WARP 加速的流量也要先经过这一段,帮不上;可以打电话让运营商查线路,或换运营商", + } + case bad(target, false): + return []string{ + "问题在运营商到暴雪的线路:家里和运营商接入都正常,往后才不稳。", + " · 这种情况 WARP 加速可能有帮助:守望先锋只有 IPv4,请用 -v6only=false 运行,开和不开各打几局对比", + } + case !measured(target): + return []string{"家里网络和运营商接入正常;暴雪服务器没测到,无法判断后面的线路。"} + default: + return []string{ + "三段都正常,网络本身没有明显问题。", + " · 游戏里仍然卡,可能是游戏服务器或本机(后台下载、同步、杀毒扫描)", + " · 不一定需要加速;想试试 WARP,守望先锋请用 -v6only=false", + } + } +} + +// compareVerdict weighs the direct route against WARP the way a game would: +// loss and jitter first, then latency. +func compareVerdict(direct, warp stats) string { + score := func(st stats) time.Duration { + return st.avg + 2*st.jitter + time.Duration(st.loss*1000)*time.Millisecond + } + d, w := score(direct), score(warp) + switch { + case w+5*time.Millisecond < d: + return "WARP 更稳,建议开着加速打游戏。" + case d+5*time.Millisecond < w: + return "直连更好:WARP 绕远了。建议关掉加速直接玩。" + default: + return "两者差不多,开不开都行;以游戏里的网络图(Ctrl+Shift+N)为准。" + } +} + +func report(hops []*series, gw, isp, target *series, wifi bool) string { + var b strings.Builder + for _, s := range hops { + if s == nil { + continue + } + if s.sent == 0 || len(s.rtts) == 0 { + fmt.Fprintf(&b, " %-26s 没有回应\n", s.name) + continue + } + fmt.Fprintf(&b, " %-26s %s\n", s.name, s.stats()) + } + b.WriteString("\n") + for _, l := range verdict(gw, isp, target, wifi) { + b.WriteString(l + "\n") + } + return b.String() +} diff --git a/diag_test.go b/diag_test.go new file mode 100644 index 0000000..8d768d4 --- /dev/null +++ b/diag_test.go @@ -0,0 +1,75 @@ +package main + +import ( + "strings" + "testing" + "time" +) + +func steady(name string, ms int, n int) *series { + s := &series{name: name, sent: n} + for i := 0; i < n; i++ { + s.rtts = append(s.rtts, time.Duration(ms)*time.Millisecond) + } + return s +} + +func lossy(name string, ms, n, lost int) *series { + s := steady(name, ms, n-lost) + s.sent = n + return s +} + +func jittery(name string, n int) *series { + s := &series{name: name, sent: n} + for i := 0; i < n; i++ { + s.rtts = append(s.rtts, time.Duration(2+(i%2)*30)*time.Millisecond) + } + return s +} + +func TestStats(t *testing.T) { + st := lossy("x", 10, 20, 2).stats() + if st.avg != 10*time.Millisecond || st.jitter != 0 || st.loss < 0.099 || st.loss > 0.101 { + t.Fatalf("%+v", st) + } + if j := jittery("x", 10).stats().jitter; j != 30*time.Millisecond { + t.Fatalf("jitter %v", j) + } + s := steady("x", 10, 20) + s.rtts[5], s.rtts[9] = 200*time.Millisecond, 300*time.Millisecond + if st := s.stats(); st.spikes != 2 || st.max != 300*time.Millisecond { + t.Fatalf("%+v", st) + } +} + +func TestVerdict(t *testing.T) { + ok, gwBad, ispBad, bliz, blizBad := steady("gw", 2, 60), jittery("gw", 60), lossy("isp", 8, 60, 12), steady("b", 70, 30), lossy("b", 70, 30, 3) + cases := []struct { + gw, isp, target *series + wifi bool + want string + }{ + {gwBad, ok, blizBad, true, "Wi-Fi"}, + {ok, ispBad, blizBad, false, "运营商接入"}, + {ok, ok, blizBad, false, "WARP 加速可能有帮助"}, + {ok, ispBad, bliz, false, "三段都正常"}, // a hop that rate-limits pings alone is not a fault + {ok, nil, nil, false, "没测到"}, + {&series{name: "gw", sent: 60}, ok, bliz, false, "三段都正常"}, // silent router is unmeasured + } + for i, c := range cases { + got := strings.Join(verdict(c.gw, c.isp, c.target, c.wifi), "\n") + if !strings.Contains(got, c.want) { + t.Errorf("case %d: want %q in\n%s", i, c.want, got) + } + } +} + +func TestCompareVerdict(t *testing.T) { + good, bad := steady("", 20, 10).stats(), lossy("", 20, 10, 2).stats() + if !strings.Contains(compareVerdict(bad, good), "WARP 更稳") || + !strings.Contains(compareVerdict(good, bad), "直连更好") || + !strings.Contains(compareVerdict(good, good), "差不多") { + t.Fatal("compare verdict") + } +} diff --git a/diag_windows.go b/diag_windows.go new file mode 100644 index 0000000..496a674 --- /dev/null +++ b/diag_windows.go @@ -0,0 +1,207 @@ +package main + +import ( + "fmt" + "net" + "net/netip" + "sync" + "time" + "unsafe" + + "golang.org/x/sys/windows" + "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" +) + +var ( + iphlpapi = windows.NewLazySystemDLL("iphlpapi.dll") + icmpCreateFile = iphlpapi.NewProc("IcmpCreateFile") + icmpCloseHandle = iphlpapi.NewProc("IcmpCloseHandle") + icmpSendEcho = iphlpapi.NewProc("IcmpSendEcho") +) + +// blizzardHost is Battle.net's US service. It is not the match server (those +// are not published), so its latency is a reference for the route towards +// Blizzard, not the in-game ping. +const blizzardHost = "us.actual.battle.net:1119" + +type ipOptionInformation struct { + TTL, TOS, Flags, OptionsSize uint8 + OptionsData uintptr +} + +type icmpEchoReply struct { + Address [4]byte + Status uint32 + RoundTripTime uint32 + DataSize uint16 + Reserved uint16 + Data uintptr + Options ipOptionInformation +} + +const ipTTLExpiredTransit = 11013 + +// ping sends one ICMP echo with the given TTL. It returns who answered (the +// target, or the router where the TTL ran out), and the round trip. +func ping(h uintptr, dst netip.Addr, ttl uint8, timeout time.Duration) (from netip.Addr, rtt time.Duration, ok bool) { + payload := []byte("WarpGameBoost diag") + reply := make([]byte, unsafe.Sizeof(icmpEchoReply{})+uintptr(len(payload))+64) + opt := ipOptionInformation{TTL: ttl} + a := dst.As4() + start := time.Now() + n, _, _ := icmpSendEcho.Call(h, uintptr(*(*uint32)(unsafe.Pointer(&a[0]))), + uintptr(unsafe.Pointer(&payload[0])), uintptr(len(payload)), uintptr(unsafe.Pointer(&opt)), + uintptr(unsafe.Pointer(&reply[0])), uintptr(len(reply)), uintptr(timeout.Milliseconds())) + elapsed := time.Since(start) + if n == 0 { + return netip.Addr{}, 0, false + } + r := (*icmpEchoReply)(unsafe.Pointer(&reply[0])) + if r.Status != 0 && r.Status != ipTTLExpiredTransit { + return netip.Addr{}, 0, false + } + // The API reports whole milliseconds; the wall clock is finer for hops + // under a millisecond and costs at most a scheduler tick on top. + return netip.AddrFrom4(r.Address), min(elapsed, time.Duration(r.RoundTripTime+1)*time.Millisecond), true +} + +func publicHop(a netip.Addr) bool { + cgnat := netip.MustParsePrefix("100.64.0.0/10") + return a.IsValid() && !a.IsPrivate() && !a.IsLoopback() && !a.IsLinkLocalUnicast() && !cgnat.Contains(a) +} + +// defaultGateway4 returns the IPv4 next hop of the preferred default route and +// whether that interface is Wi-Fi. +func defaultGateway4() (netip.Addr, bool) { + idx := defaultInterface(winipcfg.AddressFamily(windows.AF_INET), 0) + rows, err := winipcfg.GetIPForwardTable2(windows.AF_INET) + if err != nil || idx == 0 { + return netip.Addr{}, false + } + for i := range rows { + r := &rows[i] + if r.InterfaceIndex == idx && r.DestinationPrefix.PrefixLength == 0 { + gw := r.NextHop.Addr() + wifi := false + if row, err := r.InterfaceLUID.Interface(); err == nil { + wifi = row.Type == winipcfg.IfTypeIEEE80211 + } + return gw, wifi + } + } + return netip.Addr{}, false +} + +// diagnose measures the three legs a game's packets cross — this computer to +// the router, the ISP's first router, and the route on to Blizzard — before +// the tunnel is up, and says which one is unstable. +func diagnose() *series { + const samples, interval = 60, 250 * time.Millisecond + fmt.Printf("网络诊断(约 %d 秒,加 -diag=false 可跳过)…\n", samples*int(interval)/int(time.Second)+3) + h, _, _ := icmpCreateFile.Call() + if h == uintptr(windows.InvalidHandle) { + fmt.Println("无法诊断:ICMP 不可用。") + return nil + } + defer icmpCloseHandle.Call(h) + + gwAddr, wifi := defaultGateway4() + var gw, isp, target *series + var ispAddr netip.Addr + if gwAddr.IsValid() && !gwAddr.IsUnspecified() { + gw = &series{name: "电脑 → 路由器 " + gwAddr.String()} + } + // Traceroute towards Cloudflare's resolver to find where the ISP starts. + for ttl := uint8(1); ttl <= 8; ttl++ { + if from, _, ok := ping(h, netip.MustParseAddr("1.1.1.1"), ttl, time.Second); ok && publicHop(from) { + if from != netip.MustParseAddr("1.1.1.1") { + isp, ispAddr = &series{name: "→ 运营商 " + from.String()}, from + } + break + } + } + if ips, err := net.LookupIP("us.actual.battle.net"); err == nil { + for _, ip := range ips { + if ip.To4() != nil { + target = &series{name: "→ 暴雪(战网美国)"} + break + } + } + } + + var wg sync.WaitGroup + icmpSeries := func(s *series, dst netip.Addr) { + defer wg.Done() + h, _, _ := icmpCreateFile.Call() + defer icmpCloseHandle.Call(h) + tick := time.NewTicker(interval) + defer tick.Stop() + for i := 0; i < samples; i++ { + s.sent++ + if _, rtt, ok := ping(h, dst, 64, time.Second); ok { + s.rtts = append(s.rtts, rtt) + } + <-tick.C + } + } + if gw != nil { + wg.Add(1) + go icmpSeries(gw, gwAddr) + } + if isp != nil { + wg.Add(1) + go icmpSeries(isp, ispAddr) + } + if target != nil { + wg.Add(1) + go func() { + defer wg.Done() + // Blizzard does not answer pings; a TCP handshake times the round + // trip just as well. Half the samples, to stay polite. + tick := time.NewTicker(2 * interval) + defer tick.Stop() + for i := 0; i < samples/2; i++ { + target.sample() + <-tick.C + } + }() + } + wg.Wait() + + fmt.Println() + fmt.Print(report([]*series{gw, isp, target}, gw, isp, target, wifi)) + fmt.Println() + return target +} + +// sample times one TCP handshake with Blizzard. +func (s *series) sample() { + s.sent++ + start := time.Now() + if c, err := net.DialTimeout("tcp4", blizzardHost, time.Second); err == nil { + s.rtts = append(s.rtts, time.Since(start)) + c.Close() + } +} + +// compareWarp measures the same Blizzard server through the tunnel and sets +// the result beside the direct measurement from diagnose. +func compareWarp(direct *series) { + if direct == nil || len(direct.rtts) == 0 { + return + } + fmt.Println("对比:经 WARP 再测暴雪(约 8 秒)…") + via := &series{name: "经 WARP"} + for i := 0; i < 16; i++ { + via.sample() + time.Sleep(500 * time.Millisecond) + } + if len(via.rtts) == 0 { + fmt.Println(" 经 WARP 连不上暴雪。") + return + } + d, w := direct.stats(), via.stats() + fmt.Printf(" 直连 %s\n 经 WARP %s\n", d, w) + fmt.Println(" " + compareVerdict(d, w)) + fmt.Println() +} diff --git a/main_windows.go b/main_windows.go index 32698c5..bff4d80 100644 --- a/main_windows.go +++ b/main_windows.go @@ -46,6 +46,7 @@ var ( 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") ) @@ -126,6 +127,11 @@ func run() int { physIndex4 = defaultInterface(winipcfg.AddressFamily(windows.AF_INET), 0) } + var blizzard *series + if *diag { + blizzard = diagnose() + } + results := optimize(acct, "") if len(results) == 0 { fmt.Println("没有找到可用的 WARP 入口:所在网络可能封锁了 WARP。") @@ -181,6 +187,8 @@ func run() int { fmt.Println(" 只有 IPv4 的网站和游戏会连不上;玩守望先锋请加参数 -v6only=false。") } else { fmt.Println(" 仅流量模式:DNS 保持系统设置。") + fmt.Println() + compareWarp(blizzard) } fmt.Println(" 现在可以打开游戏。关闭本窗口或按 Ctrl+C 结束加速;输入 r 回车重新优选。") fmt.Println() From 3f8e0e418e4702c0614af9625ceb3b43284113f2 Mon Sep 17 00:00:00 2001 From: Freecode100Year <174155046+Freecode100Year@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:22:12 -0400 Subject: [PATCH 2/3] fix: time Blizzard with a TLS hello, not a TCP handshake WARP answers at its edge Co-Authored-By: Claude Opus 5.5 --- diag_windows.go | 57 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/diag_windows.go b/diag_windows.go index 496a674..58fce9f 100644 --- a/diag_windows.go +++ b/diag_windows.go @@ -1,6 +1,7 @@ package main import ( + "crypto/tls" "fmt" "net" "net/netip" @@ -19,10 +20,10 @@ var ( icmpSendEcho = iphlpapi.NewProc("IcmpSendEcho") ) -// blizzardHost is Battle.net's US service. It is not the match server (those -// are not published), so its latency is a reference for the route towards -// Blizzard, not the in-game ping. -const blizzardHost = "us.actual.battle.net:1119" +// blizzardHost is Blizzard's Overwatch website, served from AWS in Virginia. +// It is not a match server (those are not published), so its latency is a +// reference for the route towards Blizzard's US East, not the in-game ping. +const blizzardHost = "overwatch.blizzard.com" type ipOptionInformation struct { TTL, TOS, Flags, OptionsSize uint8 @@ -120,10 +121,10 @@ func diagnose() *series { break } } - if ips, err := net.LookupIP("us.actual.battle.net"); err == nil { + if ips, err := net.LookupIP(blizzardHost); err == nil { for _, ip := range ips { if ip.To4() != nil { - target = &series{name: "→ 暴雪(战网美国)"} + target = &series{name: "→ 暴雪(美东)"} break } } @@ -156,8 +157,8 @@ func diagnose() *series { wg.Add(1) go func() { defer wg.Done() - // Blizzard does not answer pings; a TCP handshake times the round - // trip just as well. Half the samples, to stay polite. + // Blizzard does not answer pings; a TLS hello times the round + // trip instead. Half the samples, to stay polite. tick := time.NewTicker(2 * interval) defer tick.Stop() for i := 0; i < samples/2; i++ { @@ -174,14 +175,44 @@ func diagnose() *series { return target } -// sample times one TCP handshake with Blizzard. +// sample times one round trip to Blizzard: from sending a TLS hello to the +// first byte of the answer. A TCP handshake alone would not do, because WARP +// completes handshakes at its own edge, in a few milliseconds, whatever the +// distance to the real server. func (s *series) sample() { s.sent++ - start := time.Now() - if c, err := net.DialTimeout("tcp4", blizzardHost, time.Second); err == nil { - s.rtts = append(s.rtts, time.Since(start)) - c.Close() + c, err := net.DialTimeout("tcp4", blizzardHost+":443", time.Second) + if err != nil { + return + } + defer c.Close() + tc := &timedConn{Conn: c} + c.SetDeadline(time.Now().Add(time.Second)) + tls.Client(tc, &tls.Config{ServerName: blizzardHost}).Handshake() + if !tc.answered.IsZero() { + s.rtts = append(s.rtts, tc.answered.Sub(tc.sent)) + } +} + +// timedConn notes when the first bytes went out and when the first came back. +type timedConn struct { + net.Conn + sent, answered time.Time +} + +func (c *timedConn) Write(b []byte) (int, error) { + if c.sent.IsZero() { + c.sent = time.Now() + } + return c.Conn.Write(b) +} + +func (c *timedConn) Read(b []byte) (int, error) { + n, err := c.Conn.Read(b) + if n > 0 && c.answered.IsZero() { + c.answered = time.Now() } + return n, err } // compareWarp measures the same Blizzard server through the tunnel and sets From 403b61402cdaa0960a6b6e776d655fb3973762e0 Mon Sep 17 00:00:00 2001 From: Freecode100Year <174155046+Freecode100Year@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:29:05 -0400 Subject: [PATCH 3/3] docs: network diagnosis in README, v0.6.0 notes Co-Authored-By: Claude Opus 5.5 --- README.md | 20 ++++++++++++++++++++ RELEASE_NOTES.md | 9 ++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7cbd44d..cc1745c 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,24 @@ DNS 怎么走: 4. 打开游戏。窗口每 5 秒显示一次经隧道到 Cloudflare 的延迟。 5. 不玩了就关闭窗口。 +### 网络诊断(默认开启) + +每次启动先花约 20 秒测三段,告诉你**卡在哪一段、该怎么办**,再开始加速: + +| 测哪一段 | 怎么测 | 不稳说明什么 | +| :--- | :--- | :--- | +| 电脑 → 路由器 | ping 默认网关 | 家里的 Wi-Fi/网线/路由器有问题。**WARP 帮不上**,程序会给出具体做法(插网线、5GHz、关定位、关网卡节能、开 SQM) | +| → 运营商 | 先 traceroute 找到第一个公网路由器,再 ping 它 | 运营商接入有问题,WARP 也要经过这一段,**帮不上** | +| → 暴雪(美东) | 对 `overwatch.blizzard.com`(AWS 弗吉尼亚)做 TLS 握手计时 | 前两段正常而这段不稳 = 运营商到暴雪的线路问题,**这种情况 WARP 可能有帮助** | + +用 `-v6only=false` 运行时,连上 WARP 后还会**经 WARP 再测一次暴雪**,和直连并排对比,直接告诉你开加速更稳还是直连更好。 + +说明: +- 测的是暴雪美东的网站服务器,不是对局服务器(对局服务器地址不公开),所以延迟数字只代表到暴雪美东的线路,和游戏里显示的不完全一样。 +- 测暴雪用 TLS 握手而不是普通 TCP 连接:WARP 会在自己的边缘直接完成 TCP 握手,那样测出来只有几毫秒,是假的。 +- 有些路由器不回应 ping,会显示“没有回应”,这不算故障。 +- 不想每次都等,加 `-diag=false` 跳过。 + ### 开着与关闭 | 操作 | 结果 | @@ -76,6 +94,7 @@ DNS 怎么走: | 参数 | 作用 | | :--- | :--- | +| `-diag=false` | 跳过启动时的网络诊断 | | `-v6only=false` | 恢复 IPv4 出口(仅流量模式,DNS 用系统设置)。**玩只有 IPv4 的游戏(如守望先锋)时必须加** | | `-ipv4` | 有 IPv6 时也同时测试 IPv4 入口(默认有 IPv6 就只用 IPv6 入口) | | `-count 200` | 每次优选测试的入口数量 | @@ -140,6 +159,7 @@ go build -trimpath -ldflags "-s -w" -o WarpGameBoost.exe . 版本号显示在窗口标题和第一行,也可以右键 exe →“属性”→“详细信息”查看。 +- **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 保持系统设置。 - **v0.3.0**:程序附带版本号(窗口标题、启动第一行、`-version` 参数、文件属性);exe 自带管理员权限声明,双击时直接弹出权限确认。 diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index de864bd..90988f1 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,8 @@ -新增**出口仅 IPv6**(`-v6only`,默认开启):网站只看到 Cloudflare 的 IPv6 地址,IPv4 全部禁用并立即拒绝(程序不会卡住等超时)。家里 Wi-Fi 只有 IPv4 也能用。DNS 在这个模式下改用经隧道的 Cloudflare IPv6 解析(`2606:4700:4700::1111`)。 +新增**网络诊断**(默认开启,加 `-diag=false` 跳过)。 -**玩守望先锋等只有 IPv4 服务器的游戏,必须加 `-v6only=false`**(右键 exe 创建快捷方式,在“目标”最后加 ` -v6only=false`),否则游戏连不上。加了以后与 v0.4.0 相同(仅流量模式,DNS 用系统设置)。 +启动时先花约 20 秒测三段的延迟、抖动、丢包:电脑→路由器、→运营商(traceroute 找到的第一个公网路由器)、→暴雪美东(`overwatch.blizzard.com` 的 TLS 握手计时)。然后告诉你问题在哪一段: +- 家里 Wi-Fi/路由器不稳:WARP 帮不上,给出插网线、5GHz、关定位、关网卡节能、开 SQM 等做法; +- 运营商接入不稳:WARP 也帮不上; +- 运营商到暴雪的线路不稳:WARP 可能有帮助。 -修复:本机没有 IPv6 时,以前经隧道的 IPv6 实际用不上(Windows 只给程序 IPv4 地址),现在可以正常使用。 +用 `-v6only=false` 运行(守望先锋需要)时,连上 WARP 后会**经 WARP 再测一次暴雪**,与直连并排对比,直接告诉你开加速更稳还是直连更好。