coder/tailnet/derpmap.go

266 lines
6.6 KiB
Go

package tailnet
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"os"
"strconv"
"golang.org/x/xerrors"
"tailscale.com/tailcfg"
)
const DisableSTUN = "disable"
func STUNRegions(baseRegionID int, stunAddrs []string) ([]*tailcfg.DERPRegion, error) {
regions := make([]*tailcfg.DERPRegion, 0, len(stunAddrs))
for index, stunAddr := range stunAddrs {
if stunAddr == DisableSTUN {
return []*tailcfg.DERPRegion{}, nil
}
host, rawPort, err := net.SplitHostPort(stunAddr)
if err != nil {
return nil, xerrors.Errorf("split host port for %q: %w", stunAddr, err)
}
port, err := strconv.Atoi(rawPort)
if err != nil {
return nil, xerrors.Errorf("parse port for %q: %w", stunAddr, err)
}
regionID := baseRegionID + index + 1
regions = append(regions, &tailcfg.DERPRegion{
EmbeddedRelay: false,
RegionID: regionID,
RegionCode: fmt.Sprintf("coder_stun_%d", regionID),
RegionName: fmt.Sprintf("Coder STUN %d", regionID),
Nodes: []*tailcfg.DERPNode{{
Name: fmt.Sprintf("%dstun0", regionID),
RegionID: regionID,
HostName: host,
STUNOnly: true,
STUNPort: port,
}},
})
}
return regions, nil
}
// NewDERPMap constructs a DERPMap from a set of STUN addresses and optionally a remote
// URL to fetch a mapping from e.g. https://controlplane.tailscale.com/derpmap/default.
//
//nolint:revive
func NewDERPMap(ctx context.Context, region *tailcfg.DERPRegion, stunAddrs []string, remoteURL, localPath string, disableSTUN bool) (*tailcfg.DERPMap, error) {
if remoteURL != "" && localPath != "" {
return nil, xerrors.New("a remote URL or local path must be specified, not both")
}
if disableSTUN {
stunAddrs = nil
}
// stunAddrs only applies when a default region is set. Each STUN node gets
// it's own region ID because netcheck will only try a single STUN server in
// each region before canceling the region's STUN check.
addRegions := []*tailcfg.DERPRegion{}
if region != nil {
addRegions = append(addRegions, region)
stunRegions, err := STUNRegions(region.RegionID, stunAddrs)
if err != nil {
return nil, xerrors.Errorf("create stun regions: %w", err)
}
addRegions = append(addRegions, stunRegions...)
}
derpMap := &tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{},
}
if remoteURL != "" {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, remoteURL, nil)
if err != nil {
return nil, xerrors.Errorf("create request: %w", err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, xerrors.Errorf("get derpmap: %w", err)
}
defer res.Body.Close()
err = json.NewDecoder(res.Body).Decode(&derpMap)
if err != nil {
return nil, xerrors.Errorf("fetch derpmap: %w", err)
}
}
if localPath != "" {
content, err := os.ReadFile(localPath)
if err != nil {
return nil, xerrors.Errorf("read derpmap from %q: %w", localPath, err)
}
err = json.Unmarshal(content, &derpMap)
if err != nil {
return nil, xerrors.Errorf("unmarshal derpmap: %w", err)
}
}
// Add our custom regions to the DERP map.
if len(addRegions) > 0 {
for _, region := range addRegions {
_, conflicts := derpMap.Regions[region.RegionID]
if conflicts {
return nil, xerrors.Errorf("a default region ID %d (%s - %q) conflicts with a remote region from %q", region.RegionID, region.RegionCode, region.RegionName, remoteURL)
}
derpMap.Regions[region.RegionID] = region
}
}
// Remove all STUNPorts from DERPy nodes, and fully remove all STUNOnly
// nodes.
if disableSTUN {
for _, region := range derpMap.Regions {
newNodes := make([]*tailcfg.DERPNode, 0, len(region.Nodes))
for _, node := range region.Nodes {
node.STUNPort = -1
if !node.STUNOnly {
newNodes = append(newNodes, node)
}
}
region.Nodes = newNodes
}
}
// Fail if the DERP map has no regions or no DERP nodes.
badDerpMapMsg := "A valid DERP map is required for networking to work. You must either supply your own DERP map or use the built-in DERP server"
if len(derpMap.Regions) == 0 {
return nil, xerrors.New("DERP map has no regions. " + badDerpMapMsg)
}
foundValidNode := false
regionLoop:
for _, region := range derpMap.Regions {
for _, node := range region.Nodes {
if !node.STUNOnly {
foundValidNode = true
break regionLoop
}
}
}
if !foundValidNode {
return nil, xerrors.New("DERP map has no DERP nodes. " + badDerpMapMsg)
}
return derpMap, nil
}
// CompareDERPMaps returns true if the given DERPMaps are equivalent. Ordering
// of slices is ignored.
//
// If the first map is nil, the second map must also be nil for them to be
// considered equivalent. If the second map is nil, the first map can be any
// value and the function will return true.
func CompareDERPMaps(a *tailcfg.DERPMap, b *tailcfg.DERPMap) bool {
if a == nil {
return b == nil
}
if b == nil {
return true
}
if len(a.Regions) != len(b.Regions) {
return false
}
if a.OmitDefaultRegions != b.OmitDefaultRegions {
return false
}
for id, region := range a.Regions {
other, ok := b.Regions[id]
if !ok {
return false
}
if !compareDERPRegions(region, other) {
return false
}
}
return true
}
func compareDERPRegions(a *tailcfg.DERPRegion, b *tailcfg.DERPRegion) bool {
if a == nil || b == nil {
return false
}
if a.EmbeddedRelay != b.EmbeddedRelay {
return false
}
if a.RegionID != b.RegionID {
return false
}
if a.RegionCode != b.RegionCode {
return false
}
if a.RegionName != b.RegionName {
return false
}
if a.Avoid != b.Avoid {
return false
}
if len(a.Nodes) != len(b.Nodes) {
return false
}
// Convert both slices to maps so ordering can be ignored easier.
aNodes := map[string]*tailcfg.DERPNode{}
for _, node := range a.Nodes {
aNodes[node.Name] = node
}
bNodes := map[string]*tailcfg.DERPNode{}
for _, node := range b.Nodes {
bNodes[node.Name] = node
}
for name, aNode := range aNodes {
bNode, ok := bNodes[name]
if !ok {
return false
}
if aNode.Name != bNode.Name {
return false
}
if aNode.RegionID != bNode.RegionID {
return false
}
if aNode.HostName != bNode.HostName {
return false
}
if aNode.CertName != bNode.CertName {
return false
}
if aNode.IPv4 != bNode.IPv4 {
return false
}
if aNode.IPv6 != bNode.IPv6 {
return false
}
if aNode.STUNPort != bNode.STUNPort {
return false
}
if aNode.STUNOnly != bNode.STUNOnly {
return false
}
if aNode.DERPPort != bNode.DERPPort {
return false
}
if aNode.InsecureForTests != bNode.InsecureForTests {
return false
}
if aNode.ForceHTTP != bNode.ForceHTTP {
return false
}
if aNode.STUNTestIP != bNode.STUNTestIP {
return false
}
}
return true
}