Repository: tailscale/tailscale
Pull request #15594: cmd/tailscale,ipn: add relay-server-port "tailscale set" flag and Pre…
Author: login: jwhited
Created: 2025-04-08T21:54:26Z | Merged: 2025-04-09T17:25:58Z

…fs field

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


Diff:
diff --git a/cmd/tailscale/cli/set.go b/cmd/tailscale/cli/set.go
index 07b3fe9ce38b1..37db252ad5b60 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 26db85f1334cd..d1e813b954f19 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 4050fec4648a7..65438444e162f 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 e633a2633ef03..871270b8564f1 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 5b3e95b33a75d..9d6008de1d5d6 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 91b835e3e3e19..d28d161db422e 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)


Reviews:
user: login: bradfitz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T22:10:06Z
user: login: mikeodr
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T22:10:13Z
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-08T22:20:45Z
user: login: jwhited
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T22:21:00Z
user: login: jwhited
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T22:38:38Z
user: login: jwhited
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T22:43:34Z
user: login: jwhited
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T22:44:55Z
user: login: jwhited
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T22:45:24Z
user: login: jwhited
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T22:45:43Z
user: login: jwhited
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T22:53:07Z
user: login: bradfitz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T15:51:58Z
user: login: jwhited
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T16:07:23Z
user: login: raggi
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-09T17:23:10Z

Review comments:
diff_hunk: @@ -233,6 +236,16 @@ 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)
+		}
+		port := int(uport)
+		maskedPrefs.Prefs.RelayServerPort = &(port)
path: cmd/tailscale/cli/set.go
user: login: bradfitz
body: drop parens around port. I didn't even know that was permitted :)

but it looks like it does something it doesn't do (make a new pointer, not just `&port`)

alternatively, `maskedPrefs.Prefs.RelayServerPort = ptr.To(int(uport))` if you want
created_at: 2025-04-08T22:08:00Z
author_association: MEMBER
diff_hunk: @@ -82,6 +84,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) to act as a relay server on between Tailscale nodes, or empty string to disable relay server functionality")
path: cmd/tailscale/cli/set.go
user: login: bradfitz
body: in between?
created_at: 2025-04-08T22:08:11Z
author_association: MEMBER
diff_hunk: @@ -82,6 +84,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) to act as a relay server on between Tailscale nodes, or empty string to disable relay server functionality")
path: cmd/tailscale/cli/set.go
user: login: bradfitz
body: port on what interface(s)? just the Tailscale IP(s)?
created_at: 2025-04-08T22:08:33Z
author_association: MEMBER
diff_hunk: @@ -246,6 +246,13 @@ type Prefs struct {
 	// by name.
 	DriveShares []*drive.Share
 
+	// RelayServerPort is the UDP port number to act as a relay server on between
+	// Tailscale nodes. A nil value signifies relay server functionality
path: ipn/prefs.go
user: login: bradfitz
body: what does ptr.To(0) mean?
created_at: 2025-04-08T22:09:06Z
author_association: MEMBER
diff_hunk: @@ -82,6 +84,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) to act as a relay server on between Tailscale nodes, or empty string to disable relay server functionality")
path: cmd/tailscale/cli/set.go
user: login: mikeodr
body: "To act as a relay server on between"

The on needs a fix up I think. 
created_at: 2025-04-08T22:09:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -246,6 +246,13 @@ type Prefs struct {
 	// by name.
 	DriveShares []*drive.Share
 
+	// RelayServerPort is the UDP port number to act as a relay server on between
path: ipn/prefs.go
user: login: mikeodr
body: Same here. 
created_at: 2025-04-08T22:09:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -616,7 +627,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 &&
+		reflect.DeepEqual(p.RelayServerPort, p2.RelayServerPort)
path: ipn/prefs.go
user: login: bradfitz
body: kinda overkill ... and looks like this code is already trying to avoid it

created_at: 2025-04-08T22:10:00Z
author_association: MEMBER
diff_hunk: @@ -61,6 +61,9 @@ func (src *Prefs) Clone() *Prefs {
 			}
 		}
 	}
