package client import ( "context" "crypto/tls" "fmt" "net" "net/url" "sort" "sync" "time" ) // RouteCheck is the result of checking one route's local target and its // public TLS path. type RouteCheck struct { Route string LocalUp bool PublicUp bool LocalError string PublicError string Checked time.Time } // CheckLocal dials the route target's TCP port. func CheckLocal(ctx context.Context, target string) (bool, string) { dialer := net.Dialer{Timeout: 3 * time.Second} conn, err := dialer.DialContext(ctx, "tcp", target) if err != nil { return false, err.Error() } conn.Close() return true, "" } // CheckPublic performs a TLS handshake against the public route hostname. It // goes through DNS, the public proxy/relay, the bridge and the client's TLS // certificate, so success means the external path is up. It intentionally // does not send application data, so it works for non-HTTP routes too. func CheckPublic(ctx context.Context, publicURL string) (bool, string) { u, err := url.Parse(publicURL) if err != nil || u.Hostname() == "" { return false, "no public hostname" } host := u.Hostname() port := u.Port() if port == "" { port = "443" } dialer := &net.Dialer{Timeout: 5 * time.Second} raw, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(host, port)) if err != nil { return false, err.Error() } defer raw.Close() // A TCP connect can succeed while the TLS handshake stalls (for example // when the tunnel has no bridge); bound it independently of the dialer. _ = raw.SetDeadline(time.Now().Add(5 * time.Second)) tlsConn := tls.Client(raw, &tls.Config{ ServerName: host, MinVersion: tls.VersionTLS12, }) if err := tlsConn.HandshakeContext(ctx); err != nil { return false, err.Error() } return true, "" } // CheckRoute checks one route. func CheckRoute(ctx context.Context, cfg *Config, name string) RouteCheck { result := RouteCheck{Route: name, Checked: time.Now()} route, ok := cfg.Routes[name] if !ok { result.LocalError = "route not found" result.PublicError = "route not found" return result } result.LocalUp, result.LocalError = CheckLocal(ctx, route.Target) result.PublicUp, result.PublicError = CheckPublic(ctx, cfg.RouteURL(name)) return result } // CheckAll checks every route concurrently. func CheckAll(ctx context.Context, cfg *Config) []RouteCheck { names := make([]string, 0, len(cfg.Routes)) for name := range cfg.Routes { names = append(names, name) } sort.Strings(names) results := make([]RouteCheck, len(names)) var wg sync.WaitGroup for i, name := range names { wg.Add(1) go func(i int, name string) { defer wg.Done() results[i] = CheckRoute(ctx, cfg, name) }(i, name) } wg.Wait() return results } // CheckSummary formats the counts for a UI. func CheckSummary(results []RouteCheck) string { local, public := 0, 0 for _, result := range results { if result.LocalUp { local++ } if result.PublicUp { public++ } } return fmt.Sprintf("local %d/%d up ยท public %d/%d up", local, len(results), public, len(results)) }