diff --git a/cmd/tailscale/cli/set.go b/cmd/tailscale/cli/set.go index 02d4f5a06..8048b405b 100644 --- a/cmd/tailscale/cli/set.go +++ b/cmd/tailscale/cli/set.go @@ -56,6 +56,7 @@ type setArgsT struct { updateCheck bool updateApply bool postureChecking bool + remoteConfig bool } func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet { @@ -76,6 +77,7 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet { setf.BoolVar(&setArgs.updateApply, "auto-update", false, "automatically update to the latest available version") setf.BoolVar(&setArgs.postureChecking, "posture-checking", false, "HIDDEN: allow management plane to gather device posture information") setf.BoolVar(&setArgs.runWebClient, "webclient", false, "run a web interface for managing this node, served over Tailscale at port 5252") + setf.BoolVar(&setArgs.remoteConfig, "remote-config", false, "HIDDEN: allow talinet admins to manage this node's settings") if safesocket.GOOSUsesPeerCreds(goos) { setf.StringVar(&setArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo") @@ -116,6 +118,7 @@ func runSet(ctx context.Context, args []string) (retErr error) { Hostname: setArgs.hostname, OperatorUser: setArgs.opUser, ForceDaemon: setArgs.forceDaemon, + RemoteConfig: setArgs.remoteConfig, AutoUpdate: ipn.AutoUpdatePrefs{ Check: setArgs.updateCheck, Apply: opt.NewBool(setArgs.updateApply), @@ -148,6 +151,7 @@ func runSet(ctx context.Context, args []string) (retErr error) { advertiseRoutesSet = true } }) + if maskedPrefs.IsEmpty() { return flag.ErrHelp } diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go index 43f36f819..3c6eba84b 100644 --- a/cmd/tailscale/cli/up.go +++ b/cmd/tailscale/cli/up.go @@ -723,6 +723,7 @@ func init() { addPrefFlagMapping("auto-update", "AutoUpdate.Apply") addPrefFlagMapping("advertise-connector", "AppConnector") addPrefFlagMapping("posture-checking", "PostureChecking") + addPrefFlagMapping("remote-config", "RemoteConfig") } func addPrefFlagMapping(flagName string, prefNames ...string) { diff --git a/ipn/ipn_clone.go b/ipn/ipn_clone.go index 40cc44296..01f83ecff 100644 --- a/ipn/ipn_clone.go +++ b/ipn/ipn_clone.go @@ -56,6 +56,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct { AppConnector AppConnectorPrefs PostureChecking bool NetfilterKind string + RemoteConfig bool Persist *persist.Persist }{}) diff --git a/ipn/ipn_view.go b/ipn/ipn_view.go index 18436867d..3f9c93beb 100644 --- a/ipn/ipn_view.go +++ b/ipn/ipn_view.go @@ -91,6 +91,7 @@ func (v PrefsView) AutoUpdate() AutoUpdatePrefs { return v.ж.AutoUpda func (v PrefsView) AppConnector() AppConnectorPrefs { return v.ж.AppConnector } func (v PrefsView) PostureChecking() bool { return v.ж.PostureChecking } func (v PrefsView) NetfilterKind() string { return v.ж.NetfilterKind } +func (v PrefsView) RemoteConfig() bool { return v.ж.RemoteConfig } func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() } // A compilation failure here means this code must be regenerated, with the command at the top of this file. @@ -121,6 +122,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct { AppConnector AppConnectorPrefs PostureChecking bool NetfilterKind string + RemoteConfig bool Persist *persist.Persist }{}) diff --git a/ipn/ipnlocal/c2n.go b/ipn/ipnlocal/c2n.go index 9e6af14de..a76014351 100644 --- a/ipn/ipnlocal/c2n.go +++ b/ipn/ipnlocal/c2n.go @@ -26,9 +26,13 @@ import ( "tailscale.com/clientupdate" "tailscale.com/envknob" "tailscale.com/ipn" + "tailscale.com/net/netmon" "tailscale.com/net/sockstats" "tailscale.com/posture" "tailscale.com/tailcfg" + "tailscale.com/types/logger" + "tailscale.com/types/logid" + "tailscale.com/types/ptr" "tailscale.com/util/clientmetric" "tailscale.com/util/goroutines" "tailscale.com/util/set" @@ -72,6 +76,10 @@ var c2nHandlers = map[methodAndPath]c2nHandler{ // Linux netfilter. req("POST /netfilter-kind"): handleC2NSetNetfilterKind, + + // Remote config + req("/remote-config/prefs"): handleC2NRemoteConfigPrefs, + // TODO(bradfitz): more (but not all) LocalAPI proxies for remote-config } type c2nHandler func(*LocalBackend, http.ResponseWriter, *http.Request) @@ -569,3 +577,33 @@ func handleC2NTLSCertStatus(b *LocalBackend, w http.ResponseWriter, r *http.Requ writeJSON(w, ret) } + +// NewC2NLocalAPIHandler is initialized by the localapi package's init func. +// It returns a new http.Handler that serves LocalAPI for c2n. +var NewC2NLocalAPIHandler func(b *LocalBackend, logf logger.Logf, netMon *netmon.Monitor, logID logid.PublicID) http.Handler + +func handleC2NRemoteConfigPrefs(b *LocalBackend, w http.ResponseWriter, r *http.Request) { + prefs := b.Prefs() + if !prefs.Valid() || !prefs.RemoteConfig() { + http.Error(w, "remote config not enabled", http.StatusForbidden) + return + } + if NewC2NLocalAPIHandler == nil { + http.Error(w, "remote config not wired up", http.StatusInternalServerError) + return + } + var newPath string + switch r.URL.Path { + case "/remote-config/prefs": + newPath = "/localapi/v0/prefs" + default: + http.Error(w, "unsupported path", http.StatusBadRequest) + return + } + r2 := r.WithContext(r.Context()) + r2.URL = ptr.To(*r.URL) // shallow clone + r2.URL.Path = newPath + + h := NewC2NLocalAPIHandler(b, b.logf, b.sys.NetMon.Get(), b.backendLogID) + h.ServeHTTP(w, r2) +} diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index f68f0a282..dd5b0c7bb 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -153,6 +153,16 @@ func NewHandler(b *ipnlocal.LocalBackend, logf logger.Logf, netMon *netmon.Monit return &Handler{b: b, logf: logf, netMon: netMon, backendLogID: logID, clock: tstime.StdClock{}} } +func init() { + ipnlocal.NewC2NLocalAPIHandler = func(b *ipnlocal.LocalBackend, logf logger.Logf, netMon *netmon.Monitor, logID logid.PublicID) http.Handler { + h := NewHandler(b, logf, netMon, logID) + h.PermitRead, h.PermitWrite = true, true + h.PermitCert = false + h.ConnIdentity = &ipnauth.ConnIdentity{} + return h + } +} + type Handler struct { // RequiredPassword, if non-empty, forces all HTTP // requests to have HTTP basic auth with this password. diff --git a/ipn/prefs.go b/ipn/prefs.go index 7bfbd613f..cae9e1696 100644 --- a/ipn/prefs.go +++ b/ipn/prefs.go @@ -222,6 +222,10 @@ type Prefs struct { // Linux-only. NetfilterKind string + // RemoteConfig specifies whether to allow the control server to + // manage this node's settings. + RemoteConfig bool + // The Persist field is named 'Config' in the file for backward // compatibility with earlier versions. // TODO(apenwarr): We should move this out of here, it's not a pref. @@ -293,6 +297,7 @@ type MaskedPrefs struct { AppConnectorSet bool `json:",omitempty"` PostureCheckingSet bool `json:",omitempty"` NetfilterKindSet bool `json:",omitempty"` + RemoteConfigSet bool `json:",omitempty"` } type AutoUpdatePrefsMask struct { @@ -467,6 +472,9 @@ func (p *Prefs) pretty(goos string) string { if p.ShieldsUp { sb.WriteString("shields=true ") } + if p.RemoteConfig { + sb.WriteString("remoteconfig=true ") + } if p.ExitNodeIP.IsValid() { fmt.Fprintf(&sb, "exit=%v lan=%t ", p.ExitNodeIP, p.ExitNodeAllowLANAccess) } else if !p.ExitNodeID.IsZero() { @@ -549,6 +557,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool { p.OperatorUser == p2.OperatorUser && p.Hostname == p2.Hostname && p.ForceDaemon == p2.ForceDaemon && + p.RemoteConfig == p2.RemoteConfig && compareIPNets(p.AdvertiseRoutes, p2.AdvertiseRoutes) && compareStrings(p.AdvertiseTags, p2.AdvertiseTags) && p.Persist.Equals(p2.Persist) && diff --git a/ipn/prefs_test.go b/ipn/prefs_test.go index 9251bb2bb..acb7b0feb 100644 --- a/ipn/prefs_test.go +++ b/ipn/prefs_test.go @@ -62,6 +62,7 @@ func TestPrefsEqual(t *testing.T) { "AppConnector", "PostureChecking", "NetfilterKind", + "RemoteConfig", "Persist", } if have := fieldsOf(reflect.TypeFor[Prefs]()); !reflect.DeepEqual(have, prefsHandles) { @@ -435,6 +436,11 @@ func TestPrefsPretty(t *testing.T) { "windows", "Prefs{ra=false mesh=false dns=false want=false shields=true update=off Persist=nil}", }, + { + Prefs{RemoteConfig: true}, + "windows", + "Prefs{ra=false mesh=false dns=false want=false remoteconfig=true update=off Persist=nil}", + }, { Prefs{AllowSingleHosts: true}, "windows", diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index aeb41ce7e..42353e8ef 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -129,7 +129,8 @@ type CapabilityVersion int // - 86: 2024-01-23: Client understands NodeAttrProbeUDPLifetime // - 87: 2024-02-11: UserProfile.Groups removed (added in 66) // - 88: 2024-03-05: Client understands NodeAttrSuggestExitNode -const CurrentCapabilityVersion CapabilityVersion = 88 +// - 89: 2024-03-07: c2n remote config prefs +const CurrentCapabilityVersion CapabilityVersion = 89 type StableID string