cmd/tailscale,feature/relayserver,ipn: add relay-server-static-endpoints set flag

Updates tailscale/corp#31489
Updates #17791

Signed-off-by: Jordan Whited <jordan@tailscale.com>
jwhited/relay-set-flags-config
Jordan Whited 2 weeks ago committed by Jordan Whited
parent 755309c04e
commit 7426eca163

@ -11,6 +11,7 @@ import (
"net/netip" "net/netip"
"os/exec" "os/exec"
"runtime" "runtime"
"slices"
"strconv" "strconv"
"strings" "strings"
@ -25,6 +26,7 @@ import (
"tailscale.com/types/opt" "tailscale.com/types/opt"
"tailscale.com/types/ptr" "tailscale.com/types/ptr"
"tailscale.com/types/views" "tailscale.com/types/views"
"tailscale.com/util/set"
"tailscale.com/version" "tailscale.com/version"
) )
@ -43,29 +45,30 @@ Only settings explicitly mentioned will be set. There are no default values.`,
} }
type setArgsT struct { type setArgsT struct {
acceptRoutes bool acceptRoutes bool
acceptDNS bool acceptDNS bool
exitNodeIP string exitNodeIP string
exitNodeAllowLANAccess bool exitNodeAllowLANAccess bool
shieldsUp bool shieldsUp bool
runSSH bool runSSH bool
runWebClient bool runWebClient bool
hostname string hostname string
advertiseRoutes string advertiseRoutes string
advertiseDefaultRoute bool advertiseDefaultRoute bool
advertiseConnector bool advertiseConnector bool
opUser string opUser string
acceptedRisks string acceptedRisks string
profileName string profileName string
forceDaemon bool forceDaemon bool
updateCheck bool updateCheck bool
updateApply bool updateApply bool
reportPosture bool reportPosture bool
snat bool snat bool
statefulFiltering bool statefulFiltering bool
sync bool sync bool
netfilterMode string netfilterMode string
relayServerPort string relayServerPort string
relayServerStaticEndpoints string
} }
func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet { func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
@ -88,6 +91,7 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
setf.BoolVar(&setArgs.runWebClient, "webclient", false, "expose the web interface for managing this node over Tailscale at port 5252") setf.BoolVar(&setArgs.runWebClient, "webclient", false, "expose the web interface for managing this node over Tailscale at port 5252")
setf.BoolVar(&setArgs.sync, "sync", false, hidden+"actively sync configuration from the control plane (set to false only for network failure testing)") setf.BoolVar(&setArgs.sync, "sync", false, hidden+"actively sync configuration from the control plane (set to false only for network failure testing)")
setf.StringVar(&setArgs.relayServerPort, "relay-server-port", "", "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") setf.StringVar(&setArgs.relayServerPort, "relay-server-port", "", "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")
setf.StringVar(&setArgs.relayServerStaticEndpoints, "relay-server-static-endpoints", "", "static IP:port endpoints to advertise as candidates for relay connections (comma-separated, e.g. \"[2001:db8::1]:40000,192.0.2.1:40000\") or empty string to not advertise any static endpoints")
ffcomplete.Flag(setf, "exit-node", func(args []string) ([]string, ffcomplete.ShellCompDirective, error) { ffcomplete.Flag(setf, "exit-node", func(args []string) ([]string, ffcomplete.ShellCompDirective, error) {
st, err := localClient.Status(context.Background()) st, err := localClient.Status(context.Background())
@ -248,6 +252,21 @@ func runSet(ctx context.Context, args []string) (retErr error) {
maskedPrefs.Prefs.RelayServerPort = ptr.To(int(uport)) maskedPrefs.Prefs.RelayServerPort = ptr.To(int(uport))
} }
if setArgs.relayServerStaticEndpoints != "" {
endpointsSet := make(set.Set[netip.AddrPort])
endpointsSplit := strings.Split(setArgs.relayServerStaticEndpoints, ",")
for _, s := range endpointsSplit {
ap, err := netip.ParseAddrPort(s)
if err != nil {
return fmt.Errorf("failed to set relay server static endpoints: %q is not a valid IP:port", s)
}
endpointsSet.Add(ap)
}
endpoints := endpointsSet.Slice()
slices.SortFunc(endpoints, netip.AddrPort.Compare)
maskedPrefs.Prefs.RelayServerStaticEndpoints = endpoints
}
checkPrefs := curPrefs.Clone() checkPrefs := curPrefs.Clone()
checkPrefs.ApplyEdits(maskedPrefs) checkPrefs.ApplyEdits(maskedPrefs)
if err := localClient.CheckPrefs(ctx, checkPrefs); err != nil { if err := localClient.CheckPrefs(ctx, checkPrefs); err != nil {

@ -887,6 +887,7 @@ func init() {
addPrefFlagMapping("report-posture", "PostureChecking") addPrefFlagMapping("report-posture", "PostureChecking")
addPrefFlagMapping("relay-server-port", "RelayServerPort") addPrefFlagMapping("relay-server-port", "RelayServerPort")
addPrefFlagMapping("sync", "Sync") addPrefFlagMapping("sync", "Sync")
addPrefFlagMapping("relay-server-static-endpoints", "RelayServerStaticEndpoints")
} }
func addPrefFlagMapping(flagName string, prefNames ...string) { func addPrefFlagMapping(flagName string, prefNames ...string) {

@ -9,6 +9,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/netip"
"tailscale.com/disco" "tailscale.com/disco"
"tailscale.com/feature" "tailscale.com/feature"
@ -23,6 +24,7 @@ import (
"tailscale.com/types/key" "tailscale.com/types/key"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/types/ptr" "tailscale.com/types/ptr"
"tailscale.com/types/views"
"tailscale.com/util/eventbus" "tailscale.com/util/eventbus"
"tailscale.com/wgengine/magicsock" "tailscale.com/wgengine/magicsock"
) )
@ -85,6 +87,7 @@ type relayServer interface {
AllocateEndpoint(discoA, discoB key.DiscoPublic) (endpoint.ServerEndpoint, error) AllocateEndpoint(discoA, discoB key.DiscoPublic) (endpoint.ServerEndpoint, error)
GetSessions() []status.ServerSession GetSessions() []status.ServerSession
SetDERPMapView(tailcfg.DERPMapView) SetDERPMapView(tailcfg.DERPMapView)
SetStaticAddrPorts(addrPorts views.Slice[netip.AddrPort])
} }
// extension is an [ipnext.Extension] managing the relay server on platforms // extension is an [ipnext.Extension] managing the relay server on platforms
@ -95,12 +98,13 @@ type extension struct {
ec *eventbus.Client ec *eventbus.Client
respPub *eventbus.Publisher[magicsock.UDPRelayAllocResp] respPub *eventbus.Publisher[magicsock.UDPRelayAllocResp]
mu syncs.Mutex // guards the following fields mu syncs.Mutex // guards the following fields
shutdown bool // true if Shutdown() has been called shutdown bool // true if Shutdown() has been called
rs relayServer // nil when disabled rs relayServer // nil when disabled
port *int // ipn.Prefs.RelayServerPort, nil if disabled port *int // ipn.Prefs.RelayServerPort, nil if disabled
derpMapView tailcfg.DERPMapView // latest seen over the eventbus staticEndpoints views.Slice[netip.AddrPort] // ipn.Prefs.RelayServerStaticEndpoints
hasNodeAttrDisableRelayServer bool // [tailcfg.NodeAttrDisableRelayServer] derpMapView tailcfg.DERPMapView // latest seen over the eventbus
hasNodeAttrDisableRelayServer bool // [tailcfg.NodeAttrDisableRelayServer]
} }
// Name implements [ipnext.Extension]. // Name implements [ipnext.Extension].
@ -186,6 +190,7 @@ func (e *extension) relayServerShouldBeRunningLocked() bool {
// handleRelayServerLifetimeLocked handles the lifetime of [e.rs]. // handleRelayServerLifetimeLocked handles the lifetime of [e.rs].
func (e *extension) handleRelayServerLifetimeLocked() { func (e *extension) handleRelayServerLifetimeLocked() {
defer e.handleRelayServerStaticAddrPortsLocked()
if !e.relayServerShouldBeRunningLocked() { if !e.relayServerShouldBeRunningLocked() {
e.stopRelayServerLocked() e.stopRelayServerLocked()
return return
@ -195,6 +200,13 @@ func (e *extension) handleRelayServerLifetimeLocked() {
e.tryStartRelayServerLocked() e.tryStartRelayServerLocked()
} }
func (e *extension) handleRelayServerStaticAddrPortsLocked() {
if e.rs != nil {
// TODO(jwhited): env var support
e.rs.SetStaticAddrPorts(e.staticEndpoints)
}
}
func (e *extension) selfNodeViewChanged(nodeView tailcfg.NodeView) { func (e *extension) selfNodeViewChanged(nodeView tailcfg.NodeView) {
e.mu.Lock() e.mu.Lock()
defer e.mu.Unlock() defer e.mu.Unlock()
@ -205,6 +217,7 @@ func (e *extension) selfNodeViewChanged(nodeView tailcfg.NodeView) {
func (e *extension) profileStateChanged(_ ipn.LoginProfileView, prefs ipn.PrefsView, sameNode bool) { func (e *extension) profileStateChanged(_ ipn.LoginProfileView, prefs ipn.PrefsView, sameNode bool) {
e.mu.Lock() e.mu.Lock()
defer e.mu.Unlock() defer e.mu.Unlock()
e.staticEndpoints = prefs.RelayServerStaticEndpoints()
newPort, ok := prefs.RelayServerPort().GetOk() newPort, ok := prefs.RelayServerPort().GetOk()
enableOrDisableServer := ok != (e.port != nil) enableOrDisableServer := ok != (e.port != nil)
portChanged := ok && e.port != nil && newPort != *e.port portChanged := ok && e.port != nil && newPort != *e.port

@ -5,7 +5,9 @@ package relayserver
import ( import (
"errors" "errors"
"net/netip"
"reflect" "reflect"
"slices"
"testing" "testing"
"tailscale.com/ipn" "tailscale.com/ipn"
@ -17,15 +19,21 @@ import (
"tailscale.com/types/key" "tailscale.com/types/key"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/types/ptr" "tailscale.com/types/ptr"
"tailscale.com/types/views"
) )
func Test_extension_profileStateChanged(t *testing.T) { func Test_extension_profileStateChanged(t *testing.T) {
prefsWithPortOne := ipn.Prefs{RelayServerPort: ptr.To(1)} prefsWithPortOne := ipn.Prefs{RelayServerPort: ptr.To(1)}
prefsWithNilPort := ipn.Prefs{RelayServerPort: nil} prefsWithNilPort := ipn.Prefs{RelayServerPort: nil}
prefsWithPortOneRelayEndpoints := ipn.Prefs{
RelayServerPort: ptr.To(1),
RelayServerStaticEndpoints: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:7777")},
}
type fields struct { type fields struct {
port *int port *int
rs relayServer staticEndpoints views.Slice[netip.AddrPort]
rs relayServer
} }
type args struct { type args struct {
prefs ipn.PrefsView prefs ipn.PrefsView
@ -38,6 +46,7 @@ func Test_extension_profileStateChanged(t *testing.T) {
wantPort *int wantPort *int
wantRelayServerFieldNonNil bool wantRelayServerFieldNonNil bool
wantRelayServerFieldMutated bool wantRelayServerFieldMutated bool
wantEndpoints []netip.AddrPort
}{ }{
{ {
name: "no changes non-nil port previously running", name: "no changes non-nil port previously running",
@ -53,6 +62,52 @@ func Test_extension_profileStateChanged(t *testing.T) {
wantRelayServerFieldNonNil: true, wantRelayServerFieldNonNil: true,
wantRelayServerFieldMutated: false, wantRelayServerFieldMutated: false,
}, },
{
name: "set addr ports unchanged port previously running",
fields: fields{
port: ptr.To(1),
rs: mockRelayServerNotZeroVal(),
},
args: args{
prefs: prefsWithPortOneRelayEndpoints.View(),
sameNode: true,
},
wantPort: ptr.To(1),
wantRelayServerFieldNonNil: true,
wantRelayServerFieldMutated: false,
wantEndpoints: prefsWithPortOneRelayEndpoints.RelayServerStaticEndpoints,
},
{
name: "set addr ports not previously running",
fields: fields{
port: nil,
rs: nil,
},
args: args{
prefs: prefsWithPortOneRelayEndpoints.View(),
sameNode: true,
},
wantPort: ptr.To(1),
wantRelayServerFieldNonNil: true,
wantRelayServerFieldMutated: true,
wantEndpoints: prefsWithPortOneRelayEndpoints.RelayServerStaticEndpoints,
},
{
name: "clear addr ports unchanged port previously running",
fields: fields{
port: ptr.To(1),
staticEndpoints: views.SliceOf(prefsWithPortOneRelayEndpoints.RelayServerStaticEndpoints),
rs: mockRelayServerNotZeroVal(),
},
args: args{
prefs: prefsWithPortOne.View(),
sameNode: true,
},
wantPort: ptr.To(1),
wantRelayServerFieldNonNil: true,
wantRelayServerFieldMutated: false,
wantEndpoints: nil,
},
{ {
name: "prefs port nil", name: "prefs port nil",
fields: fields{ fields: fields{
@ -160,6 +215,7 @@ func Test_extension_profileStateChanged(t *testing.T) {
return &mockRelayServer{}, nil return &mockRelayServer{}, nil
} }
e.port = tt.fields.port e.port = tt.fields.port
e.staticEndpoints = tt.fields.staticEndpoints
e.rs = tt.fields.rs e.rs = tt.fields.rs
defer e.Shutdown() defer e.Shutdown()
e.profileStateChanged(ipn.LoginProfileView{}, tt.args.prefs, tt.args.sameNode) e.profileStateChanged(ipn.LoginProfileView{}, tt.args.prefs, tt.args.sameNode)
@ -174,24 +230,34 @@ func Test_extension_profileStateChanged(t *testing.T) {
if tt.wantRelayServerFieldMutated != !reflect.DeepEqual(tt.fields.rs, e.rs) { if tt.wantRelayServerFieldMutated != !reflect.DeepEqual(tt.fields.rs, e.rs) {
t.Errorf("wantRelayServerFieldMutated: %v != !reflect.DeepEqual(tt.fields.rs, e.rs): %v", tt.wantRelayServerFieldMutated, !reflect.DeepEqual(tt.fields.rs, e.rs)) t.Errorf("wantRelayServerFieldMutated: %v != !reflect.DeepEqual(tt.fields.rs, e.rs): %v", tt.wantRelayServerFieldMutated, !reflect.DeepEqual(tt.fields.rs, e.rs))
} }
if !slices.Equal(tt.wantEndpoints, e.staticEndpoints.AsSlice()) {
t.Errorf("wantEndpoints: %v != %v", tt.wantEndpoints, e.staticEndpoints.AsSlice())
}
if e.rs != nil && !slices.Equal(tt.wantEndpoints, e.rs.(*mockRelayServer).addrPorts.AsSlice()) {
t.Errorf("wantEndpoints: %v != %v", tt.wantEndpoints, e.rs.(*mockRelayServer).addrPorts.AsSlice())
}
}) })
} }
} }
func mockRelayServerNotZeroVal() *mockRelayServer { func mockRelayServerNotZeroVal() *mockRelayServer {
return &mockRelayServer{true} return &mockRelayServer{set: true}
} }
type mockRelayServer struct { type mockRelayServer struct {
set bool set bool
addrPorts views.Slice[netip.AddrPort]
} }
func (mockRelayServer) Close() error { return nil } func (m *mockRelayServer) Close() error { return nil }
func (mockRelayServer) AllocateEndpoint(_, _ key.DiscoPublic) (endpoint.ServerEndpoint, error) { func (m *mockRelayServer) AllocateEndpoint(_, _ key.DiscoPublic) (endpoint.ServerEndpoint, error) {
return endpoint.ServerEndpoint{}, errors.New("not implemented") return endpoint.ServerEndpoint{}, errors.New("not implemented")
} }
func (mockRelayServer) GetSessions() []status.ServerSession { return nil } func (m *mockRelayServer) GetSessions() []status.ServerSession { return nil }
func (mockRelayServer) SetDERPMapView(tailcfg.DERPMapView) { return } func (m *mockRelayServer) SetDERPMapView(tailcfg.DERPMapView) { return }
func (m *mockRelayServer) SetStaticAddrPorts(aps views.Slice[netip.AddrPort]) {
m.addrPorts = aps
}
type mockSafeBackend struct { type mockSafeBackend struct {
sys *tsd.System sys *tsd.System

@ -64,46 +64,48 @@ func (src *Prefs) Clone() *Prefs {
if dst.RelayServerPort != nil { if dst.RelayServerPort != nil {
dst.RelayServerPort = ptr.To(*src.RelayServerPort) dst.RelayServerPort = ptr.To(*src.RelayServerPort)
} }
dst.RelayServerStaticEndpoints = append(src.RelayServerStaticEndpoints[:0:0], src.RelayServerStaticEndpoints...)
dst.Persist = src.Persist.Clone() dst.Persist = src.Persist.Clone()
return dst return dst
} }
// A compilation failure here means this code must be regenerated, with the command at the top of this file. // A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _PrefsCloneNeedsRegeneration = Prefs(struct { var _PrefsCloneNeedsRegeneration = Prefs(struct {
ControlURL string ControlURL string
RouteAll bool RouteAll bool
ExitNodeID tailcfg.StableNodeID ExitNodeID tailcfg.StableNodeID
ExitNodeIP netip.Addr ExitNodeIP netip.Addr
AutoExitNode ExitNodeExpression AutoExitNode ExitNodeExpression
InternalExitNodePrior tailcfg.StableNodeID InternalExitNodePrior tailcfg.StableNodeID
ExitNodeAllowLANAccess bool ExitNodeAllowLANAccess bool
CorpDNS bool CorpDNS bool
RunSSH bool RunSSH bool
RunWebClient bool RunWebClient bool
WantRunning bool WantRunning bool
LoggedOut bool LoggedOut bool
ShieldsUp bool ShieldsUp bool
AdvertiseTags []string AdvertiseTags []string
Hostname string Hostname string
NotepadURLs bool NotepadURLs bool
ForceDaemon bool ForceDaemon bool
Egg bool Egg bool
AdvertiseRoutes []netip.Prefix AdvertiseRoutes []netip.Prefix
AdvertiseServices []string AdvertiseServices []string
Sync opt.Bool Sync opt.Bool
NoSNAT bool NoSNAT bool
NoStatefulFiltering opt.Bool NoStatefulFiltering opt.Bool
NetfilterMode preftype.NetfilterMode NetfilterMode preftype.NetfilterMode
OperatorUser string OperatorUser string
ProfileName string ProfileName string
AutoUpdate AutoUpdatePrefs AutoUpdate AutoUpdatePrefs
AppConnector AppConnectorPrefs AppConnector AppConnectorPrefs
PostureChecking bool PostureChecking bool
NetfilterKind string NetfilterKind string
DriveShares []*drive.Share DriveShares []*drive.Share
RelayServerPort *int RelayServerPort *int
AllowSingleHosts marshalAsTrueInJSON RelayServerStaticEndpoints []netip.AddrPort
Persist *persist.Persist AllowSingleHosts marshalAsTrueInJSON
Persist *persist.Persist
}{}) }{})
// Clone makes a deep copy of ServeConfig. // Clone makes a deep copy of ServeConfig.

@ -448,6 +448,13 @@ func (v PrefsView) RelayServerPort() views.ValuePointer[int] {
return views.ValuePointerOf(v.ж.RelayServerPort) return views.ValuePointerOf(v.ж.RelayServerPort)
} }
// RelayServerStaticEndpoints are static IP:port endpoints to advertise as
// candidates for relay connections. Only relevant when RelayServerPort is
// non-nil.
func (v PrefsView) RelayServerStaticEndpoints() views.Slice[netip.AddrPort] {
return views.SliceOf(v.ж.RelayServerStaticEndpoints)
}
// AllowSingleHosts was a legacy field that was always true // AllowSingleHosts was a legacy field that was always true
// for the past 4.5 years. It controlled whether Tailscale // for the past 4.5 years. It controlled whether Tailscale
// peers got /32 or /128 routes for each other. // peers got /32 or /128 routes for each other.
@ -468,40 +475,41 @@ 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. // A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _PrefsViewNeedsRegeneration = Prefs(struct { var _PrefsViewNeedsRegeneration = Prefs(struct {
ControlURL string ControlURL string
RouteAll bool RouteAll bool
ExitNodeID tailcfg.StableNodeID ExitNodeID tailcfg.StableNodeID
ExitNodeIP netip.Addr ExitNodeIP netip.Addr
AutoExitNode ExitNodeExpression AutoExitNode ExitNodeExpression
InternalExitNodePrior tailcfg.StableNodeID InternalExitNodePrior tailcfg.StableNodeID
ExitNodeAllowLANAccess bool ExitNodeAllowLANAccess bool
CorpDNS bool CorpDNS bool
RunSSH bool RunSSH bool
RunWebClient bool RunWebClient bool
WantRunning bool WantRunning bool
LoggedOut bool LoggedOut bool
ShieldsUp bool ShieldsUp bool
AdvertiseTags []string AdvertiseTags []string
Hostname string Hostname string
NotepadURLs bool NotepadURLs bool
ForceDaemon bool ForceDaemon bool
Egg bool Egg bool
AdvertiseRoutes []netip.Prefix AdvertiseRoutes []netip.Prefix
AdvertiseServices []string AdvertiseServices []string
Sync opt.Bool Sync opt.Bool
NoSNAT bool NoSNAT bool
NoStatefulFiltering opt.Bool NoStatefulFiltering opt.Bool
NetfilterMode preftype.NetfilterMode NetfilterMode preftype.NetfilterMode
OperatorUser string OperatorUser string
ProfileName string ProfileName string
AutoUpdate AutoUpdatePrefs AutoUpdate AutoUpdatePrefs
AppConnector AppConnectorPrefs AppConnector AppConnectorPrefs
PostureChecking bool PostureChecking bool
NetfilterKind string NetfilterKind string
DriveShares []*drive.Share DriveShares []*drive.Share
RelayServerPort *int RelayServerPort *int
AllowSingleHosts marshalAsTrueInJSON RelayServerStaticEndpoints []netip.AddrPort
Persist *persist.Persist AllowSingleHosts marshalAsTrueInJSON
Persist *persist.Persist
}{}) }{})
// View returns a read-only view of ServeConfig. // View returns a read-only view of ServeConfig.

@ -288,6 +288,11 @@ type Prefs struct {
// non-nil/enabled. // non-nil/enabled.
RelayServerPort *int `json:",omitempty"` RelayServerPort *int `json:",omitempty"`
// RelayServerStaticEndpoints are static IP:port endpoints to advertise as
// candidates for relay connections. Only relevant when RelayServerPort is
// non-nil.
RelayServerStaticEndpoints []netip.AddrPort `json:",omitempty"`
// AllowSingleHosts was a legacy field that was always true // AllowSingleHosts was a legacy field that was always true
// for the past 4.5 years. It controlled whether Tailscale // for the past 4.5 years. It controlled whether Tailscale
// peers got /32 or /128 routes for each other. // peers got /32 or /128 routes for each other.
@ -350,38 +355,39 @@ type AppConnectorPrefs struct {
type MaskedPrefs struct { type MaskedPrefs struct {
Prefs Prefs
ControlURLSet bool `json:",omitempty"` ControlURLSet bool `json:",omitempty"`
RouteAllSet bool `json:",omitempty"` RouteAllSet bool `json:",omitempty"`
ExitNodeIDSet bool `json:",omitempty"` ExitNodeIDSet bool `json:",omitempty"`
ExitNodeIPSet bool `json:",omitempty"` ExitNodeIPSet bool `json:",omitempty"`
AutoExitNodeSet bool `json:",omitempty"` AutoExitNodeSet bool `json:",omitempty"`
InternalExitNodePriorSet bool `json:",omitempty"` // Internal; can't be set by LocalAPI clients InternalExitNodePriorSet bool `json:",omitempty"` // Internal; can't be set by LocalAPI clients
ExitNodeAllowLANAccessSet bool `json:",omitempty"` ExitNodeAllowLANAccessSet bool `json:",omitempty"`
CorpDNSSet bool `json:",omitempty"` CorpDNSSet bool `json:",omitempty"`
RunSSHSet bool `json:",omitempty"` RunSSHSet bool `json:",omitempty"`
RunWebClientSet bool `json:",omitempty"` RunWebClientSet bool `json:",omitempty"`
WantRunningSet bool `json:",omitempty"` WantRunningSet bool `json:",omitempty"`
LoggedOutSet bool `json:",omitempty"` LoggedOutSet bool `json:",omitempty"`
ShieldsUpSet bool `json:",omitempty"` ShieldsUpSet bool `json:",omitempty"`
AdvertiseTagsSet bool `json:",omitempty"` AdvertiseTagsSet bool `json:",omitempty"`
HostnameSet bool `json:",omitempty"` HostnameSet bool `json:",omitempty"`
NotepadURLsSet bool `json:",omitempty"` NotepadURLsSet bool `json:",omitempty"`
ForceDaemonSet bool `json:",omitempty"` ForceDaemonSet bool `json:",omitempty"`
EggSet bool `json:",omitempty"` EggSet bool `json:",omitempty"`
AdvertiseRoutesSet bool `json:",omitempty"` AdvertiseRoutesSet bool `json:",omitempty"`
AdvertiseServicesSet bool `json:",omitempty"` AdvertiseServicesSet bool `json:",omitempty"`
SyncSet bool `json:",omitzero"` SyncSet bool `json:",omitzero"`
NoSNATSet bool `json:",omitempty"` NoSNATSet bool `json:",omitempty"`
NoStatefulFilteringSet bool `json:",omitempty"` NoStatefulFilteringSet bool `json:",omitempty"`
NetfilterModeSet bool `json:",omitempty"` NetfilterModeSet bool `json:",omitempty"`
OperatorUserSet bool `json:",omitempty"` OperatorUserSet bool `json:",omitempty"`
ProfileNameSet bool `json:",omitempty"` ProfileNameSet bool `json:",omitempty"`
AutoUpdateSet AutoUpdatePrefsMask `json:",omitzero"` AutoUpdateSet AutoUpdatePrefsMask `json:",omitzero"`
AppConnectorSet bool `json:",omitempty"` AppConnectorSet bool `json:",omitempty"`
PostureCheckingSet bool `json:",omitempty"` PostureCheckingSet bool `json:",omitempty"`
NetfilterKindSet bool `json:",omitempty"` NetfilterKindSet bool `json:",omitempty"`
DriveSharesSet bool `json:",omitempty"` DriveSharesSet bool `json:",omitempty"`
RelayServerPortSet bool `json:",omitempty"` RelayServerPortSet bool `json:",omitempty"`
RelayServerStaticEndpointsSet bool `json:",omitzero"`
} }
// SetsInternal reports whether mp has any of the Internal*Set field bools set // SetsInternal reports whether mp has any of the Internal*Set field bools set
@ -621,6 +627,9 @@ func (p *Prefs) pretty(goos string) string {
if buildfeatures.HasRelayServer && p.RelayServerPort != nil { if buildfeatures.HasRelayServer && p.RelayServerPort != nil {
fmt.Fprintf(&sb, "relayServerPort=%d ", *p.RelayServerPort) fmt.Fprintf(&sb, "relayServerPort=%d ", *p.RelayServerPort)
} }
if buildfeatures.HasRelayServer && len(p.RelayServerStaticEndpoints) > 0 {
fmt.Fprintf(&sb, "relayServerStaticEndpoints=%v ", p.RelayServerStaticEndpoints)
}
if p.Persist != nil { if p.Persist != nil {
sb.WriteString(p.Persist.Pretty()) sb.WriteString(p.Persist.Pretty())
} else { } else {
@ -685,7 +694,8 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
p.PostureChecking == p2.PostureChecking && p.PostureChecking == p2.PostureChecking &&
slices.EqualFunc(p.DriveShares, p2.DriveShares, drive.SharesEqual) && slices.EqualFunc(p.DriveShares, p2.DriveShares, drive.SharesEqual) &&
p.NetfilterKind == p2.NetfilterKind && p.NetfilterKind == p2.NetfilterKind &&
compareIntPtrs(p.RelayServerPort, p2.RelayServerPort) compareIntPtrs(p.RelayServerPort, p2.RelayServerPort) &&
slices.Equal(p.RelayServerStaticEndpoints, p2.RelayServerStaticEndpoints)
} }
func (au AutoUpdatePrefs) Pretty() string { func (au AutoUpdatePrefs) Pretty() string {

@ -69,6 +69,7 @@ func TestPrefsEqual(t *testing.T) {
"NetfilterKind", "NetfilterKind",
"DriveShares", "DriveShares",
"RelayServerPort", "RelayServerPort",
"RelayServerStaticEndpoints",
"AllowSingleHosts", "AllowSingleHosts",
"Persist", "Persist",
} }
@ -90,6 +91,16 @@ func TestPrefsEqual(t *testing.T) {
} }
return ns return ns
} }
aps := func(strs ...string) (ret []netip.AddrPort) {
for _, s := range strs {
n, err := netip.ParseAddrPort(s)
if err != nil {
panic(err)
}
ret = append(ret, n)
}
return ret
}
tests := []struct { tests := []struct {
a, b *Prefs a, b *Prefs
want bool want bool
@ -369,6 +380,16 @@ func TestPrefsEqual(t *testing.T) {
&Prefs{RelayServerPort: relayServerPort(1)}, &Prefs{RelayServerPort: relayServerPort(1)},
false, false,
}, },
{
&Prefs{RelayServerStaticEndpoints: aps("[2001:db8::1]:40000", "192.0.2.1:40000")},
&Prefs{RelayServerStaticEndpoints: aps("[2001:db8::1]:40000", "192.0.2.1:40000")},
true,
},
{
&Prefs{RelayServerStaticEndpoints: aps("[2001:db8::1]:40000", "192.0.2.2:40000")},
&Prefs{RelayServerStaticEndpoints: aps("[2001:db8::1]:40000", "192.0.2.1:40000")},
false,
},
} }
for i, tt := range tests { for i, tt := range tests {
got := tt.a.Equals(tt.b) got := tt.a.Equals(tt.b)

Loading…
Cancel
Save