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 <jordan@tailscale.com>
This commit is contained in:
Jordan Whited 2025-04-09 10:25:57 -07:00 committed by GitHub
parent 7e296923ab
commit e17abbf461
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 61 additions and 1 deletions

View File

@ -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 {

View File

@ -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) {

View File

@ -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
}{})

View File

@ -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
}{})

View File

@ -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

View File

@ -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)