netmapcache.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. // Copyright (c) Tailscale Inc & contributors
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. // Package netmapcache implements a persistent cache for [netmap.NetworkMap]
  4. // values, allowing a client to start up using stale but previously-valid state
  5. // even if a connection to the control plane is not immediately available.
  6. package netmapcache
  7. import (
  8. "cmp"
  9. "context"
  10. "crypto/sha256"
  11. "encoding/hex"
  12. jsonv1 "encoding/json"
  13. "errors"
  14. "fmt"
  15. "io/fs"
  16. "iter"
  17. "os"
  18. "path/filepath"
  19. "slices"
  20. "strings"
  21. "time"
  22. "tailscale.com/feature/buildfeatures"
  23. "tailscale.com/tailcfg"
  24. "tailscale.com/types/netmap"
  25. "tailscale.com/util/mak"
  26. "tailscale.com/util/set"
  27. )
  28. var (
  29. // ErrKeyNotFound is a sentinel error reported by implementations of the [Store]
  30. // interface when loading a key that is not found in the store.
  31. ErrKeyNotFound = errors.New("storage key not found")
  32. // ErrCacheNotAvailable is a sentinel error reported by cache methods when
  33. // the netmap caching feature is not enabled in the build.
  34. ErrCacheNotAvailable = errors.New("netmap cache is not available")
  35. )
  36. // A Cache manages a columnar cache of a [netmap.NetworkMap]. Each Cache holds
  37. // a single netmap value; use [Cache.Store] to update or replace the cached
  38. // value and [Cache.Load] to read the cached value.
  39. type Cache struct {
  40. store Store
  41. // wantKeys records the storage keys from the last write or load of a cached
  42. // netmap. This is used to prune keys that are no longer referenced after an
  43. // update.
  44. wantKeys set.Set[string]
  45. // lastWrote records the last values written to each stored key.
  46. //
  47. // TODO(creachadair): This is meant to avoid disk writes, but I'm not
  48. // convinced we need it. Or maybe just track hashes of the content rather
  49. // than caching a complete copy.
  50. lastWrote map[string]lastWrote
  51. }
  52. // NewCache constructs a new empty [Cache] from the given [Store].
  53. // It will panic if s == nil.
  54. func NewCache(s Store) *Cache {
  55. if s == nil {
  56. panic("a non-nil Store is required")
  57. }
  58. return &Cache{
  59. store: s,
  60. wantKeys: make(set.Set[string]),
  61. lastWrote: make(map[string]lastWrote),
  62. }
  63. }
  64. type lastWrote struct {
  65. digest string
  66. at time.Time
  67. }
  68. func (c *Cache) writeJSON(ctx context.Context, key string, v any) error {
  69. j, err := jsonv1.Marshal(v)
  70. if err != nil {
  71. return fmt.Errorf("JSON marshalling %q: %w", key, err)
  72. }
  73. // TODO(creachadair): Maybe use a hash instead of the contents? Do we need
  74. // this at all?
  75. last, ok := c.lastWrote[key]
  76. if ok && cacheDigest(j) == last.digest {
  77. return nil
  78. }
  79. if err := c.store.Store(ctx, key, j); err != nil {
  80. return err
  81. }
  82. // Track the storage keys the current map is using, for storage GC.
  83. c.wantKeys.Add(key)
  84. c.lastWrote[key] = lastWrote{
  85. digest: cacheDigest(j),
  86. at: time.Now(),
  87. }
  88. return nil
  89. }
  90. func (c *Cache) removeUnwantedKeys(ctx context.Context) error {
  91. var errs []error
  92. for key, err := range c.store.List(ctx, "") {
  93. if err != nil {
  94. errs = append(errs, err)
  95. break
  96. }
  97. if !c.wantKeys.Contains(key) {
  98. if err := c.store.Remove(ctx, key); err != nil {
  99. errs = append(errs, fmt.Errorf("remove key %q: %w", key, err))
  100. }
  101. delete(c.lastWrote, key) // even if removal failed, we don't want it
  102. }
  103. }
  104. return errors.Join(errs...)
  105. }
  106. // FileStore implements the [Store] interface using a directory of files, in
  107. // which each key is encoded as a filename in the directory.
  108. // The caller is responsible to ensure the directory path exists before
  109. // using the store methods.
  110. type FileStore string
  111. // List implements part of the [Store] interface.
  112. func (s FileStore) List(ctx context.Context, prefix string) iter.Seq2[string, error] {
  113. return func(yield func(string, error) bool) {
  114. des, err := os.ReadDir(string(s))
  115. if os.IsNotExist(err) {
  116. return // nothing to read
  117. } else if err != nil {
  118. yield("", err)
  119. return
  120. }
  121. // os.ReadDir reports entries already sorted, and the encoding preserves that.
  122. for _, de := range des {
  123. key, err := hex.DecodeString(de.Name())
  124. if err != nil {
  125. yield("", err)
  126. return
  127. }
  128. name := string(key)
  129. if !strings.HasPrefix(name, prefix) {
  130. continue
  131. } else if !yield(name, nil) {
  132. return
  133. }
  134. }
  135. }
  136. }
  137. // Load implements part of the [Store] interface.
  138. func (s FileStore) Load(ctx context.Context, key string) ([]byte, error) {
  139. return os.ReadFile(filepath.Join(string(s), hex.EncodeToString([]byte(key))))
  140. }
  141. // Store implements part of the [Store] interface.
  142. func (s FileStore) Store(ctx context.Context, key string, value []byte) error {
  143. return os.WriteFile(filepath.Join(string(s), hex.EncodeToString([]byte(key))), value, 0600)
  144. }
  145. // Remove implements part of the [Store] interface.
  146. func (s FileStore) Remove(ctx context.Context, key string) error {
  147. err := os.Remove(filepath.Join(string(s), hex.EncodeToString([]byte(key))))
  148. if errors.Is(err, fs.ErrNotExist) {
  149. return nil
  150. }
  151. return err
  152. }
  153. // Store records nm in the cache, replacing any previously-cached values.
  154. func (c *Cache) Store(ctx context.Context, nm *netmap.NetworkMap) error {
  155. if !buildfeatures.HasCacheNetMap || nm == nil || nm.Cached {
  156. return nil
  157. }
  158. if selfID := nm.User(); selfID == 0 {
  159. return errors.New("no user in netmap")
  160. }
  161. clear(c.wantKeys)
  162. if err := c.writeJSON(ctx, "misc", netmapMisc{
  163. MachineKey: &nm.MachineKey,
  164. CollectServices: &nm.CollectServices,
  165. DisplayMessages: &nm.DisplayMessages,
  166. TKAEnabled: &nm.TKAEnabled,
  167. TKAHead: &nm.TKAHead,
  168. Domain: &nm.Domain,
  169. DomainAuditLogID: &nm.DomainAuditLogID,
  170. }); err != nil {
  171. return err
  172. }
  173. if err := c.writeJSON(ctx, "dns", netmapDNS{DNS: &nm.DNS}); err != nil {
  174. return err
  175. }
  176. if err := c.writeJSON(ctx, "derpmap", netmapDERPMap{DERPMap: &nm.DERPMap}); err != nil {
  177. return err
  178. }
  179. if err := c.writeJSON(ctx, "self", netmapNode{Node: &nm.SelfNode}); err != nil {
  180. return err
  181. // N.B. The NodeKey and AllCaps fields can be recovered from SelfNode on
  182. // load, and do not need to be stored separately.
  183. }
  184. for _, p := range nm.Peers {
  185. key := fmt.Sprintf("peer-%s", p.StableID())
  186. if err := c.writeJSON(ctx, key, netmapNode{Node: &p}); err != nil {
  187. return err
  188. }
  189. }
  190. for uid, u := range nm.UserProfiles {
  191. key := fmt.Sprintf("user-%d", uid)
  192. if err := c.writeJSON(ctx, key, netmapUserProfile{UserProfile: &u}); err != nil {
  193. return err
  194. }
  195. }
  196. if buildfeatures.HasSSH && nm.SSHPolicy != nil {
  197. if err := c.writeJSON(ctx, "ssh", netmapSSH{SSHPolicy: &nm.SSHPolicy}); err != nil {
  198. return err
  199. }
  200. }
  201. return c.removeUnwantedKeys(ctx)
  202. }
  203. // Load loads the cached [netmap.NetworkMap] value stored in c, if one is available.
  204. // It reports [ErrCacheNotAvailable] if no cached data are available.
  205. // On success, the Cached field of the returned network map is true.
  206. func (c *Cache) Load(ctx context.Context) (*netmap.NetworkMap, error) {
  207. if !buildfeatures.HasCacheNetMap {
  208. return nil, ErrCacheNotAvailable
  209. }
  210. nm := netmap.NetworkMap{Cached: true}
  211. // At minimum, we require that the cache contain a "self" node, or the data
  212. // are not usable.
  213. if self, err := c.store.Load(ctx, "self"); errors.Is(err, ErrKeyNotFound) {
  214. return nil, ErrCacheNotAvailable
  215. } else if err := jsonv1.Unmarshal(self, &netmapNode{Node: &nm.SelfNode}); err != nil {
  216. return nil, err
  217. }
  218. c.wantKeys.Add("self")
  219. // If we successfully recovered a SelfNode, pull out its related fields.
  220. if s := nm.SelfNode; s.Valid() {
  221. nm.NodeKey = s.Key()
  222. nm.AllCaps = make(set.Set[tailcfg.NodeCapability])
  223. for _, c := range s.Capabilities().All() {
  224. nm.AllCaps.Add(c)
  225. }
  226. for c := range s.CapMap().All() {
  227. nm.AllCaps.Add(c)
  228. }
  229. }
  230. // Unmarshal the contents of each specified cache entry directly into the
  231. // fields of the output. See the comment in types.go for more detail.
  232. if err := c.readJSON(ctx, "misc", &netmapMisc{
  233. MachineKey: &nm.MachineKey,
  234. CollectServices: &nm.CollectServices,
  235. DisplayMessages: &nm.DisplayMessages,
  236. TKAEnabled: &nm.TKAEnabled,
  237. TKAHead: &nm.TKAHead,
  238. Domain: &nm.Domain,
  239. DomainAuditLogID: &nm.DomainAuditLogID,
  240. }); err != nil {
  241. return nil, err
  242. }
  243. if err := c.readJSON(ctx, "dns", &netmapDNS{DNS: &nm.DNS}); err != nil {
  244. return nil, err
  245. }
  246. if err := c.readJSON(ctx, "derpmap", &netmapDERPMap{DERPMap: &nm.DERPMap}); err != nil {
  247. return nil, err
  248. }
  249. for key, err := range c.store.List(ctx, "peer-") {
  250. if err != nil {
  251. return nil, err
  252. }
  253. var peer tailcfg.NodeView
  254. if err := c.readJSON(ctx, key, &netmapNode{Node: &peer}); err != nil {
  255. return nil, err
  256. }
  257. nm.Peers = append(nm.Peers, peer)
  258. }
  259. slices.SortFunc(nm.Peers, func(a, b tailcfg.NodeView) int { return cmp.Compare(a.ID(), b.ID()) })
  260. for key, err := range c.store.List(ctx, "user-") {
  261. if err != nil {
  262. return nil, err
  263. }
  264. var up tailcfg.UserProfileView
  265. if err := c.readJSON(ctx, key, &netmapUserProfile{UserProfile: &up}); err != nil {
  266. return nil, err
  267. }
  268. mak.Set(&nm.UserProfiles, up.ID(), up)
  269. }
  270. if err := c.readJSON(ctx, "ssh", &netmapSSH{SSHPolicy: &nm.SSHPolicy}); err != nil {
  271. return nil, err
  272. }
  273. return &nm, nil
  274. }
  275. func (c *Cache) readJSON(ctx context.Context, key string, value any) error {
  276. data, err := c.store.Load(ctx, key)
  277. if errors.Is(err, ErrKeyNotFound) {
  278. return nil
  279. } else if err != nil {
  280. return err
  281. }
  282. if err := jsonv1.Unmarshal(data, value); err != nil {
  283. return err
  284. }
  285. c.wantKeys.Add(key)
  286. c.lastWrote[key] = lastWrote{digest: cacheDigest(data), at: time.Now()}
  287. return nil
  288. }
  289. // Store is the interface to persistent key-value storage used by a [Cache].
  290. type Store interface {
  291. // List lists all the stored keys having the specified prefixes, in
  292. // lexicographic order.
  293. //
  294. // Each pair yielded by the iterator is either a valid storage key and a nil
  295. // error, or an empty key and a non-nil error. After reporting an error, the
  296. // iterator must immediately return.
  297. List(ctx context.Context, prefix string) iter.Seq2[string, error]
  298. // Load fetches the contents of the specified key.
  299. // If the key is not found in the store, Load must report [ErrKeyNotFound].
  300. Load(ctx context.Context, key string) ([]byte, error)
  301. // Store marshals and stores the contents of the specified value under key.
  302. // If the key already exists, its contents are replaced.
  303. Store(ctx context.Context, key string, value []byte) error
  304. // Remove removes the specified key from the store. If the key does not exist,
  305. // Remove reports success (nil).
  306. Remove(ctx context.Context, key string) error
  307. }
  308. // cacheDigest computes a string digest of the specified data, for use in
  309. // detecting cache hits.
  310. func cacheDigest(data []byte) string { h := sha256.Sum256(data); return string(h[:]) }