diff --git a/cmd/derpprobe/derpprobe.go b/cmd/derpprobe/derpprobe.go index 3adde9f81..bf66943c5 100644 --- a/cmd/derpprobe/derpprobe.go +++ b/cmd/derpprobe/derpprobe.go @@ -9,7 +9,9 @@ import ( "bytes" "context" crand "crypto/rand" + "crypto/x509" "encoding/json" + "errors" "flag" "fmt" "html" @@ -33,11 +35,21 @@ var ( listen = flag.String("listen", ":8030", "HTTP listen address") ) +// certReissueAfter is the time after which we expect all certs to be +// reissued, at minimum. +// +// This is currently set to the date of the LetsEncrypt ALPN revocation event of Jan 2022: +// https://community.letsencrypt.org/t/questions-about-renewing-before-tls-alpn-01-revocations/170449 +// +// If there's another revocation event, bump this again. +var certReissueAfter = time.Unix(1643226768, 0) + var ( mu sync.Mutex state = map[nodePair]pairStatus{} lastDERPMap *tailcfg.DERPMap lastDERPMapAt time.Time + certs = map[string]*x509.Certificate{} ) func main() { @@ -46,6 +58,12 @@ func main() { log.Fatal(http.ListenAndServe(*listen, http.HandlerFunc(serve))) } +func setCert(name string, cert *x509.Certificate) { + mu.Lock() + defer mu.Unlock() + certs[name] = cert +} + type overallStatus struct { good, bad []string } @@ -93,6 +111,27 @@ func getOverallStatus() (o overallStatus) { } } } + + var subjs []string + for k := range certs { + subjs = append(subjs, k) + } + sort.Strings(subjs) + + soon := time.Now().Add(14 * 24 * time.Hour) // in 2 weeks; autocert does 30 days by default + for _, s := range subjs { + cert := certs[s] + if cert.NotBefore.Before(certReissueAfter) { + o.addBadf("cert %q needs reissuing; NotBefore=%v", s, cert.NotBefore.Format(time.RFC3339)) + continue + } + if cert.NotAfter.Before(soon) { + o.addBadf("cert %q expiring soon (%v); wasn't auto-refreshed", s, cert.NotAfter.Format(time.RFC3339)) + continue + } + o.addGoodf("cert %q good %v - %v", s, cert.NotBefore.Format(time.RFC3339), cert.NotAfter.Format(time.RFC3339)) + } + return } @@ -359,6 +398,21 @@ func newConn(ctx context.Context, dm *tailcfg.DERPMap, n *tailcfg.DERPNode) (*de if err != nil { return nil, err } + cs, ok := dc.TLSConnectionState() + if !ok { + dc.Close() + return nil, errors.New("no TLS state") + } + if len(cs.PeerCertificates) == 0 { + dc.Close() + return nil, errors.New("no peer certificates") + } + if cs.ServerName != n.HostName { + dc.Close() + return nil, fmt.Errorf("TLS server name %q != derp hostname %q", cs.ServerName, n.HostName) + } + setCert(cs.ServerName, cs.PeerCertificates[0]) + errc := make(chan error, 1) go func() { m, err := dc.Recv() diff --git a/derp/derphttp/derphttp_client.go b/derp/derphttp/derphttp_client.go index 1e47e1e13..e520faf26 100644 --- a/derp/derphttp/derphttp_client.go +++ b/derp/derphttp/derphttp_client.go @@ -72,6 +72,7 @@ type Client struct { client *derp.Client connGen int // incremented once per new connection; valid values are >0 serverPubKey key.NodePublic + tlsState *tls.ConnectionState pingOut map[derp.PingMessage]chan<- bool // chan to send to on pong } @@ -124,6 +125,17 @@ func (c *Client) Connect(ctx context.Context) error { return err } +// TLSConnectionState returns the last TLS connection state, if any. +// The client must already be connected. +func (c *Client) TLSConnectionState() (_ *tls.ConnectionState, ok bool) { + c.mu.Lock() + defer c.mu.Unlock() + if c.closed || c.client == nil { + return nil, false + } + return c.tlsState, c.tlsState != nil +} + // ServerPublicKey returns the server's public key. // // It only returns a non-zero value once a connection has succeeded @@ -318,6 +330,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien var httpConn net.Conn // a TCP conn or a TLS conn; what we speak HTTP to var serverPub key.NodePublic // or zero if unknown (if not using TLS or TLS middlebox eats it) var serverProtoVersion int + var tlsState *tls.ConnectionState if c.useHTTPS() { tlsConn := c.tlsClient(tcpConn, node) httpConn = tlsConn @@ -340,9 +353,10 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien // Note that we're not specifically concerned about TLS downgrade // attacks. TLS handles that fine: // https://blog.gypsyengineer.com/en/security/how-does-tls-1-3-protect-against-downgrade-attacks.html - connState := tlsConn.ConnectionState() - if connState.Version >= tls.VersionTLS13 { - serverPub, serverProtoVersion = parseMetaCert(connState.PeerCertificates) + cs := tlsConn.ConnectionState() + tlsState = &cs + if cs.Version >= tls.VersionTLS13 { + serverPub, serverProtoVersion = parseMetaCert(cs.PeerCertificates) } } else { httpConn = tcpConn @@ -409,6 +423,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien c.serverPubKey = derpClient.ServerPublicKey() c.client = derpClient c.netConn = tcpConn + c.tlsState = tlsState c.connGen++ return c.client, c.connGen, nil }