From 9e2df8093fa01ef9acaefaec588992d19fd5ea04 Mon Sep 17 00:00:00 2001 From: Lee Briggs Date: Wed, 5 Nov 2025 09:28:20 -0500 Subject: [PATCH] cmd/tailscale/cli: configure a static endpoint flag on tailscale set Fixes #13462 Signed-off-by: Lee Briggs --- cmd/tailscale/cli/set.go | 22 ++++++++++++++++++++++ cmd/tailscale/cli/up.go | 1 + ipn/ipn_clone.go | 2 ++ ipn/ipn_view.go | 7 +++++++ ipn/ipnlocal/local.go | 7 +++++++ ipn/prefs.go | 5 +++++ 6 files changed, 44 insertions(+) diff --git a/cmd/tailscale/cli/set.go b/cmd/tailscale/cli/set.go index 43f8bbbc3..9ff543a87 100644 --- a/cmd/tailscale/cli/set.go +++ b/cmd/tailscale/cli/set.go @@ -65,6 +65,7 @@ type setArgsT struct { statefulFiltering bool netfilterMode string relayServerPort string + staticEndpoints string } func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet { @@ -86,6 +87,7 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet { setf.BoolVar(&setArgs.reportPosture, "report-posture", false, "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", "", "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.staticEndpoints, "endpoint", "", "static endpoint(s) to advertise (comma-separated, e.g. \"192.168.1.100:41641,203.0.113.1:41641\")") ffcomplete.Flag(setf, "exit-node", func(args []string) ([]string, ffcomplete.ShellCompDirective, error) { st, err := localClient.Status(context.Background()) @@ -185,6 +187,26 @@ func runSet(ctx context.Context, args []string) (retErr error) { } } + if setArgs.staticEndpoints != "" { + const max = 10 // reasonable limit for static endpoints + remain := setArgs.staticEndpoints + var endpoints []netip.AddrPort + for remain != "" && len(endpoints) < max { + var s string + s, remain, _ = strings.Cut(remain, ",") + s = strings.TrimSpace(s) + if s == "" { + continue + } + ap, err := netip.ParseAddrPort(s) + if err != nil { + return fmt.Errorf("invalid endpoint %q: %v", s, err) + } + endpoints = append(endpoints, ap) + } + maskedPrefs.Prefs.StaticEndpoints = endpoints + } + warnOnAdvertiseRoutes(ctx, &maskedPrefs.Prefs) if err := checkExitNodeRisk(ctx, &maskedPrefs.Prefs, setArgs.acceptedRisks); err != nil { return err diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go index 91a6b6087..97e6ef791 100644 --- a/cmd/tailscale/cli/up.go +++ b/cmd/tailscale/cli/up.go @@ -889,6 +889,7 @@ func init() { addPrefFlagMapping("advertise-connector", "AppConnector") addPrefFlagMapping("report-posture", "PostureChecking") addPrefFlagMapping("relay-server-port", "RelayServerPort") + addPrefFlagMapping("endpoint", "StaticEndpoints") } func addPrefFlagMapping(flagName string, prefNames ...string) { diff --git a/ipn/ipn_clone.go b/ipn/ipn_clone.go index 3d2670947..349f633ce 100644 --- a/ipn/ipn_clone.go +++ b/ipn/ipn_clone.go @@ -64,6 +64,7 @@ func (src *Prefs) Clone() *Prefs { if dst.RelayServerPort != nil { dst.RelayServerPort = ptr.To(*src.RelayServerPort) } + dst.StaticEndpoints = append(src.StaticEndpoints[:0:0], src.StaticEndpoints...) dst.Persist = src.Persist.Clone() return dst } @@ -101,6 +102,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct { NetfilterKind string DriveShares []*drive.Share RelayServerPort *int + StaticEndpoints []netip.AddrPort AllowSingleHosts marshalAsTrueInJSON Persist *persist.Persist }{}) diff --git a/ipn/ipn_view.go b/ipn/ipn_view.go index ba5477a6d..1f9d89294 100644 --- a/ipn/ipn_view.go +++ b/ipn/ipn_view.go @@ -442,6 +442,12 @@ func (v PrefsView) RelayServerPort() views.ValuePointer[int] { return views.ValuePointerOf(v.ж.RelayServerPort) } +// StaticEndpoints are additional, user-defined endpoints that this node +// should advertise amongst its wireguard endpoints. +func (v PrefsView) StaticEndpoints() views.Slice[netip.AddrPort] { + return views.SliceOf(v.ж.StaticEndpoints) +} + // 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. @@ -493,6 +499,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct { NetfilterKind string DriveShares []*drive.Share RelayServerPort *int + StaticEndpoints []netip.AddrPort AllowSingleHosts marshalAsTrueInJSON Persist *persist.Persist }{}) diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index ffab4b69d..3865ad604 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -4656,6 +4656,13 @@ func (b *LocalBackend) setPrefsLockedOnEntry(newp *ipn.Prefs, unlock unlockOnce) b.resetAlwaysOnOverrideLocked() } + // Update static endpoints if they've changed + if !oldp.Valid() || !slices.Equal(oldp.StaticEndpoints().AsSlice(), prefs.StaticEndpoints().AsSlice()) { + if ms, ok := b.sys.MagicSock.GetOK(); ok { + ms.SetStaticEndpoints(prefs.StaticEndpoints()) + } + } + unlock.UnlockEarly() if oldp.ShieldsUp() != newp.ShieldsUp || hostInfoChanged { diff --git a/ipn/prefs.go b/ipn/prefs.go index 81dd1c1c3..99c80f6c1 100644 --- a/ipn/prefs.go +++ b/ipn/prefs.go @@ -282,6 +282,10 @@ type Prefs struct { // non-nil/enabled. RelayServerPort *int `json:",omitempty"` + // StaticEndpoints are additional, user-defined endpoints that this node + // should advertise amongst its wireguard endpoints. + StaticEndpoints []netip.AddrPort `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. @@ -375,6 +379,7 @@ type MaskedPrefs struct { NetfilterKindSet bool `json:",omitempty"` DriveSharesSet bool `json:",omitempty"` RelayServerPortSet bool `json:",omitempty"` + StaticEndpointsSet bool `json:",omitempty"` } // SetsInternal reports whether mp has any of the Internal*Set field bools set