tsd, all: add Sys.ExtraRootCAs, plumb through TLS dial paths

Add ExtraRootCAs *x509.CertPool to tsd.System and plumb it through
the control client, noise transport, DERP, and wgengine layers so
that platforms like Android can inject user-installed CA certificates
into Go's TLS verification.

tlsdial.Config now honors base.RootCAs as additional trusted roots,
tried after system roots and before the baked-in LetsEncrypt fallback.
SetConfigExpectedCert gets the same treatment for domain-fronted DERP.

The Android client will set sys.ExtraRootCAs with a pool built from
x509.SystemCertPool + user-installed certs obtained via the Android
KeyStore API, replacing the current SSL_CERT_DIR environment variable
approach.

Updates #8085

Change-Id: Iecce0fd140cd5aa0331b124e55a7045e24d8e0c2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-04-07 19:09:19 +00:00
committed by Brad Fitzpatrick
parent c4cb5eb809
commit a182b864ac
13 changed files with 108 additions and 4 deletions
+12
View File
@@ -9,6 +9,8 @@ import (
"context"
"crypto"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/binary"
"encoding/json"
"errors"
@@ -74,6 +76,7 @@ type Direct struct {
logf logger.Logf
netMon *netmon.Monitor // non-nil
health *health.Tracker
extraRootCAs *x509.CertPool // additional trusted root CAs; or nil
busClient *eventbus.Client
clientVersionPub *eventbus.Publisher[tailcfg.ClientVersion]
autoUpdatePub *eventbus.Publisher[AutoUpdate]
@@ -141,6 +144,7 @@ type Options struct {
NoiseTestClient *http.Client // optional HTTP client to use for noise RPCs (tests only)
DebugFlags []string // debug settings to send to control
HealthTracker *health.Tracker
ExtraRootCAs *x509.CertPool // additional trusted root CAs; or nil
PopBrowserURL func(url string) // optional func to open browser
Dialer *tsdial.Dialer // non-nil
C2NHandler http.Handler // or nil
@@ -297,6 +301,12 @@ func NewDirect(opts Options) (*Direct, error) {
f(tr)
}
}
if opts.ExtraRootCAs != nil {
if tr.TLSClientConfig == nil {
tr.TLSClientConfig = &tls.Config{}
}
tr.TLSClientConfig.RootCAs = opts.ExtraRootCAs
}
tr.TLSClientConfig = tlsdial.Config(opts.HealthTracker, tr.TLSClientConfig)
var dialFunc netx.DialFunc
dialFunc, interceptedDial = makeScreenTimeDetectingDialFunc(opts.Dialer.SystemDial)
@@ -324,6 +334,7 @@ func NewDirect(opts Options) (*Direct, error) {
debugFlags: opts.DebugFlags,
netMon: netMon,
health: opts.HealthTracker,
extraRootCAs: opts.ExtraRootCAs,
pinger: opts.Pinger,
polc: cmp.Or(opts.PolicyClient, policyclient.Client(policyclient.NoPolicyClient{})),
popBrowser: opts.PopBrowserURL,
@@ -1631,6 +1642,7 @@ func (c *Direct) getNoiseClient() (*ts2021.Client, error) {
Logf: c.logf,
NetMon: c.netMon,
HealthTracker: c.health,
ExtraRootCAs: c.extraRootCAs,
DialPlan: dp,
})
if err != nil {
+3
View File
@@ -479,6 +479,9 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, optAddr netip.Ad
// Disable HTTP2, since h2 can't do protocol switching.
tr.TLSClientConfig.NextProtos = []string{}
tr.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}
if a.ExtraRootCAs != nil {
tr.TLSClientConfig.RootCAs = a.ExtraRootCAs
}
tr.TLSClientConfig = tlsdial.Config(a.HealthTracker, tr.TLSClientConfig)
if !tr.TLSClientConfig.InsecureSkipVerify {
panic("unexpected") // should be set by tlsdial.Config
+4
View File
@@ -4,6 +4,7 @@
package controlhttp
import (
"crypto/x509"
"net/http"
"net/url"
"sync/atomic"
@@ -85,6 +86,9 @@ type Dialer struct {
// HealthTracker, if non-nil, is the health tracker to use.
HealthTracker *health.Tracker
// ExtraRootCAs, if non-nil, specifies additional trusted root CAs for TLS.
ExtraRootCAs *x509.CertPool
// DialPlan, if set, contains instructions from the control server on
// how to connect to it. If present, we will try the methods in this
// plan before falling back to DNS.
+5
View File
@@ -7,6 +7,7 @@ import (
"bytes"
"cmp"
"context"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
@@ -86,6 +87,9 @@ type ClientOpts struct {
// HealthTracker, if non-nil, is the health tracker to use.
HealthTracker *health.Tracker
// ExtraRootCAs, if non-nil, specifies additional trusted root CAs for TLS.
ExtraRootCAs *x509.CertPool
// DialPlan, if set, is a function that should return an explicit plan
// on how to connect to the server.
DialPlan func() *tailcfg.ControlDialPlan
@@ -252,6 +256,7 @@ func (nc *Client) dial(ctx context.Context) (*Conn, error) {
Logf: nc.logf,
NetMon: nc.opts.NetMon,
HealthTracker: nc.opts.HealthTracker,
ExtraRootCAs: nc.opts.ExtraRootCAs,
Clock: tstime.StdClock{},
}
clientConn, err := chd.Dial(ctx)