package netcheck import ( "context" "fmt" "net" "strings" "time" "vpnclient/internal/config" "vpnclient/internal/linknorm" "vpnclient/internal/protocols/hysteria2" ) // Result is a TCP reachability measurement to a proxy host. type Result struct { Name string `json:"name"` Host string `json:"host"` Port string `json:"port"` Ms int64 `json:"ms"` OK bool `json:"ok"` Error string `json:"error,omitempty"` } // PingProxyURI measures TCP 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. func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyURI string) Result { res := Result{Name: name} if strings.TrimSpace(proxyURI) == "" { res.Error = "нет ссылки сервера" return res } var host, port string if proto == config.ProtocolHysteria2 || hysteria2.Detect(proxyURI) { h, p, err := hysteria2.HostPort(proxyURI) if err != nil { res.Error = err.Error() return res } host, port = h, p } else { normalized, _, _, err := linknorm.Normalize(proto, proxyURI) if err != nil { 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:] } if at := strings.LastIndex(rest, "@"); at >= 0 { rest = rest[at+1:] } if i := strings.IndexAny(rest, "/?#"); i >= 0 { rest = rest[:i] } host = rest port = "443" if i := strings.LastIndex(rest, ":"); i >= 0 { host = rest[:i] port = rest[i+1:] } } res.Host = host res.Port = port if host == "" { res.Error = "нет хоста" return res } d := net.Dialer{Timeout: 4 * time.Second} start := time.Now() conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(host, port)) if err != nil { res.Error = fmt.Sprintf("недоступен: %v", err) return res } _ = conn.Close() res.Ms = time.Since(start).Milliseconds() res.OK = true return res }