diff --git a/control/controlhttp/client.go b/control/controlhttp/client.go index fe9ccccac..7c84d214e 100644 --- a/control/controlhttp/client.go +++ b/control/controlhttp/client.go @@ -32,12 +32,14 @@ "net/http/httptrace" "net/netip" "net/url" + "runtime" "sort" "sync/atomic" "time" "tailscale.com/control/controlbase" "tailscale.com/envknob" + "tailscale.com/health" "tailscale.com/net/dnscache" "tailscale.com/net/dnsfallback" "tailscale.com/net/netutil" @@ -396,12 +398,28 @@ func (a *Dialer) resolver() *dnscache.Resolver { } } +func isLoopback(a net.Addr) bool { + if ta, ok := a.(*net.TCPAddr); ok { + return ta.IP.IsLoopback() + } + return false +} + +var macOSScreenTime = health.Register(&health.Warnable{ + Code: "macos-screen-time", + 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, +}) + // tryURLUpgrade connects to u, and tries to upgrade it to a net.Conn. If addr // is valid, then no DNS is used and the connection will be made to the // provided address. // // Only the provided ctx is used, not a.ctx. -func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr, init []byte) (net.Conn, error) { +func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr, init []byte) (_ net.Conn, retErr error) { var dns *dnscache.Resolver // If we were provided an address to dial, then create a resolver that just @@ -423,6 +441,30 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr, dialer = stdDialer.DialContext } + // On macOS, see if Screen Time is blocking things. + if runtime.GOOS == "darwin" { + var proxydIntercepted atomic.Bool // intercepted by macOS webfilterproxyd + origDialer := dialer + dialer = func(ctx context.Context, network, address string) (net.Conn, error) { + c, err := origDialer(ctx, network, address) + if err != nil { + return nil, err + } + if isLoopback(c.LocalAddr()) && isLoopback(c.RemoteAddr()) { + proxydIntercepted.Store(true) + } + return c, nil + } + defer func() { + if retErr != nil && proxydIntercepted.Load() { + a.HealthTracker.SetUnhealthy(macOSScreenTime, nil) + retErr = fmt.Errorf("macOS Screen Time is blocking network access: %w", retErr) + } else { + a.HealthTracker.SetHealthy(macOSScreenTime) + } + }() + } + tr := http.DefaultTransport.(*http.Transport).Clone() defer tr.CloseIdleConnections() tr.Proxy = a.getProxyFunc()