Public source validation / validate (push) Failing after 3m8s
247 lines
10 KiB
Go
247 lines
10 KiB
Go
package probe
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"errors"
|
|
"io"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/netip"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type fakeHTTPDoer struct {
|
|
response *http.Response
|
|
err error
|
|
request *http.Request
|
|
}
|
|
|
|
func (f *fakeHTTPDoer) Do(request *http.Request) (*http.Response, error) {
|
|
f.request = request
|
|
return f.response, f.err
|
|
}
|
|
|
|
func probeDefinition(probeType string) Definition {
|
|
return Definition{
|
|
ID: "probe-1",
|
|
ServiceID: "service-1",
|
|
Name: "probe",
|
|
Type: probeType,
|
|
Target: Target{Scheme: "http", Host: "service.internal", Port: 8080, Path: "/health"},
|
|
Interval: 30 * time.Second,
|
|
Timeout: 2 * time.Second,
|
|
Enabled: true,
|
|
Revision: 1,
|
|
VerifyTLS: false,
|
|
FollowRedirects: false,
|
|
}
|
|
}
|
|
|
|
func executorPolicy() NetworkPolicy {
|
|
return NetworkPolicy{Revision: 1, AllowedNetworks: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8"), netip.MustParsePrefix("127.0.0.0/8")}}
|
|
}
|
|
|
|
func executorResolver(_ context.Context, _ string) ([]netip.Addr, error) {
|
|
return []netip.Addr{netip.MustParseAddr("10.10.0.8")}, nil
|
|
}
|
|
|
|
func TestProbeExecutorHTTPAssertionsAndStatus(t *testing.T) {
|
|
body := `{"ok":true,"message":"ready"}`
|
|
doer := &fakeHTTPDoer{response: &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body))}}
|
|
definition := probeDefinition(TypeHTTP)
|
|
definition.ContentAssertion = map[string]any{"keyword": "ready", "json": map[string]any{"ok": true}}
|
|
executor := ProbeExecutor{Policy: executorPolicy(), Resolver: resolverFunc(executorResolver), HTTP: doer}
|
|
result, err := executor.Execute(context.Background(), definition)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.State != "up" || result.StatusCode == nil || *result.StatusCode != http.StatusOK {
|
|
t.Fatalf("unexpected HTTP result: %+v", result)
|
|
}
|
|
if doer.request == nil || doer.request.Method != http.MethodGet || doer.request.URL.String() != "http://service.internal:8080/health" {
|
|
t.Fatalf("unexpected request: %+v", doer.request)
|
|
}
|
|
}
|
|
|
|
func TestProbeExecutorHTTPFailureBodyLimitAndRedaction(t *testing.T) {
|
|
definition := probeDefinition(TypeHTTP)
|
|
definition.ExpectedStatusCodes = []int{http.StatusOK}
|
|
definition.SecretReference = "secret-ref-should-never-leak"
|
|
failureDoer := &fakeHTTPDoer{response: &http.Response{StatusCode: http.StatusServiceUnavailable, Body: io.NopCloser(strings.NewReader("down"))}}
|
|
failure, err := (ProbeExecutor{Policy: executorPolicy(), Resolver: resolverFunc(executorResolver), HTTP: failureDoer}).Execute(context.Background(), definition)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if failure.State != "down" || failure.ErrorClass != "status_not_expected" || strings.Contains(failure.ErrorMessage, definition.SecretReference) {
|
|
t.Fatalf("unexpected failure result: %+v", failure)
|
|
}
|
|
if failureDoer.request == nil || failureDoer.request.Header.Get("Authorization") != "" {
|
|
t.Fatal("probe credentials were placed in the request")
|
|
}
|
|
|
|
limitedPolicy := executorPolicy()
|
|
limitedPolicy.MaxResponseBytes = 4
|
|
largeDoer := &fakeHTTPDoer{response: &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("12345"))}}
|
|
large, err := (ProbeExecutor{Policy: limitedPolicy, Resolver: resolverFunc(executorResolver), HTTP: largeDoer}).Execute(context.Background(), definition)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if large.State != "unknown" || large.ErrorClass != "response_too_large" {
|
|
t.Fatalf("unexpected bounded response result: %+v", large)
|
|
}
|
|
}
|
|
|
|
func TestProbeExecutorHTTPRedirectPolicyAndTimeout(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
|
if request.URL.Path == "/start" {
|
|
http.Redirect(response, request, "/final", http.StatusFound)
|
|
return
|
|
}
|
|
_, _ = response.Write([]byte("final"))
|
|
}))
|
|
defer server.Close()
|
|
serverURL, err := url.Parse(server.URL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
port := mustPort(serverURL.Port())
|
|
resolver := resolverFunc(func(_ context.Context, host string) ([]netip.Addr, error) {
|
|
if host == "service.internal" {
|
|
return []netip.Addr{netip.MustParseAddr("127.0.0.1")}, nil
|
|
}
|
|
return nil, errors.New("unexpected host")
|
|
})
|
|
definition := probeDefinition(TypeHTTP)
|
|
definition.Target = Target{Scheme: "http", Host: "service.internal", Port: port, Path: "/start"}
|
|
definition.FollowRedirects = false
|
|
withoutRedirect, err := (ProbeExecutor{Policy: executorPolicy(), Resolver: resolver}).Execute(context.Background(), definition)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if withoutRedirect.State != "up" || withoutRedirect.StatusCode == nil || *withoutRedirect.StatusCode != http.StatusFound {
|
|
t.Fatalf("redirect was not safely stopped: %+v", withoutRedirect)
|
|
}
|
|
definition.FollowRedirects = true
|
|
withRedirect, err := (ProbeExecutor{Policy: executorPolicy(), Resolver: resolver}).Execute(context.Background(), definition)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if withRedirect.State != "up" || withRedirect.StatusCode == nil || *withRedirect.StatusCode != http.StatusOK {
|
|
t.Fatalf("redirect was not followed within policy: %+v", withRedirect)
|
|
}
|
|
}
|
|
|
|
func TestProbeExecutorTimeoutAndDNS(t *testing.T) {
|
|
definition := probeDefinition(TypeHTTP)
|
|
definition.Timeout = time.Second
|
|
// Use a doer that releases only when the executor's per-probe context expires.
|
|
blockingDoer := httpDoerFunc(func(request *http.Request) (*http.Response, error) {
|
|
<-request.Context().Done()
|
|
return nil, request.Context().Err()
|
|
})
|
|
result, err := (ProbeExecutor{Policy: executorPolicy(), Resolver: resolverFunc(executorResolver), HTTP: blockingDoer}).Execute(context.Background(), definition)
|
|
if !errors.Is(err, context.DeadlineExceeded) || result.ErrorClass != "transport_error" {
|
|
t.Fatalf("unexpected timeout result=%+v err=%v", result, err)
|
|
}
|
|
|
|
dnsDefinition := probeDefinition(TypeDNS)
|
|
dnsDefinition.Target = Target{Host: "dns.internal"}
|
|
dnsResolver := resolverFunc(func(_ context.Context, host string) ([]netip.Addr, error) {
|
|
if host != "dns.internal" {
|
|
return nil, errors.New("unexpected host")
|
|
}
|
|
return []netip.Addr{netip.MustParseAddr("10.0.0.1"), netip.MustParseAddr("10.0.0.2")}, nil
|
|
})
|
|
dnsResult, err := (ProbeExecutor{Policy: executorPolicy(), Resolver: dnsResolver}).Execute(context.Background(), dnsDefinition)
|
|
if err != nil || dnsResult.State != "up" || dnsResult.Attributes["addressCount"] != 2 {
|
|
t.Fatalf("unexpected DNS result=%+v err=%v", dnsResult, err)
|
|
}
|
|
}
|
|
|
|
func TestProbeExecutorTCPAndUnsupportedICMP(t *testing.T) {
|
|
definition := probeDefinition(TypeTCP)
|
|
definition.Target = Target{Host: "tcp.internal", Port: 443}
|
|
calledNetwork := ""
|
|
tcpResult, err := (ProbeExecutor{Policy: executorPolicy(), Resolver: resolverFunc(executorResolver), DialContext: func(_ context.Context, network, _ string) (net.Conn, error) {
|
|
calledNetwork = network
|
|
client, peer := net.Pipe()
|
|
_ = peer.Close()
|
|
return client, nil
|
|
}}).Execute(context.Background(), definition)
|
|
if err != nil || tcpResult.State != "up" || calledNetwork != "tcp" {
|
|
t.Fatalf("unexpected TCP result=%+v err=%v network=%s", tcpResult, err, calledNetwork)
|
|
}
|
|
|
|
icmpDefinition := probeDefinition(TypeICMP)
|
|
icmpDefinition.Target = Target{Host: "gateway.internal"}
|
|
icmpResult, err := (ProbeExecutor{Policy: executorPolicy()}).Execute(context.Background(), icmpDefinition)
|
|
if err != nil || icmpResult.State != "unknown" || icmpResult.ErrorClass != "unsupported" {
|
|
t.Fatalf("unexpected ICMP fallback result=%+v err=%v", icmpResult, err)
|
|
}
|
|
}
|
|
|
|
func TestProbeExecutorTLSCertificateFacts(t *testing.T) {
|
|
observed := time.Date(2026, time.August, 2, 12, 0, 0, 0, time.UTC)
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
certificateTemplate := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "tls.internal"}, DNSNames: []string{"tls.internal"}, NotBefore: observed.Add(-time.Hour), NotAfter: observed.Add(90 * 24 * time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}}
|
|
certificateDER, err := x509.CreateCertificate(rand.Reader, certificateTemplate, certificateTemplate, &privateKey.PublicKey, privateKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
serverCertificate := tls.Certificate{Certificate: [][]byte{certificateDER}, PrivateKey: privateKey}
|
|
definition := probeDefinition(TypeTLS)
|
|
definition.Target = Target{Scheme: "https", Host: "tls.internal", Port: 443}
|
|
definition.VerifyTLS = false
|
|
tlsResult, err := (ProbeExecutor{
|
|
Policy: executorPolicy(),
|
|
Resolver: resolverFunc(func(context.Context, string) ([]netip.Addr, error) {
|
|
return []netip.Addr{netip.MustParseAddr("10.0.0.3")}, nil
|
|
}),
|
|
Now: func() time.Time { return observed },
|
|
DialContext: func(_ context.Context, network, _ string) (net.Conn, error) {
|
|
if network != "tcp" {
|
|
return nil, errors.New("unexpected TLS network")
|
|
}
|
|
client, server := net.Pipe()
|
|
go func() {
|
|
defer server.Close()
|
|
_ = tls.Server(server, &tls.Config{Certificates: []tls.Certificate{serverCertificate}}).Handshake()
|
|
}()
|
|
return client, nil
|
|
},
|
|
}).Execute(context.Background(), definition)
|
|
if err != nil || tlsResult.State != "up" || tlsResult.Certificate == nil || tlsResult.Certificate.VerificationState != "valid" || tlsResult.Certificate.HostnameValid == nil || !*tlsResult.Certificate.HostnameValid {
|
|
t.Fatalf("unexpected TLS result=%+v err=%v", tlsResult, err)
|
|
}
|
|
|
|
attention := *certificateTemplate
|
|
attention.NotAfter = observed.Add(7 * 24 * time.Hour)
|
|
attentionResult := certificateFromX509(&attention, definition, func() time.Time { return observed })
|
|
if attentionResult.VerificationState != "attention" {
|
|
t.Fatalf("expected certificate attention, got %+v", attentionResult)
|
|
}
|
|
invalid := *certificateTemplate
|
|
invalid.DNSNames = []string{"other.internal"}
|
|
invalidResult := certificateFromX509(&invalid, definition, func() time.Time { return observed })
|
|
if invalidResult.VerificationState != "invalid" || invalidResult.HostnameValid == nil || *invalidResult.HostnameValid {
|
|
t.Fatalf("expected invalid hostname, got %+v", invalidResult)
|
|
}
|
|
}
|
|
|
|
type httpDoerFunc func(*http.Request) (*http.Response, error)
|
|
|
|
func (f httpDoerFunc) Do(request *http.Request) (*http.Response, error) { return f(request) }
|