Files
coder/tailnet/derpmap.go
2023-06-21 22:02:05 +00:00

100 lines
2.7 KiB
Go

package tailnet
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"os"
"strconv"
"golang.org/x/xerrors"
"tailscale.com/tailcfg"
)
// 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
}
if region != nil {
for index, stunAddr := range stunAddrs {
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)
}
region.Nodes = append([]*tailcfg.DERPNode{{
Name: fmt.Sprintf("%dstun%d", region.RegionID, index),
RegionID: region.RegionID,
HostName: host,
STUNOnly: true,
STUNPort: port,
}}, region.Nodes...)
}
}
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)
}
}
if region != nil {
_, conflicts := derpMap.Regions[region.RegionID]
if conflicts {
return nil, xerrors.Errorf("the default region ID conflicts with a remote region from %q", 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
}
}
return derpMap, nil
}