serve.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. // Copyright (c) Tailscale Inc & contributors
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build linux
  4. package main
  5. import (
  6. "bytes"
  7. "context"
  8. "encoding/json"
  9. "log"
  10. "os"
  11. "path/filepath"
  12. "reflect"
  13. "sync/atomic"
  14. "time"
  15. "github.com/fsnotify/fsnotify"
  16. "tailscale.com/client/local"
  17. "tailscale.com/ipn"
  18. "tailscale.com/kube/certs"
  19. "tailscale.com/kube/kubetypes"
  20. klc "tailscale.com/kube/localclient"
  21. "tailscale.com/types/netmap"
  22. )
  23. // watchServeConfigChanges watches path for changes, and when it sees one, reads
  24. // the serve config from it, replacing ${TS_CERT_DOMAIN} with certDomain, and
  25. // applies it to lc. It exits when ctx is canceled. cdChanged is a channel that
  26. // is written to when the certDomain changes, causing the serve config to be
  27. // re-read and applied.
  28. func watchServeConfigChanges(ctx context.Context, cdChanged <-chan bool, certDomainAtomic *atomic.Pointer[string], lc *local.Client, kc *kubeClient, cfg *settings) {
  29. if certDomainAtomic == nil {
  30. panic("certDomainAtomic must not be nil")
  31. }
  32. var tickChan <-chan time.Time
  33. var eventChan <-chan fsnotify.Event
  34. if w, err := fsnotify.NewWatcher(); err != nil {
  35. // Creating a new fsnotify watcher would fail for example if inotify was not able to create a new file descriptor.
  36. // See https://github.com/tailscale/tailscale/issues/15081
  37. log.Printf("serve proxy: failed to create fsnotify watcher, timer-only mode: %v", err)
  38. ticker := time.NewTicker(5 * time.Second)
  39. defer ticker.Stop()
  40. tickChan = ticker.C
  41. } else {
  42. defer w.Close()
  43. if err := w.Add(filepath.Dir(cfg.ServeConfigPath)); err != nil {
  44. log.Fatalf("serve proxy: failed to add fsnotify watch: %v", err)
  45. }
  46. eventChan = w.Events
  47. }
  48. var certDomain string
  49. var prevServeConfig *ipn.ServeConfig
  50. var cm *certs.CertManager
  51. if cfg.CertShareMode == "rw" {
  52. cm = certs.NewCertManager(klc.New(lc), log.Printf)
  53. }
  54. for {
  55. select {
  56. case <-ctx.Done():
  57. return
  58. case <-cdChanged:
  59. certDomain = *certDomainAtomic.Load()
  60. case <-tickChan:
  61. case <-eventChan:
  62. // We can't do any reasonable filtering on the event because of how
  63. // k8s handles these mounts. So just re-read the file and apply it
  64. // if it's changed.
  65. }
  66. sc, err := readServeConfig(cfg.ServeConfigPath, certDomain)
  67. if err != nil {
  68. log.Fatalf("serve proxy: failed to read serve config: %v", err)
  69. }
  70. if sc == nil {
  71. log.Printf("serve proxy: no serve config at %q, skipping", cfg.ServeConfigPath)
  72. continue
  73. }
  74. if prevServeConfig != nil && reflect.DeepEqual(sc, prevServeConfig) {
  75. continue
  76. }
  77. if err := updateServeConfig(ctx, sc, certDomain, lc); err != nil {
  78. log.Fatalf("serve proxy: error updating serve config: %v", err)
  79. }
  80. if kc != nil && kc.canPatch {
  81. if err := kc.storeHTTPSEndpoint(ctx, certDomain); err != nil {
  82. log.Fatalf("serve proxy: error storing HTTPS endpoint: %v", err)
  83. }
  84. }
  85. prevServeConfig = sc
  86. if cfg.CertShareMode != "rw" {
  87. continue
  88. }
  89. if err := cm.EnsureCertLoops(ctx, sc); err != nil {
  90. log.Fatalf("serve proxy: error ensuring cert loops: %v", err)
  91. }
  92. }
  93. }
  94. func certDomainFromNetmap(nm *netmap.NetworkMap) string {
  95. if len(nm.DNS.CertDomains) == 0 {
  96. return ""
  97. }
  98. return nm.DNS.CertDomains[0]
  99. }
  100. // localClient is a subset of [local.Client] that can be mocked for testing.
  101. type localClient interface {
  102. SetServeConfig(context.Context, *ipn.ServeConfig) error
  103. CertPair(context.Context, string) ([]byte, []byte, error)
  104. }
  105. func updateServeConfig(ctx context.Context, sc *ipn.ServeConfig, certDomain string, lc localClient) error {
  106. if !isValidHTTPSConfig(certDomain, sc) {
  107. return nil
  108. }
  109. log.Printf("serve proxy: applying serve config")
  110. return lc.SetServeConfig(ctx, sc)
  111. }
  112. func isValidHTTPSConfig(certDomain string, sc *ipn.ServeConfig) bool {
  113. if certDomain == kubetypes.ValueNoHTTPS && hasHTTPSEndpoint(sc) {
  114. log.Printf(
  115. `serve proxy: this node is configured as a proxy that exposes an HTTPS endpoint to tailnet,
  116. (perhaps a Kubernetes operator Ingress proxy) but it is not able to issue TLS certs, so this will likely not work.
  117. To make it work, ensure that HTTPS is enabled for your tailnet, see https://tailscale.com/kb/1153/enabling-https for more details.`)
  118. return false
  119. }
  120. return true
  121. }
  122. func hasHTTPSEndpoint(cfg *ipn.ServeConfig) bool {
  123. if cfg == nil {
  124. return false
  125. }
  126. for _, tcpCfg := range cfg.TCP {
  127. if tcpCfg.HTTPS {
  128. return true
  129. }
  130. }
  131. return false
  132. }
  133. // readServeConfig reads the ipn.ServeConfig from path, replacing
  134. // ${TS_CERT_DOMAIN} with certDomain.
  135. func readServeConfig(path, certDomain string) (*ipn.ServeConfig, error) {
  136. if path == "" {
  137. return nil, nil
  138. }
  139. j, err := os.ReadFile(path)
  140. if err != nil {
  141. if os.IsNotExist(err) {
  142. return nil, nil
  143. }
  144. return nil, err
  145. }
  146. // Serve config can be provided by users as well as the Kubernetes Operator (for its proxies). User-provided
  147. // config could be empty for reasons.
  148. if len(j) == 0 {
  149. log.Printf("serve proxy: serve config file is empty, skipping")
  150. return nil, nil
  151. }
  152. j = bytes.ReplaceAll(j, []byte("${TS_CERT_DOMAIN}"), []byte(certDomain))
  153. var sc ipn.ServeConfig
  154. if err := json.Unmarshal(j, &sc); err != nil {
  155. return nil, err
  156. }
  157. return &sc, nil
  158. }