Release 3.8.0.3: fix Hy2/AWG UDP ping false negatives.
This commit is contained in:
+37
-18
@@ -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)"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user