From e17abbf461c0c014da0b38ec4bf44b00f2bc7ee4 Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Wed, 9 Apr 2025 10:25:57 -0700 Subject: [PATCH] cmd/tailscale,ipn: add relay-server-port "tailscale set" flag and Prefs field (#15594) This flag is currently no-op and hidden. The flag does round trip through the related pref. Subsequent commits will tie them to net/udprelay.Server. There is no corresponding "tailscale up" flag, enabling/disabling of the relay server will only be supported via "tailscale set". This is a string flag in order to support disablement via empty string as a port value of 0 means "enable the server and listen on a random unused port". Disablement via empty string also follows existing flag convention, e.g. advertise-routes. Early internal discussions settled on "tailscale set --relay="", but the author felt this was too ambiguous around client vs server, and may cause confusion in the future if we add related flags. Updates tailscale/corp#27502 Signed-off-by: Jordan Whited --- cmd/tailscale/cli/set.go | 13 +++++++++++++ cmd/tailscale/cli/up.go | 1 + ipn/ipn_clone.go | 4 ++++ ipn/ipn_view.go | 5 +++++ ipn/prefs.go | 25 ++++++++++++++++++++++++- ipn/prefs_test.go | 14 ++++++++++++++ 6 files changed, 61 insertions(+), 1 deletion(-) diff --git a/cmd/tailscale/cli/set.go b/cmd/tailscale/cli/set.go index 07b3fe9ce..37db252ad 100644 --- a/cmd/tailscale/cli/set.go +++ b/cmd/tailscale/cli/set.go @@ -11,6 +11,7 @@ import ( "net/netip" "os/exec" "runtime" + "strconv" "strings" "github.com/peterbourgon/ff/v3/ffcli" @@ -22,6 +23,7 @@ import ( "tailscale.com/net/tsaddr" "tailscale.com/safesocket" "tailscale.com/types/opt" + "tailscale.com/types/ptr" "tailscale.com/types/views" "tailscale.com/version" ) @@ -62,6 +64,7 @@ type setArgsT struct { snat bool statefulFiltering bool netfilterMode string + relayServerPort string } func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet { @@ -82,6 +85,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, "expose the web interface for managing this node over Tailscale at port 5252") + setf.StringVar(&setArgs.relayServerPort, "relay-server-port", "", hidden+"UDP port number (0 will pick a random unused port) for the relay server to bind to, on all interfaces, or empty string to disable relay server functionality") ffcomplete.Flag(setf, "exit-node", func(args []string) ([]string, ffcomplete.ShellCompDirective, error) { st, err := localClient.Status(context.Background()) @@ -233,6 +237,15 @@ func runSet(ctx context.Context, args []string) (retErr error) { } } } + + if setArgs.relayServerPort != "" { + uport, err := strconv.ParseUint(setArgs.relayServerPort, 10, 16) + if err != nil { + return fmt.Errorf("failed to set relay server port: %v", err) + } + maskedPrefs.Prefs.RelayServerPort = ptr.To(int(uport)) + } + checkPrefs := curPrefs.Clone() checkPrefs.ApplyEdits(maskedPrefs) if err := localClient.CheckPrefs(ctx, checkPrefs); err != nil { diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go index 26db85f13..d1e813b95 100644 --- a/cmd/tailscale/cli/up.go +++ b/cmd/tailscale/cli/up.go @@ -773,6 +773,7 @@ func init() { addPrefFlagMapping("auto-update", "AutoUpdate.Apply") addPrefFlagMapping("advertise-connector", "AppConnector") addPrefFlagMapping("posture-checking", "PostureChecking") + addPrefFlagMapping("relay-server-port", "RelayServerPort") } func addPrefFlagMapping(flagName string, prefNames ...string) { diff --git a/ipn/ipn_clone.go b/ipn/ipn_clone.go index 4050fec46..65438444e 100644 --- a/ipn/ipn_clone.go +++ b/ipn/ipn_clone.go @@ -61,6 +61,9 @@ func (src *Prefs) Clone() *Prefs { } } } + if dst.RelayServerPort != nil { + dst.RelayServerPort = ptr.To(*src.RelayServerPort) + } dst.Persist = src.Persist.Clone() return dst } @@ -96,6 +99,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct { PostureChecking bool NetfilterKind string DriveShares []*drive.Share + RelayServerPort *int AllowSingleHosts marshalAsTrueInJSON Persist *persist.Persist }{}) diff --git a/ipn/ipn_view.go b/ipn/ipn_view.go index e633a2633..871270b85 100644 --- a/ipn/ipn_view.go +++ b/ipn/ipn_view.go @@ -166,6 +166,10 @@ func (v PrefsView) NetfilterKind() string { return v.ж.Netfilte func (v PrefsView) DriveShares() views.SliceView[*drive.Share, drive.ShareView] { return views.SliceOfViews[*drive.Share, drive.ShareView](v.ж.DriveShares) } +func (v PrefsView) RelayServerPort() views.ValuePointer[int] { + return views.ValuePointerOf(v.ж.RelayServerPort) +} + func (v PrefsView) AllowSingleHosts() marshalAsTrueInJSON { return v.ж.AllowSingleHosts } func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() } @@ -200,6 +204,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct { PostureChecking bool NetfilterKind string DriveShares []*drive.Share + RelayServerPort *int AllowSingleHosts marshalAsTrueInJSON Persist *persist.Persist }{}) diff --git a/ipn/prefs.go b/ipn/prefs.go index 5b3e95b33..9d6008de1 100644 --- a/ipn/prefs.go +++ b/ipn/prefs.go @@ -246,6 +246,14 @@ type Prefs struct { // by name. DriveShares []*drive.Share + // RelayServerPort is the UDP port number for the relay server to bind to, + // on all interfaces. A non-nil zero value signifies a random unused port + // should be used. A nil value signifies relay server functionality + // should be disabled. This field is currently experimental, and therefore + // no guarantees are made about its current naming and functionality when + // non-nil/enabled. + RelayServerPort *int `json:",omitempty"` + // AllowSingleHosts was a legacy field that was always true // for the past 4.5 years. It controlled whether Tailscale // peers got /32 or /127 routes for each other. @@ -337,6 +345,7 @@ type MaskedPrefs struct { PostureCheckingSet bool `json:",omitempty"` NetfilterKindSet bool `json:",omitempty"` DriveSharesSet bool `json:",omitempty"` + RelayServerPortSet bool `json:",omitempty"` } // SetsInternal reports whether mp has any of the Internal*Set field bools set @@ -555,6 +564,9 @@ func (p *Prefs) pretty(goos string) string { } sb.WriteString(p.AutoUpdate.Pretty()) sb.WriteString(p.AppConnector.Pretty()) + if p.RelayServerPort != nil { + fmt.Fprintf(&sb, "relayServerPort=%d ", *p.RelayServerPort) + } if p.Persist != nil { sb.WriteString(p.Persist.Pretty()) } else { @@ -616,7 +628,8 @@ func (p *Prefs) Equals(p2 *Prefs) bool { p.AppConnector == p2.AppConnector && p.PostureChecking == p2.PostureChecking && slices.EqualFunc(p.DriveShares, p2.DriveShares, drive.SharesEqual) && - p.NetfilterKind == p2.NetfilterKind + p.NetfilterKind == p2.NetfilterKind && + compareIntPtrs(p.RelayServerPort, p2.RelayServerPort) } func (au AutoUpdatePrefs) Pretty() string { @@ -636,6 +649,16 @@ func (ap AppConnectorPrefs) Pretty() string { return "" } +func compareIntPtrs(a, b *int) bool { + if (a == nil) != (b == nil) { + return false + } + if a == nil { + return true + } + return *a == *b +} + // NewPrefs returns the default preferences to use. func NewPrefs() *Prefs { // Provide default values for options which might be missing diff --git a/ipn/prefs_test.go b/ipn/prefs_test.go index 91b835e3e..d28d161db 100644 --- a/ipn/prefs_test.go +++ b/ipn/prefs_test.go @@ -65,6 +65,7 @@ func TestPrefsEqual(t *testing.T) { "PostureChecking", "NetfilterKind", "DriveShares", + "RelayServerPort", "AllowSingleHosts", "Persist", } @@ -73,6 +74,9 @@ func TestPrefsEqual(t *testing.T) { have, prefsHandles) } + relayServerPort := func(port int) *int { + return &port + } nets := func(strs ...string) (ns []netip.Prefix) { for _, s := range strs { n, err := netip.ParsePrefix(s) @@ -341,6 +345,16 @@ func TestPrefsEqual(t *testing.T) { &Prefs{AdvertiseServices: []string{"svc:tux", "svc:amelie"}}, false, }, + { + &Prefs{RelayServerPort: relayServerPort(0)}, + &Prefs{RelayServerPort: nil}, + false, + }, + { + &Prefs{RelayServerPort: relayServerPort(0)}, + &Prefs{RelayServerPort: relayServerPort(1)}, + false, + }, } for i, tt := range tests { got := tt.a.Equals(tt.b)