control/controlclient: add more Screen Time blocking detection

Updates #9658
Updates #12545

Change-Id: Iec1dad354a75f145567b4055d77b1c1db27c89e2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Co-authored-by: Andrea Gottardo <andrea@gottardo.me>
This commit is contained in:
Brad Fitzpatrick 2024-06-20 11:09:50 -07:00 committed by GitHub
parent bd50a3457d
commit fd3efd9bad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 60 additions and 4 deletions

View File

@ -15,6 +15,7 @@
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/httptest"
"net/netip"
@ -25,6 +26,7 @@
"slices"
"strings"
"sync"
"sync/atomic"
"time"
"go4.org/mem"
@ -62,6 +64,7 @@
// Direct is the client that connects to a tailcontrol server for a node.
type Direct struct {
httpc *http.Client // HTTP client used to talk to tailcontrol
interceptedDial *atomic.Bool // if non-nil, pointer to bool whether ScreenTime intercepted our dial
dialer *tsdial.Dialer
dnsCache *dnscache.Resolver
controlKnobs *controlknobs.Knobs // always non-nil
@ -258,23 +261,28 @@ func NewDirect(opts Options) (*Direct, error) {
// etc set).
httpc = http.DefaultClient
}
var interceptedDial *atomic.Bool
if httpc == nil {
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.Proxy = tshttpproxy.ProxyFromEnvironment
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
tr.TLSClientConfig = tlsdial.Config(serverURL.Hostname(), opts.HealthTracker, tr.TLSClientConfig)
tr.DialContext = dnscache.Dialer(opts.Dialer.SystemDial, dnsCache)
tr.DialTLSContext = dnscache.TLSDialer(opts.Dialer.SystemDial, dnsCache, tr.TLSClientConfig)
var dialFunc dialFunc
dialFunc, interceptedDial = makeScreenTimeDetectingDialFunc(opts.Dialer.SystemDial)
tr.DialContext = dnscache.Dialer(dialFunc, dnsCache)
tr.DialTLSContext = dnscache.TLSDialer(dialFunc, dnsCache, tr.TLSClientConfig)
tr.ForceAttemptHTTP2 = true
// Disable implicit gzip compression; the various
// handlers (register, map, set-dns, etc) do their own
// zstd compression per naclbox.
tr.DisableCompression = true
httpc = &http.Client{Transport: tr}
}
c := &Direct{
httpc: httpc,
interceptedDial: interceptedDial,
controlKnobs: opts.ControlKnobs,
getMachinePrivKey: opts.GetMachinePrivateKey,
serverURL: opts.ServerURL,
@ -464,6 +472,16 @@ func (c *Direct) hostInfoLocked() *tailcfg.Hostinfo {
return hi
}
var macOSScreenTime = health.Register(&health.Warnable{
Code: "macos-screen-time-controlclient",
Severity: health.SeverityHigh,
Title: "Tailscale blocked by Screen Time",
Text: func(args health.Args) string {
return "macOS Screen Time seems to be blocking Tailscale. Try disabling Screen Time in System Settings > Screen Time > Content & Privacy > Access to Web Content."
},
ImpactsConnectivity: true,
})
func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, newURL string, nks tkatype.MarshaledSignature, err error) {
if c.panicOnUse {
panic("tainted client")
@ -505,6 +523,11 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
c.logf("doLogin(regen=%v, hasUrl=%v)", regen, opt.URL != "")
if serverKey.IsZero() {
keys, err := loadServerPubKeys(ctx, c.httpc, c.serverURL)
if err != nil && c.interceptedDial != nil && c.interceptedDial.Load() {
c.health.SetUnhealthy(macOSScreenTime, nil)
} else {
c.health.SetHealthy(macOSScreenTime)
}
if err != nil {
return regen, opt.URL, nil, err
}
@ -1664,6 +1687,38 @@ func addLBHeader(req *http.Request, nodeKey key.NodePublic) {
}
}
type dialFunc = func(ctx context.Context, network, addr string) (net.Conn, error)
// makeScreenTimeDetectingDialFunc returns dialFunc, optionally wrapped (on
// Apple systems) with a func that sets the returned atomic.Bool for whether
// Screen Time seemed to intercept the connection.
//
// The returned *atomic.Bool is nil on non-Apple systems.
func makeScreenTimeDetectingDialFunc(dial dialFunc) (dialFunc, *atomic.Bool) {
switch runtime.GOOS {
case "darwin", "ios":
// Continue below.
default:
return dial, nil
}
ab := new(atomic.Bool)
return func(ctx context.Context, network, addr string) (net.Conn, error) {
c, err := dial(ctx, network, addr)
if err != nil {
return nil, err
}
ab.Store(isTCPLoopback(c.LocalAddr()) && isTCPLoopback(c.RemoteAddr()))
return c, nil
}, ab
}
func isTCPLoopback(a net.Addr) bool {
if ta, ok := a.(*net.TCPAddr); ok {
return ta.IP.IsLoopback()
}
return false
}
var (
metricMapRequestsActive = clientmetric.NewGauge("controlclient_map_requests_active")

View File

@ -406,8 +406,9 @@ func isLoopback(a net.Addr) bool {
}
var macOSScreenTime = health.Register(&health.Warnable{
Code: "macos-screen-time",
Title: "Tailscale blocked by Screen Time",
Code: "macos-screen-time",
Severity: health.SeverityHigh,
Title: "Tailscale blocked by Screen Time",
Text: func(args health.Args) string {
return "macOS Screen Time seems to be blocking Tailscale. Try disabling Screen Time in System Settings > Screen Time > Content & Privacy > Access to Web Content."
},