Browse Source

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 <[email protected]>
Will Norris 2 years ago
parent
commit
28ad910840
8 changed files with 58 additions and 0 deletions
  1. 3 0
      cmd/tailscale/cli/cli_test.go
  2. 5 0
      ipn/conf.go
  3. 1 0
      ipn/ipn_clone.go
  4. 2 0
      ipn/ipn_view.go
  5. 14 0
      ipn/ipnlocal/local.go
  6. 10 0
      ipn/localapi/localapi.go
  7. 22 0
      ipn/prefs.go
  8. 1 0
      ipn/prefs_test.go

+ 3 - 0
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)
 	}

+ 5 - 0
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

+ 1 - 0
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

+ 2 - 0
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

+ 14 - 0
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.

+ 10 - 0
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:

+ 22 - 0
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()

+ 1 - 0
ipn/prefs_test.go

@@ -43,6 +43,7 @@ func TestPrefsEqual(t *testing.T) {
 		"ExitNodeAllowLANAccess",
 		"CorpDNS",
 		"RunSSH",
+		"RunWebClient",
 		"WantRunning",
 		"LoggedOut",
 		"ShieldsUp",