Release 3.8.0.3: fix Hy2/AWG UDP ping false negatives.

This commit is contained in:
M4
2026-07-30 01:49:19 +03:00
parent 6a7480dceb
commit bec6c8392d
16 changed files with 129 additions and 45 deletions
+37 -18
View File
@@ -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)"