// Copyright (c) 2026, Pim van Pelt package prober import ( "context" "crypto/tls" "fmt" "io" "net" "net/http" "strconv" "strings" buildinfo "git.ipng.ch/ipng/vpp-maglev/cmd" "git.ipng.ch/ipng/vpp-maglev/internal/health" ) var userAgent = "maglev-healthchecker/" + buildinfo.Version() + " (+https://git.ipng.ch/ipng/vpp-maglev)" // HTTPProbe sends a plain HTTP GET to cfg.Target inside the healthcheck netns. func HTTPProbe(ctx context.Context, cfg ProbeConfig) health.ProbeResult { return doHTTPProbe(ctx, cfg, false) } // HTTPSProbe sends an HTTP GET over TLS to cfg.Target inside the healthcheck netns. func HTTPSProbe(ctx context.Context, cfg ProbeConfig) health.ProbeResult { return doHTTPProbe(ctx, cfg, true) } func doHTTPProbe(ctx context.Context, cfg ProbeConfig, useTLS bool) health.ProbeResult { if cfg.HTTP == nil { return health.ProbeResult{OK: false, Layer: health.LayerUnknown, Code: "UNKNOWN", Detail: "missing HTTP params"} } p := cfg.HTTP port := cfg.Port if port == 0 { if useTLS { port = 443 } else { port = 80 } } // Always use "http" scheme: TLS (if any) is already applied to conn during // the netns dial phase. Using "https" here would cause http.Transport to // wrap conn in a second TLS layer, producing "http: server gave HTTP // response to HTTPS client". target := fmt.Sprintf("http://%s%s", net.JoinHostPort(cfg.Target.String(), strconv.Itoa(int(port))), p.Path) hostHeader := p.Host if hostHeader == "" { hostHeader = cfg.Target.String() } // Dial (and optionally handshake) inside the healthcheck netns. // The socket retains its netns after creation, so HTTP can be done outside. var conn net.Conn dialErr := inNetns(cfg.HealthCheckNetns, func() error { dialer := &net.Dialer{Timeout: cfg.Timeout} if cfg.ProbeSrc != nil { dialer.LocalAddr = &net.TCPAddr{IP: cfg.ProbeSrc} } c, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(cfg.Target.String(), strconv.Itoa(int(port)))) if err != nil { return err } if useTLS { tlsConn := tls.Client(c, tlsConfig(p.ServerName, p.InsecureSkipVerify)) if err := tlsConn.HandshakeContext(ctx); err != nil { _ = c.Close() return err } conn = tlsConn } else { conn = c } return nil }) if dialErr != nil { if isTimeout(dialErr) { return health.ProbeResult{OK: false, Layer: health.LayerL4, Code: "L4TOUT", Detail: dialErr.Error()} } // Distinguish TLS handshake failures (L6) from TCP connect failures (L4). // conn is non-nil only when TCP succeeded but TLS handshake failed. if useTLS && conn == nil && isTLSError(dialErr) { if isTimeout(dialErr) { return health.ProbeResult{OK: false, Layer: health.LayerL6, Code: "L6TOUT", Detail: dialErr.Error()} } return health.ProbeResult{OK: false, Layer: health.LayerL6, Code: "L6RSP", Detail: dialErr.Error()} } return health.ProbeResult{OK: false, Layer: health.LayerL4, Code: "L4CON", Detail: dialErr.Error()} } defer func() { _ = conn.Close() }() transport := &http.Transport{ DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { return conn, nil }, DisableKeepAlives: true, } client := &http.Client{ Transport: transport, Timeout: cfg.Timeout, CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse // never follow redirects }, } req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil) if err != nil { return health.ProbeResult{OK: false, Layer: health.LayerL7, Code: "L7RSP", Detail: err.Error()} } req.Host = hostHeader req.Header.Set("User-Agent", userAgent) resp, err := client.Do(req) if err != nil { if isTimeout(err) { return health.ProbeResult{OK: false, Layer: health.LayerL7, Code: "L7TOUT", Detail: err.Error()} } return health.ProbeResult{OK: false, Layer: health.LayerL7, Code: "L7RSP", Detail: err.Error()} } defer func() { _ = resp.Body.Close() }() if resp.StatusCode < p.ResponseCodeMin || resp.StatusCode > p.ResponseCodeMax { return health.ProbeResult{ OK: false, Layer: health.LayerL7, Code: "L7STS", Detail: fmt.Sprintf("HTTP %d (want %d-%d)", resp.StatusCode, p.ResponseCodeMin, p.ResponseCodeMax), } } if p.ResponseRegexp != nil { body, err := io.ReadAll(resp.Body) if err != nil { return health.ProbeResult{OK: false, Layer: health.LayerL7, Code: "L7TOUT", Detail: err.Error()} } if !p.ResponseRegexp.Match(body) { return health.ProbeResult{ OK: false, Layer: health.LayerL7, Code: "L7RSP", Detail: fmt.Sprintf("body did not match regexp %q", p.ResponseRegexp), } } } return health.ProbeResult{OK: true, Layer: health.LayerL7, Code: "L7OK"} } // isTLSError returns true if err originated from the TLS layer. func isTLSError(err error) bool { if err == nil { return false } _, ok := err.(tls.AlertError) return ok || strings.Contains(err.Error(), "tls:") }