From 28ad910840e0deaf63a1c5b24d4be6085d4e8494 Mon Sep 17 00:00:00 2001 From: Will Norris Date: Thu, 26 Oct 2023 11:35:41 -0700 Subject: [PATCH] ipn: add user pref for running web client This is not currently exposed as a user-settable preference through `tailscale up` or `tailscale set`. Instead, the preference is set when turning the web client on and off via localapi. In a subsequent commit, the pref will be used to automatically start the web client on startup when appropriate. Updates tailscale/corp#14335 Signed-off-by: Will Norris --- cmd/tailscale/cli/cli_test.go | 3 +++ ipn/conf.go | 5 +++++ ipn/ipn_clone.go | 1 + ipn/ipn_view.go | 2 ++ ipn/ipnlocal/local.go | 14 ++++++++++++++ ipn/localapi/localapi.go | 10 ++++++++++ ipn/prefs.go | 22 ++++++++++++++++++++++ ipn/prefs_test.go | 1 + 8 files changed, 58 insertions(+) diff --git a/cmd/tailscale/cli/cli_test.go b/cmd/tailscale/cli/cli_test.go index 91d65c0f9..dca90c9da 100644 --- a/cmd/tailscale/cli/cli_test.go +++ b/cmd/tailscale/cli/cli_test.go @@ -810,6 +810,9 @@ func TestPrefFlagMapping(t *testing.T) { case "Egg": // Not applicable. continue + case "RunWebClient": + // TODO(tailscale/corp#14335): Currently behind a feature flag. + continue } t.Errorf("unexpected new ipn.Pref field %q is not handled by up.go (see addPrefFlagMapping and checkForAccidentalSettingReverts)", prefName) } diff --git a/ipn/conf.go b/ipn/conf.go index a4ab2deb9..d55146d58 100644 --- a/ipn/conf.go +++ b/ipn/conf.go @@ -36,6 +36,7 @@ type ConfigVAlpha struct { PostureChecking opt.Bool `json:",omitempty"` RunSSHServer opt.Bool `json:",omitempty"` // Tailscale SSH + RunWebClient opt.Bool `json:",omitempty"` ShieldsUp opt.Bool `json:",omitempty"` AutoUpdate *AutoUpdatePrefs `json:",omitempty"` ServeConfigTemp *ServeConfig `json:",omitempty"` // TODO(bradfitz,maisem): make separate stable type for this @@ -113,6 +114,10 @@ func (c *ConfigVAlpha) ToPrefs() (MaskedPrefs, error) { mp.RunSSH = c.RunSSHServer.EqualBool(true) mp.RunSSHSet = true } + if c.RunWebClient != "" { + mp.RunWebClient = c.RunWebClient.EqualBool(true) + mp.RunWebClientSet = true + } if c.ShieldsUp != "" { mp.ShieldsUp = c.ShieldsUp.EqualBool(true) mp.ShieldsUpSet = true diff --git a/ipn/ipn_clone.go b/ipn/ipn_clone.go index 7f38f9397..3d0dbda34 100644 --- a/ipn/ipn_clone.go +++ b/ipn/ipn_clone.go @@ -38,6 +38,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct { ExitNodeAllowLANAccess bool CorpDNS bool RunSSH bool + RunWebClient bool WantRunning bool LoggedOut bool ShieldsUp bool diff --git a/ipn/ipn_view.go b/ipn/ipn_view.go index a3ef660fa..af5870f49 100644 --- a/ipn/ipn_view.go +++ b/ipn/ipn_view.go @@ -71,6 +71,7 @@ func (v PrefsView) ExitNodeIP() netip.Addr { return v.ж.ExitNodeIP func (v PrefsView) ExitNodeAllowLANAccess() bool { return v.ж.ExitNodeAllowLANAccess } func (v PrefsView) CorpDNS() bool { return v.ж.CorpDNS } func (v PrefsView) RunSSH() bool { return v.ж.RunSSH } +func (v PrefsView) RunWebClient() bool { return v.ж.RunWebClient } func (v PrefsView) WantRunning() bool { return v.ж.WantRunning } func (v PrefsView) LoggedOut() bool { return v.ж.LoggedOut } func (v PrefsView) ShieldsUp() bool { return v.ж.ShieldsUp } @@ -100,6 +101,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct { ExitNodeAllowLANAccess bool CorpDNS bool RunSSH bool + RunWebClient bool WantRunning bool LoggedOut bool ShieldsUp bool diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index c81df8af2..daae32c5d 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -168,6 +168,7 @@ type LocalBackend struct { logFlushFunc func() // or nil if SetLogFlusher wasn't called em *expiryManager // non-nil sshAtomicBool atomic.Bool + webclientAtomicBool atomic.Bool shutdownCalled bool // if Shutdown has been called debugSink *capture.Sink sockstatLogger *sockstatlog.Logger @@ -2500,6 +2501,7 @@ func (b *LocalBackend) setTCPPortsIntercepted(ports []uint16) { // and shouldInterceptTCPPortAtomic from the prefs p, which may be !Valid(). func (b *LocalBackend) setAtomicValuesFromPrefsLocked(p ipn.PrefsView) { b.sshAtomicBool.Store(p.Valid() && p.RunSSH() && envknob.CanSSHD()) + b.webclientAtomicBool.Store(p.Valid() && p.RunWebClient()) if !p.Valid() { b.containsViaIPFuncAtomic.Store(tsaddr.FalseContainsIPFunc()) @@ -2918,6 +2920,11 @@ func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (ipn.PrefsView, error) { b.logf("EditPrefs requests SSH, but disabled by envknob; returning error") return ipn.PrefsView{}, errors.New("Tailscale SSH server administratively disabled.") } + if p1.RunWebClient && !envknob.Bool("TS_DEBUG_WEB_UI") { + b.mu.Unlock() + b.logf("EditPrefs requests web client, but disabled by envknob; returning error") + return ipn.PrefsView{}, errors.New("web ui flag not set") + } if p1.View().Equals(p0) { b.mu.Unlock() return stripKeysFromPrefs(p0), nil @@ -3010,6 +3017,9 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) ipn b.sshServer = nil } } + if oldp.ShouldWebClientBeRunning() && !newp.ShouldWebClientBeRunning() { + b.WebShutdown() + } if netMap != nil { newProfile := netMap.UserProfiles[netMap.User()] if newLoginName := newProfile.LoginName; newLoginName != "" { @@ -4146,6 +4156,10 @@ func (b *LocalBackend) ResetForClientDisconnect() { func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Load() && envknob.CanSSHD() } +func (b *LocalBackend) ShouldRunWebClient() bool { + return b.webclientAtomicBool.Load() && envknob.Bool("TS_DEBUG_WEB_UI") +} + // ShouldHandleViaIP reports whether ip is an IPv6 address in the // Tailscale ULA's v6 "via" range embedding an IPv4 address to be forwarded to // by Tailscale. diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index ec6b97265..7edddde96 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -2249,10 +2249,20 @@ func (h *Handler) serveWeb(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } + // try to set pref, but ignore errors + _, _ = h.b.EditPrefs(&ipn.MaskedPrefs{ + Prefs: ipn.Prefs{RunWebClient: true}, + RunWebClientSet: true, + }) w.WriteHeader(http.StatusOK) return case "/localapi/v0/web/stop": h.b.WebShutdown() + // try to set pref, but ignore errors + _, _ = h.b.EditPrefs(&ipn.MaskedPrefs{ + Prefs: ipn.Prefs{RunWebClient: false}, + RunWebClientSet: true, + }) w.WriteHeader(http.StatusOK) return default: diff --git a/ipn/prefs.go b/ipn/prefs.go index 148293426..01b3d620e 100644 --- a/ipn/prefs.go +++ b/ipn/prefs.go @@ -112,6 +112,11 @@ type Prefs struct { // policies as configured by the Tailnet's admin(s). RunSSH bool + // RunWebClient bool is whether this node should run a web client, + // permitting access to peers according to the + // policies as configured by the Tailnet's admin(s). + RunWebClient bool + // WantRunning indicates whether networking should be active on // this node. WantRunning bool @@ -236,6 +241,7 @@ type MaskedPrefs struct { ExitNodeAllowLANAccessSet bool `json:",omitempty"` CorpDNSSet bool `json:",omitempty"` RunSSHSet bool `json:",omitempty"` + RunWebClientSet bool `json:",omitempty"` WantRunningSet bool `json:",omitempty"` LoggedOutSet bool `json:",omitempty"` ShieldsUpSet bool `json:",omitempty"` @@ -350,6 +356,9 @@ func (p *Prefs) pretty(goos string) string { if p.RunSSH { sb.WriteString("ssh=true ") } + if p.RunWebClient { + sb.WriteString("webclient=true ") + } if p.LoggedOut { sb.WriteString("loggedout=true ") } @@ -431,6 +440,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool { p.ExitNodeAllowLANAccess == p2.ExitNodeAllowLANAccess && p.CorpDNS == p2.CorpDNS && p.RunSSH == p2.RunSSH && + p.RunWebClient == p2.RunWebClient && p.WantRunning == p2.WantRunning && p.LoggedOut == p2.LoggedOut && p.NotepadURLs == p2.NotepadURLs && @@ -691,6 +701,18 @@ func (p *Prefs) ShouldSSHBeRunning() bool { return p.WantRunning && p.RunSSH } +// ShouldWebClientBeRunning reports whether the web client server should be running based on +// the prefs. +func (p PrefsView) ShouldWebClientBeRunning() bool { + return p.Valid() && p.ж.ShouldWebClientBeRunning() +} + +// ShouldWebClientBeRunning reports whether the web client server should be running based on +// the prefs. +func (p *Prefs) ShouldWebClientBeRunning() bool { + return p.WantRunning && p.RunWebClient +} + // PrefsFromBytes deserializes Prefs from a JSON blob. func PrefsFromBytes(b []byte) (*Prefs, error) { p := NewPrefs() diff --git a/ipn/prefs_test.go b/ipn/prefs_test.go index 20d1bbfac..4fe24a8e7 100644 --- a/ipn/prefs_test.go +++ b/ipn/prefs_test.go @@ -43,6 +43,7 @@ func TestPrefsEqual(t *testing.T) { "ExitNodeAllowLANAccess", "CorpDNS", "RunSSH", + "RunWebClient", "WantRunning", "LoggedOut", "ShieldsUp",