mirror of
https://github.com/tailscale/tailscale.git
synced 2025-10-15 10:49:18 +00:00
prober: allow custom tls.Config for TLS probes (#17186)
Updates https://github.com/tailscale/corp/issues/28569 Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"cmp"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -68,7 +69,7 @@ type derpProber struct {
|
||||
ProbeMap ProbeClass
|
||||
|
||||
// Probe classes for probing individual derpers.
|
||||
tlsProbeFn func(string) ProbeClass
|
||||
tlsProbeFn func(string, *tls.Config) ProbeClass
|
||||
udpProbeFn func(string, int) ProbeClass
|
||||
meshProbeFn func(string, string) ProbeClass
|
||||
bwProbeFn func(string, string, int64) ProbeClass
|
||||
@@ -206,7 +207,7 @@ func (d *derpProber) probeMapFn(ctx context.Context) error {
|
||||
if d.probes[n] == nil {
|
||||
log.Printf("adding DERP TLS probe for %s (%s) every %v", server.Name, region.RegionName, d.tlsInterval)
|
||||
derpPort := cmp.Or(server.DERPPort, 443)
|
||||
d.probes[n] = d.p.Run(n, d.tlsInterval, labels, d.tlsProbeFn(fmt.Sprintf("%s:%d", server.HostName, derpPort)))
|
||||
d.probes[n] = d.p.Run(n, d.tlsInterval, labels, d.tlsProbeFn(fmt.Sprintf("%s:%d", server.HostName, derpPort), nil))
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -74,7 +74,7 @@ func TestDerpProber(t *testing.T) {
|
||||
p: p,
|
||||
derpMapURL: srv.URL,
|
||||
tlsInterval: time.Second,
|
||||
tlsProbeFn: func(_ string) ProbeClass { return FuncProbe(func(context.Context) error { return nil }) },
|
||||
tlsProbeFn: func(_ string, _ *tls.Config) ProbeClass { return FuncProbe(func(context.Context) error { return nil }) },
|
||||
udpInterval: time.Second,
|
||||
udpProbeFn: func(_ string, _ int) ProbeClass { return FuncProbe(func(context.Context) error { return nil }) },
|
||||
meshInterval: time.Second,
|
||||
|
@@ -5,6 +5,7 @@ package prober_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -40,7 +41,7 @@ func ExampleForEachAddr() {
|
||||
|
||||
// This function is called every time we discover a new IP address to check.
|
||||
makeTLSProbe := func(addr netip.Addr) []*prober.Probe {
|
||||
pf := prober.TLSWithIP(*hostname, netip.AddrPortFrom(addr, 443))
|
||||
pf := prober.TLSWithIP(netip.AddrPortFrom(addr, 443), &tls.Config{ServerName: *hostname})
|
||||
if *verbose {
|
||||
logger := logger.WithPrefix(log.Printf, fmt.Sprintf("[tls %s]: ", addr))
|
||||
pf = probeLogWrapper(logger, pf)
|
||||
|
@@ -9,9 +9,9 @@ import (
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/multierr"
|
||||
@@ -28,33 +28,31 @@ const letsEncryptStartedStaplingCRL int64 = 1746576000 // 2025-05-07 00:00:00 UT
|
||||
// The ProbeFunc connects to a hostPort (host:port string), does a TLS
|
||||
// handshake, verifies that the hostname matches the presented certificate,
|
||||
// checks certificate validity time and OCSP revocation status.
|
||||
func TLS(hostPort string) ProbeClass {
|
||||
//
|
||||
// The TLS config is optional and may be nil.
|
||||
func TLS(hostPort string, config *tls.Config) ProbeClass {
|
||||
return ProbeClass{
|
||||
Probe: func(ctx context.Context) error {
|
||||
certDomain, _, err := net.SplitHostPort(hostPort)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return probeTLS(ctx, certDomain, hostPort)
|
||||
return probeTLS(ctx, config, hostPort)
|
||||
},
|
||||
Class: "tls",
|
||||
}
|
||||
}
|
||||
|
||||
// TLSWithIP is like TLS, but dials the provided dialAddr instead
|
||||
// of using DNS resolution. The certDomain is the expected name in
|
||||
// the cert (and the SNI name to send).
|
||||
func TLSWithIP(certDomain string, dialAddr netip.AddrPort) ProbeClass {
|
||||
// TLSWithIP is like TLS, but dials the provided dialAddr instead of using DNS
|
||||
// resolution. Use config.ServerName to send SNI and validate the name in the
|
||||
// cert.
|
||||
func TLSWithIP(dialAddr netip.AddrPort, config *tls.Config) ProbeClass {
|
||||
return ProbeClass{
|
||||
Probe: func(ctx context.Context) error {
|
||||
return probeTLS(ctx, certDomain, dialAddr.String())
|
||||
return probeTLS(ctx, config, dialAddr.String())
|
||||
},
|
||||
Class: "tls",
|
||||
}
|
||||
}
|
||||
|
||||
func probeTLS(ctx context.Context, certDomain string, dialHostPort string) error {
|
||||
dialer := &tls.Dialer{Config: &tls.Config{ServerName: certDomain}}
|
||||
func probeTLS(ctx context.Context, config *tls.Config, dialHostPort string) error {
|
||||
dialer := &tls.Dialer{Config: config}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", dialHostPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to %q: %w", dialHostPort, err)
|
||||
@@ -108,6 +106,10 @@ func validateConnState(ctx context.Context, cs *tls.ConnectionState) (returnerr
|
||||
}
|
||||
|
||||
if len(leafCert.CRLDistributionPoints) == 0 {
|
||||
if !slices.Contains(leafCert.Issuer.Organization, "Let's Encrypt") {
|
||||
// LE certs contain a CRL, but certs from other CAs might not.
|
||||
return
|
||||
}
|
||||
if leafCert.NotBefore.Before(time.Unix(letsEncryptStartedStaplingCRL, 0)) {
|
||||
// Certificate might not have a CRL.
|
||||
return
|
||||
|
@@ -83,7 +83,7 @@ func TestTLSConnection(t *testing.T) {
|
||||
srv.StartTLS()
|
||||
defer srv.Close()
|
||||
|
||||
err = probeTLS(context.Background(), "fail.example.com", srv.Listener.Addr().String())
|
||||
err = probeTLS(context.Background(), &tls.Config{ServerName: "fail.example.com"}, srv.Listener.Addr().String())
|
||||
// The specific error message here is platform-specific ("certificate is not trusted"
|
||||
// on macOS and "certificate signed by unknown authority" on Linux), so only check
|
||||
// that it contains the word 'certificate'.
|
||||
@@ -269,40 +269,54 @@ func TestCRL(t *testing.T) {
|
||||
name string
|
||||
cert *x509.Certificate
|
||||
crlBytes []byte
|
||||
issuer pkix.Name
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
"ValidCert",
|
||||
leafCertParsed,
|
||||
emptyRlBytes,
|
||||
caCert.Issuer,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"RevokedCert",
|
||||
leafCertParsed,
|
||||
rlBytes,
|
||||
caCert.Issuer,
|
||||
"has been revoked on",
|
||||
},
|
||||
{
|
||||
"EmptyCRL",
|
||||
leafCertParsed,
|
||||
emptyRlBytes,
|
||||
caCert.Issuer,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"NoCRL",
|
||||
"NoCRLLetsEncrypt",
|
||||
leafCertParsed,
|
||||
nil,
|
||||
pkix.Name{CommonName: "tlsprobe.test", Organization: []string{"Let's Encrypt"}},
|
||||
"no CRL server presented in leaf cert for",
|
||||
},
|
||||
{
|
||||
"NoCRLOtherCA",
|
||||
leafCertParsed,
|
||||
nil,
|
||||
caCert.Issuer,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"NotBeforeCRLStaplingDate",
|
||||
noCRLStapledParsed,
|
||||
nil,
|
||||
caCert.Issuer,
|
||||
"",
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.cert.Issuer = tt.issuer
|
||||
cs := &tls.ConnectionState{PeerCertificates: []*x509.Certificate{tt.cert, caCert}}
|
||||
if tt.crlBytes != nil {
|
||||
crlServer.crlBytes = tt.crlBytes
|
||||
|
Reference in New Issue
Block a user