1
0

bits.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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 := min(min(uint64(64)-bit, remaining), b.length-pos)
  71. var mask uint64
  72. if take == 64 {
  73. mask = math.MaxUint64
  74. } else {
  75. mask = ((uint64(1) << take) - 1) << bit
  76. }
  77. wasSet += uint64(mathbits.OnesCount64(b.bits[word] & mask))
  78. b.bits[word] &^= mask
  79. remaining -= take
  80. pos = (pos + take) & b.lengthMask
  81. // Clear whole words, keeping track of the number of set bits
  82. for remaining >= 64 {
  83. word = pos >> 6
  84. wasSet += uint64(mathbits.OnesCount64(b.bits[word]))
  85. b.bits[word] = 0
  86. remaining -= 64
  87. pos = (pos + 64) & b.lengthMask
  88. }
  89. // Clear the remaining partial word
  90. if remaining > 0 {
  91. word = pos >> 6
  92. mask = (uint64(1) << remaining) - 1
  93. wasSet += uint64(mathbits.OnesCount64(b.bits[word] & mask))
  94. b.bits[word] &^= mask
  95. }
  96. return wasSet
  97. }
  98. func (b *Bits) strictlyWithinWindow(i uint64) bool {
  99. // Handle the case where the window hasn't slid yet. This avoids u64 underflow.
  100. inWarmup := b.current < b.length
  101. if i < b.length && inWarmup {
  102. return true
  103. }
  104. // Next, if the packet is in-window, see if we've seen it before
  105. if i > b.current-b.length {
  106. return true
  107. }
  108. return false //not within window!
  109. }
  110. // Check returns true if i is within (or way out in front of) the window, and not a replay
  111. func (b *Bits) Check(l *slog.Logger, i uint64) bool {
  112. // If i is the next number, return true.
  113. if i > b.current {
  114. return true
  115. }
  116. if b.strictlyWithinWindow(i) {
  117. return !b.get(i)
  118. }
  119. // Not within the window
  120. if l.Enabled(context.Background(), slog.LevelDebug) {
  121. l.Debug("rejected a packet (top)", "current", b.current, "incoming", i)
  122. }
  123. return false
  124. }
  125. // Update has three branches:
  126. // - i == b.current+1: fast path; advance the cursor by one and lose-count
  127. // the slot we just stomped (only past warmup; see the i > b.length guard
  128. // below).
  129. // - i > b.current+1: jump path; clear all slots between current and i
  130. // (or up to a full window's worth, whichever is smaller) via clearRange,
  131. // then mark i. Two arms here: a warmup arm that handles the very first
  132. // window before the cursor has slid, and a steady-state arm that treats
  133. // every cleared empty slot as a lost packet.
  134. // - i <= b.current: in-window check for duplicates; out-of-window otherwise.
  135. //
  136. // NewBits seeds bits[0]=1 so counter 0 looks "received" — Update never
  137. // clears that marker during warmup (clearRange skips position 0 when
  138. // startPos=1), and once b.current >= b.length the marker is no longer
  139. // consulted. The marker prevents a fictitious "lost" hit on the first real
  140. // counter.
  141. func (b *Bits) Update(l *slog.Logger, i uint64) bool {
  142. // Fast path: i is the next expected counter. Split out so the function
  143. // stays small and avoids paying for the slow paths' slog argument-build
  144. // stack frame on every call. The bit read/test/write is inlined to
  145. // touch the backing word once.
  146. if i == b.current+1 {
  147. pos := i & b.lengthMask
  148. word := pos >> 6
  149. mask := uint64(1) << (pos & 63)
  150. w := b.bits[word]
  151. if i > b.length && w&mask == 0 {
  152. b.lostCounter.Inc(1)
  153. }
  154. b.bits[word] = w | mask
  155. b.current = i
  156. return true
  157. }
  158. return b.updateSlow(l, i)
  159. }
  160. // updateSlow handles jumps, in-window backfill, dupes, and out-of-window.
  161. func (b *Bits) updateSlow(l *slog.Logger, i uint64) bool {
  162. // If i is a jump, adjust the window, record lost, update current, and return true
  163. if i > b.current {
  164. end := min(i, b.current+b.length)
  165. count := end - b.current
  166. startPos := (b.current + 1) & b.lengthMask
  167. var lost int64
  168. if b.current >= b.length {
  169. // Steady state: every cleared slot is past warmup, so any unset
  170. // bit we evict is a lost packet from the previous cycle.
  171. wasSet := b.clearRange(startPos, count)
  172. lost = int64(count) - int64(wasSet)
  173. } else {
  174. // Warmup (the very first window). Some cleared slots represent
  175. // packets <= length where eviction is not "lost" in the usual
  176. // sense. This branch is taken at most once per connection so we
  177. // don't bother optimizing it.
  178. for n := b.current + 1; n <= end; n++ {
  179. if !b.get(n) && n > b.length {
  180. lost++
  181. }
  182. }
  183. b.clearRange(startPos, count)
  184. }
  185. // Anything past the new window can never be backfilled, so it's lost.
  186. if i > b.current+b.length {
  187. lost += int64(i - b.current - b.length)
  188. }
  189. b.lostCounter.Inc(lost)
  190. b.set(i)
  191. b.current = i
  192. return true
  193. }
  194. // If i is within the current window but below the current counter, check to see if it's a duplicate
  195. if b.strictlyWithinWindow(i) {
  196. pos := i & b.lengthMask
  197. word := pos >> 6
  198. mask := uint64(1) << (pos & 63)
  199. w := b.bits[word]
  200. if b.current == i || w&mask != 0 {
  201. if l.Enabled(context.Background(), slog.LevelDebug) {
  202. l.Debug("Receive window",
  203. "accepted", false,
  204. "currentCounter", b.current,
  205. "incomingCounter", i,
  206. "reason", "duplicate",
  207. )
  208. }
  209. b.dupeCounter.Inc(1)
  210. return false
  211. }
  212. b.bits[word] = w | mask
  213. return true
  214. }
  215. // In all other cases, fail and don't change current.
  216. b.outOfWindowCounter.Inc(1)
  217. if l.Enabled(context.Background(), slog.LevelDebug) {
  218. l.Debug("Receive window",
  219. "accepted", false,
  220. "currentCounter", b.current,
  221. "incomingCounter", i,
  222. "reason", "nonsense",
  223. )
  224. }
  225. return false
  226. }