util/cloudenv: add support for DigitalOcean

Updates #4984

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ib229eb40af36a80e6b0fd1dd0cabb07f0d50a7d1
This commit is contained in:
Andrew Dunham 2024-02-09 18:16:33 -05:00
parent 55b372a79f
commit c1c50cfcc0
2 changed files with 59 additions and 3 deletions

View File

@ -8,6 +8,7 @@ import (
"context"
"encoding/json"
"log"
"math/rand"
"net"
"net/http"
"os"
@ -16,6 +17,7 @@ import (
"time"
"tailscale.com/syncs"
"tailscale.com/types/lazy"
)
// CommonNonRoutableMetadataIP is the IP address of the metadata server
@ -40,9 +42,10 @@ const AzureResolverIP = "168.63.129.16"
type Cloud string
const (
AWS = Cloud("aws") // Amazon Web Services (EC2 in particular)
Azure = Cloud("azure") // Microsoft Azure
GCP = Cloud("gcp") // Google Cloud
AWS = Cloud("aws") // Amazon Web Services (EC2 in particular)
Azure = Cloud("azure") // Microsoft Azure
GCP = Cloud("gcp") // Google Cloud
DigitalOcean = Cloud("digitalocean") // DigitalOcean
)
// ResolverIP returns the cloud host's recursive DNS server or the
@ -55,10 +58,27 @@ func (c Cloud) ResolverIP() string {
return AWSResolverIP
case Azure:
return AzureResolverIP
case DigitalOcean:
return getDigitalOceanResolver()
}
return ""
}
var (
// https://docs.digitalocean.com/support/check-your-droplets-network-configuration/
digitalOceanResolvers = []string{"67.207.67.2", "67.207.67.3"}
digitalOceanResolver lazy.SyncValue[string]
)
func getDigitalOceanResolver() string {
// Randomly select one of the available resolvers so we don't overload
// one of them by sending all traffic there.
return digitalOceanResolver.Get(func() string {
rn := rand.New(rand.NewSource(time.Now().UnixNano()))
return digitalOceanResolvers[rn.Intn(len(digitalOceanResolvers))]
})
}
// HasInternalTLD reports whether c is a cloud environment
// whose ResolverIP serves *.internal records.
func (c Cloud) HasInternalTLD() bool {
@ -98,6 +118,12 @@ func getCloud() Cloud {
return AWS
}
sysVendor := readFileTrimmed("/sys/class/dmi/id/sys_vendor")
if sysVendor == "DigitalOcean" {
return DigitalOcean
}
// TODO(andrew): "Vultr" is also valid if we need it
prod := readFileTrimmed("/sys/class/dmi/id/product_name")
if prod == "Google Compute Engine" {
return GCP
@ -109,6 +135,7 @@ func getCloud() Cloud {
// Azure, or maybe all Hyper-V?
hitMetadata = true
}
default:
// TODO(bradfitz): use Win32_SystemEnclosure from WMI or something on
// Windows to see if it's a physical machine and skip the cloud check

View File

@ -0,0 +1,29 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package cloudenv
import (
"flag"
"net/netip"
"testing"
)
var extNetwork = flag.Bool("use-external-network", false, "use the external network in tests")
// Informational only since we can run tests in a variety of places.
func TestGetCloud(t *testing.T) {
if !*extNetwork {
t.Skip("skipping test without --use-external-network")
}
cloud := getCloud()
t.Logf("Cloud: %q", cloud)
t.Logf("Cloud.HasInternalTLD: %v", cloud.HasInternalTLD())
t.Logf("Cloud.ResolverIP: %q", cloud.ResolverIP())
}
func TestGetDigitalOceanResolver(t *testing.T) {
addr := netip.MustParseAddr(getDigitalOceanResolver())
t.Logf("got: %v", addr)
}