Browse Source

net/tlsdial: call out firewalls blocking Tailscale in health warnings (#13840)

Updates tailscale/tailscale#13839

Adds a new blockblame package which can detect common MITM SSL certificates used by network appliances. We use this in `tlsdial` to display a dedicated health warning when we cannot connect to control, and a network appliance MITM attack is detected.

Signed-off-by: Andrea Gottardo <[email protected]>
Andrea Gottardo 1 year ago
parent
commit
fd77965f23

+ 1 - 0
cmd/derper/depaware.txt

@@ -113,6 +113,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
         tailscale.com/net/stunserver                                 from tailscale.com/cmd/derper
    L    tailscale.com/net/tcpinfo                                    from tailscale.com/derp
         tailscale.com/net/tlsdial                                    from tailscale.com/derp/derphttp
+        tailscale.com/net/tlsdial/blockblame                         from tailscale.com/net/tlsdial
         tailscale.com/net/tsaddr                                     from tailscale.com/ipn+
      💣 tailscale.com/net/tshttpproxy                                from tailscale.com/derp/derphttp+
         tailscale.com/net/wsconn                                     from tailscale.com/cmd/derper+

+ 1 - 0
cmd/k8s-operator/depaware.txt

@@ -735,6 +735,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
         tailscale.com/net/stun                                       from tailscale.com/ipn/localapi+
    L    tailscale.com/net/tcpinfo                                    from tailscale.com/derp
         tailscale.com/net/tlsdial                                    from tailscale.com/control/controlclient+
+        tailscale.com/net/tlsdial/blockblame                         from tailscale.com/net/tlsdial
         tailscale.com/net/tsaddr                                     from tailscale.com/client/web+
         tailscale.com/net/tsdial                                     from tailscale.com/control/controlclient+
      💣 tailscale.com/net/tshttpproxy                                from tailscale.com/clientupdate/distsign+

+ 1 - 0
cmd/tailscale/depaware.txt

@@ -121,6 +121,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
         tailscale.com/net/stun                                       from tailscale.com/net/netcheck
    L    tailscale.com/net/tcpinfo                                    from tailscale.com/derp
         tailscale.com/net/tlsdial                                    from tailscale.com/cmd/tailscale/cli+
+        tailscale.com/net/tlsdial/blockblame                         from tailscale.com/net/tlsdial
         tailscale.com/net/tsaddr                                     from tailscale.com/client/web+
      💣 tailscale.com/net/tshttpproxy                                from tailscale.com/clientupdate/distsign+
         tailscale.com/net/wsconn                                     from tailscale.com/control/controlhttp+

+ 1 - 0
cmd/tailscaled/depaware.txt

@@ -322,6 +322,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
         tailscale.com/net/stun                                       from tailscale.com/ipn/localapi+
    L    tailscale.com/net/tcpinfo                                    from tailscale.com/derp
         tailscale.com/net/tlsdial                                    from tailscale.com/control/controlclient+
+        tailscale.com/net/tlsdial/blockblame                         from tailscale.com/net/tlsdial
         tailscale.com/net/tsaddr                                     from tailscale.com/client/web+
         tailscale.com/net/tsdial                                     from tailscale.com/cmd/tailscaled+
      💣 tailscale.com/net/tshttpproxy                                from tailscale.com/clientupdate/distsign+

+ 104 - 0
net/tlsdial/blockblame/blockblame.go

@@ -0,0 +1,104 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package blockblame blames specific firewall manufacturers for blocking Tailscale,
+// by analyzing the SSL certificate presented when attempting to connect to a remote
+// server.
+package blockblame
+
+import (
+	"crypto/x509"
+	"strings"
+)
+
+// VerifyCertificate checks if the given certificate c is issued by a firewall manufacturer
+// that is known to block Tailscale connections. It returns true and the Manufacturer of
+// the equipment if it is, or false and nil if it is not.
+func VerifyCertificate(c *x509.Certificate) (m *Manufacturer, ok bool) {
+	for _, m := range Manufacturers {
+		if m.match != nil && m.match(c) {
+			return m, true
+		}
+	}
+	return nil, false
+}
+
+// Manufacturer represents a firewall manufacturer that may be blocking Tailscale.
+type Manufacturer struct {
+	// Name is the name of the firewall manufacturer to be
+	// mentioned in health warning messages, e.g. "Fortinet".
+	Name string
+	// match is a function that returns true if the given certificate looks like it might
+	// be issued by this manufacturer.
+	match matchFunc
+}
+
+var Manufacturers = []*Manufacturer{
+	{
+		Name:  "Aruba Networks",
+		match: issuerContains("Aruba"),
+	},
+	{
+		Name:  "Cisco",
+		match: issuerContains("Cisco"),
+	},
+	{
+		Name: "Fortinet",
+		match: matchAny(
+			issuerContains("Fortinet"),
+			certEmail("[email protected]"),
+		),
+	},
+	{
+		Name:  "Huawei",
+		match: certEmail("[email protected]"),
+	},
+	{
+		Name: "Palo Alto Networks",
+		match: matchAny(
+			issuerContains("Palo Alto Networks"),
+			issuerContains("PAN-FW"),
+		),
+	},
+	{
+		Name:  "Sophos",
+		match: issuerContains("Sophos"),
+	},
+	{
+		Name: "Ubiquiti",
+		match: matchAny(
+			issuerContains("UniFi"),
+			issuerContains("Ubiquiti"),
+		),
+	},
+}
+
+type matchFunc func(*x509.Certificate) bool
+
+func issuerContains(s string) matchFunc {
+	return func(c *x509.Certificate) bool {
+		return strings.Contains(strings.ToLower(c.Issuer.String()), strings.ToLower(s))
+	}
+}
+
+func certEmail(v string) matchFunc {
+	return func(c *x509.Certificate) bool {
+		for _, email := range c.EmailAddresses {
+			if strings.Contains(strings.ToLower(email), strings.ToLower(v)) {
+				return true
+			}
+		}
+		return false
+	}
+}
+
+func matchAny(fs ...matchFunc) matchFunc {
+	return func(c *x509.Certificate) bool {
+		for _, f := range fs {
+			if f(c) {
+				return true
+			}
+		}
+		return false
+	}
+}

+ 54 - 0
net/tlsdial/blockblame/blockblame_test.go

@@ -0,0 +1,54 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package blockblame
+
+import (
+	"crypto/x509"
+	"encoding/pem"
+	"testing"
+)
+
+const controlplaneDotTailscaleDotComPEM = `
+-----BEGIN CERTIFICATE-----
+MIIDkzCCAxqgAwIBAgISA2GOahsftpp59yuHClbDuoduMAoGCCqGSM49BAMDMDIx
+CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
+NjAeFw0yNDEwMTIxNjE2NDVaFw0yNTAxMTAxNjE2NDRaMCUxIzAhBgNVBAMTGmNv
+bnRyb2xwbGFuZS50YWlsc2NhbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD
+QgAExfraDUc1t185zuGtZlnPDtEJJSDBqvHN4vQcXSzSTPSAdDYHcA8fL5woU2Kg
+jK/2C0wm/rYy2Rre/ulhkS4wB6OCAhswggIXMA4GA1UdDwEB/wQEAwIHgDAdBgNV
+HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4E
+FgQUpArnpDj8Yh6NTgMOZjDPx0TuLmcwHwYDVR0jBBgwFoAUkydGmAOpUWiOmNbE
+QkjbI79YlNIwVQYIKwYBBQUHAQEESTBHMCEGCCsGAQUFBzABhhVodHRwOi8vZTYu
+by5sZW5jci5vcmcwIgYIKwYBBQUHMAKGFmh0dHA6Ly9lNi5pLmxlbmNyLm9yZy8w
+JQYDVR0RBB4wHIIaY29udHJvbHBsYW5lLnRhaWxzY2FsZS5jb20wEwYDVR0gBAww
+CjAIBgZngQwBAgEwggEDBgorBgEEAdZ5AgQCBIH0BIHxAO8AdgDgkrP8DB3I52g2
+H95huZZNClJ4GYpy1nLEsE2lbW9UBAAAAZKBujCyAAAEAwBHMEUCIQDHMgUaL4H9
+ZJa090ZOpBeEVu3+t+EF4HlHI1NqAai6uQIgeY/lLfjAXfcVgxBHHR4zjd0SzhaP
+TREHXzwxzN/8blkAdQDPEVbu1S58r/OHW9lpLpvpGnFnSrAX7KwB0lt3zsw7CAAA
+AZKBujh8AAAEAwBGMEQCICQwhMk45t9aiFjfwOC/y6+hDbszqSCpIv63kFElweUy
+AiAqTdkqmbqUVpnav5JdWkNERVAIlY4jqrThLsCLZYbNszAKBggqhkjOPQQDAwNn
+ADBkAjALyfgAt1XQp1uSfxy4GapR5OsmjEMBRVq6IgsPBlCRBfmf0Q3/a6mF0pjb
+Sj4oa+cCMEhZk4DmBTIdZY9zjuh8s7bXNfKxUQS0pEhALtXqyFr+D5dF7JcQo9+s
+Z98JY7/PCA==
+-----END CERTIFICATE-----`
+
+func TestVerifyCertificateOurControlPlane(t *testing.T) {
+	p, _ := pem.Decode([]byte(controlplaneDotTailscaleDotComPEM))
+	if p == nil {
+		t.Fatalf("failed to extract certificate bytes for controlplane.tailscale.com")
+		return
+	}
+	cert, err := x509.ParseCertificate(p.Bytes)
+	if err != nil {
+		t.Fatalf("failed to parse certificate: %v", err)
+		return
+	}
+	m, found := VerifyCertificate(cert)
+	if found {
+		t.Fatalf("expected to not get a result for the controlplane.tailscale.com certificate")
+	}
+	if m != nil {
+		t.Fatalf("expected nil manufacturer for controlplane.tailscale.com certificate")
+	}
+}

+ 30 - 2
net/tlsdial/tlsdial.go

@@ -27,6 +27,7 @@ import (
 	"tailscale.com/envknob"
 	"tailscale.com/health"
 	"tailscale.com/hostinfo"
+	"tailscale.com/net/tlsdial/blockblame"
 )
 
 var counterFallbackOK int32 // atomic
@@ -44,6 +45,16 @@ var debug = envknob.RegisterBool("TS_DEBUG_TLS_DIAL")
 // Headscale, etc.
 var tlsdialWarningPrinted sync.Map // map[string]bool
 
+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,
+})
+
 // Config returns a tls.Config for connecting to a server.
 // If base is non-nil, it's cloned as the base config before
 // being configured and returned.
@@ -86,12 +97,29 @@ func Config(host string, ht *health.Tracker, base *tls.Config) *tls.Config {
 
 		// Perform some health checks on this certificate before we do
 		// any verification.
+		var cert *x509.Certificate
 		var selfSignedIssuer string
-		if certs := cs.PeerCertificates; len(certs) > 0 && certIsSelfSigned(certs[0]) {
-			selfSignedIssuer = certs[0].Issuer.String()
+		if certs := cs.PeerCertificates; len(certs) > 0 {
+			cert = certs[0]
+			if certIsSelfSigned(cert) {
+				selfSignedIssuer = cert.Issuer.String()
+			}
 		}
 		if ht != nil {
 			defer func() {
+				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)
+				}
 				if retErr != nil && selfSignedIssuer != "" {
 					// Self-signed certs are never valid.
 					//