mirror of
https://github.com/tailscale/tailscale.git
synced 2026-09-14 23:11:39 -04:00
Historically, when DERP regions were switched away from strings to numeric identifiers in PR #14641, tailcfg.Node.HomeDERP was declared as an int instead of its own type. This PR declares a new tailcfg.DERPRegionID type, represented by an int64, and converts the following fields to use this type: - netcheck.Report.PreferredDERP - netcheck.Report.RegionLatency - netcheck.Report.RegionV4Latency - netcheck.Report.RegionV6Latency - tailcfg.DERPHomeParams.RegionScore - tailcfg.DERPMap.Regions - tailcfg.DERPNode.RegionID - tailcfg.DERPRegion.RegionID - tailcfg.NetInfo.PreferredDERP - tailcfg.Node.HomeDERP - tailcfg.PeerChange.DERPRegion - tailcfg.PingResponse.DERPRegionID Note that the original field was an int, while the new field is backed by an int64. This change makes DERPRegionID the same size on both 32-bit and 64-bit architectures. Fixes: #20165 Change-Id: Ic6f795a6d791dd16f756f246d5a02085443e212f Signed-off-by: Simon Law <sfllaw@tailscale.com>
278 lines
10 KiB
Go
278 lines
10 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
||
// SPDX-License-Identifier: BSD-3-Clause
|
||
|
||
package tailcfg
|
||
|
||
import (
|
||
"fmt"
|
||
"net/netip"
|
||
"slices"
|
||
"strconv"
|
||
|
||
"tailscale.com/types/key"
|
||
)
|
||
|
||
// DERPMap describes the set of DERP packet relay servers that are available.
|
||
//
|
||
// API maturity: this type is subject to minor changes over time, though
|
||
// its general shape is stable.
|
||
type DERPMap struct {
|
||
// HomeParams, if non-nil, is a change in home parameters.
|
||
//
|
||
// The rest of the DEPRMap fields, if zero, means unchanged.
|
||
HomeParams *DERPHomeParams `json:",omitempty"`
|
||
|
||
// Regions is the set of geographic regions running DERP node(s).
|
||
//
|
||
// It's keyed by the DERPRegion.RegionID.
|
||
//
|
||
// The numbers are not necessarily contiguous.
|
||
Regions map[DERPRegionID]*DERPRegion
|
||
|
||
// OmitDefaultRegions specifies to not use Tailscale's DERP servers, and only use those
|
||
// specified in this DERPMap. If there are none set outside of the defaults, this is a noop.
|
||
//
|
||
// This field is only meaningful if the Regions map is non-nil (indicating a change).
|
||
OmitDefaultRegions bool `json:"omitDefaultRegions,omitempty"`
|
||
}
|
||
|
||
// RegionIDs returns the sorted region IDs.
|
||
func (m *DERPMap) RegionIDs() []DERPRegionID {
|
||
ret := make([]DERPRegionID, 0, len(m.Regions))
|
||
for rid := range m.Regions {
|
||
ret = append(ret, rid)
|
||
}
|
||
slices.Sort(ret)
|
||
return ret
|
||
}
|
||
|
||
// DERPHomeParams contains parameters from the server related to selecting a
|
||
// DERP home region (sometimes referred to as the "preferred DERP").
|
||
type DERPHomeParams struct {
|
||
// RegionScore scales latencies of DERP regions by a given scaling
|
||
// factor when determining which region to use as the home
|
||
// ("preferred") DERP. Scores in the range (0, 1) will cause this
|
||
// region to be proportionally more preferred, and scores in the range
|
||
// (1, ∞) will penalize a region.
|
||
//
|
||
// If a region is not present in this map, it is treated as having a
|
||
// score of 1.0.
|
||
//
|
||
// Scores should not be 0 or negative; such scores will be ignored.
|
||
//
|
||
// A nil map means no change from the previous value (if any); an empty
|
||
// non-nil map can be sent to reset all scores back to 1.0.
|
||
RegionScore map[DERPRegionID]float64 `json:",omitempty"`
|
||
}
|
||
|
||
// DERPRegionID is a unique integer for a geographic region.
|
||
//
|
||
// It corresponds to the legacy derpN.tailscale.com hostnames used by older clients.
|
||
// (Older clients will continue to resolve derpN.tailscale.com when contacting peers,
|
||
// rather than use the server-provided DERPMap)
|
||
//
|
||
// It must be non-zero, positive, and guaranteed to fit in a JavaScript number.
|
||
//
|
||
// IDs in range 900-999 are reserved for end users to run their own DERP nodes.
|
||
type DERPRegionID int64
|
||
|
||
// ParseDERPRegionID parses s and returns a [DERPRegionID].
|
||
// It returns an error if s isn’t a valid region ID.
|
||
func ParseDERPRegionID(s string) (DERPRegionID, error) {
|
||
id, err := strconv.ParseInt(s, 10, 64)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
rid := DERPRegionID(id)
|
||
if !rid.IsValid() {
|
||
return 0, fmt.Errorf("invalid region ID: %s", s)
|
||
}
|
||
return rid, nil
|
||
}
|
||
|
||
// IsValid reports whether id is non-zero, positive, and fits in a JavaScript number.
|
||
func (id DERPRegionID) IsValid() bool {
|
||
const maxSafeJavaScriptInteger DERPRegionID = 1<<53 - 1 // Number.MAX_SAFE_INTEGER
|
||
return id > 0 && id <= maxSafeJavaScriptInteger
|
||
}
|
||
|
||
// Int64 returns the int64 representation of id.
|
||
func (id DERPRegionID) Int64() int64 {
|
||
return int64(id)
|
||
}
|
||
|
||
// String implements the [fmt.Stringer] interface.
|
||
func (id DERPRegionID) String() string {
|
||
return strconv.FormatInt(int64(id), 10)
|
||
}
|
||
|
||
// DERPRegion is a geographic region running DERP relay node(s).
|
||
//
|
||
// Client nodes discover which region they're closest to, advertise
|
||
// that "home" DERP region (previously called "home node", when there
|
||
// was only 1 node per region) and maintain a persistent connection
|
||
// that region as long as it's the closest. Client nodes will further
|
||
// connect to other regions as necessary to communicate with peers
|
||
// advertising other regions as their homes.
|
||
type DERPRegion struct {
|
||
// RegionID is a unique integer for a geographic region.
|
||
//
|
||
// It corresponds to the legacy derpN.tailscale.com hostnames
|
||
// used by older clients. (Older clients will continue to resolve
|
||
// derpN.tailscale.com when contacting peers, rather than use
|
||
// the server-provided DERPMap)
|
||
//
|
||
// RegionIDs must be non-zero, positive, and guaranteed to fit
|
||
// in a JavaScript number.
|
||
//
|
||
// RegionIDs in range 900-999 are reserved for end users to run their
|
||
// own DERP nodes.
|
||
RegionID DERPRegionID
|
||
|
||
// RegionCode is a short name for the region. It's usually a popular
|
||
// city or airport code in the region: "nyc", "sf", "sin",
|
||
// "fra", etc.
|
||
RegionCode string
|
||
|
||
// RegionName is a long English name for the region: "New York City",
|
||
// "San Francisco", "Singapore", "Frankfurt", etc.
|
||
RegionName string
|
||
|
||
// Latitude, Longitude are optional geographical coordinates of the DERP region's city, in degrees.
|
||
Latitude float64 `json:",omitempty"`
|
||
Longitude float64 `json:",omitempty"`
|
||
|
||
// Avoid is whether the client should avoid picking this as its home region.
|
||
// The region should only be used if a peer is there. Clients already using
|
||
// this region as their home should migrate away to a new region without
|
||
// 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"`
|
||
|
||
// 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
|
||
// priority order for the current client. Client TLS
|
||
// connections should ideally only go to the first entry
|
||
// (falling back to the second if necessary). STUN packets
|
||
// should go to the first 1 or 2.
|
||
//
|
||
// If nodes within a region route packets amongst themselves,
|
||
// but not to other regions. That said, each user/domain
|
||
// should get a the same preferred node order, so if all nodes
|
||
// for a user/network pick the first one (as they should, when
|
||
// things are healthy), the inter-cluster routing is minimal
|
||
// to zero.
|
||
Nodes []*DERPNode
|
||
}
|
||
|
||
// DERPNode describes a DERP packet relay node running within a DERPRegion.
|
||
type DERPNode struct {
|
||
// Name is a unique node name (across all regions).
|
||
// It is not a host name.
|
||
// It's typically of the form "1b", "2a", "3b", etc. (region
|
||
// ID + suffix within that region)
|
||
Name string
|
||
|
||
// RegionID is the RegionID of the DERPRegion that this node
|
||
// is running in.
|
||
RegionID DERPRegionID
|
||
|
||
// HostName is the DERP node's hostname.
|
||
//
|
||
// It is required but need not be unique; multiple nodes may
|
||
// have the same HostName but vary in configuration otherwise.
|
||
HostName string
|
||
|
||
// CertName optionally specifies the expected TLS cert common
|
||
// name. If empty, HostName is used. If CertName is non-empty,
|
||
// HostName is only used for the TCP dial (if IPv4/IPv6 are
|
||
// not present) + TLS ClientHello.
|
||
//
|
||
// As a special case, if CertName starts with "sha256-raw:",
|
||
// then the rest of the string is a hex-encoded SHA256 of the
|
||
// cert to expect. This is used for self-signed certs.
|
||
// In this case, the HostName field will typically be an IP
|
||
// address literal.
|
||
CertName string `json:",omitempty"`
|
||
|
||
// IPv4 optionally forces an IPv4 address to use, instead of using DNS.
|
||
// If empty, A record(s) from DNS lookups of HostName are used.
|
||
// If the string is not an IPv4 address, IPv4 is not used; the
|
||
// conventional string to disable IPv4 (and not use DNS) is
|
||
// "none".
|
||
IPv4 string `json:",omitempty"`
|
||
|
||
// IPv6 optionally forces an IPv6 address to use, instead of using DNS.
|
||
// If empty, AAAA record(s) from DNS lookups of HostName are used.
|
||
// If the string is not an IPv6 address, IPv6 is not used; the
|
||
// conventional string to disable IPv6 (and not use DNS) is
|
||
// "none".
|
||
IPv6 string `json:",omitempty"`
|
||
|
||
// Port optionally specifies a STUN port to use.
|
||
// Zero means 3478.
|
||
// To disable STUN on this node, use -1.
|
||
STUNPort int `json:",omitempty"`
|
||
|
||
// STUNOnly marks a node as only a STUN server and not a DERP
|
||
// server.
|
||
STUNOnly bool `json:",omitempty"`
|
||
|
||
// DERPPort optionally provides an alternate TLS port number
|
||
// for the DERP HTTPS server.
|
||
//
|
||
// If zero, 443 is used.
|
||
DERPPort int `json:",omitempty"`
|
||
|
||
// InsecureForTests is used by unit tests to disable TLS verification.
|
||
// It should not be set by users.
|
||
InsecureForTests bool `json:",omitempty"`
|
||
|
||
// STUNTestIP is used in tests to override the STUN server's IP.
|
||
// If empty, it's assumed to be the same as the DERP server.
|
||
STUNTestIP string `json:",omitempty"`
|
||
|
||
// CanPort80 specifies whether this DERP node is accessible over HTTP
|
||
// on port 80 specifically. This is used for captive portal checks.
|
||
CanPort80 bool `json:",omitempty"`
|
||
}
|
||
|
||
func (n *DERPNode) IsTestNode() bool {
|
||
return n.STUNTestIP != "" || n.IPv4 == "127.0.0.1"
|
||
}
|
||
|
||
// DotInvalid is a fake DNS TLD used in tests for an invalid hostname.
|
||
const DotInvalid = ".invalid"
|
||
|
||
// DERPAdmitClientRequest is the JSON request body of a POST to derper's
|
||
// --verify-client-url admission controller URL.
|
||
type DERPAdmitClientRequest struct {
|
||
NodePublic key.NodePublic // key to query for admission
|
||
Source netip.Addr // derp client's IP address
|
||
}
|
||
|
||
// DERPAdmitClientResponse is the response to a DERPAdmitClientRequest.
|
||
type DERPAdmitClientResponse struct {
|
||
Allow bool // whether to permit client
|
||
|
||
// TODO(bradfitz,maisem): bandwidth limits, etc?
|
||
}
|