Release 3.8.2.1: reliability, UI probe/logs, CI, signing gates.

Clear sysproxy on core death; probe+reconnect; subscription prune;
Best ignores soft UDP; Windows tray; Android protocol gate; CI workflows.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
M4
2026-07-30 02:44:48 +03:00
co-authored by Cursor
parent 6b6c13c933
commit 621c847cb3
39 changed files with 913 additions and 143 deletions
+17 -15
View File
@@ -24,6 +24,7 @@ type Result struct {
Port string `json:"port"`
Ms int64 `json:"ms"`
OK bool `json:"ok"`
Soft bool `json:"soft,omitempty"` // UDP soft-up (timeout, no ICMP refuse)
Error string `json:"error,omitempty"`
}
@@ -90,12 +91,13 @@ func PingAll(ctx context.Context, targets []Target) []Result {
return sorted
}
// BestOK returns the lowest-latency successful result, if any.
// BestOK returns the lowest-latency hard OK result (excludes UDP soft-up).
// Soft-up nodes stay visible in the list but are not chosen as «Лучший».
func BestOK(results []Result) (Result, bool) {
var best Result
found := false
for _, r := range results {
if !r.OK {
if !r.OK || r.Soft {
continue
}
if !found || r.Ms < best.Ms {
@@ -186,11 +188,12 @@ func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyU
}
var (
rtt time.Duration
err error
rtt time.Duration
soft bool
err error
)
if hy2 || isAWG {
rtt, err = probeUDP(ctx, host, port)
rtt, soft, err = probeUDP(ctx, host, port)
} else {
rtt, err = probeTCP(ctx, host, port)
}
@@ -204,6 +207,7 @@ func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyU
}
res.Ms = ms
res.OK = true
res.Soft = soft
return res
}
@@ -223,18 +227,17 @@ func probeTCP(ctx context.Context, host, port string) (time.Duration, error) {
// 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) {
// - any reply → hard up (RTT to reply)
// - short read-timeout after a successful write → soft-up (display only; not for Best)
func probeUDP(ctx context.Context, host, port string) (time.Duration, bool, 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 0, err
return 0, false, err
}
defer conn.Close()
// 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) {
@@ -243,20 +246,19 @@ func probeUDP(ctx context.Context, host, port string) (time.Duration, error) {
_ = conn.SetDeadline(deadline)
if _, err := conn.Write([]byte{0}); err != nil {
return 0, err
return 0, false, err
}
afterWrite := time.Now()
buf := make([]byte, 64)
_, err = conn.Read(buf)
if err == nil {
return time.Since(start), nil
return time.Since(start), false, nil
}
if ne, ok := err.(net.Error); ok && ne.Timeout() {
// No ICMP refuse → endpoint is plausible; latency without the wait pad.
return afterWrite.Sub(start), nil
return afterWrite.Sub(start), true, nil
}
return 0, err
return 0, false, err
}
func friendlyDialError(err error, udp bool) string {
+12 -3
View File
@@ -47,10 +47,13 @@ func TestProbeUDPSoftOKWhenSilent(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
start := time.Now()
rtt, err := probeUDP(ctx, "127.0.0.1", port)
rtt, soft, err := probeUDP(ctx, "127.0.0.1", port)
if err != nil {
t.Fatalf("expected soft-ok, got err=%v", err)
}
if !soft {
t.Fatal("expected soft=true")
}
if rtt <= 0 {
t.Fatalf("rtt=%v", rtt)
}
@@ -59,12 +62,15 @@ func TestProbeUDPSoftOKWhenSilent(t *testing.T) {
}
res := PingProfile(ctx, "silent", config.ProtocolHysteria2, "hysteria2://x@127.0.0.1:"+port+"/")
if !res.OK {
if !res.OK || !res.Soft {
t.Fatalf("PingProfile soft-ok failed: %+v", res)
}
if res.Ms < 1 {
t.Fatalf("ms=%d", res.Ms)
}
if _, ok := BestOK([]Result{res}); ok {
t.Fatal("BestOK must ignore soft-up")
}
}
func TestProbeUDPReplyOK(t *testing.T) {
@@ -87,10 +93,13 @@ func TestProbeUDPReplyOK(t *testing.T) {
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
rtt, err := probeUDP(ctx, "127.0.0.1", port)
rtt, soft, err := probeUDP(ctx, "127.0.0.1", port)
if err != nil {
t.Fatal(err)
}
if soft {
t.Fatal("reply should be hard-ok")
}
if rtt <= 0 {
t.Fatalf("rtt=%v", rtt)
}