+	if dst.RelayServerPort != nil {
+		dst.RelayServerPort = ptr.To(*src.RelayServerPort)
+	}
path: ipn/ipn_clone.go
body: The check `if dst.RelayServerPort != nil` is incorrect in the Clone method. This is checking the destination's RelayServerPort which would always be nil at this point. You should be checking `if src.RelayServerPort != nil` instead, following the pattern used for other pointer fields.
created_at: 2025-04-08T22:20:45Z
author_association: NONE
diff_hunk: @@ -233,6 +236,16 @@ 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)
+		}
+		port := int(uport)
+		maskedPrefs.Prefs.RelayServerPort = &(port)
path: cmd/tailscale/cli/set.go
user: login: jwhited
body: Parens were unintentional, updated in https://github.com/tailscale/tailscale/pull/15594/commits/1592c6beabcaf5c5d8e6e14d7d54579f3cca8c67.
created_at: 2025-04-08T22:21:00Z
author_association: MEMBER
diff_hunk: @@ -82,6 +84,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) to act as a relay server on between Tailscale nodes, or empty string to disable relay server functionality")
path: cmd/tailscale/cli/set.go
user: login: jwhited
body: All interfaces (0.0.0.0 and ::) as this is an underlay relay.

How does https://github.com/tailscale/tailscale/pull/15594/commits/411280aa9c55d537072a58e5808fb8b403bd7661 sound?

Trying to fit relay server vs relay client relationship into the flag description ends up being a bit of a mouthful.
created_at: 2025-04-08T22:38:38Z
author_association: MEMBER
diff_hunk: @@ -246,6 +246,13 @@ type Prefs struct {
 	// by name.
 	DriveShares []*drive.Share
 
+	// RelayServerPort is the UDP port number to act as a relay server on between
+	// Tailscale nodes. A nil value signifies relay server functionality
path: ipn/prefs.go
user: login: jwhited
body: A random unused port will be used. Updated in https://github.com/tailscale/tailscale/pull/15594/commits/c3d6be9fa7c6b01f26ba6af456e71187f50ad02e.
created_at: 2025-04-08T22:43:34Z
author_association: MEMBER
diff_hunk: @@ -616,7 +627,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 &&
+		reflect.DeepEqual(p.RelayServerPort, p2.RelayServerPort)
path: ipn/prefs.go
user: login: jwhited
body: Overkill in what sense? Importing reflect vs writing a similar equality check by hand?
created_at: 2025-04-08T22:44:55Z
author_association: MEMBER
diff_hunk: @@ -246,6 +246,13 @@ type Prefs struct {
 	// by name.
 	DriveShares []*drive.Share
 
+	// RelayServerPort is the UDP port number to act as a relay server on between
path: ipn/prefs.go
user: login: jwhited
body: Updated in https://github.com/tailscale/tailscale/commit/c3d6be9fa7c6b01f26ba6af456e71187f50ad02e.
created_at: 2025-04-08T22:45:23Z
author_association: MEMBER
diff_hunk: @@ -82,6 +84,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) to act as a relay server on between Tailscale nodes, or empty string to disable relay server functionality")
path: cmd/tailscale/cli/set.go
user: login: jwhited
body: Updated in https://github.com/tailscale/tailscale/pull/15594/commits/411280aa9c55d537072a58e5808fb8b403bd7661.
created_at: 2025-04-08T22:45:42Z
author_association: MEMBER
diff_hunk: @@ -61,6 +61,9 @@ func (src *Prefs) Clone() *Prefs {
 			}
 		}
 	}
+	if dst.RelayServerPort != nil {
+		dst.RelayServerPort = ptr.To(*src.RelayServerPort)
+	}
path: ipn/ipn_clone.go
user: login: jwhited
body: dst.RelayServerPort is already a shallow copy, so if src.RelayServerPort was non-nil, it will be non-nil in dst, and we will then deep copy its pointed to value.
created_at: 2025-04-08T22:53:07Z
author_association: MEMBER
diff_hunk: @@ -616,7 +627,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 &&
+		reflect.DeepEqual(p.RelayServerPort, p2.RelayServerPort)
path: ipn/prefs.go
user: login: bradfitz
body: yeah, reflect.DeepEqual is pretty slow. allocating a map just to compare whether two integers are equal is a bit much.

