Release 1.3.1: fix hy2 UDP ping and shop link open.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Navis
2026-07-28 07:33:26 +03:00
co-authored by Cursor
parent 5d2cb6d76a
commit f7fded5b40
11 changed files with 163 additions and 29 deletions
+70 -8
View File
@@ -12,7 +12,7 @@ import (
"vpnclient/internal/protocols/hysteria2"
)
// Result is a TCP reachability measurement to a proxy host.
// Result is a reachability measurement to a proxy host.
type Result struct {
Name string `json:"name"`
Host string `json:"host"`
@@ -22,12 +22,13 @@ type Result struct {
Error string `json:"error,omitempty"`
}
// PingProxyURI measures TCP connect time to the host in a share/proxy URI.
// PingProxyURI measures connect time to the host in a share/proxy URI.
func PingProxyURI(ctx context.Context, name, proxyURI string) Result {
return PingProfile(ctx, name, "", proxyURI)
}
// PingProfile pings using protocol hints when available.
// Naive uses TCP; Hysteria 2 uses UDP (QUIC) — TCP would always look "refused".
func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyURI string) Result {
res := Result{Name: name}
if strings.TrimSpace(proxyURI) == "" {
@@ -35,8 +36,9 @@ func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyU
return res
}
hy2 := proto == config.ProtocolHysteria2 || hysteria2.Detect(proxyURI)
var host, port string
if proto == config.ProtocolHysteria2 || hysteria2.Detect(proxyURI) {
if hy2 {
h, p, err := hysteria2.HostPort(proxyURI)
if err != nil {
res.Error = err.Error()
@@ -49,7 +51,6 @@ func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyU
res.Error = err.Error()
return res
}
// Parse host from normalized URI without importing net/url dance twice
rest := normalized
if i := strings.Index(rest, "://"); i >= 0 {
rest = rest[i+3:]
@@ -75,15 +76,76 @@ func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyU
return res
}
d := net.Dialer{Timeout: 4 * time.Second}
start := time.Now()
conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(host, port))
var err error
if hy2 {
err = probeUDP(ctx, host, port)
} else {
err = probeTCP(ctx, host, port)
}
if err != nil {
res.Error = fmt.Sprintf("недоступен: %v", err)
res.Error = friendlyDialError(err, hy2)
return res
}
_ = conn.Close()
res.Ms = time.Since(start).Milliseconds()
res.OK = true
return res
}
func probeTCP(ctx context.Context, host, port string) error {
d := net.Dialer{Timeout: 4 * time.Second}
conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(host, port))
if err != nil {
return err
}
_ = conn.Close()
return nil
}
func probeUDP(ctx context.Context, host, port string) error {
d := net.Dialer{Timeout: 4 * time.Second}
conn, err := d.DialContext(ctx, "udp", net.JoinHostPort(host, port))
if err != nil {
return err
}
defer conn.Close()
deadline := time.Now().Add(1500 * time.Millisecond)
if dl, ok := ctx.Deadline(); ok && dl.Before(deadline) {
deadline = dl
}
_ = conn.SetDeadline(deadline)
// Any datagram is enough to exercise the UDP path; QUIC rarely answers a bare probe.
if _, err := conn.Write([]byte{0}); err != nil {
return err
}
buf := make([]byte, 64)
_, err = conn.Read(buf)
if err == nil {
return nil
}
if ne, ok := err.(net.Error); ok && ne.Timeout() {
// No ICMP unreachable → port is typically open or filtered; treat as reachable for UI ping.
return nil
}
return err
}
func friendlyDialError(err error, hy2 bool) string {
msg := err.Error()
lower := strings.ToLower(msg)
switch {
case strings.Contains(lower, "refused"):
if hy2 {
return "UDP порт недоступен (connection refused)"
}
return "порт закрыт (connection refused)"
case strings.Contains(lower, "timeout") || strings.Contains(lower, "timed out"):
return "таймаут"
case strings.Contains(lower, "no such host"):
return "DNS: хост не найден"
default:
return fmt.Sprintf("недоступен: %v", err)
}
}