// Package health actively probes every proxy by fetching a URL through the // proxy itself, keeps per-proxy threshold state, and pushes status-affecting // transitions to the reconciler over a channel. The engine owns health // state; the reconciler owns its representation in the Proxy's status. package health import ( "context" "crypto/tls" "fmt" "net" "net/http" "net/url" "slices" "time" crawlv1alpha1 "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/api/v1alpha1" ) // probeResult is the outcome of a single through-the-proxy probe. type probeResult struct { ok bool latency time.Duration err error } // probe fetches hc.ProbeURL through the proxy at proxyURL. For an https // probe URL the transport issues CONNECT to the proxy and TLS-handshakes // through the tunnel; a proxy that accepts TCP but cannot egress answers // CONNECT with a non-200, which client.Do surfaces as an error, not a // response — so success requires err == nil AND an expected status code. // tlsCfg is nil in production (system roots); tests and private-CA setups // inject their own. func probe(ctx context.Context, proxyURL *url.URL, hc crawlv1alpha1.HealthCheckSpec, tlsCfg *tls.Config) probeResult { timeout := time.Duration(hc.TimeoutSeconds) * time.Second transport := &http.Transport{ Proxy: http.ProxyURL(proxyURL), // Load-bearing: with keep-alives on, net/http caches the established // CONNECT tunnel and later probes would never re-exercise CONNECT — // exactly the failure this probe exists to catch. DisableKeepAlives: true, ForceAttemptHTTP2: false, TLSHandshakeTimeout: timeout, ResponseHeaderTimeout: timeout, TLSClientConfig: tlsCfg, DialContext: (&net.Dialer{Timeout: timeout}).DialContext, } defer transport.CloseIdleConnections() client := &http.Client{Transport: transport, Timeout: timeout} req, err := http.NewRequestWithContext(ctx, http.MethodGet, hc.ProbeURL, nil) if err != nil { return probeResult{err: fmt.Errorf("building probe request: %w", err)} } start := time.Now() resp, err := client.Do(req) latency := time.Since(start) if err != nil { return probeResult{latency: latency, err: err} } defer func() { _ = resp.Body.Close() }() if !slices.Contains(hc.ExpectedStatusCodes, int32(resp.StatusCode)) { return probeResult{latency: latency, err: fmt.Errorf("unexpected status %d", resp.StatusCode)} } return probeResult{ok: true, latency: latency} }