@ -11,9 +11,11 @@
package tlsdial
package tlsdial
import (
import (
"bytes"
"crypto/tls"
"crypto/tls"
"crypto/x509"
"crypto/x509"
"errors"
"errors"
"fmt"
"log"
"log"
"os"
"os"
"sync"
"sync"
@ -21,6 +23,7 @@ import (
"time"
"time"
"tailscale.com/envknob"
"tailscale.com/envknob"
"tailscale.com/health"
)
)
var counterFallbackOK int32 // atomic
var counterFallbackOK int32 // atomic
@ -33,6 +36,11 @@ var sslKeyLogFile = os.Getenv("SSLKEYLOGFILE")
var debug = envknob . RegisterBool ( "TS_DEBUG_TLS_DIAL" )
var debug = envknob . RegisterBool ( "TS_DEBUG_TLS_DIAL" )
// tlsdialWarningPrinted tracks whether we've printed a warning about a given
// hostname already, to avoid log spam for users with custom DERP servers,
// Headscale, etc.
var tlsdialWarningPrinted sync . Map // map[string]bool
// Config returns a tls.Config for connecting to a server.
// Config returns a tls.Config for connecting to a server.
// If base is non-nil, it's cloned as the base config before
// If base is non-nil, it's cloned as the base config before
// being configured and returned.
// being configured and returned.
@ -66,6 +74,16 @@ func Config(host string, base *tls.Config) *tls.Config {
// (with the baked-in fallback root) in the VerifyConnection hook.
// (with the baked-in fallback root) in the VerifyConnection hook.
conf . InsecureSkipVerify = true
conf . InsecureSkipVerify = true
conf . VerifyConnection = func ( cs tls . ConnectionState ) error {
conf . VerifyConnection = func ( cs tls . ConnectionState ) error {
// Perform some health checks on this certificate before we do
// any verification.
if certIsSelfSigned ( cs . PeerCertificates [ 0 ] ) {
// Self-signed certs are never valid.
health . SetTLSConnectionError ( cs . ServerName , fmt . Errorf ( "certificate is self-signed" ) )
} else {
// Ensure we clear any error state for this ServerName.
health . SetTLSConnectionError ( cs . ServerName , nil )
}
// First try doing x509 verification with the system's
// First try doing x509 verification with the system's
// root CA pool.
// root CA pool.
opts := x509 . VerifyOptions {
opts := x509 . VerifyOptions {
@ -79,18 +97,27 @@ func Config(host string, base *tls.Config) *tls.Config {
if debug ( ) {
if debug ( ) {
log . Printf ( "tlsdial(sys %q): %v" , host , errSys )
log . Printf ( "tlsdial(sys %q): %v" , host , errSys )
}
}
if errSys == nil {
return nil
}
// If that failed, because the system's CA roots are old
// Always verify with our baked-in Let's Encrypt certificate,
// or broken, fall back to trying LetsEncrypt at least.
// so we can log an informational message. This is useful for
// detecting SSL MiTM.
opts . Roots = bakedInRoots ( )
opts . Roots = bakedInRoots ( )
_ , err := cs . PeerCertificates [ 0 ] . Verify ( opts )
_ , bak edE rr := cs . PeerCertificates [ 0 ] . Verify ( opts )
if debug ( ) {
if debug ( ) {
log . Printf ( "tlsdial(bake %q): %v" , host , err )
log . Printf ( "tlsdial(bake %q): %v" , host , bakedErr )
} else if bakedErr != nil {
if _ , loaded := tlsdialWarningPrinted . LoadOrStore ( host , true ) ; ! loaded {
if errSys == nil {
log . Printf ( "tlsdial: warning: server cert for %q is not a Let's Encrypt cert" , host )
} else {
log . Printf ( "tlsdial: error: server cert for %q failed to verify and is not a Let's Encrypt cert" , host )
}
}
}
}
if err == nil {
if errSys == nil {
return nil
} else if bakedErr == nil {
atomic . AddInt32 ( & counterFallbackOK , 1 )
atomic . AddInt32 ( & counterFallbackOK , 1 )
return nil
return nil
}
}
@ -99,6 +126,12 @@ func Config(host string, base *tls.Config) *tls.Config {
return conf
return conf
}
}
func certIsSelfSigned ( cert * x509 . Certificate ) bool {
// A certificate is determined to be self-signed if the certificate's
// subject is the same as its issuer.
return bytes . Equal ( cert . RawSubject , cert . RawIssuer )
}
// SetConfigExpectedCert modifies c to expect and verify that the server returns
// SetConfigExpectedCert modifies c to expect and verify that the server returns
// a certificate for the provided certDNSName.
// a certificate for the provided certDNSName.
//
//