Browse Source

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="<port>",
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 <[email protected]>
Jordan Whited 11 months ago
parent
commit
e17abbf461
6 changed files with 61 additions and 1 deletions
  1. 13 0
      cmd/tailscale/cli/set.go
  2. 1 0
      cmd/tailscale/cli/up.go
  3. 4 0
      ipn/ipn_clone.go
  4. 5 0
      ipn/ipn_view.go
  5. 24 1
      ipn/prefs.go
  6. 14 0
      ipn/prefs_test.go

+ 13 - 0
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 {

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

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

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

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

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