Browse Source

util/cloudenv: add support for DigitalOcean

Updates #4984

Signed-off-by: Andrew Dunham <[email protected]>
Change-Id: Ib229eb40af36a80e6b0fd1dd0cabb07f0d50a7d1
Andrew Dunham 2 years ago
parent
commit
c1c50cfcc0
2 changed files with 59 additions and 3 deletions
  1. 30 3
      util/cloudenv/cloudenv.go
  2. 29 0
      util/cloudenv/cloudenv_test.go

+ 30 - 3
util/cloudenv/cloudenv.go

@@ -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

+ 29 - 0
util/cloudenv/cloudenv_test.go

@@ -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)
+}