tailcfg: add Hostinfo.ExitNodeID to report the selected exit node (#16625)

When a client selects a particular exit node, Control may use that as
a signal for deciding other routes.

This patch causes the client to report whenever the current exit node
changes, through tailcfg.Hostinfo.ExitNodeID. It relies on a properly
set ipn.Prefs.ExitNodeID, which should already be resolved by
`tailscale set`.

Updates tailscale/corp#30536

Signed-off-by: Simon Law <sfllaw@tailscale.com>
This commit is contained in:
Simon Law
2025-07-22 13:54:28 -07:00
committed by GitHub
parent 19faaff95c
commit 729d6532ff
6 changed files with 83 additions and 16 deletions

View File

@@ -166,7 +166,8 @@ type CapabilityVersion int
// - 119: 2025-07-10: Client uses Hostinfo.Location.Priority to prioritize one route over another.
// - 120: 2025-07-15: Client understands peer relay disco messages, and implements peer client and relay server functions
// - 121: 2025-07-19: Client understands peer relay endpoint alloc with [disco.AllocateUDPRelayEndpointRequest] & [disco.AllocateUDPRelayEndpointResponse]
const CurrentCapabilityVersion CapabilityVersion = 121
// - 122: 2025-07-21: Client sends Hostinfo.ExitNodeID to report which exit node it has selected, if any.
const CurrentCapabilityVersion CapabilityVersion = 122
// ID is an integer ID for a user, node, or login allocated by the
// control plane.
@@ -875,6 +876,7 @@ type Hostinfo struct {
UserspaceRouter opt.Bool `json:",omitempty"` // if the client's subnet router is running in userspace (netstack) mode
AppConnector opt.Bool `json:",omitempty"` // if the client is running the app-connector service
ServicesHash string `json:",omitempty"` // opaque hash of the most recent list of tailnet services, change in hash indicates config should be fetched via c2n
ExitNodeID StableNodeID `json:",omitzero"` // the clients selected exit node, empty when unselected.
// Location represents geographical location data about a
// Tailscale host. Location is optional and only set if

View File

@@ -186,6 +186,7 @@ var _HostinfoCloneNeedsRegeneration = Hostinfo(struct {
UserspaceRouter opt.Bool
AppConnector opt.Bool
ServicesHash string
ExitNodeID StableNodeID
Location *Location
TPM *TPMInfo
StateEncrypted opt.Bool

View File

@@ -67,6 +67,7 @@ func TestHostinfoEqual(t *testing.T) {
"UserspaceRouter",
"AppConnector",
"ServicesHash",
"ExitNodeID",
"Location",
"TPM",
"StateEncrypted",
@@ -273,6 +274,21 @@ func TestHostinfoEqual(t *testing.T) {
&Hostinfo{IngressEnabled: true},
false,
},
{
&Hostinfo{ExitNodeID: "stable-exit"},
&Hostinfo{ExitNodeID: "stable-exit"},
true,
},
{
&Hostinfo{ExitNodeID: ""},
&Hostinfo{},
true,
},
{
&Hostinfo{ExitNodeID: ""},
&Hostinfo{ExitNodeID: "stable-exit"},
false,
},
}
for i, tt := range tests {
got := tt.a.Equal(tt.b)

View File

@@ -300,6 +300,7 @@ func (v HostinfoView) Userspace() opt.Bool { return v.ж.User
func (v HostinfoView) UserspaceRouter() opt.Bool { return v.ж.UserspaceRouter }
func (v HostinfoView) AppConnector() opt.Bool { return v.ж.AppConnector }
func (v HostinfoView) ServicesHash() string { return v.ж.ServicesHash }
func (v HostinfoView) ExitNodeID() StableNodeID { return v.ж.ExitNodeID }
func (v HostinfoView) Location() LocationView { return v.ж.Location.View() }
func (v HostinfoView) TPM() views.ValuePointer[TPMInfo] { return views.ValuePointerOf(v.ж.TPM) }
@@ -345,6 +346,7 @@ var _HostinfoViewNeedsRegeneration = Hostinfo(struct {
UserspaceRouter opt.Bool
AppConnector opt.Bool
ServicesHash string
ExitNodeID StableNodeID
Location *Location
TPM *TPMInfo
StateEncrypted opt.Bool