created_at: 2025-04-09T15:51:58Z
author_association: MEMBER
diff_hunk: @@ -616,7 +627,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 &&
+		reflect.DeepEqual(p.RelayServerPort, p2.RelayServerPort)
path: ipn/prefs.go
user: login: jwhited
body: Updated in https://github.com/tailscale/tailscale/pull/15594/commits/68a710250304e113561db2b36e733a179b7e2fb3.
created_at: 2025-04-09T16:07:23Z
author_association: MEMBER

PR comments:
user: login: ghost
created_at: 2025-04-08T22:20:18Z
body: <h3>Pull Request Revisions</h3>
<table><tr><th>Revision</th><th>Description</th></tr><tr><td><a href="https://review.ai/pr/tailscale/tailscale/15594?rev=10">r10</a></td><td><details open><summary>Added relay server port configuration</summary>Introduced new <code>RelayServerPort</code> field in preferences, allowing configuration of UDP port for relay server, with CLI and internal support added</details></td></tr><tr><td><a href="https://review.ai/pr/tailscale/tailscale/15594?rev=9">r9</a></td><td>No changes since last revision</td></tr><tr><td><a href="https://review.ai/pr/tailscale/tailscale/15594?rev=8">r8</a></td><td><details><summary>Added compareIntPtrs pointer comparison function</summary>Replaced <code>reflect.DeepEqual</code> with a custom <code>compareIntPtrs</code> function for integer pointer comparisons in <code>Equals</code> method</details></td></tr><tr><td><a href="https://review.ai/pr/tailscale/tailscale/15594?rev=7">r7</a></td><td>No changes since last revision</td></tr><tr><td><a href="https://review.ai/pr/tailscale/tailscale/15594?rev=6">r6</a></td><td><details><summary>Updated relay port logging label</summary>Changed log output from "relayPort" to "relayServerPort" in prefs.go pretty print method</details></td></tr><tr><td><a href="https://review.ai/pr/tailscale/tailscale/15594?rev=5">r5</a></td><td><details><summary>Added space after relay port</summary>Modified pretty-printing of RelayServerPort to add a trailing space after port number</details></td></tr><tr><td><a href="https://review.ai/pr/tailscale/tailscale/15594?rev=4">r4</a></td><td><details><summary>Clarified relay server port comments</summary>Updated documentation for RelayServerPort to clarify port binding and zero value behavior</details></td></tr><tr><td><a href="https://review.ai/pr/tailscale/tailscale/15594?rev=3">r3</a></td><td><details><summary>Updated relay server port description</summary>Clarified relay server port flag description to specify binding on all interfaces</details></td></tr><tr><td><a href="https://review.ai/pr/tailscale/tailscale/15594?rev=2">r2</a></td><td><details><summary>Simplified relay server port assignment</summary>Replaced manual pointer creation with <code>ptr.To()</code> utility for setting relay server port</details></td></tr><tr><td><a href="https://review.ai/pr/tailscale/tailscale/15594?rev=1">r1</a></td><td><details><summary>Added relay server port configuration</summary>Introduced <code>RelayServerPort</code> configuration option to control UDP port for Tailscale relay server functionality</details></td></tr></table>✅ AI review completed for <a href="https://review.ai/pr/tailscale/tailscale/15594?rev=10">r10</a><details><summary>Help</summary>
React with emojis to give feedback on AI-generated reviews:
<ul>
<li>👍 means the feedback was helpful and actionable</li>
<li>👎 means the feedback was incorrect or unhelpful</li>
</ul>
💬 Replying to feedback with a comment helps us improve the system. Your input also contributes to shaping future interactions with the AI reviewer.
<br/><br/>
We'd love to hear from you—reach out anytime at <a href="mailto:team@review.ai">team@review.ai</a>.
</details>
author_association: NONE
user: login: raggi
created_at: 2025-04-09T17:32:39Z
body: I filed a bug for that flake: https://github.com/tailscale/tailscale/issues/15607

author_association: MEMBER