Browse Source

cmd/tailscaled, ipn/conffile: support ec2 user-data config file

Updates #1412
Updates #1866

Change-Id: I4d08fb233b80c2078b3b28ffc18559baabb4a081
Signed-off-by: Brad Fitzpatrick <[email protected]>
Brad Fitzpatrick 1 year ago
parent
commit
1ea100e2e5

+ 1 - 0
cmd/tailscaled/depaware.txt

@@ -320,6 +320,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
      💣 tailscale.com/net/tshttpproxy                                from tailscale.com/clientupdate/distsign+
         tailscale.com/net/tstun                                      from tailscale.com/cmd/tailscaled+
         tailscale.com/net/wsconn                                     from tailscale.com/control/controlhttp+
+        tailscale.com/omit                                           from tailscale.com/ipn/conffile
         tailscale.com/paths                                          from tailscale.com/client/tailscale+
      💣 tailscale.com/portlist                                       from tailscale.com/ipn/ipnlocal
         tailscale.com/posture                                        from tailscale.com/ipn/ipnlocal

+ 2 - 2
cmd/tailscaled/tailscaled.go

@@ -118,7 +118,7 @@ var args struct {
 	tunname string
 
 	cleanUp        bool
-	confFile       string
+	confFile       string // empty, file path, or "vm:user-data"
 	debug          string
 	port           uint16
 	statepath      string
@@ -169,7 +169,7 @@ func main() {
 	flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket")
 	flag.BoolVar(&printVersion, "version", false, "print version information and exit")
 	flag.BoolVar(&args.disableLogs, "no-logs-no-support", false, "disable log uploads; this also disables any technical support")
-	flag.StringVar(&args.confFile, "config", "", "path to config file")
+	flag.StringVar(&args.confFile, "config", "", "path to config file, or 'vm:user-data' to use the VM's user-data (EC2)")
 
 	if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "tailscale" && beCLI != nil {
 		beCLI()

+ 59 - 0
ipn/conffile/cloudconf.go

@@ -0,0 +1,59 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package conffile
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+
+	"tailscale.com/omit"
+)
+
+func getEC2MetadataToken() (string, error) {
+	if omit.AWS {
+		return "", omit.Err
+	}
+	req, _ := http.NewRequest("PUT", "http://169.254.169.254/latest/api/token", nil)
+	req.Header.Add("X-aws-ec2-metadata-token-ttl-seconds", "300")
+	res, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return "", fmt.Errorf("failed to get metadata token: %w", err)
+	}
+	defer res.Body.Close()
+	if res.StatusCode != 200 {
+		return "", fmt.Errorf("failed to get metadata token: %v", res.Status)
+	}
+	all, err := io.ReadAll(res.Body)
+	if err != nil {
+		return "", fmt.Errorf("failed to read metadata token: %w", err)
+	}
+	return strings.TrimSpace(string(all)), nil
+}
+
+func readVMUserData() ([]byte, error) {
+	// TODO(bradfitz): support GCP, Azure, Proxmox/cloud-init
+	// (NoCloud/ConfigDrive ISO), etc.
+
+	if omit.AWS {
+		return nil, omit.Err
+	}
+	token, tokErr := getEC2MetadataToken()
+	req, _ := http.NewRequest("GET", "http://169.254.169.254/latest/user-data", nil)
+	req.Header.Add("X-aws-ec2-metadata-token", token)
+	res, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer res.Body.Close()
+	if res.StatusCode != 200 {
+		if tokErr != nil {
+			return nil, fmt.Errorf("failed to get VM user data: %v; also failed to get metadata token: %v", res.Status, tokErr)
+		}
+		return nil, errors.New(res.Status)
+	}
+	return io.ReadAll(res.Body)
+}

+ 12 - 3
ipn/conffile/conffile.go

@@ -17,7 +17,7 @@ import (
 
 // Config describes a config file.
 type Config struct {
-	Path    string // disk path of HuJSON
+	Path    string // disk path of HuJSON, or VMUserDataPath
 	Raw     []byte // raw bytes from disk, in HuJSON form
 	Std     []byte // standardized JSON form
 	Version string // "alpha0" for now
@@ -35,13 +35,22 @@ func (c *Config) WantRunning() bool {
 	return c != nil && !c.Parsed.Enabled.EqualBool(false)
 }
 
+// VMUserDataPath is a sentinel value for Load to use to get the data
+// from the VM's metadata service's user-data field.
+const VMUserDataPath = "vm:user-data"
+
 // Load reads and parses the config file at the provided path on disk.
 func Load(path string) (*Config, error) {
 	var c Config
 	c.Path = path
-
 	var err error
-	c.Raw, err = os.ReadFile(path)
+
+	switch path {
+	case VMUserDataPath:
+		c.Raw, err = readVMUserData()
+	default:
+		c.Raw, err = os.ReadFile(path)
+	}
 	if err != nil {
 		return nil, err
 	}

+ 9 - 0
omit/aws_def.go

@@ -0,0 +1,9 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !ts_omit_aws
+
+package omit
+
+// AWS is whether AWS support should be omitted from the build.
+const AWS = false

+ 9 - 0
omit/aws_omit.go

@@ -0,0 +1,9 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build ts_omit_aws
+
+package omit
+
+// AWS is whether AWS support should be omitted from the build.
+const AWS = true

+ 12 - 0
omit/omit.go

@@ -0,0 +1,12 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package omit provides consts to access Tailscale ts_omit_FOO build tags.
+// They're often more convenient to eliminate some away locally with a const
+// rather than using build tags.
+package omit
+
+import "errors"
+
+// Err is an error that can be returned by functions in this package.
+var Err = errors.New("feature not linked into binary per ts_omit build tag")