2023-01-27 21:37:20 +00:00
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
2020-04-25 20:24:53 +00:00
2023-08-24 03:21:37 +00:00
// Package tlsdial generates tls.Config values and does x509 validation of
// certs. It bakes in the LetsEncrypt roots so even if the user's machine
// doesn't have TLS roots, we can at least connect to Tailscale's LetsEncrypt
// services. It's the unified point where we can add shared policy on outgoing
// TLS connections from the three places in the client that connect to Tailscale
// (logs, control, DERP).
2020-04-25 20:24:53 +00:00
package tlsdial
2020-06-01 16:01:37 +00:00
import (
2023-02-01 19:29:44 +00:00
"bytes"
2023-08-24 03:21:37 +00:00
"context"
2020-06-01 16:01:37 +00:00
"crypto/tls"
"crypto/x509"
"errors"
2023-02-01 19:29:44 +00:00
"fmt"
2021-10-01 04:13:38 +00:00
"log"
2023-08-24 03:21:37 +00:00
"net"
"net/http"
2021-10-01 04:13:38 +00:00
"os"
"sync"
"sync/atomic"
2020-06-01 16:01:37 +00:00
"time"
2022-01-24 18:52:57 +00:00
"tailscale.com/envknob"
2023-02-01 19:29:44 +00:00
"tailscale.com/health"
2024-08-10 20:46:47 +00:00
"tailscale.com/hostinfo"
2024-10-19 00:35:46 +00:00
"tailscale.com/net/tlsdial/blockblame"
2020-06-01 16:01:37 +00:00
)
2020-04-25 20:24:53 +00:00
2021-10-01 04:13:38 +00:00
var counterFallbackOK int32 // atomic
// If SSLKEYLOGFILE is set, it's a file to which we write our TLS private keys
// in a way that WireShark can read.
//
// See https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format
var sslKeyLogFile = os . Getenv ( "SSLKEYLOGFILE" )
2022-09-14 19:49:39 +00:00
var debug = envknob . RegisterBool ( "TS_DEBUG_TLS_DIAL" )
2021-10-01 04:13:38 +00:00
2023-02-01 19:29:44 +00:00
// 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
2024-10-19 00:35:46 +00:00
var mitmBlockWarnable = health . Register ( & health . Warnable {
Code : "blockblame-mitm-detected" ,
Title : "Network may be blocking Tailscale" ,
Text : func ( args health . Args ) string {
return fmt . Sprintf ( "Network equipment from %q may be blocking Tailscale traffic on this network. Connect to another network, or contact your network administrator for assistance." , args [ "manufacturer" ] )
} ,
Severity : health . SeverityMedium ,
ImpactsConnectivity : true ,
} )
2020-06-01 16:01:37 +00:00
// Config returns a tls.Config for connecting to a server.
2020-04-25 20:24:53 +00:00
// If base is non-nil, it's cloned as the base config before
// being configured and returned.
tsd, ipnlocal, etc: add tsd.System.HealthTracker, start some plumbing
This adds a health.Tracker to tsd.System, accessible via
a new tsd.System.HealthTracker method.
In the future, that new method will return a tsd.System-specific
HealthTracker, so multiple tsnet.Servers in the same process are
isolated. For now, though, it just always returns the temporary
health.Global value. That permits incremental plumbing over a number
of changes. When the second to last health.Global reference is gone,
then the tsd.System.HealthTracker implementation can return a private
Tracker.
The primary plumbing this does is adding it to LocalBackend and its
dozen and change health calls. A few misc other callers are also
plumbed. Subsequent changes will flesh out other parts of the tree
(magicsock, controlclient, etc).
Updates #11874
Updates #4136
Change-Id: Id51e73cfc8a39110425b6dc19d18b3975eac75ce
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-26 03:29:20 +00:00
// If ht is non-nil, it's used to report health errors.
func Config ( host string , ht * health . Tracker , base * tls . Config ) * tls . Config {
2020-04-25 20:24:53 +00:00
var conf * tls . Config
if base == nil {
conf = new ( tls . Config )
} else {
conf = base . Clone ( )
}
conf . ServerName = host
2021-10-01 04:13:38 +00:00
if n := sslKeyLogFile ; n != "" {
f , err := os . OpenFile ( n , os . O_CREATE | os . O_APPEND | os . O_WRONLY , 0600 )
if err != nil {
log . Fatal ( err )
}
log . Printf ( "WARNING: writing to SSLKEYLOGFILE %v" , n )
conf . KeyLogWriter = f
}
if conf . InsecureSkipVerify {
panic ( "unexpected base.InsecureSkipVerify" )
}
if conf . VerifyConnection != nil {
panic ( "unexpected base.VerifyConnection" )
}
// Set InsecureSkipVerify to prevent crypto/tls from doing its
// own cert verification, as do the same work that it'd do
// (with the baked-in fallback root) in the VerifyConnection hook.
conf . InsecureSkipVerify = true
2024-07-31 16:35:52 +00:00
conf . VerifyConnection = func ( cs tls . ConnectionState ) ( retErr error ) {
2024-08-10 20:46:47 +00:00
if host == "log.tailscale.io" && hostinfo . IsNATLabGuestVM ( ) {
// Allow log.tailscale.io TLS MITM for integration tests when
// the client's running within a NATLab VM.
return nil
}
2023-02-01 19:29:44 +00:00
// Perform some health checks on this certificate before we do
// any verification.
2024-10-19 00:35:46 +00:00
var cert * x509 . Certificate
2024-07-31 16:35:52 +00:00
var selfSignedIssuer string
2024-10-19 00:35:46 +00:00
if certs := cs . PeerCertificates ; len ( certs ) > 0 {
cert = certs [ 0 ]
if certIsSelfSigned ( cert ) {
selfSignedIssuer = cert . Issuer . String ( )
}
2024-07-31 16:35:52 +00:00
}
tsd, ipnlocal, etc: add tsd.System.HealthTracker, start some plumbing
This adds a health.Tracker to tsd.System, accessible via
a new tsd.System.HealthTracker method.
In the future, that new method will return a tsd.System-specific
HealthTracker, so multiple tsnet.Servers in the same process are
isolated. For now, though, it just always returns the temporary
health.Global value. That permits incremental plumbing over a number
of changes. When the second to last health.Global reference is gone,
then the tsd.System.HealthTracker implementation can return a private
Tracker.
The primary plumbing this does is adding it to LocalBackend and its
dozen and change health calls. A few misc other callers are also
plumbed. Subsequent changes will flesh out other parts of the tree
(magicsock, controlclient, etc).
Updates #11874
Updates #4136
Change-Id: Id51e73cfc8a39110425b6dc19d18b3975eac75ce
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-26 03:29:20 +00:00
if ht != nil {
2024-07-31 16:35:52 +00:00
defer func ( ) {
2024-10-19 00:35:46 +00:00
if retErr != nil && cert != nil {
// Is it a MITM SSL certificate from a well-known network appliance manufacturer?
// Show a dedicated warning.
m , ok := blockblame . VerifyCertificate ( cert )
if ok {
log . Printf ( "tlsdial: server cert for %q looks like %q equipment (could be blocking Tailscale)" , host , m . Name )
ht . SetUnhealthy ( mitmBlockWarnable , health . Args { "manufacturer" : m . Name } )
} else {
ht . SetHealthy ( mitmBlockWarnable )
}
} else {
ht . SetHealthy ( mitmBlockWarnable )
}
2024-07-31 16:35:52 +00:00
if retErr != nil && selfSignedIssuer != "" {
// Self-signed certs are never valid.
//
// TODO(bradfitz): plumb down the selfSignedIssuer as a
// structured health warning argument.
ht . SetTLSConnectionError ( cs . ServerName , fmt . Errorf ( "likely intercepted connection; certificate is self-signed by %v" , selfSignedIssuer ) )
} else {
// Ensure we clear any error state for this ServerName.
ht . SetTLSConnectionError ( cs . ServerName , nil )
if selfSignedIssuer != "" {
// Log the self-signed issuer, but don't treat it as an error.
log . Printf ( "tlsdial: warning: server cert for %q passed x509 validation but is self-signed by %q" , host , selfSignedIssuer )
}
}
} ( )
2023-02-01 19:29:44 +00:00
}
2021-10-01 04:13:38 +00:00
// First try doing x509 verification with the system's
// root CA pool.
opts := x509 . VerifyOptions {
DNSName : cs . ServerName ,
Intermediates : x509 . NewCertPool ( ) ,
}
for _ , cert := range cs . PeerCertificates [ 1 : ] {
opts . Intermediates . AddCert ( cert )
}
_ , errSys := cs . PeerCertificates [ 0 ] . Verify ( opts )
2022-09-14 19:49:39 +00:00
if debug ( ) {
2021-10-01 04:13:38 +00:00
log . Printf ( "tlsdial(sys %q): %v" , host , errSys )
}
2023-02-01 19:29:44 +00:00
// Always verify with our baked-in Let's Encrypt certificate,
// so we can log an informational message. This is useful for
// detecting SSL MiTM.
2021-10-01 04:13:38 +00:00
opts . Roots = bakedInRoots ( )
2023-02-01 19:29:44 +00:00
_ , bakedErr := cs . PeerCertificates [ 0 ] . Verify ( opts )
2022-09-14 19:49:39 +00:00
if debug ( ) {
2023-02-01 19:29:44 +00:00
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 )
}
}
2021-10-01 04:13:38 +00:00
}
2023-02-01 19:29:44 +00:00
if errSys == nil {
return nil
} else if bakedErr == nil {
2021-10-01 04:13:38 +00:00
atomic . AddInt32 ( & counterFallbackOK , 1 )
return nil
}
return errSys
}
2020-04-25 20:24:53 +00:00
return conf
}
2020-06-01 16:01:37 +00:00
2023-02-01 19:29:44 +00:00
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 )
}
2020-06-01 16:01:37 +00:00
// SetConfigExpectedCert modifies c to expect and verify that the server returns
// a certificate for the provided certDNSName.
2021-10-01 04:13:38 +00:00
//
// This is for user-configurable client-side domain fronting support,
// where we send one SNI value but validate a different cert.
2020-06-01 16:01:37 +00:00
func SetConfigExpectedCert ( c * tls . Config , certDNSName string ) {
if c . ServerName == certDNSName {
return
}
if c . ServerName == "" {
c . ServerName = certDNSName
return
}
if c . VerifyPeerCertificate != nil {
panic ( "refusing to override tls.Config.VerifyPeerCertificate" )
}
// Set InsecureSkipVerify to prevent crypto/tls from doing its
// own cert verification, but do the same work that it'd do
// (but using certDNSName) in the VerifyPeerCertificate hook.
c . InsecureSkipVerify = true
2021-10-01 04:13:38 +00:00
c . VerifyConnection = nil
2020-06-01 16:01:37 +00:00
c . VerifyPeerCertificate = func ( rawCerts [ ] [ ] byte , _ [ ] [ ] * x509 . Certificate ) error {
if len ( rawCerts ) == 0 {
return errors . New ( "no certs presented" )
}
certs := make ( [ ] * x509 . Certificate , len ( rawCerts ) )
for i , asn1Data := range rawCerts {
cert , err := x509 . ParseCertificate ( asn1Data )
if err != nil {
return err
}
certs [ i ] = cert
}
opts := x509 . VerifyOptions {
CurrentTime : time . Now ( ) ,
DNSName : certDNSName ,
Intermediates : x509 . NewCertPool ( ) ,
}
for _ , cert := range certs [ 1 : ] {
opts . Intermediates . AddCert ( cert )
}
2021-10-01 04:13:38 +00:00
_ , errSys := certs [ 0 ] . Verify ( opts )
2022-09-14 19:49:39 +00:00
if debug ( ) {
2021-10-01 04:13:38 +00:00
log . Printf ( "tlsdial(sys %q/%q): %v" , c . ServerName , certDNSName , errSys )
}
if errSys == nil {
return nil
}
opts . Roots = bakedInRoots ( )
2020-06-01 16:01:37 +00:00
_ , err := certs [ 0 ] . Verify ( opts )
2022-09-14 19:49:39 +00:00
if debug ( ) {
2021-10-01 04:13:38 +00:00
log . Printf ( "tlsdial(bake %q/%q): %v" , c . ServerName , certDNSName , err )
}
if err == nil {
return nil
}
return errSys
2020-06-01 16:01:37 +00:00
}
}
2021-10-01 04:13:38 +00:00
2023-08-24 03:21:37 +00:00
// NewTransport returns a new HTTP transport that verifies TLS certs using this
// package, including its baked-in LetsEncrypt fallback roots.
func NewTransport ( ) * http . Transport {
return & http . Transport {
DialTLSContext : func ( ctx context . Context , network , addr string ) ( net . Conn , error ) {
host , _ , err := net . SplitHostPort ( addr )
if err != nil {
return nil , err
}
var d tls . Dialer
tsd, ipnlocal, etc: add tsd.System.HealthTracker, start some plumbing
This adds a health.Tracker to tsd.System, accessible via
a new tsd.System.HealthTracker method.
In the future, that new method will return a tsd.System-specific
HealthTracker, so multiple tsnet.Servers in the same process are
isolated. For now, though, it just always returns the temporary
health.Global value. That permits incremental plumbing over a number
of changes. When the second to last health.Global reference is gone,
then the tsd.System.HealthTracker implementation can return a private
Tracker.
The primary plumbing this does is adding it to LocalBackend and its
dozen and change health calls. A few misc other callers are also
plumbed. Subsequent changes will flesh out other parts of the tree
(magicsock, controlclient, etc).
Updates #11874
Updates #4136
Change-Id: Id51e73cfc8a39110425b6dc19d18b3975eac75ce
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-26 03:29:20 +00:00
d . Config = Config ( host , nil , nil )
2023-08-24 03:21:37 +00:00
return d . DialContext ( ctx , network , addr )
} ,
}
}
2021-10-01 04:13:38 +00:00
/ *
letsEncryptX1 is the LetsEncrypt X1 root :
Certificate :
2022-08-02 16:33:46 +00:00
Data :
Version : 3 ( 0x2 )
Serial Number :
82 : 10 : cf : b0 : d2 : 40 : e3 : 59 : 44 : 63 : e0 : bb : 63 : 82 : 8 b : 00
Signature Algorithm : sha256WithRSAEncryption
Issuer : C = US , O = Internet Security Research Group , CN = ISRG Root X1
Validity
Not Before : Jun 4 11 : 04 : 38 2015 GMT
Not After : Jun 4 11 : 04 : 38 2035 GMT
Subject : C = US , O = Internet Security Research Group , CN = ISRG Root X1
Subject Public Key Info :
Public Key Algorithm : rsaEncryption
RSA Public - Key : ( 4096 bit )
2021-10-01 04:13:38 +00:00
We bake it into the binary as a fallback verification root ,
in case the system we ' re running on doesn ' t have it .
( Tailscale runs on some ancient devices . )
To test that this code is working on Debian / Ubuntu :
$ sudo mv / usr / share / ca - certificates / mozilla / ISRG_Root_X1 . crt { , . old }
$ sudo update - ca - certificates
Then restart tailscaled . To also test dnsfallback ' s use of it , nuke
your / etc / resolv . conf and it should still start & run fine .
* /
const letsEncryptX1 = `
-- -- - BEGIN CERTIFICATE -- -- -
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi / vVqbvYATyjb3miGbESTtrFj / RQSa78f0uoxmyF +
0 TM8ukj13Xnfs7j / EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4 + 3 mX6U
A5 / TR5d8mUgjU + g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8 + o + u3dpjq + sW
T8KOEUt + zwvo / 7 V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm / ELNKjD + Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x + UC
B5iPNgiV5 + I3lg02dZ77DnKxHZu8A / lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1 / ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0 + UXgVCwOPjdAvBbI + e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY / rOPNk3sgrDQoo //fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7 / vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB / wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH / MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu + ILlaS / V9lZL
ubhzEFnTIZd + 50 xx + 7 LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe + GnY + EgPbk6ZGQ
3 BebYhtF8GaV0nxvwuo77x / Py9auJ / GpsMiu / X1 + mvoiBOv / 2 X / qkSsisRcOj / KK
NFtY2PwByVS5uCbMiogziUwthDyC3 + 6 WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ + GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF + l + / + sKAIuvtd7u + Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4 + 63 SM1N95R1NbdWhscdCb + ZAJzVc
oyi3B43njTOQ5yOf + 1 CceWxG1bQVs5ZufpsMljq4Ui0 / 1 lvh + wjChP4kqKOJ2qxq
4 RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U / t7y0Ff / 9 yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc / ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn / eR44 / KJ4EBs + lVDR3veyJm + kXQ99b21 / + jh5Xos1AnX5iItreGCc =
-- -- - END CERTIFICATE -- -- -
`
var bakedInRootsOnce struct {
sync . Once
p * x509 . CertPool
}
func bakedInRoots ( ) * x509 . CertPool {
bakedInRootsOnce . Do ( func ( ) {
p := x509 . NewCertPool ( )
if ! p . AppendCertsFromPEM ( [ ] byte ( letsEncryptX1 ) ) {
panic ( "bogus PEM" )
}
bakedInRootsOnce . p = p
} )
return bakedInRootsOnce . p
}