1
0

bits.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. package nebula
  2. import (
  3. "context"
  4. "fmt"
  5. "log/slog"
  6. "math"
  7. mathbits "math/bits"
  8. "github.com/rcrowley/go-metrics"
  9. )
  10. const bitsPerWord = 64
  11. // Bits is a sliding-window anti-replay tracker. The window is stored as a
  12. // circular bitmap packed into uint64 words (8x denser than a []bool), so a
  13. // length-N window costs N/8 bytes. length must be a power of two.
  14. type Bits struct {
  15. length uint64
  16. lengthMask uint64
  17. current uint64
  18. bits []uint64
  19. lostCounter metrics.Counter
  20. dupeCounter metrics.Counter
  21. outOfWindowCounter metrics.Counter
  22. }
  23. func NewBits(length uint64) *Bits {
  24. if length == 0 || length&(length-1) != 0 {
  25. panic(fmt.Sprintf("Bits length must be a power of two, got %d", length))
  26. }
  27. nWords := length / bitsPerWord
  28. if nWords == 0 {
  29. nWords = 1
  30. }
  31. b := &Bits{
  32. length: length,
  33. lengthMask: length - 1,
  34. bits: make([]uint64, nWords),
  35. current: 0,
  36. lostCounter: metrics.GetOrRegisterCounter("network.packets.lost", nil),
  37. dupeCounter: metrics.GetOrRegisterCounter("network.packets.duplicate", nil),
  38. outOfWindowCounter: metrics.GetOrRegisterCounter("network.packets.out_of_window", nil),
  39. }
  40. // There is no counter value 0, mark it to avoid counting a lost packet later.
  41. b.bits[0] = 1
  42. return b
  43. }
  44. func (b *Bits) get(i uint64) bool {
  45. pos := i & b.lengthMask
  46. //bit-shifting by 6 because i is a bit index, not a u64 index, and we need to find the u64 without bit in it
  47. return b.bits[pos>>6]&(uint64(1)<<(pos&63)) != 0
  48. }
  49. func (b *Bits) set(i uint64) {
  50. pos := i & b.lengthMask
  51. b.bits[pos>>6] |= uint64(1) << (pos & 63)
  52. }
  53. // clearRange clears `count` bits starting at circular position `startPos`
  54. // (already masked to [0, length)) and returns how many of them were set
  55. // before the clear. count must be in [1, length].
  56. func (b *Bits) clearRange(startPos, count uint64) uint64 {
  57. wasSet := uint64(0)
  58. if count >= b.length {
  59. for _, w := range b.bits {
  60. wasSet += uint64(mathbits.OnesCount64(w))
  61. }
  62. clear(b.bits)
  63. return wasSet
  64. }
  65. pos := startPos
  66. remaining := count
  67. // handle the potential partial word before pos becomes u64 aligned
  68. word := pos >> 6
  69. bit := pos & 63
  70. take := uint64(64) - bit
  71. if take > remaining {
  72. take = remaining
  73. }
  74. if take > b.length-pos {
  75. take = b.length - pos
  76. }
  77. var mask uint64
  78. if take == 64 {
  79. mask = math.MaxUint64
  80. } else {
  81. mask = ((uint64(1) << take) - 1) << bit
  82. }
  83. wasSet += uint64(mathbits.OnesCount64(b.bits[word] & mask))
  84. b.bits[word] &^= mask
  85. remaining -= take
  86. pos = (pos + take) & b.lengthMask
  87. // Clear whole words, keeping track of the number of set bits
  88. for remaining >= 64 {
  89. word = pos >> 6
  90. wasSet += uint64(mathbits.OnesCount64(b.bits[word]))
  91. b.bits[word] = 0
  92. remaining -= 64
  93. pos = (pos + 64) & b.lengthMask
  94. }
  95. // Clear the remaining partial word
  96. if remaining > 0 {
  97. word = pos >> 6
  98. mask = (uint64(1) << remaining) - 1
  99. wasSet += uint64(mathbits.OnesCount64(b.bits[word] & mask))
  100. b.bits[word] &^= mask
  101. }
  102. return wasSet
  103. }
  104. func (b *Bits) strictlyWithinWindow(i uint64) bool {
  105. // Handle the case where the window hasn't slid yet. This avoids u64 underflow.
  106. inWarmup := b.current < b.length
  107. if i < b.length && inWarmup {
  108. return true
  109. }
  110. // Next, if the packet is in-window, see if we've seen it before
  111. if i > b.current-b.length {
  112. return true
  113. }
  114. return false //not within window!
  115. }
  116. // Check returns true if i is within (or way out in front of) the window, and not a replay
  117. func (b *Bits) Check(l *slog.Logger, i uint64) bool {
  118. // If i is the next number, return true.
  119. if i > b.current {
  120. return true
  121. }
  122. if b.strictlyWithinWindow(i) {
  123. return !b.get(i)
  124. }
  125. // Not within the window
  126. if l.Enabled(context.Background(), slog.LevelDebug) {
  127. l.Debug("rejected a packet (top)", "current", b.current, "incoming", i)
  128. }
  129. return false
  130. }
  131. // Update has three branches:
  132. // - i == b.current+1: fast path; advance the cursor by one and lose-count
  133. // the slot we just stomped (only past warmup; see the i > b.length guard
  134. // below).
  135. // - i > b.current+1: jump path; clear all slots between current and i
  136. // (or up to a full window's worth, whichever is smaller) via clearRange,
  137. // then mark i. Two arms here: a warmup arm that handles the very first
  138. // window before the cursor has slid, and a steady-state arm that treats
  139. // every cleared empty slot as a lost packet.
  140. // - i <= b.current: in-window check for duplicates; out-of-window otherwise.
  141. //
  142. // NewBits seeds bits[0]=1 so counter 0 looks "received" — Update never
  143. // clears that marker during warmup (clearRange skips position 0 when
  144. // startPos=1), and once b.current >= b.length the marker is no longer
  145. // consulted. The marker prevents a fictitious "lost" hit on the first real
  146. // counter.
  147. func (b *Bits) Update(l *slog.Logger, i uint64) bool {
  148. // Fast path: i is the next expected counter. Split out so the function
  149. // stays small and avoids paying for the slow paths' slog argument-build
  150. // stack frame on every call. The bit read/test/write is inlined to
  151. // touch the backing word once.
  152. if i == b.current+1 {
  153. pos := i & b.lengthMask
  154. word := pos >> 6
  155. mask := uint64(1) << (pos & 63)
  156. w := b.bits[word]
  157. if i > b.length && w&mask == 0 {
  158. b.lostCounter.Inc(1)
  159. }
  160. b.bits[word] = w | mask
  161. b.current = i
  162. return true
  163. }
  164. return b.updateSlow(l, i)
  165. }
  166. // updateSlow handles jumps, in-window backfill, dupes, and out-of-window.
  167. func (b *Bits) updateSlow(l *slog.Logger, i uint64) bool {
  168. // If i is a jump, adjust the window, record lost, update current, and return true
  169. if i > b.current {
  170. end := i
  171. if end > b.current+b.length {
  172. end = b.current + b.length
  173. }
  174. count := end - b.current
  175. startPos := (b.current + 1) & b.lengthMask
  176. var lost int64
  177. if b.current >= b.length {
  178. // Steady state: every cleared slot is past warmup, so any unset
  179. // bit we evict is a lost packet from the previous cycle.
  180. wasSet := b.clearRange(startPos, count)
  181. lost = int64(count) - int64(wasSet)
  182. } else {
  183. // Warmup (the very first window). Some cleared slots represent
  184. // packets <= length where eviction is not "lost" in the usual
  185. // sense. This branch is taken at most once per connection so we
  186. // don't bother optimizing it.
  187. for n := b.current + 1; n <= end; n++ {
  188. if !b.get(n) && n > b.length {
  189. lost++
  190. }
  191. }
  192. b.clearRange(startPos, count)
  193. }
  194. // Anything past the new window can never be backfilled, so it's lost.
  195. if i > b.current+b.length {
  196. lost += int64(i - b.current - b.length)
  197. }
  198. b.lostCounter.Inc(lost)
  199. b.set(i)
  200. b.current = i
  201. return true
  202. }
  203. // If i is within the current window but below the current counter, check to see if it's a duplicate
  204. if b.strictlyWithinWindow(i) {
  205. pos := i & b.lengthMask
  206. word := pos >> 6
  207. mask := uint64(1) << (pos & 63)
  208. w := b.bits[word]
  209. if b.current == i || w&mask != 0 {
  210. if l.Enabled(context.Background(), slog.LevelDebug) {
  211. l.Debug("Receive window",
  212. "accepted", false,
  213. "currentCounter", b.current,
  214. "incomingCounter", i,
  215. "reason", "duplicate",
  216. )
  217. }
  218. b.dupeCounter.Inc(1)
  219. return false
  220. }
  221. b.bits[word] = w | mask
  222. return true
  223. }
  224. // In all other cases, fail and don't change current.
  225. b.outOfWindowCounter.Inc(1)
  226. if l.Enabled(context.Background(), slog.LevelDebug) {
  227. l.Debug("Receive window",
  228. "accepted", false,
  229. "currentCounter", b.current,
  230. "incomingCounter", i,
  231. "reason", "nonsense",
  232. )
  233. }
  234. return false
  235. }