diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 54605d950..39a2fb0ec 100644 --- a/cmd/tailscaled/depaware.txt +++ b/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 diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index a1b9499d9..77a595dac 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -118,7 +118,7 @@ func defaultPort() uint16 { 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() diff --git a/ipn/conffile/cloudconf.go b/ipn/conffile/cloudconf.go new file mode 100644 index 000000000..650611cf1 --- /dev/null +++ b/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) +} diff --git a/ipn/conffile/conffile.go b/ipn/conffile/conffile.go index 837094639..0b4670c42 100644 --- a/ipn/conffile/conffile.go +++ b/ipn/conffile/conffile.go @@ -17,7 +17,7 @@ // 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 } diff --git a/omit/aws_def.go b/omit/aws_def.go new file mode 100644 index 000000000..8ae539736 --- /dev/null +++ b/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 diff --git a/omit/aws_omit.go b/omit/aws_omit.go new file mode 100644 index 000000000..5b6957d5b --- /dev/null +++ b/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 diff --git a/omit/omit.go b/omit/omit.go new file mode 100644 index 000000000..018cfba94 --- /dev/null +++ b/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")