cloudenv.go 5.8 KB

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