cloudinfo.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build !(ios || android || js)
  4. // Package cloudinfo provides cloud metadata utilities.
  5. package cloudinfo
  6. import (
  7. "context"
  8. "errors"
  9. "fmt"
  10. "io"
  11. "net"
  12. "net/http"
  13. "net/netip"
  14. "slices"
  15. "strings"
  16. "time"
  17. "tailscale.com/feature/buildfeatures"
  18. "tailscale.com/types/logger"
  19. "tailscale.com/util/cloudenv"
  20. )
  21. const maxCloudInfoWait = 2 * time.Second
  22. // CloudInfo holds state used in querying instance metadata (IMDS) endpoints.
  23. type CloudInfo struct {
  24. client http.Client
  25. logf logger.Logf
  26. // The following parameters are fixed for the lifetime of the cloudInfo
  27. // object, but are used for testing.
  28. cloud cloudenv.Cloud
  29. endpoint string
  30. }
  31. // New constructs a new [*CloudInfo] that will log to the provided logger instance.
  32. func New(logf logger.Logf) *CloudInfo {
  33. if !buildfeatures.HasCloud {
  34. return nil
  35. }
  36. tr := &http.Transport{
  37. DisableKeepAlives: true,
  38. Dial: (&net.Dialer{
  39. Timeout: maxCloudInfoWait,
  40. }).Dial,
  41. }
  42. return &CloudInfo{
  43. client: http.Client{Transport: tr},
  44. logf: logf,
  45. cloud: cloudenv.Get(),
  46. endpoint: "http://" + cloudenv.CommonNonRoutableMetadataIP,
  47. }
  48. }
  49. // GetPublicIPs returns any public IPs attached to the current cloud instance,
  50. // if the tailscaled process is running in a known cloud and there are any such
  51. // IPs present.
  52. //
  53. // Currently supports only AWS.
  54. func (ci *CloudInfo) GetPublicIPs(ctx context.Context) ([]netip.Addr, error) {
  55. if !buildfeatures.HasCloud {
  56. return nil, nil
  57. }
  58. switch ci.cloud {
  59. case cloudenv.AWS:
  60. ret, err := ci.getAWS(ctx)
  61. ci.logf("[v1] cloudinfo.GetPublicIPs: AWS: %v, %v", ret, err)
  62. return ret, err
  63. }
  64. return nil, nil
  65. }
  66. // getAWSMetadata makes a request to the AWS metadata service at the given
  67. // path, authenticating with the provided IMDSv2 token. The returned metadata
  68. // is split by newline and returned as a slice.
  69. func (ci *CloudInfo) getAWSMetadata(ctx context.Context, token, path string) ([]string, error) {
  70. req, err := http.NewRequestWithContext(ctx, "GET", ci.endpoint+path, nil)
  71. if err != nil {
  72. return nil, fmt.Errorf("creating request to %q: %w", path, err)
  73. }
  74. req.Header.Set("X-aws-ec2-metadata-token", token)
  75. resp, err := ci.client.Do(req)
  76. if err != nil {
  77. return nil, fmt.Errorf("making request to metadata service %q: %w", path, err)
  78. }
  79. defer resp.Body.Close()
  80. switch resp.StatusCode {
  81. case http.StatusOK:
  82. // Good
  83. case http.StatusNotFound:
  84. // Nothing found, but this isn't an error; just return
  85. return nil, nil
  86. default:
  87. return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
  88. }
  89. body, err := io.ReadAll(resp.Body)
  90. if err != nil {
  91. return nil, fmt.Errorf("reading response body for %q: %w", path, err)
  92. }
  93. return strings.Split(strings.TrimSpace(string(body)), "\n"), nil
  94. }
  95. // getAWS returns all public IPv4 and IPv6 addresses present in the AWS instance metadata.
  96. func (ci *CloudInfo) getAWS(ctx context.Context) ([]netip.Addr, error) {
  97. ctx, cancel := context.WithTimeout(ctx, maxCloudInfoWait)
  98. defer cancel()
  99. // Get a token so we can query the metadata service.
  100. req, err := http.NewRequestWithContext(ctx, "PUT", ci.endpoint+"/latest/api/token", nil)
  101. if err != nil {
  102. return nil, fmt.Errorf("creating token request: %w", err)
  103. }
  104. req.Header.Set("X-Aws-Ec2-Metadata-Token-Ttl-Seconds", "10")
  105. resp, err := ci.client.Do(req)
  106. if err != nil {
  107. return nil, fmt.Errorf("making token request to metadata service: %w", err)
  108. }
  109. body, err := io.ReadAll(resp.Body)
  110. resp.Body.Close()
  111. if err != nil {
  112. return nil, fmt.Errorf("reading token response body: %w", err)
  113. }
  114. token := string(body)
  115. server := resp.Header.Get("Server")
  116. if server != "EC2ws" {
  117. return nil, fmt.Errorf("unexpected server header: %q", server)
  118. }
  119. // Iterate over all interfaces and get their public IP addresses, both IPv4 and IPv6.
  120. macAddrs, err := ci.getAWSMetadata(ctx, token, "/latest/meta-data/network/interfaces/macs/")
  121. if err != nil {
  122. return nil, fmt.Errorf("getting interface MAC addresses: %w", err)
  123. }
  124. var (
  125. addrs []netip.Addr
  126. errs []error
  127. )
  128. addAddr := func(addr string) {
  129. ip, err := netip.ParseAddr(addr)
  130. if err != nil {
  131. errs = append(errs, fmt.Errorf("parsing IP address %q: %w", addr, err))
  132. return
  133. }
  134. addrs = append(addrs, ip)
  135. }
  136. for _, mac := range macAddrs {
  137. ips, err := ci.getAWSMetadata(ctx, token, "/latest/meta-data/network/interfaces/macs/"+mac+"/public-ipv4s")
  138. if err != nil {
  139. errs = append(errs, fmt.Errorf("getting IPv4 addresses for %q: %w", mac, err))
  140. continue
  141. }
  142. for _, ip := range ips {
  143. addAddr(ip)
  144. }
  145. // Try querying for IPv6 addresses.
  146. ips, err = ci.getAWSMetadata(ctx, token, "/latest/meta-data/network/interfaces/macs/"+mac+"/ipv6s")
  147. if err != nil {
  148. errs = append(errs, fmt.Errorf("getting IPv6 addresses for %q: %w", mac, err))
  149. continue
  150. }
  151. for _, ip := range ips {
  152. addAddr(ip)
  153. }
  154. }
  155. // Sort the returned addresses for determinism.
  156. slices.SortFunc(addrs, func(a, b netip.Addr) int {
  157. return a.Compare(b)
  158. })
  159. // Preferentially return any addresses we found, even if there were errors.
  160. if len(addrs) > 0 {
  161. return addrs, nil
  162. }
  163. if len(errs) > 0 {
  164. return nil, fmt.Errorf("getting IP addresses: %w", errors.Join(errs...))
  165. }
  166. return nil, nil
  167. }