diff --git a/README.md b/README.md index 51f7c9f..c3ad878 100644 --- a/README.md +++ b/README.md @@ -248,7 +248,8 @@ https://evilfox.win/ - безопасный lifecycle Connect/Disconnect (ожидание Stop, mutex Windows sysproxy); - SHA-256 проверка при установке cores (naive / hy2 / xray); - snapshot конфига в UI (`Config().Clone`); -- 3.8.0+2: обновление Navis.app через zip+SHA; Start вне mutex; getState без секретов; подпись/notarize scripts; UI CSS/JS split. +- 3.8.0+2: обновление Navis.app через zip+SHA; Start вне mutex; getState без секретов; подпись/notarize scripts; UI CSS/JS split; +- 3.8.0+3: пинг Hy2/AWG по UDP — soft-ok при тишине (без ложных «недоступен»). В 2.7.3: - восстановление системного прокси после аварийного завершения; diff --git a/build-macos.bat b/build-macos.bat index 683724c..efa883c 100644 --- a/build-macos.bat +++ b/build-macos.bat @@ -34,11 +34,11 @@ if errorlevel 1 exit /b 1 go build -o "tools\packmac\packmac.exe" .\tools\packmac if errorlevel 1 exit /b 1 -tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 3.8.0 -build 3.8.0.2 -arch arm64 +tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 3.8.0 -build 3.8.0.3 -arch arm64 if errorlevel 1 exit /b 1 -tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 3.8.0 -build 3.8.0.2 -arch amd64 +tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 3.8.0 -build 3.8.0.3 -arch amd64 if errorlevel 1 exit /b 1 -tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 3.8.0 -build 3.8.0.2 -arch universal +tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 3.8.0 -build 3.8.0.3 -arch universal if errorlevel 1 exit /b 1 echo Built Mac GUI + CLI: diff --git a/dist/navis-release/darwin-arm64/Navis b/dist/navis-release/darwin-arm64/Navis index f157b2d..272f4ab 100755 Binary files a/dist/navis-release/darwin-arm64/Navis and b/dist/navis-release/darwin-arm64/Navis differ diff --git a/dist/navis-release/darwin-arm64/Navis-cli b/dist/navis-release/darwin-arm64/Navis-cli index b740710..2ebd472 100755 Binary files a/dist/navis-release/darwin-arm64/Navis-cli and b/dist/navis-release/darwin-arm64/Navis-cli differ diff --git a/dist/navis-release/darwin-arm64/Navis.app.zip b/dist/navis-release/darwin-arm64/Navis.app.zip index 94c65cf..05f0dc7 100644 Binary files a/dist/navis-release/darwin-arm64/Navis.app.zip and b/dist/navis-release/darwin-arm64/Navis.app.zip differ diff --git a/dist/navis-release/darwin-arm64/Navis.dmg b/dist/navis-release/darwin-arm64/Navis.dmg index 6de1f01..59a595b 100644 Binary files a/dist/navis-release/darwin-arm64/Navis.dmg and b/dist/navis-release/darwin-arm64/Navis.dmg differ diff --git a/dist/navis-release/darwin-arm64/Navis.version b/dist/navis-release/darwin-arm64/Navis.version index 181a7cf..1346430 100644 --- a/dist/navis-release/darwin-arm64/Navis.version +++ b/dist/navis-release/darwin-arm64/Navis.version @@ -1 +1 @@ -3.8.0+2 +3.8.0+3 diff --git a/dist/navis-release/darwin-arm64/VERSION b/dist/navis-release/darwin-arm64/VERSION index 9ad370d..e63c5be 100644 --- a/dist/navis-release/darwin-arm64/VERSION +++ b/dist/navis-release/darwin-arm64/VERSION @@ -1 +1 @@ -3.8.0.2 +3.8.0.3 diff --git a/dist/navis-release/update.json b/dist/navis-release/update.json index 0bde978..8817460 100644 --- a/dist/navis-release/update.json +++ b/dist/navis-release/update.json @@ -1,6 +1,6 @@ { "version": "3.8.0", - "notes": "Navis 3.8.0+2: Mac .app zip update; EnsureCore lock; getState без секретов; подпись/notarize pipeline; UI split.", + "notes": "Navis 3.8.0+3: UDP ping Hy2/AWG (soft-ok без ложных «недоступен»); Mac .app zip update; EnsureCore lock; getState без секретов.", "platform": "windows-amd64", "os": "windows", "arch": "amd64", @@ -16,13 +16,13 @@ }, "darwin-arm64": { "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis", - "sha256": "6e15ad79e5ec1a1f74c48bc8f189ab8694b8f3f67f4be204420a12cf0fd622fd", + "sha256": "bd507de6f434f53f95954c8e411cc1f8bb526d5b5353b1242de5bc9fb673670f", "os": "darwin", "arch": "arm64", "dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg", "zip_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.app.zip", - "zip_sha256": "65041e154356a33525b9769f851bcccfc79d0c2a1200fb9a47bc22dd95cdf2ef", - "dmg_sha256": "46617b7cbf22e92a5ef85740ed9473cd1bc9219c9de5841ce0ac46b8eb946912" + "zip_sha256": "2aaac597f7246c67fc825cd11f49d2b4fc0fa6bd9b89a1b1e4290e6a96c01de4", + "dmg_sha256": "26ddacc3b3bc372aa4833336e60fb32862fb30ee555a8a6e460811f45f9f5cee" }, "darwin-amd64": { "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis", diff --git a/dist/update.json b/dist/update.json index 0bde978..8817460 100644 --- a/dist/update.json +++ b/dist/update.json @@ -1,6 +1,6 @@ { "version": "3.8.0", - "notes": "Navis 3.8.0+2: Mac .app zip update; EnsureCore lock; getState без секретов; подпись/notarize pipeline; UI split.", + "notes": "Navis 3.8.0+3: UDP ping Hy2/AWG (soft-ok без ложных «недоступен»); Mac .app zip update; EnsureCore lock; getState без секретов.", "platform": "windows-amd64", "os": "windows", "arch": "amd64", @@ -16,13 +16,13 @@ }, "darwin-arm64": { "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis", - "sha256": "6e15ad79e5ec1a1f74c48bc8f189ab8694b8f3f67f4be204420a12cf0fd622fd", + "sha256": "bd507de6f434f53f95954c8e411cc1f8bb526d5b5353b1242de5bc9fb673670f", "os": "darwin", "arch": "arm64", "dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg", "zip_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.app.zip", - "zip_sha256": "65041e154356a33525b9769f851bcccfc79d0c2a1200fb9a47bc22dd95cdf2ef", - "dmg_sha256": "46617b7cbf22e92a5ef85740ed9473cd1bc9219c9de5841ce0ac46b8eb946912" + "zip_sha256": "2aaac597f7246c67fc825cd11f49d2b4fc0fa6bd9b89a1b1e4290e6a96c01de4", + "dmg_sha256": "26ddacc3b3bc372aa4833336e60fb32862fb30ee555a8a6e460811f45f9f5cee" }, "darwin-amd64": { "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis", diff --git a/internal/netcheck/ping.go b/internal/netcheck/ping.go index f19bc27..60cbb74 100644 --- a/internal/netcheck/ping.go +++ b/internal/netcheck/ping.go @@ -163,67 +163,86 @@ func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyU return res } - start := time.Now() - var err error + var ( + rtt time.Duration + err error + ) if hy2 || isAWG { - err = probeUDP(ctx, host, port) + rtt, err = probeUDP(ctx, host, port) } else { - err = probeTCP(ctx, host, port) + rtt, err = probeTCP(ctx, host, port) } if err != nil { res.Error = friendlyDialError(err, hy2 || isAWG) return res } - res.Ms = time.Since(start).Milliseconds() + ms := rtt.Milliseconds() + if ms < 1 { + ms = 1 + } + res.Ms = ms res.OK = true return res } -func probeTCP(ctx context.Context, host, port string) error { +func probeTCP(ctx context.Context, host, port string) (time.Duration, error) { + start := time.Now() d := net.Dialer{Timeout: 3 * time.Second} conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(host, port)) if err != nil { - return err + return 0, err } _ = conn.Close() - return nil + return time.Since(start), nil } -func probeUDP(ctx context.Context, host, port string) error { +// probeUDP checks Hy2/AWG endpoints. +// +// UDP VPN ports usually ignore a probe datagram (no reply). Treating read-timeout +// as failure marked live nodes as down. Semantics: +// - ICMP / connection refused → down +// - any reply → up (RTT to reply) +// - short read-timeout after a successful write → soft-up (RTT ≈ DNS+dial+write) +func probeUDP(ctx context.Context, host, port string) (time.Duration, error) { + start := time.Now() d := net.Dialer{Timeout: 3 * time.Second} conn, err := d.DialContext(ctx, "udp", net.JoinHostPort(host, port)) if err != nil { - return err + return 0, err } defer conn.Close() - deadline := time.Now().Add(1200 * time.Millisecond) + // Brief window only to catch ICMP port-unreachable; do not wait for an app reply. + icmpWait := 350 * time.Millisecond + deadline := time.Now().Add(icmpWait) if dl, ok := ctx.Deadline(); ok && dl.Before(deadline) { deadline = dl } _ = conn.SetDeadline(deadline) if _, err := conn.Write([]byte{0}); err != nil { - return err + return 0, err } + afterWrite := time.Now() + buf := make([]byte, 64) _, err = conn.Read(buf) if err == nil { - return nil + return time.Since(start), nil } if ne, ok := err.(net.Error); ok && ne.Timeout() { - // UDP is connectionless: no reply does NOT mean the port is open. - return fmt.Errorf("udp timeout") + // No ICMP refuse → endpoint is plausible; latency without the wait pad. + return afterWrite.Sub(start), nil } - return err + return 0, err } -func friendlyDialError(err error, hy2 bool) string { +func friendlyDialError(err error, udp bool) string { msg := err.Error() lower := strings.ToLower(msg) switch { case strings.Contains(lower, "refused"): - if hy2 { + if udp { return "UDP порт недоступен (connection refused)" } return "порт закрыт (connection refused)" diff --git a/internal/netcheck/ping_test.go b/internal/netcheck/ping_test.go index 2d627ef..28330d6 100644 --- a/internal/netcheck/ping_test.go +++ b/internal/netcheck/ping_test.go @@ -2,7 +2,7 @@ package netcheck import ( "context" - "strings" + "net" "testing" "time" @@ -12,7 +12,7 @@ import ( func TestFriendlyDialError(t *testing.T) { err := &netError{s: "dial tcp 1.2.3.4:22514: connectex: No connection could be made because the target machine actively refused it."} got := friendlyDialError(err, true) - if !strings.Contains(got, "UDP") { + if got == "" || got == err.Error() { t.Fatalf("got %q", got) } } @@ -31,3 +31,67 @@ func TestPingProfileEmpty(t *testing.T) { t.Fatalf("%+v", r) } } + +func TestProbeUDPSoftOKWhenSilent(t *testing.T) { + pc, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer pc.Close() + // Intentionally do not Read — mimics Hy2/AWG ignoring garbage probes. + _, port, err := net.SplitHostPort(pc.LocalAddr().String()) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + start := time.Now() + rtt, err := probeUDP(ctx, "127.0.0.1", port) + if err != nil { + t.Fatalf("expected soft-ok, got err=%v", err) + } + if rtt <= 0 { + t.Fatalf("rtt=%v", rtt) + } + if time.Since(start) > 900*time.Millisecond { + t.Fatalf("soft-ok waited too long: %v (should not use long timeout)", time.Since(start)) + } + + res := PingProfile(ctx, "silent", config.ProtocolHysteria2, "hysteria2://x@127.0.0.1:"+port+"/") + if !res.OK { + t.Fatalf("PingProfile soft-ok failed: %+v", res) + } + if res.Ms < 1 { + t.Fatalf("ms=%d", res.Ms) + } +} + +func TestProbeUDPReplyOK(t *testing.T) { + pc, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer pc.Close() + go func() { + buf := make([]byte, 64) + n, addr, err := pc.ReadFrom(buf) + if err != nil || n == 0 { + return + } + _, _ = pc.WriteTo([]byte{1}, addr) + }() + _, port, err := net.SplitHostPort(pc.LocalAddr().String()) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + rtt, err := probeUDP(ctx, "127.0.0.1", port) + if err != nil { + t.Fatal(err) + } + if rtt <= 0 { + t.Fatalf("rtt=%v", rtt) + } +} diff --git a/internal/update/update.go b/internal/update/update.go index 5a1539e..d2cbe4d 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -22,7 +22,7 @@ const CurrentVersion = "3.8.0" // BuildNumber is the monotonic build within CurrentVersion (Windows FileVersion 4th part, // macOS CFBundleVersion suffix, Android versionCode low digits). Bump on every release build. -const BuildNumber = 2 +const BuildNumber = 3 // DefaultManifestURL is the update feed (hosted in the project git repo). const DefaultManifestURL = "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json" diff --git a/scripts/build-macos-arm64.sh b/scripts/build-macos-arm64.sh index 417276e..2022e5c 100755 --- a/scripts/build-macos-arm64.sh +++ b/scripts/build-macos-arm64.sh @@ -65,8 +65,8 @@ h = hashlib.sha256(binp.read_bytes()).hexdigest() zh = hashlib.sha256(zipp.read_bytes()).hexdigest() if zipp.exists() else "" dh = hashlib.sha256(dmgp.read_bytes()).hexdigest() if dmgp.exists() else "" notes = ( - f"Navis {ver}+{build}: Mac .app zip update; EnsureCore lock; " - f"getState без секретов; подпись/notarize pipeline; UI split." + f"Navis {ver}+{build}: UDP ping Hy2/AWG (soft-ok без ложных «недоступен»); " + f"Mac .app zip update; EnsureCore lock; getState без секретов." ) paths = [ Path('dist/update.json'), diff --git a/server/update.json b/server/update.json index 0bde978..8817460 100644 --- a/server/update.json +++ b/server/update.json @@ -1,6 +1,6 @@ { "version": "3.8.0", - "notes": "Navis 3.8.0+2: Mac .app zip update; EnsureCore lock; getState без секретов; подпись/notarize pipeline; UI split.", + "notes": "Navis 3.8.0+3: UDP ping Hy2/AWG (soft-ok без ложных «недоступен»); Mac .app zip update; EnsureCore lock; getState без секретов.", "platform": "windows-amd64", "os": "windows", "arch": "amd64", @@ -16,13 +16,13 @@ }, "darwin-arm64": { "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis", - "sha256": "6e15ad79e5ec1a1f74c48bc8f189ab8694b8f3f67f4be204420a12cf0fd622fd", + "sha256": "bd507de6f434f53f95954c8e411cc1f8bb526d5b5353b1242de5bc9fb673670f", "os": "darwin", "arch": "arm64", "dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg", "zip_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.app.zip", - "zip_sha256": "65041e154356a33525b9769f851bcccfc79d0c2a1200fb9a47bc22dd95cdf2ef", - "dmg_sha256": "46617b7cbf22e92a5ef85740ed9473cd1bc9219c9de5841ce0ac46b8eb946912" + "zip_sha256": "2aaac597f7246c67fc825cd11f49d2b4fc0fa6bd9b89a1b1e4290e6a96c01de4", + "dmg_sha256": "26ddacc3b3bc372aa4833336e60fb32862fb30ee555a8a6e460811f45f9f5cee" }, "darwin-amd64": { "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis", diff --git a/versioninfo.json b/versioninfo.json index d18c22a..b4b8007 100644 --- a/versioninfo.json +++ b/versioninfo.json @@ -4,13 +4,13 @@ "Major": 3, "Minor": 8, "Patch": 0, - "Build": 2 + "Build": 3 }, "ProductVersion": { "Major": 3, "Minor": 8, "Patch": 0, - "Build": 2 + "Build": 3 }, "FileFlagsMask": "3f", "FileFlags": "00", @@ -21,12 +21,12 @@ "StringFileInfo": { "CompanyName": "EvilFox", "FileDescription": "Navis — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)", - "FileVersion": "3.8.0.2", + "FileVersion": "3.8.0.3", "InternalName": "Navis", "LegalCopyright": "Copyright (c) EvilFox", "OriginalFilename": "Navis.exe", "ProductName": "Navis", - "ProductVersion": "3.8.0.2", + "ProductVersion": "3.8.0.3", "Comments": "Open-source VPN/proxy client. https://evilfox.win/" }, "VarFileInfo": {