mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
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 <andrea@gottardo.me>
This commit is contained in:
parent
e711ee5d22
commit
fd77965f23
@ -113,6 +113,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
|||||||
tailscale.com/net/stunserver from tailscale.com/cmd/derper
|
tailscale.com/net/stunserver from tailscale.com/cmd/derper
|
||||||
L tailscale.com/net/tcpinfo from tailscale.com/derp
|
L tailscale.com/net/tcpinfo from tailscale.com/derp
|
||||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
|
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/tsaddr from tailscale.com/ipn+
|
||||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||||
tailscale.com/net/wsconn from tailscale.com/cmd/derper+
|
tailscale.com/net/wsconn from tailscale.com/cmd/derper+
|
||||||
|
@ -735,6 +735,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
|||||||
tailscale.com/net/stun from tailscale.com/ipn/localapi+
|
tailscale.com/net/stun from tailscale.com/ipn/localapi+
|
||||||
L tailscale.com/net/tcpinfo from tailscale.com/derp
|
L tailscale.com/net/tcpinfo from tailscale.com/derp
|
||||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
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/tsaddr from tailscale.com/client/web+
|
||||||
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
|
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
|
||||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
|
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
|
||||||
|
@ -121,6 +121,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
tailscale.com/net/stun from tailscale.com/net/netcheck
|
tailscale.com/net/stun from tailscale.com/net/netcheck
|
||||||
L tailscale.com/net/tcpinfo from tailscale.com/derp
|
L tailscale.com/net/tcpinfo from tailscale.com/derp
|
||||||
tailscale.com/net/tlsdial from tailscale.com/cmd/tailscale/cli+
|
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/tsaddr from tailscale.com/client/web+
|
||||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
|
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
|
||||||
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
|
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
|
||||||
|
@ -322,6 +322,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
tailscale.com/net/stun from tailscale.com/ipn/localapi+
|
tailscale.com/net/stun from tailscale.com/ipn/localapi+
|
||||||
L tailscale.com/net/tcpinfo from tailscale.com/derp
|
L tailscale.com/net/tcpinfo from tailscale.com/derp
|
||||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
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/tsaddr from tailscale.com/client/web+
|
||||||
tailscale.com/net/tsdial from tailscale.com/cmd/tailscaled+
|
tailscale.com/net/tsdial from tailscale.com/cmd/tailscaled+
|
||||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
|
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
|
||||||
|
104
net/tlsdial/blockblame/blockblame.go
Normal file
104
net/tlsdial/blockblame/blockblame.go
Normal file
@ -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("support@fortinet.com"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Huawei",
|
||||||
|
match: certEmail("mobile@huawei.com"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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
net/tlsdial/blockblame/blockblame_test.go
Normal file
54
net/tlsdial/blockblame/blockblame_test.go
Normal file
@ -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")
|
||||||
|
}
|
||||||
|
}
|
@ -27,6 +27,7 @@
|
|||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/health"
|
"tailscale.com/health"
|
||||||
"tailscale.com/hostinfo"
|
"tailscale.com/hostinfo"
|
||||||
|
"tailscale.com/net/tlsdial/blockblame"
|
||||||
)
|
)
|
||||||
|
|
||||||
var counterFallbackOK int32 // atomic
|
var counterFallbackOK int32 // atomic
|
||||||
@ -44,6 +45,16 @@
|
|||||||
// Headscale, etc.
|
// Headscale, etc.
|
||||||
var tlsdialWarningPrinted sync.Map // map[string]bool
|
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.
|
// 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.
|
||||||
@ -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
|
// Perform some health checks on this certificate before we do
|
||||||
// any verification.
|
// any verification.
|
||||||
|
var cert *x509.Certificate
|
||||||
var selfSignedIssuer string
|
var selfSignedIssuer string
|
||||||
if certs := cs.PeerCertificates; len(certs) > 0 && certIsSelfSigned(certs[0]) {
|
if certs := cs.PeerCertificates; len(certs) > 0 {
|
||||||
selfSignedIssuer = certs[0].Issuer.String()
|
cert = certs[0]
|
||||||
|
if certIsSelfSigned(cert) {
|
||||||
|
selfSignedIssuer = cert.Issuer.String()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ht != nil {
|
if ht != nil {
|
||||||
defer func() {
|
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 != "" {
|
if retErr != nil && selfSignedIssuer != "" {
|
||||||
// Self-signed certs are never valid.
|
// Self-signed certs are never valid.
|
||||||
//
|
//
|
||||||
|
Loading…
Reference in New Issue
Block a user