tailcfg: add DERPRegion.NoMeasureNoHome, deprecate+document Avoid [cap 115]

Fixes tailscale/corp#24697

Change-Id: Ib81994b5ded3dc87a1eef079eb268906a2acb3f8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2025-03-07 17:12:07 -07:00 committed by Brad Fitzpatrick
parent 346a35f612
commit eb3313e825
7 changed files with 58 additions and 30 deletions

View File

@ -89,7 +89,7 @@ func availableEndpoints(derpMap *tailcfg.DERPMap, preferredDERPRegionID int, log
// Use the DERP IPs as captive portal detection endpoints. Using IPs is better than hostnames // Use the DERP IPs as captive portal detection endpoints. Using IPs is better than hostnames
// because they do not depend on DNS resolution. // because they do not depend on DNS resolution.
for _, region := range derpMap.Regions { for _, region := range derpMap.Regions {
if region.Avoid { if region.Avoid || region.NoMeasureNoHome {
continue continue
} }
for _, node := range region.Nodes { for _, node := range region.Nodes {

View File

@ -387,6 +387,9 @@ type probePlan map[string][]probe
func sortRegions(dm *tailcfg.DERPMap, last *Report, preferredDERP int) (prev []*tailcfg.DERPRegion) { func sortRegions(dm *tailcfg.DERPMap, last *Report, preferredDERP int) (prev []*tailcfg.DERPRegion) {
prev = make([]*tailcfg.DERPRegion, 0, len(dm.Regions)) prev = make([]*tailcfg.DERPRegion, 0, len(dm.Regions))
for _, reg := range dm.Regions { for _, reg := range dm.Regions {
if reg.NoMeasureNoHome {
continue
}
// include an otherwise avoid region if it is the current preferred region // include an otherwise avoid region if it is the current preferred region
if reg.Avoid && reg.RegionID != preferredDERP { if reg.Avoid && reg.RegionID != preferredDERP {
continue continue
@ -533,7 +536,7 @@ func makeProbePlanInitial(dm *tailcfg.DERPMap, ifState *netmon.State) (plan prob
plan = make(probePlan) plan = make(probePlan)
for _, reg := range dm.Regions { for _, reg := range dm.Regions {
if len(reg.Nodes) == 0 { if reg.NoMeasureNoHome || len(reg.Nodes) == 0 {
continue continue
} }

View File

@ -455,7 +455,7 @@ func TestMakeProbePlan(t *testing.T) {
basicMap := &tailcfg.DERPMap{ basicMap := &tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{}, Regions: map[int]*tailcfg.DERPRegion{},
} }
for rid := 1; rid <= 5; rid++ { for rid := 1; rid <= 6; rid++ {
var nodes []*tailcfg.DERPNode var nodes []*tailcfg.DERPNode
for nid := 0; nid < rid; nid++ { for nid := 0; nid < rid; nid++ {
nodes = append(nodes, &tailcfg.DERPNode{ nodes = append(nodes, &tailcfg.DERPNode{
@ -467,8 +467,9 @@ func TestMakeProbePlan(t *testing.T) {
}) })
} }
basicMap.Regions[rid] = &tailcfg.DERPRegion{ basicMap.Regions[rid] = &tailcfg.DERPRegion{
RegionID: rid, RegionID: rid,
Nodes: nodes, Nodes: nodes,
NoMeasureNoHome: rid == 6,
} }
} }

View File

@ -96,12 +96,32 @@ type DERPRegion struct {
Latitude float64 `json:",omitempty"` Latitude float64 `json:",omitempty"`
Longitude float64 `json:",omitempty"` Longitude float64 `json:",omitempty"`
// Avoid is whether the client should avoid picking this as its home // Avoid is whether the client should avoid picking this as its home region.
// region. The region should only be used if a peer is there. // The region should only be used if a peer is there. Clients already using
// Clients already using this region as their home should migrate // this region as their home should migrate away to a new region without
// away to a new region without Avoid set. // Avoid set.
//
// Deprecated: because of bugs in past implementations combined with unclear
// docs that caused people to think the bugs were intentional, this field is
// deprecated. It was never supposed to cause STUN/DERP measurement probes,
// but due to bugs, it sometimes did. And then some parts of the code began
// to rely on that property. But then we were unable to use this field for
// its original purpose, nor its later imagined purpose, because various
// parts of the codebase thought it meant one thing and others thought it
// meant another. But it did something in the middle instead. So we're retiring
// it. Use NoMeasureNoHome instead.
Avoid bool `json:",omitempty"` Avoid bool `json:",omitempty"`
// NoMeasureNoHome says that this regions should not be measured for its
// latency distance (STUN, HTTPS, etc) or availability (e.g. captive portal
// checks) and should never be selected as the node's home region. However,
// if a peer declares this region as its home, then this client is allowed
// to connect to it for the purpose of communicating with that peer.
//
// This is what the now deprecated Avoid bool was supposed to mean
// originally but had implementation bugs and documentation omissions.
NoMeasureNoHome bool `json:",omitempty"`
// Nodes are the DERP nodes running in this region, in // Nodes are the DERP nodes running in this region, in
// priority order for the current client. Client TLS // priority order for the current client. Client TLS
// connections should ideally only go to the first entry // connections should ideally only go to the first entry

View File

@ -159,7 +159,8 @@ type CapabilityVersion int
// - 112: 2025-01-14: Client interprets AllowedIPs of nil as meaning same as Addresses // - 112: 2025-01-14: Client interprets AllowedIPs of nil as meaning same as Addresses
// - 113: 2025-01-20: Client communicates to control whether funnel is enabled by sending Hostinfo.IngressEnabled (#14688) // - 113: 2025-01-20: Client communicates to control whether funnel is enabled by sending Hostinfo.IngressEnabled (#14688)
// - 114: 2025-01-30: NodeAttrMaxKeyDuration CapMap defined, clients might use it (no tailscaled code change) (#14829) // - 114: 2025-01-30: NodeAttrMaxKeyDuration CapMap defined, clients might use it (no tailscaled code change) (#14829)
const CurrentCapabilityVersion CapabilityVersion = 114 // - 115: 2025-03-07: Client understands DERPRegion.NoMeasureNoHome.
const CurrentCapabilityVersion CapabilityVersion = 115
// ID is an integer ID for a user, node, or login allocated by the // ID is an integer ID for a user, node, or login allocated by the
// control plane. // control plane.

View File

@ -416,13 +416,14 @@ func (src *DERPRegion) Clone() *DERPRegion {
// 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 _DERPRegionCloneNeedsRegeneration = DERPRegion(struct { var _DERPRegionCloneNeedsRegeneration = DERPRegion(struct {
RegionID int RegionID int
RegionCode string RegionCode string
RegionName string RegionName string
Latitude float64 Latitude float64
Longitude float64 Longitude float64
Avoid bool Avoid bool
Nodes []*DERPNode NoMeasureNoHome bool
Nodes []*DERPNode
}{}) }{})
// Clone makes a deep copy of DERPMap. // Clone makes a deep copy of DERPMap.

View File

@ -880,25 +880,27 @@ func (v *DERPRegionView) UnmarshalJSON(b []byte) error {
return nil return nil
} }
func (v DERPRegionView) RegionID() int { return v.ж.RegionID } func (v DERPRegionView) RegionID() int { return v.ж.RegionID }
func (v DERPRegionView) RegionCode() string { return v.ж.RegionCode } func (v DERPRegionView) RegionCode() string { return v.ж.RegionCode }
func (v DERPRegionView) RegionName() string { return v.ж.RegionName } func (v DERPRegionView) RegionName() string { return v.ж.RegionName }
func (v DERPRegionView) Latitude() float64 { return v.ж.Latitude } func (v DERPRegionView) Latitude() float64 { return v.ж.Latitude }
func (v DERPRegionView) Longitude() float64 { return v.ж.Longitude } func (v DERPRegionView) Longitude() float64 { return v.ж.Longitude }
func (v DERPRegionView) Avoid() bool { return v.ж.Avoid } func (v DERPRegionView) Avoid() bool { return v.ж.Avoid }
func (v DERPRegionView) NoMeasureNoHome() bool { return v.ж.NoMeasureNoHome }
func (v DERPRegionView) Nodes() views.SliceView[*DERPNode, DERPNodeView] { func (v DERPRegionView) Nodes() views.SliceView[*DERPNode, DERPNodeView] {
return views.SliceOfViews[*DERPNode, DERPNodeView](v.ж.Nodes) return views.SliceOfViews[*DERPNode, DERPNodeView](v.ж.Nodes)
} }
// 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 _DERPRegionViewNeedsRegeneration = DERPRegion(struct { var _DERPRegionViewNeedsRegeneration = DERPRegion(struct {
RegionID int RegionID int
RegionCode string RegionCode string
RegionName string RegionName string
Latitude float64 Latitude float64
Longitude float64 Longitude float64
Avoid bool Avoid bool
Nodes []*DERPNode NoMeasureNoHome bool
Nodes []*DERPNode
}{}) }{})
// View returns a read-only view of DERPMap. // View returns a read-only view of DERPMap.