2022-09-01 01:09:44 +00:00
package tailnet
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
2022-09-11 21:45:49 +00:00
"os"
2022-09-01 01:09:44 +00:00
"strconv"
"golang.org/x/xerrors"
"tailscale.com/tailcfg"
)
2023-08-27 19:46:44 +00:00
const DisableSTUN = "disable"
2023-08-10 19:04:17 +00:00
func STUNRegions ( baseRegionID int , stunAddrs [ ] string ) ( [ ] * tailcfg . DERPRegion , error ) {
regions := make ( [ ] * tailcfg . DERPRegion , 0 , len ( stunAddrs ) )
2023-08-01 15:50:43 +00:00
for index , stunAddr := range stunAddrs {
2023-08-27 19:46:44 +00:00
if stunAddr == DisableSTUN {
2023-08-10 19:04:17 +00:00
return [ ] * tailcfg . DERPRegion { } , nil
2023-08-01 15:50:43 +00:00
}
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 )
}
2023-08-10 19:04:17 +00:00
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 ,
} } ,
} )
2023-08-01 15:50:43 +00:00
}
2023-08-10 19:04:17 +00:00
return regions , nil
2023-08-01 15:50:43 +00:00
}
2022-09-01 01:09:44 +00:00
// 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.
2023-06-21 22:02:05 +00:00
//
//nolint:revive
func NewDERPMap ( ctx context . Context , region * tailcfg . DERPRegion , stunAddrs [ ] string , remoteURL , localPath string , disableSTUN bool ) ( * tailcfg . DERPMap , error ) {
2022-09-11 21:45:49 +00:00
if remoteURL != "" && localPath != "" {
return nil , xerrors . New ( "a remote URL or local path must be specified, not both" )
}
2023-06-21 22:02:05 +00:00
if disableSTUN {
stunAddrs = nil
}
2023-08-10 19:04:17 +00:00
// 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 { }
2022-09-02 23:47:25 +00:00
if region != nil {
2023-08-10 19:04:17 +00:00
addRegions = append ( addRegions , region )
stunRegions , err := STUNRegions ( region . RegionID , stunAddrs )
2023-08-01 15:50:43 +00:00
if err != nil {
2023-08-10 19:04:17 +00:00
return nil , xerrors . Errorf ( "create stun regions: %w" , err )
2022-09-01 01:09:44 +00:00
}
2023-08-10 19:04:17 +00:00
addRegions = append ( addRegions , stunRegions ... )
2022-09-01 01:09:44 +00:00
}
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 )
}
}
2022-09-11 21:45:49 +00:00
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 )
}
}
2023-08-10 19:04:17 +00:00
// 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
2022-09-02 23:47:25 +00:00
}
2022-09-01 01:09:44 +00:00
}
2023-08-10 19:04:17 +00:00
2023-06-21 22:02:05 +00:00
// 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
}
}
2023-11-06 13:04:07 +00:00
// 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 )
}
2022-09-01 01:09:44 +00:00
return derpMap , nil
}
2023-07-26 16:21:04 +00:00
// 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
}