sync.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build !ts_omit_tailnetlock
  4. package tka
  5. import (
  6. "errors"
  7. "fmt"
  8. "os"
  9. )
  10. const (
  11. // Max iterations searching for any intersection.
  12. maxSyncIter = 2000
  13. // Max iterations searching for a head intersection.
  14. maxSyncHeadIntersectionIter = 400
  15. )
  16. // ErrNoIntersection is returned when a shared AUM could
  17. // not be determined when evaluating a remote sync offer.
  18. var ErrNoIntersection = errors.New("no intersection")
  19. // SyncOffer conveys information about the current head & ancestor AUMs,
  20. // for the purpose of synchronization with some remote end.
  21. //
  22. // Ancestors should contain a subset of the ancestors of the chain.
  23. // The last entry in that slice is the oldest-known AUM in the chain.
  24. type SyncOffer struct {
  25. Head AUMHash
  26. Ancestors []AUMHash
  27. }
  28. // ToSyncOffer creates a SyncOffer from the fields received in
  29. // a [tailcfg.TKASyncOfferRequest].
  30. func ToSyncOffer(head string, ancestors []string) (SyncOffer, error) {
  31. var out SyncOffer
  32. if err := out.Head.UnmarshalText([]byte(head)); err != nil {
  33. return SyncOffer{}, fmt.Errorf("head.UnmarshalText: %v", err)
  34. }
  35. out.Ancestors = make([]AUMHash, len(ancestors))
  36. for i, a := range ancestors {
  37. if err := out.Ancestors[i].UnmarshalText([]byte(a)); err != nil {
  38. return SyncOffer{}, fmt.Errorf("ancestor[%d].UnmarshalText: %v", i, err)
  39. }
  40. }
  41. return out, nil
  42. }
  43. // FromSyncOffer marshals the fields of a SyncOffer so they can be
  44. // sent in a [tailcfg.TKASyncOfferRequest].
  45. func FromSyncOffer(offer SyncOffer) (head string, ancestors []string, err error) {
  46. headBytes, err := offer.Head.MarshalText()
  47. if err != nil {
  48. return "", nil, fmt.Errorf("head.MarshalText: %v", err)
  49. }
  50. ancestors = make([]string, len(offer.Ancestors))
  51. for i, ancestor := range offer.Ancestors {
  52. hash, err := ancestor.MarshalText()
  53. if err != nil {
  54. return "", nil, fmt.Errorf("ancestor[%d].MarshalText: %v", i, err)
  55. }
  56. ancestors[i] = string(hash)
  57. }
  58. return string(headBytes), ancestors, nil
  59. }
  60. const (
  61. // The starting number of AUMs to skip when listing
  62. // ancestors in a SyncOffer.
  63. ancestorsSkipStart = 4
  64. // How many bits to advance the skip count when listing
  65. // ancestors in a SyncOffer.
  66. //
  67. // 2 bits, so (4<<2), so after skipping 4 it skips 16.
  68. ancestorsSkipShift = 2
  69. )
  70. // SyncOffer returns an abbreviated description of the current AUM
  71. // chain, which can be used to synchronize with another (untrusted)
  72. // Authority instance.
  73. //
  74. // The returned SyncOffer structure should be transmitted to the remote
  75. // Authority, which should call MissingAUMs() using it to determine
  76. // AUMs which need to be transmitted. This list of AUMs from the remote
  77. // can then be applied locally with Inform().
  78. //
  79. // This SyncOffer + AUM exchange should be performed by both ends,
  80. // because it's possible that either end has AUMs that the other needs
  81. // to find out about.
  82. func (a *Authority) SyncOffer(storage Chonk) (SyncOffer, error) {
  83. oldest := a.oldestAncestor.Hash()
  84. out := SyncOffer{
  85. Head: a.Head(),
  86. Ancestors: make([]AUMHash, 0, 6), // 6 chosen arbitrarily.
  87. }
  88. // We send some subset of our ancestors to help the remote
  89. // find a more-recent 'head intersection'.
  90. // The number of AUMs between each ancestor entry gets
  91. // exponentially larger.
  92. var (
  93. skipAmount uint64 = ancestorsSkipStart
  94. curs AUMHash = a.Head()
  95. )
  96. for i := uint64(0); i < maxSyncHeadIntersectionIter; i++ {
  97. if i > 0 && (i%skipAmount) == 0 {
  98. out.Ancestors = append(out.Ancestors, curs)
  99. skipAmount = skipAmount << ancestorsSkipShift
  100. }
  101. parent, err := storage.AUM(curs)
  102. if err != nil {
  103. if err != os.ErrNotExist {
  104. return SyncOffer{}, err
  105. }
  106. break
  107. }
  108. // We add the oldest later on, so don't duplicate.
  109. if parent.Hash() == oldest {
  110. break
  111. }
  112. copy(curs[:], parent.PrevAUMHash)
  113. }
  114. out.Ancestors = append(out.Ancestors, oldest)
  115. return out, nil
  116. }
  117. // intersection describes how to synchronize AUMs with a remote
  118. // authority.
  119. type intersection struct {
  120. // if true, no exchange of AUMs is needed.
  121. upToDate bool
  122. // headIntersection is the latest common AUM on the remote. In other
  123. // words, we need to send all AUMs since this one.
  124. headIntersection *AUMHash
  125. // tailIntersection is the oldest common AUM on the remote. In other
  126. // words, we diverge with the remote after this AUM, so we both need
  127. // to transmit our AUM chain starting here.
  128. tailIntersection *AUMHash
  129. }
  130. // computeSyncIntersection determines the common AUMs between a local and
  131. // remote SyncOffer. This intersection can be used to synchronize both
  132. // sides.
  133. func computeSyncIntersection(storage Chonk, localOffer, remoteOffer SyncOffer) (*intersection, error) {
  134. // Simple case: up to date.
  135. if remoteOffer.Head == localOffer.Head {
  136. return &intersection{upToDate: true, headIntersection: &localOffer.Head}, nil
  137. }
  138. // Case: 'head intersection'
  139. // If we have the remote's head, it's more likely than not that
  140. // we have updates that build on that head. To confirm this,
  141. // we iterate backwards through our chain to see if the given
  142. // head is an ancestor of our current chain.
  143. //
  144. // In other words:
  145. // <Us> A -> B -> C
  146. // <Them> A -> B
  147. // ∴ their head intersects with our chain, we need to send C
  148. var hasRemoteHead bool
  149. _, err := storage.AUM(remoteOffer.Head)
  150. if err != nil {
  151. if err != os.ErrNotExist {
  152. return nil, err
  153. }
  154. } else {
  155. hasRemoteHead = true
  156. }
  157. if hasRemoteHead {
  158. curs := localOffer.Head
  159. for range maxSyncHeadIntersectionIter {
  160. parent, err := storage.AUM(curs)
  161. if err != nil {
  162. if err != os.ErrNotExist {
  163. return nil, err
  164. }
  165. break
  166. }
  167. if parent.Hash() == remoteOffer.Head {
  168. h := parent.Hash()
  169. return &intersection{headIntersection: &h}, nil
  170. }
  171. copy(curs[:], parent.PrevAUMHash)
  172. }
  173. }
  174. // Case: 'tail intersection'
  175. // So we don't have a clue what the remote's head is, but
  176. // if one of the ancestors they gave us is part of our chain,
  177. // then there's an intersection, which is a starting point for
  178. // the remote to send us AUMs from.
  179. //
  180. // We iterate the list of ancestors in order because the remote
  181. // ordered them such that the newer ones are earlier, so with
  182. // a bit of luck we can use an earlier one and hence do less work /
  183. // transmit fewer AUMs.
  184. for _, a := range remoteOffer.Ancestors {
  185. state, err := computeStateAt(storage, maxSyncIter, a)
  186. if err != nil {
  187. if err != os.ErrNotExist {
  188. return nil, fmt.Errorf("computeStateAt: %v", err)
  189. }
  190. continue
  191. }
  192. end, _, err := fastForward(storage, maxSyncIter, state, func(curs AUM, _ State) bool {
  193. return curs.Hash() == localOffer.Head
  194. })
  195. if err != nil {
  196. return nil, err
  197. }
  198. // fastForward can terminate before the done condition if there are
  199. // no more children left, so we check again before considering this
  200. // an intersection.
  201. if end.Hash() == localOffer.Head {
  202. return &intersection{tailIntersection: &a}, nil
  203. }
  204. }
  205. return nil, ErrNoIntersection
  206. }
  207. // MissingAUMs returns AUMs a remote may be missing based on the
  208. // remotes' SyncOffer.
  209. func (a *Authority) MissingAUMs(storage Chonk, remoteOffer SyncOffer) ([]AUM, error) {
  210. localOffer, err := a.SyncOffer(storage)
  211. if err != nil {
  212. return nil, fmt.Errorf("local syncOffer: %v", err)
  213. }
  214. intersection, err := computeSyncIntersection(storage, localOffer, remoteOffer)
  215. if err != nil {
  216. return nil, fmt.Errorf("intersection: %v", err)
  217. }
  218. if intersection.upToDate {
  219. return nil, nil
  220. }
  221. out := make([]AUM, 0, 12) // 12 chosen arbitrarily.
  222. if intersection.headIntersection != nil {
  223. state, err := computeStateAt(storage, maxSyncIter, *intersection.headIntersection)
  224. if err != nil {
  225. return nil, err
  226. }
  227. _, _, err = fastForward(storage, maxSyncIter, state, func(curs AUM, _ State) bool {
  228. if curs.Hash() != *intersection.headIntersection {
  229. out = append(out, curs)
  230. }
  231. return false
  232. })
  233. return out, err
  234. }
  235. if intersection.tailIntersection != nil {
  236. state, err := computeStateAt(storage, maxSyncIter, *intersection.tailIntersection)
  237. if err != nil {
  238. return nil, err
  239. }
  240. _, _, err = fastForward(storage, maxSyncIter, state, func(curs AUM, _ State) bool {
  241. if curs.Hash() != *intersection.tailIntersection {
  242. out = append(out, curs)
  243. }
  244. return false
  245. })
  246. return out, err
  247. }
  248. panic("unreachable")
  249. }