cloudenv.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. // Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. // Package cloudenv reports which known cloud environment we're running in.
  5. package cloudenv
  6. import (
  7. "context"
  8. "encoding/json"
  9. "log"
  10. "net"
  11. "net/http"
  12. "os"
  13. "runtime"
  14. "strings"
  15. "time"
  16. "tailscale.com/syncs"
  17. )
  18. // CommonNonRoutableMetadataIP is the IP address of the metadata server
  19. // on Amazon EC2, Google Compute Engine, and Azure. It's not routable.
  20. // (169.254.0.0/16 is a Link Local range: RFC 3927)
  21. const CommonNonRoutableMetadataIP = "169.254.169.254"
  22. // GoogleMetadataAndDNSIP is the metadata IP used by Google Cloud.
  23. // It's also the *.internal DNS server, and proxies to 8.8.8.8.
  24. const GoogleMetadataAndDNSIP = "169.254.169.254"
  25. // AWSResolverIP is the IP address of the AWS DNS server.
  26. // See https://docs.aws.amazon.com/vpc/latest/userguide/vpc-dns.html
  27. const AWSResolverIP = "169.254.169.253"
  28. // AzureResolverIP is Azure's DNS resolver IP.
  29. // See https://docs.microsoft.com/en-us/azure/virtual-network/what-is-ip-address-168-63-129-16
  30. const AzureResolverIP = "168.63.129.16"
  31. // Cloud is a recognize cloud environment with properties that
  32. // Tailscale can specialize for in places.
  33. type Cloud string
  34. const (
  35. AWS = Cloud("aws") // Amazon Web Services (EC2 in particular)
  36. Azure = Cloud("azure") // Microsoft Azure
  37. GCP = Cloud("gcp") // Google Cloud
  38. )
  39. // ResolverIP returns the cloud host's recursive DNS server or the
  40. // empty string if not available.
  41. func (c Cloud) ResolverIP() string {
  42. switch c {
  43. case GCP:
  44. return GoogleMetadataAndDNSIP
  45. case AWS:
  46. return AWSResolverIP
  47. case Azure:
  48. return AzureResolverIP
  49. }
  50. return ""
  51. }
  52. // HasInternalTLD reports whether c is a cloud environment
  53. // whose ResolverIP serves *.internal records.
  54. func (c Cloud) HasInternalTLD() bool {
  55. switch c {
  56. case GCP, AWS:
  57. return true
  58. }
  59. return false
  60. }
  61. var cloudAtomic syncs.AtomicValue[Cloud]
  62. // Get returns the current cloud, or the empty string if unknown.
  63. func Get() Cloud {
  64. if c, ok := cloudAtomic.LoadOk(); ok {
  65. return c
  66. }
  67. c := getCloud()
  68. cloudAtomic.Store(c) // even if empty
  69. return c
  70. }
  71. func readFileTrimmed(name string) string {
  72. v, _ := os.ReadFile(name)
  73. return strings.TrimSpace(string(v))
  74. }
  75. func getCloud() Cloud {
  76. var hitMetadata bool
  77. switch runtime.GOOS {
  78. case "android", "ios", "darwin":
  79. // Assume these aren't running on a cloud.
  80. return ""
  81. case "linux":
  82. biosVendor := readFileTrimmed("/sys/class/dmi/id/bios_vendor")
  83. if biosVendor == "Amazon EC2" || strings.HasSuffix(biosVendor, ".amazon") {
  84. return AWS
  85. }
  86. prod := readFileTrimmed("/sys/class/dmi/id/product_name")
  87. if prod == "Google Compute Engine" {
  88. return GCP
  89. }
  90. if prod == "Google" { // old GCP VMs, it seems
  91. hitMetadata = true
  92. }
  93. if prod == "Virtual Machine" || biosVendor == "Microsoft Corporation" {
  94. // Azure, or maybe all Hyper-V?
  95. hitMetadata = true
  96. }
  97. default:
  98. // TODO(bradfitz): use Win32_SystemEnclosure from WMI or something on
  99. // Windows to see if it's a physical machine and skip the cloud check
  100. // early. Otherwise use similar clues as Linux about whether we should
  101. // burn up to 2 seconds waiting for a metadata server that might not be
  102. // there. And for BSDs, look where the /sys stuff is.
  103. return ""
  104. }
  105. if !hitMetadata {
  106. return ""
  107. }
  108. const maxWait = 2 * time.Second
  109. tr := &http.Transport{
  110. DisableKeepAlives: true,
  111. Dial: (&net.Dialer{
  112. Timeout: maxWait,
  113. }).Dial,
  114. }
  115. ctx, cancel := context.WithTimeout(context.Background(), maxWait)
  116. defer cancel()
  117. // We want to hit CommonNonRoutableMetadataIP to see if we're on AWS, GCP,
  118. // or Azure. All three (and many others) use the same metadata IP.
  119. //
  120. // But to avoid triggering the AWS CloudWatch "MetadataNoToken" metric (for which
  121. // there might be an alert registered?), make our initial request be a token
  122. // request. This only works on AWS, but the failing HTTP response on other clouds gives
  123. // us enough clues about which cloud we're on.
  124. req, err := http.NewRequestWithContext(ctx, "PUT", "http://"+CommonNonRoutableMetadataIP+"/latest/api/token", strings.NewReader(""))
  125. if err != nil {
  126. log.Printf("cloudenv: [unexpected] error creating request: %v", err)
  127. return ""
  128. }
  129. req.Header.Set("X-Aws-Ec2-Metadata-Token-Ttl-Seconds", "5")
  130. res, err := tr.RoundTrip(req)
  131. if err != nil {
  132. return ""
  133. }
  134. res.Body.Close()
  135. if res.Header.Get("Metadata-Flavor") == "Google" {
  136. return GCP
  137. }
  138. server := res.Header.Get("Server")
  139. if server == "EC2ws" {
  140. return AWS
  141. }
  142. if strings.HasPrefix(server, "Microsoft") {
  143. // e.g. "Microsoft-IIS/10.0"
  144. req, _ := http.NewRequestWithContext(ctx, "GET", "http://"+CommonNonRoutableMetadataIP+"/metadata/instance/compute?api-version=2021-02-01", nil)
  145. req.Header.Set("Metadata", "true")
  146. res, err := tr.RoundTrip(req)
  147. if err != nil {
  148. return ""
  149. }
  150. defer res.Body.Close()
  151. var meta struct {
  152. AzEnvironment string `json:"azEnvironment"`
  153. }
  154. if err := json.NewDecoder(res.Body).Decode(&meta); err != nil {
  155. return ""
  156. }
  157. if strings.HasPrefix(meta.AzEnvironment, "Azure") {
  158. return Azure
  159. }
  160. return ""
  161. }
  162. // TODO: more, as needed.
  163. return ""
  164. }