redhound 1.0.1 → 2.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -0
  3. data/README.md +97 -31
  4. data/docs/API.md +104 -0
  5. data/docs/FILTERS.md +37 -0
  6. data/docs/MIGRATION.md +23 -0
  7. data/docs/PROTOCOLS.md +54 -0
  8. data/docs/USAGE.md +86 -0
  9. data/docs/VALIDATION.md +175 -0
  10. data/docs/json-schema.json +64 -0
  11. data/exe/redhound +2 -1
  12. data/lib/redhound/analysis/flow_key.rb +59 -0
  13. data/lib/redhound/analysis/flow_table.rb +124 -0
  14. data/lib/redhound/analysis/ip_reassembler.rb +209 -0
  15. data/lib/redhound/analysis/stats.rb +150 -0
  16. data/lib/redhound/analysis/tcp_analysis.rb +43 -0
  17. data/lib/redhound/analysis/tcp_reassembler.rb +176 -0
  18. data/lib/redhound/analysis/tcp_stream.rb +156 -0
  19. data/lib/redhound/analysis.rb +157 -0
  20. data/lib/redhound/capture/bsd/bpf_device.rb +155 -0
  21. data/lib/redhound/capture/bsd/constants.rb +29 -0
  22. data/lib/redhound/capture/bsd/ifreq.rb +41 -0
  23. data/lib/redhound/capture/file_source.rb +214 -0
  24. data/lib/redhound/capture/interface.rb +70 -0
  25. data/lib/redhound/capture/linktype.rb +30 -0
  26. data/lib/redhound/capture/linux/constants.rb +37 -0
  27. data/lib/redhound/capture/linux/cooked.rb +18 -0
  28. data/lib/redhound/capture/linux/ifreq.rb +42 -0
  29. data/lib/redhound/capture/linux/packet_socket.rb +224 -0
  30. data/lib/redhound/capture/linux/sockaddr_ll.rb +33 -0
  31. data/lib/redhound/capture/linux/tpacket_v3.rb +137 -0
  32. data/lib/redhound/capture/source.rb +79 -0
  33. data/lib/redhound/capture/stats.rb +22 -0
  34. data/lib/redhound/capture.rb +70 -0
  35. data/lib/redhound/cli/command.rb +42 -0
  36. data/lib/redhound/cli/options.rb +133 -0
  37. data/lib/redhound/cli/privileges.rb +29 -0
  38. data/lib/redhound/cli/runner.rb +129 -0
  39. data/lib/redhound/context.rb +20 -0
  40. data/lib/redhound/cursor.rb +69 -0
  41. data/lib/redhound/diagnostic.rb +7 -0
  42. data/lib/redhound/dissector.rb +152 -0
  43. data/lib/redhound/engine.rb +69 -0
  44. data/lib/redhound/errors.rb +38 -0
  45. data/lib/redhound/field.rb +62 -0
  46. data/lib/redhound/file/format.rb +63 -0
  47. data/lib/redhound/file/pcap_reader.rb +43 -0
  48. data/lib/redhound/file/pcap_writer.rb +79 -0
  49. data/lib/redhound/file/pcapng_reader.rb +221 -0
  50. data/lib/redhound/file/pcapng_writer.rb +151 -0
  51. data/lib/redhound/file/rotating_writer.rb +98 -0
  52. data/lib/redhound/filter/analyzer.rb +197 -0
  53. data/lib/redhound/filter/bpf/assembler.rb +121 -0
  54. data/lib/redhound/filter/bpf/disassembler.rb +58 -0
  55. data/lib/redhound/filter/bpf/validator.rb +77 -0
  56. data/lib/redhound/filter/bpf/vm.rb +109 -0
  57. data/lib/redhound/filter/codegen.rb +413 -0
  58. data/lib/redhound/filter/lexer.rb +46 -0
  59. data/lib/redhound/filter/parser.rb +206 -0
  60. data/lib/redhound/filter/program.rb +55 -0
  61. data/lib/redhound/filter.rb +30 -0
  62. data/lib/redhound/layer.rb +75 -0
  63. data/lib/redhound/output/fields.rb +20 -0
  64. data/lib/redhound/output/follow.rb +64 -0
  65. data/lib/redhound/output/hexdump.rb +29 -0
  66. data/lib/redhound/output/json.rb +32 -0
  67. data/lib/redhound/output/summary.rb +84 -0
  68. data/lib/redhound/output/timestamp.rb +34 -0
  69. data/lib/redhound/output/tree.rb +25 -0
  70. data/lib/redhound/packet.rb +77 -0
  71. data/lib/redhound/protocols/arp.rb +39 -0
  72. data/lib/redhound/protocols/checksum.rb +38 -0
  73. data/lib/redhound/protocols/data.rb +23 -0
  74. data/lib/redhound/protocols/dhcp.rb +140 -0
  75. data/lib/redhound/protocols/dns.rb +268 -0
  76. data/lib/redhound/protocols/ethernet.rb +24 -0
  77. data/lib/redhound/protocols/http.rb +185 -0
  78. data/lib/redhound/protocols/icmp.rb +205 -0
  79. data/lib/redhound/protocols/ipv4.rb +56 -0
  80. data/lib/redhound/protocols/ipv6.rb +68 -0
  81. data/lib/redhound/protocols/link.rb +70 -0
  82. data/lib/redhound/protocols/llc.rb +37 -0
  83. data/lib/redhound/protocols/ntp.rb +45 -0
  84. data/lib/redhound/protocols/tcp.rb +88 -0
  85. data/lib/redhound/protocols/tls.rb +218 -0
  86. data/lib/redhound/protocols/tunnel.rb +48 -0
  87. data/lib/redhound/protocols/udp.rb +32 -0
  88. data/lib/redhound/protocols/vlan.rb +23 -0
  89. data/lib/redhound/reader.rb +8 -0
  90. data/lib/redhound/registry.rb +64 -0
  91. data/lib/redhound/stream_dissector.rb +168 -0
  92. data/lib/redhound/util/seq.rb +21 -0
  93. data/lib/redhound/version.rb +2 -1
  94. data/lib/redhound/writer.rb +25 -43
  95. data/lib/redhound.rb +81 -9
  96. data/sig/generated/redhound/analysis/flow_key.rbs +15 -0
  97. data/sig/generated/redhound/analysis/flow_table.rbs +102 -0
  98. data/sig/generated/redhound/analysis/ip_reassembler.rbs +52 -0
  99. data/sig/generated/redhound/analysis/stats.rbs +80 -0
  100. data/sig/generated/redhound/analysis/tcp_analysis.rbs +15 -0
  101. data/sig/generated/redhound/analysis/tcp_reassembler.rbs +39 -0
  102. data/sig/generated/redhound/analysis/tcp_stream.rbs +60 -0
  103. data/sig/generated/redhound/analysis.rbs +38 -0
  104. data/sig/generated/redhound/capture/bsd/bpf_device.rbs +39 -0
  105. data/sig/generated/redhound/capture/bsd/constants.rbs +40 -0
  106. data/sig/generated/redhound/capture/bsd/ifreq.rbs +15 -0
  107. data/sig/generated/redhound/capture/file_source.rbs +67 -0
  108. data/sig/generated/redhound/capture/interface.rbs +53 -0
  109. data/sig/generated/redhound/capture/linktype.rbs +20 -0
  110. data/sig/generated/redhound/capture/linux/constants.rbs +58 -0
  111. data/sig/generated/redhound/capture/linux/cooked.rbs +15 -0
  112. data/sig/generated/redhound/capture/linux/ifreq.rbs +18 -0
  113. data/sig/generated/redhound/capture/linux/packet_socket.rbs +64 -0
  114. data/sig/generated/redhound/capture/linux/sockaddr_ll.rbs +31 -0
  115. data/sig/generated/redhound/capture/linux/tpacket_v3.rbs +35 -0
  116. data/sig/generated/redhound/capture/source.rbs +44 -0
  117. data/sig/generated/redhound/capture/stats.rbs +25 -0
  118. data/sig/generated/redhound/capture.rbs +14 -0
  119. data/sig/generated/redhound/cli/command.rbs +12 -0
  120. data/sig/generated/redhound/cli/options.rbs +25 -0
  121. data/sig/generated/redhound/cli/privileges.rbs +12 -0
  122. data/sig/generated/redhound/cli/runner.rbs +27 -0
  123. data/sig/generated/redhound/context.rbs +27 -0
  124. data/sig/generated/redhound/cursor.rbs +59 -0
  125. data/sig/generated/redhound/diagnostic.rbs +21 -0
  126. data/sig/generated/redhound/dissector.rbs +117 -0
  127. data/sig/generated/redhound/engine.rbs +23 -0
  128. data/sig/generated/redhound/errors.rbs +56 -0
  129. data/sig/generated/redhound/field.rbs +74 -0
  130. data/sig/generated/redhound/file/format.rbs +39 -0
  131. data/sig/generated/redhound/file/pcap_reader.rbs +19 -0
  132. data/sig/generated/redhound/file/pcap_writer.rbs +38 -0
  133. data/sig/generated/redhound/file/pcapng_reader.rbs +54 -0
  134. data/sig/generated/redhound/file/pcapng_writer.rbs +50 -0
  135. data/sig/generated/redhound/file/rotating_writer.rbs +42 -0
  136. data/sig/generated/redhound/filter/analyzer.rbs +42 -0
  137. data/sig/generated/redhound/filter/bpf/assembler.rbs +39 -0
  138. data/sig/generated/redhound/filter/bpf/disassembler.rbs +18 -0
  139. data/sig/generated/redhound/filter/bpf/validator.rbs +30 -0
  140. data/sig/generated/redhound/filter/bpf/vm.rbs +28 -0
  141. data/sig/generated/redhound/filter/codegen.rbs +101 -0
  142. data/sig/generated/redhound/filter/lexer.rbs +15 -0
  143. data/sig/generated/redhound/filter/parser.rbs +65 -0
  144. data/sig/generated/redhound/filter/program.rbs +48 -0
  145. data/sig/generated/redhound/filter.rbs +10 -0
  146. data/sig/generated/redhound/layer.rbs +81 -0
  147. data/sig/generated/redhound/output/fields.rbs +15 -0
  148. data/sig/generated/redhound/output/follow.rbs +21 -0
  149. data/sig/generated/redhound/output/hexdump.rbs +15 -0
  150. data/sig/generated/redhound/output/json.rbs +18 -0
  151. data/sig/generated/redhound/output/summary.rbs +21 -0
  152. data/sig/generated/redhound/output/timestamp.rbs +21 -0
  153. data/sig/generated/redhound/output/tree.rbs +15 -0
  154. data/sig/generated/redhound/packet.rbs +82 -0
  155. data/sig/generated/redhound/protocols/arp.rbs +15 -0
  156. data/sig/generated/redhound/protocols/checksum.rbs +15 -0
  157. data/sig/generated/redhound/protocols/data.rbs +17 -0
  158. data/sig/generated/redhound/protocols/dhcp.rbs +31 -0
  159. data/sig/generated/redhound/protocols/dns.rbs +42 -0
  160. data/sig/generated/redhound/protocols/ethernet.rbs +15 -0
  161. data/sig/generated/redhound/protocols/http.rbs +36 -0
  162. data/sig/generated/redhound/protocols/icmp.rbs +41 -0
  163. data/sig/generated/redhound/protocols/ipv4.rbs +15 -0
  164. data/sig/generated/redhound/protocols/ipv6.rbs +24 -0
  165. data/sig/generated/redhound/protocols/link.rbs +33 -0
  166. data/sig/generated/redhound/protocols/llc.rbs +15 -0
  167. data/sig/generated/redhound/protocols/ntp.rbs +17 -0
  168. data/sig/generated/redhound/protocols/tcp.rbs +25 -0
  169. data/sig/generated/redhound/protocols/tls.rbs +51 -0
  170. data/sig/generated/redhound/protocols/tunnel.rbs +24 -0
  171. data/sig/generated/redhound/protocols/udp.rbs +18 -0
  172. data/sig/generated/redhound/protocols/vlan.rbs +12 -0
  173. data/sig/generated/redhound/reader.rbs +7 -0
  174. data/sig/generated/redhound/registry.rbs +45 -0
  175. data/sig/generated/redhound/stream_dissector.rbs +44 -0
  176. data/sig/generated/redhound/util/seq.rbs +20 -0
  177. data/sig/generated/redhound/version.rbs +1 -0
  178. data/sig/generated/redhound/writer.rbs +6 -19
  179. data/sig/generated/redhound.rbs +18 -0
  180. data/sig/shims/io.rbs +7 -0
  181. data/sig/shims/process.rbs +3 -0
  182. metadata +177 -55
  183. data/CODE_OF_CONDUCT.md +0 -132
  184. data/Dockerfile +0 -7
  185. data/Rakefile +0 -15
  186. data/Steepfile +0 -4
  187. data/docker-compose.yml +0 -9
  188. data/lib/redhound/analyzer.rb +0 -34
  189. data/lib/redhound/builder/packet_mreq.rb +0 -56
  190. data/lib/redhound/builder/socket.rb +0 -35
  191. data/lib/redhound/builder.rb +0 -4
  192. data/lib/redhound/command.rb +0 -67
  193. data/lib/redhound/l2/ether.rb +0 -68
  194. data/lib/redhound/l2/protocol.rb +0 -33
  195. data/lib/redhound/l2.rb +0 -4
  196. data/lib/redhound/l3/arp.rb +0 -114
  197. data/lib/redhound/l3/base.rb +0 -49
  198. data/lib/redhound/l3/ipv4.rb +0 -95
  199. data/lib/redhound/l3/ipv6.rb +0 -69
  200. data/lib/redhound/l3/protocol.rb +0 -173
  201. data/lib/redhound/l3/resolver.rb +0 -30
  202. data/lib/redhound/l3.rb +0 -9
  203. data/lib/redhound/l4/base.rb +0 -31
  204. data/lib/redhound/l4/icmp.rb +0 -71
  205. data/lib/redhound/l4/resolver.rb +0 -28
  206. data/lib/redhound/l4/udp.rb +0 -63
  207. data/lib/redhound/l4.rb +0 -6
  208. data/lib/redhound/receiver.rb +0 -45
  209. data/lib/redhound/resolver.rb +0 -22
  210. data/lib/redhound/source/socket.rb +0 -18
  211. data/lib/redhound/source.rb +0 -3
  212. data/rbs_collection.lock.yaml +0 -20
  213. data/rbs_collection.yaml +0 -17
  214. data/sig/generated/redhound/analyzer.rbs +0 -14
  215. data/sig/generated/redhound/builder/packet_mreq.rbs +0 -39
  216. data/sig/generated/redhound/builder/socket.rbs +0 -24
  217. data/sig/generated/redhound/command.rbs +0 -19
  218. data/sig/generated/redhound/l2/ether.rbs +0 -41
  219. data/sig/generated/redhound/l2/protocol.rbs +0 -15
  220. data/sig/generated/redhound/l3/arp.rbs +0 -57
  221. data/sig/generated/redhound/l3/base.rbs +0 -28
  222. data/sig/generated/redhound/l3/ipv4.rbs +0 -53
  223. data/sig/generated/redhound/l3/ipv6.rbs +0 -38
  224. data/sig/generated/redhound/l3/protocol.rbs +0 -16
  225. data/sig/generated/redhound/l3/resolver.rbs +0 -16
  226. data/sig/generated/redhound/l4/base.rbs +0 -19
  227. data/sig/generated/redhound/l4/icmp.rbs +0 -33
  228. data/sig/generated/redhound/l4/resolver.rbs +0 -16
  229. data/sig/generated/redhound/l4/udp.rbs +0 -36
  230. data/sig/generated/redhound/receiver.rbs +0 -19
  231. data/sig/generated/redhound/resolver.rbs +0 -11
  232. data/sig/generated/redhound/source/socket.rbs +0 -13
@@ -0,0 +1,175 @@
1
+ # Release validation
2
+
3
+ ## Adversarial review after rc1
4
+
5
+ The review checked the design, roadmap, work procedures and phase0 patch against
6
+ the running implementation, rather than treating a green test suite as proof
7
+ that every acceptance gate had passed. Reproduced failures included truncated
8
+ headers losing readable fields, incorrect ICMP error fields and dynamic field
9
+ locations, missed split HTTP identification, unreported reassembly loss at EOF
10
+ and flow eviction, underestimated Ruby buffer capacity, unbounded pcapng
11
+ metadata, source timeout violations and capture writer descriptor leaks.
12
+ Regression checks accompany the fixes.
13
+
14
+ The parser fuzz now retains each fixture's real link type during structured
15
+ mutations. This prevents tests from stopping at an unrelated link header before
16
+ reaching the mutated protocol. `ruby bench/analysis.rb` exercises default state
17
+ limits with 60,000 conversations, 40,000 fragmented datagram identities and 80
18
+ large TCP streams, checking every update and final buffer release. Its retained
19
+ state accounting includes String capacity and object storage. Process RSS is
20
+ reported separately: Ruby may retain allocator pages after state is released.
21
+
22
+ The manual [Live acceptance workflow](../.github/workflows/acceptance.yml) and
23
+ `bench/live.rb` record requested/sent/captured counts, gaps, byte digests, kernel
24
+ timestamps, drops, RSS and descriptor counts. An insufficient sender rate fails
25
+ the check. Controlled aarch64 runs passed 50,000 pps with simultaneous socket and
26
+ ring capture and 80,000 pps with socket capture for ten seconds. These short
27
+ checks do not replace the hour-long comparison or the x86_64 targets.
28
+ The strengthened live harness checks complete expected wire bytes, every
29
+ timestamp and the entire zero-based sequence. Its mixed TCP/UDP workload and
30
+ capture-only CPU affinity are recorded alongside implementation and harness
31
+ digests. The earlier long-running snapshot uses the original UDP workload and
32
+ verifier; its comparison requires identical complete-frame digests and sampled
33
+ timestamps, and does not establish the new mixed single-core throughput gate.
34
+
35
+ Local adversarial runtime validation on 2026-10-02 passed all 244 non-live examples
36
+ on Linux, with only the macOS-specific interface test skipped. After adding
37
+ bounded file prefetch and live-harness regressions, the complete macOS Rake task
38
+ passed its 257 examples (20 tshark comparisons are run on Linux), RBS generation
39
+ and Steep, with 92.92% total line coverage. Coverage gates passed; the earlier
40
+ Linux check measured 92.43% total line coverage. The strengthened one-million-case parser fuzz completed without an
41
+ exception. Default state-pressure checks reached exactly 256 MiB accounted
42
+ state without exceeding it and released IP/TCP buffers at EOF. Native RSS
43
+ reached 308 MiB despite retaining only 107 MiB of Ruby heap before finalization;
44
+ this is not reported as a 256 MiB process-RSS guarantee.
45
+ The independent [manual CI and million-case fuzz run](https://github.com/ydah/redhound/actions/runs/36885293639)
46
+ also passed every job on the adversarial runtime fixes.
47
+
48
+ The GitHub maintenance configuration now includes weekly Bundler/Actions
49
+ dependency updates, actionlint, codespell, strict yamllint, zizmor and Ruby/Actions
50
+ CodeQL analysis. Action revisions are pinned and checkout credentials are not
51
+ retained. All four lint workflows passed on main; the final CodeQL run succeeded
52
+ with no open alerts. HTTP header trimming uses
53
+ bounded byte ranges while preserving SP/HTAB-only whitespace semantics; controls
54
+ remain visible to malformed-header diagnostics.
55
+ After this final parser change, all 16 application-boundary/live-harness examples,
56
+ the one-million-case native fuzz run (94.25 seconds), and Steep passed.
57
+ All 5,013 binary boundary/random inputs preserved the previous trimming bytes
58
+ and encoding. Linux tcpdump/tshark comparisons passed all 26 examples.
59
+
60
+ Ruby 4.0 introduced an additional restriction in
61
+ [`IO::Buffer.map`](https://docs.ruby-lang.org/en/4.0/IO/Buffer.html#method-c-map):
62
+ it rejects files with zero size even when a mapping size is supplied. Linux
63
+ AF_PACKET sockets have zero file size, so the pure-Ruby ring backend cannot map
64
+ them on that runtime. Ruby 3.3/3.4 ring support remains available; Ruby 4.0 uses
65
+ the socket backend. No C extension or Fiddle workaround is introduced. Ring
66
+ availability on Ruby 4.0 remains an upstream API constraint on full acceptance.
67
+
68
+ Long-running controlled tests started at 2026-10-01 14:55:57 UTC with a frozen
69
+ capture implementation digest
70
+ `084591da70d195146b37aff76de7ec9495085d045684decc26da6c34c4ebc20e`:
71
+
72
+ | Check | Requested workload | Evidence directory | Earliest completion |
73
+ | --- | --- | --- | --- |
74
+ | aarch64 socket/ring comparison | 50,000 pps, 3,600 seconds | `tmp/acceptance-arm-hour-final` | 2026-10-01 15:55:58 UTC |
75
+ | Bounded ring pcapng rotation | 100 pps, 259,200 seconds | `tmp/acceptance-rotation-72h-final` | 2026-10-04 14:55:58 UTC |
76
+
77
+ These directories contain local private captures and are excluded from Git.
78
+ Completion requires the final `result.json` to pass, inspection of the periodic
79
+ resource samples, and successful external validation of rotated files. The
80
+ controlled veth soak does not establish the separate busy physical-interface
81
+ 24-hour gate. Results and unfulfilled conditions are recorded explicitly below.
82
+
83
+ The frozen rotation run first reported 1,178 kernel drops and three ring freezes
84
+ at the 4,200-second sample, after zero drops at 3,600 seconds. The macOS power log
85
+ records hibernation/wake in that interval with a 10.614-second recovery delay.
86
+ This correlation does not isolate the cause, and the run is not loss-free
87
+ acceptance evidence. Its periodic resource observations are retained. External
88
+ capinfos/tshark inspection now runs after capture closes, validating the three
89
+ retained rotation files rather than blocking packet reads at every rotation.
90
+ A four-second rotation smoke captured and verified all 4,000 frames without
91
+ drops, gaps or malformed records and passed both external tools.
92
+
93
+ A fresh frozen-source rotation run started at 2026-10-01 16:22:04 UTC, using
94
+ capture digest
95
+ `7fd49cc4c24664c298c99dbf35276f64e67a1195093c3e1c4f9190fa9d234014`
96
+ and harness digest
97
+ `55b36abe852549836350d51ea9994b71ded5409794d853777f0634ca6830c760`.
98
+ Its private evidence is in `tmp/acceptance-rotation-72h-rc2`; completion cannot
99
+ be checked before 2026-10-04 16:22:06 UTC. macOS idle/system sleep assertions
100
+ were confirmed on AC power with a 72-hour-plus-ten-minute `caffeinate` timer.
101
+ This run is still in progress and does not count as completed acceptance.
102
+
103
+ The one-hour aarch64 comparison completed successfully: each backend captured
104
+ all 180,000,000 frames with zero drops/gaps and the same complete-byte digest.
105
+ All 18,000 paired timestamp samples were identical. Ring final drain was
106
+ 47.638 ms; descriptor counts remained 10/11. Last-half-hour RSS ranged from
107
+ 31.2–40.2 MiB for sockets and 86.5–86.6 MiB for rings. P6-06 is complete, so
108
+ aarch64/arm64 Linux now tries ring capture automatically, retaining socket
109
+ fallback when mapping is unavailable. This does not complete the separate
110
+ 72-hour rotation soak or the mixed x86_64 throughput gates.
111
+
112
+ The physical macOS en0 interface is active, but `/dev/bpf*` requires administrator
113
+ access and `sudo -n` requires a password in this environment. The user could not
114
+ start the privileged run, so the 24-hour physical-interface check has not begun.
115
+ An independent three-second observation measured approximately 82 packets/s and
116
+ 21 kB/s; it does not establish a busy workload. `bench/soak.rb` is ready to record
117
+ 24-hour elapsed time, bounded private pcapng rotation, traffic rates, RSS and
118
+ descriptor counts once capture privileges are available. Its one-second Linux
119
+ smoke and interrupted-run checks passed, including capinfos/tshark file checks.
120
+
121
+ ## 2.0.0.rc1
122
+
123
+ The release candidate implements the functional parts of the v2 roadmap: the packet/dissector API, network and
124
+ application protocols, Ruby cBPF compiler/VM, Linux socket/ring and macOS BPF
125
+ backends, capture files/rotation/privilege drop, bounded stateful analysis,
126
+ statistics, follow output and documentation. This is a release candidate,
127
+ not a claim that time-dependent GA acceptance checks have completed.
128
+
129
+ Reproducible checks:
130
+
131
+ - `bundle exec rake`: RSpec, generated RBS and Steep.
132
+ - `REDHOUND_COVERAGE_GATE=1 bundle exec rake`: at least 90% total line coverage
133
+ and 95% protocol line coverage.
134
+ - `bundle exec rspec spec/differential`: tcpdump filter match/bytecode parity
135
+ (163 expressions across five link types), tshark protocol and stateful analysis
136
+ field/statistics/follow comparisons.
137
+ - `FUZZ_ITERATIONS=1000000 bundle exec rspec spec/fuzz`: strict parser fuzzing.
138
+ - `sudo -E env "PATH=$PATH" REDHOUND_LIVE=1 bundle exec rspec --tag live`:
139
+ native capture, attached filters, kernel time, snaplen, stop and loopback.
140
+ - `sudo -E env "PATH=$PATH" REDHOUND_LIVE=1 REDHOUND_NETNS=1 bundle exec rspec spec/integration/netns_capture_spec.rb --tag live`:
141
+ Linux veth socket/ring parity and offloaded VLAN restoration/filtering.
142
+ - `bundle exec yard stats --list-undoc`: documented public API.
143
+ - `actionlint`: test and release workflow validation.
144
+
145
+ Fixtures are generated without runtime dependencies. Golden output freezes
146
+ addresses and timestamps, and validates all JSON snapshots against the schema.
147
+ The CI matrix checks Ruby 3.3/3.4/4.0/head, Linux/macOS live capture and tool
148
+ comparisons. Scheduled runs execute one million fuzz cases. Benchmark jobs warn
149
+ on a summary throughput regression over 15% compared with the previous commit.
150
+
151
+ Local validation on 2026-10-01: Linux aarch64 Ruby 3.4 passed all 171 non-live
152
+ examples, including tcpdump/tshark differential checks, and five live capture
153
+ examples using socket and ring backends. Native macOS Ruby 4.0 passed unit,
154
+ golden and filter checks; BPF ioctl constants match the installed SDK. Native
155
+ BPF loopback and en0 Ethernet capture, attached filters, timestamps, truncation,
156
+ termination, privilege dropping and pcapng metadata passed the macOS CI job.
157
+ The Linux namespace test passed socket/ring parity and offloaded VLAN checks.
158
+ Ruby 3.3/3.4/4.0/head all passed the CI type, signature and coverage gates.
159
+ Whole-library line coverage measured 90.92%; isolated protocol line coverage
160
+ measured 99.90%. See [benchmarks](../bench/RESULTS.md)
161
+ for throughput figures and measurement limits.
162
+
163
+ ## Remaining GA acceptance gates
164
+
165
+ - Run a 24-hour continuous capture stability check.
166
+ - Run a 72-hour rotation soak and record RSS, descriptor counts and drop rates.
167
+ - Confirm x86_64 single-core throughput and live drop targets on the specified
168
+ workload; file benchmarks alone cannot establish loss-free live throughput.
169
+ - Keep rc1 available for two weeks, classify reports, and fix critical/high
170
+ failures before GA.
171
+
172
+ These elapsed-time/hardware gates are not marked complete by unit tests.
173
+ Promote to 2.0.0 only after their evidence is recorded. A 1.x maintenance
174
+ end date is set at GA plus six months; no maintenance branch is created by this
175
+ main-only implementation.
@@ -0,0 +1,64 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/ydah/redhound/blob/main/docs/json-schema.json",
4
+ "title": "Redhound packet",
5
+ "description": "One packet object emitted by Packet#to_h, NDJSON, or an element of the JSON output array. Nanosecond timestamps and lengths are integers; byte fields use hexadecimal strings; repeated fields use arrays.",
6
+ "type": "object",
7
+ "required": ["frame", "layers", "diagnostics"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "frame": {
11
+ "type": "object",
12
+ "required": ["number", "time_epoch_ns", "time", "caplen", "len", "interface", "direction", "linktype"],
13
+ "additionalProperties": false,
14
+ "properties": {
15
+ "number": { "type": "integer", "minimum": 1 },
16
+ "time_epoch_ns": { "type": "integer" },
17
+ "time": { "type": "string", "description": "UTC calendar time with nine fractional digits; years outside 0000..9999 use expanded signed/long year notation. time_epoch_ns preserves the exact instant.", "pattern": "^-?[0-9]{4,}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{9}Z$" },
18
+ "caplen": { "type": "integer", "minimum": 0 },
19
+ "len": { "type": "integer", "minimum": 0 },
20
+ "interface": { "type": ["string", "null"] },
21
+ "direction": { "enum": ["in", "out", null] },
22
+ "linktype": { "type": "integer", "minimum": 0, "maximum": 65535 }
23
+ }
24
+ },
25
+ "layers": {
26
+ "type": "array",
27
+ "items": {
28
+ "type": "object",
29
+ "required": ["protocol", "offset", "length", "embedded", "fields"],
30
+ "additionalProperties": false,
31
+ "properties": {
32
+ "protocol": { "type": "string" },
33
+ "offset": { "type": "integer", "minimum": 0 },
34
+ "length": { "type": "integer", "minimum": 0 },
35
+ "embedded": { "type": "boolean" },
36
+ "fields": { "type": "object", "additionalProperties": { "$ref": "#/$defs/fieldValue" } }
37
+ }
38
+ }
39
+ },
40
+ "diagnostics": {
41
+ "type": "array",
42
+ "items": {
43
+ "type": "object",
44
+ "required": ["layer", "severity", "code", "message", "field"],
45
+ "additionalProperties": false,
46
+ "properties": {
47
+ "layer": { "type": "string" },
48
+ "severity": { "enum": ["note", "warn", "warning", "error"] },
49
+ "code": { "type": "string" },
50
+ "message": { "type": "string" },
51
+ "field": { "type": ["string", "null"] }
52
+ }
53
+ }
54
+ }
55
+ },
56
+ "$defs": {
57
+ "fieldValue": {
58
+ "anyOf": [
59
+ { "type": ["string", "number", "boolean", "null"] },
60
+ { "type": "array", "items": { "$ref": "#/$defs/fieldValue" } }
61
+ ]
62
+ }
63
+ }
64
+ }
data/exe/redhound CHANGED
@@ -4,4 +4,5 @@
4
4
  $LOAD_PATH << File.join(__dir__, '../lib')
5
5
  require 'redhound'
6
6
 
7
- Redhound::Command.new.run(ARGV.dup)
7
+ RubyVM::YJIT.enable if defined?(RubyVM::YJIT) && RubyVM::YJIT.respond_to?(:enable) && !ARGV.include?('--no-yjit')
8
+ exit Redhound::CLI::Command.new.run(ARGV.dup)
@@ -0,0 +1,59 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Analysis
7
+ # @api private
8
+ module FlowKey
9
+ # @rbs (Packet packet, ?Symbol? type) -> untyped
10
+ def self.from(packet, type = nil)
11
+ layers = packet.layers.reject(&:embedded)
12
+ if type == :eth
13
+ layer = layers.find { |item| item.protocol == :eth }
14
+ return nil unless layer && layer[:src] && layer[:dst]
15
+ return normalize(:eth, [layer.display(:src), 0], [layer.display(:dst), 0])
16
+ end
17
+ transport = layers.reverse.find { |item| %i[tcp udp icmp icmpv6].include?(item.protocol) }
18
+ if type && %i[tcp udp].include?(type)
19
+ return nil unless transport && transport.protocol == type
20
+ end
21
+ network = if type == :ip || type == :ipv6
22
+ layers.find { |item| item.protocol == (type == :ip ? :ipv4 : :ipv6) }
23
+ elsif transport
24
+ position = layers.index(transport) #: Integer
25
+ layers.take(position).reverse.find { |item| %i[ipv4 ipv6].include?(item.protocol) }
26
+ else
27
+ layers.reverse.find { |item| %i[ipv4 ipv6].include?(item.protocol) }
28
+ end
29
+ return nil unless network && network[:src] && network[:dst]
30
+ proto = type || transport&.protocol
31
+ unless proto
32
+ position = layers.index(network) #: Integer
33
+ extensions = layers.drop(position + 1).take_while { |item| item.protocol == :ipv6_ext }
34
+ number = network.protocol == :ipv4 ? network[:proto] : extensions.last&.[](:next) || network[:nxt]
35
+ proto = "ip_proto_#{number}".to_sym
36
+ end
37
+ if %i[tcp udp].include?(proto)
38
+ return nil unless transport && transport[:srcport] && transport[:dstport]
39
+ ports = [transport[:srcport], transport[:dstport]]
40
+ elsif %i[icmp icmpv6].include?(proto)
41
+ return nil unless transport
42
+ # Echo request/reply share a type series and identifier in both directions.
43
+ series = { 8 => 0, 128 => 129 }.fetch(transport[:type], transport[:type])
44
+ ports = [[series, transport[:ident] || 0], [series, transport[:ident] || 0]]
45
+ else
46
+ ports = [0, 0]
47
+ end
48
+ normalize(proto, [network.display(:src), ports[0]], [network.display(:dst), ports[1]])
49
+ end
50
+
51
+ # @rbs (Symbol proto, untyped src, untyped dst) -> untyped
52
+ def self.normalize(proto, src, dst)
53
+ reverse = (src <=> dst).positive?
54
+ a, b = reverse ? [dst, src] : [src, dst]
55
+ [[proto, *a, *b].freeze, reverse ? 1 : 0]
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,124 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Analysis
7
+ # @api private
8
+ class Flow
9
+ BASE_BYTES = 4096
10
+ attr_reader :key, :id, :first_ns, :packets, :bytes, :streams, :applications, :acks, :keep_alives, :http_methods, :probes, :probe_disabled
11
+ attr_accessor :last_ns, :closed_ns, :syn_ns, :syn_direction, :synack_ns, :synack_seq, :initial_rtt, :accounted_bytes
12
+ # @rbs (untyped key, Integer id, Integer timestamp_ns) -> void
13
+ def initialize(key, id, timestamp_ns)
14
+ @key, @id, @first_ns, @last_ns = key, id, timestamp_ns, timestamp_ns
15
+ @packets, @bytes, @streams, @applications, @acks = [0, 0], [0, 0], [nil, nil], [nil, nil], [nil, nil]
16
+ @keep_alives = [false, false]
17
+ @http_methods = [Array.new, Array.new] #: Array[untyped]
18
+ @probes = [Array.new, Array.new] #: Array[untyped]
19
+ @probe_disabled = [false, false]
20
+ @closed_ns = @syn_ns = @syn_direction = @synack_ns = @synack_seq = @initial_rtt = nil # @rbs untyped
21
+ @accounted_bytes = BASE_BYTES
22
+ end
23
+ # @rbs () -> bool
24
+ def closed? = !@closed_ns.nil?
25
+ # @rbs () -> Integer
26
+ def bytesize = BASE_BYTES + @streams.compact.sum(&:bytesize) + @applications.compact.sum(&:bytesize) + @http_methods.sum { |methods| methods.size * 16 } + @probes.sum { |parts| parts.sum { |data, _frame| data.bytesize * 2 + 128 } }
27
+ end
28
+
29
+ # @api private
30
+ class FlowTable
31
+ attr_reader :evicted, :expired
32
+ attr_accessor :on_remove
33
+ # @rbs (?max_flows: Integer) -> void
34
+ def initialize(max_flows: 100_000)
35
+ raise ArgumentError, 'max_flows must be positive' unless max_flows.positive?
36
+ @max_flows, @next_id, @evicted, @expired, @clock = max_flows, 0, 0, 0, 0
37
+ @next_sweep, @bytes, @tcp_bytes = 0, 0, 0
38
+ @on_remove = nil # @rbs untyped
39
+ @entries = {} #: Hash[untyped, Flow]
40
+ end
41
+ # @rbs (Packet packet) -> untyped
42
+ def update(packet)
43
+ advance(packet.timestamp_ns)
44
+ result = FlowKey.from(packet)
45
+ return nil unless result
46
+ key, direction = result
47
+ flow = @entries.delete(key)
48
+ transport = packet.layers.reverse.find { |layer| layer.protocol == :tcp && !layer.embedded }
49
+ if flow && transport && transport[:flags] && transport[:flags] & 0x12 == 2
50
+ previous = flow.streams[direction]
51
+ if flow.closed? || (previous && previous.base_seq && previous.base_seq != transport[:seq])
52
+ discard(flow)
53
+ flow = nil
54
+ end
55
+ end
56
+ unless flow
57
+ evict if @entries.size >= @max_flows
58
+ flow = Flow.new(key, key[0] == :tcp ? @next_id : -1, @clock)
59
+ @next_id += 1 if key[0] == :tcp
60
+ @bytes += flow.accounted_bytes
61
+ end
62
+ flow.last_ns = @clock
63
+ flow.packets[direction] += 1
64
+ flow.bytes[direction] += packet.original_length
65
+ flow.closed_ns = @clock if transport && transport[:flags] && transport[:flags] & 4 != 0
66
+ @entries[key] = flow
67
+ [flow, direction]
68
+ end
69
+ # @rbs (Integer now_ns) -> void
70
+ def advance(now_ns)
71
+ @clock = [@clock, now_ns].max
72
+ expire(@clock) if @clock >= @next_sweep
73
+ end
74
+ # @rbs (Integer now_ns) -> void
75
+ def expire(now_ns)
76
+ @next_sweep = now_ns + 1_000_000_000
77
+ @entries.delete_if do |_key, flow|
78
+ timeout = flow.closed? ? 10 : flow.key[0] == :tcp ? 120 : 30
79
+ stale = now_ns - flow.last_ns >= timeout * 1_000_000_000
80
+ if stale
81
+ discard(flow)
82
+ @expired += 1
83
+ end
84
+ stale
85
+ end
86
+ end
87
+ # @rbs (?Symbol? type) -> Flow?
88
+ def evict(type = nil)
89
+ pair = if type
90
+ key = @entries.each_key.find { |item| item[0] == type }
91
+ key ? [key, @entries.delete(key)] : nil
92
+ else
93
+ @entries.shift
94
+ end
95
+ return nil unless pair
96
+ @evicted += 1
97
+ discard(pair[1])
98
+ pair[1]
99
+ end
100
+ # @rbs (Flow flow) -> void
101
+ def discard(flow)
102
+ @bytes -= flow.accounted_bytes
103
+ @tcp_bytes -= flow.accounted_bytes - Flow::BASE_BYTES if flow.key[0] == :tcp
104
+ @on_remove&.call(flow)
105
+ end
106
+ # @rbs (Flow flow) -> void
107
+ def account(flow)
108
+ size = flow.bytesize
109
+ delta = size - flow.accounted_bytes
110
+ @bytes += delta
111
+ @tcp_bytes += delta if flow.key[0] == :tcp
112
+ flow.accounted_bytes = size
113
+ end
114
+ # @rbs () -> Integer
115
+ def size = @entries.size
116
+ # @rbs () -> Array[Flow]
117
+ def values = @entries.values
118
+ # @rbs () -> Integer
119
+ def bytesize = @bytes
120
+ # @rbs () -> Integer
121
+ def tcp_bytesize = @tcp_bytes
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,209 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Analysis
7
+ # @api private
8
+ class IpReassembler
9
+ attr_reader :evicted, :expired
10
+ # @rbs (?max_bytes: Integer, ?timeout_ns: Integer) -> void
11
+ def initialize(max_bytes: 16 << 20, timeout_ns: 30_000_000_000)
12
+ raise ArgumentError, 'fragment limits must be positive' unless max_bytes.positive? && timeout_ns.positive?
13
+ @max_bytes, @timeout_ns, @clock, @evicted, @expired = max_bytes, timeout_ns, 0, 0, 0
14
+ @next_sweep = 0
15
+ @bytes = 0
16
+ @entries, @discarded = {}, {} # @rbs untyped
17
+ end
18
+ # @rbs (Packet packet) -> untyped
19
+ def descriptor(packet)
20
+ layers = packet.layers.reject(&:embedded)
21
+ layers.each_with_index do |layer, index|
22
+ if layer.protocol == :ipv4 && layer[:src] && layer[:dst] && (layer[:mf] == 1 || layer[:frag_offset].to_i.positive?)
23
+ return [layer, layer, [4, layer[:src], layer[:dst], layer[:proto], layer[:id]], layer[:frag_offset], layer[:mf] == 1]
24
+ elsif layer.protocol == :ipv6_ext && layer[:type] == 44 && layer[:fragment_offset] && layer[:identification]
25
+ # RFC 6946: atomic fragments bypass queued fragments and tombstones.
26
+ next if layer[:fragment_offset].zero? && layer[:more] == 0
27
+ network = layers.take(index).reverse.find { |item| item.protocol == :ipv6 }
28
+ return [network, layer, [6, network[:src], network[:dst], layer[:identification]], layer[:fragment_offset], layer[:more] == 1] if network && network[:src] && network[:dst]
29
+ end
30
+ end
31
+ nil
32
+ end
33
+ # @rbs (Packet packet) -> Packet?
34
+ def update(packet)
35
+ advance(packet.timestamp_ns)
36
+ info = descriptor(packet)
37
+ return nil unless info
38
+ network, fragment, key, offset, more = info
39
+ return nil if @discarded[key]
40
+ return nil if network.error? || fragment.error?
41
+ length = network.payload_end - fragment.payload_offset
42
+ if offset + length > 65_535 || length.negative? || (more && (length.zero? || length % 8 != 0))
43
+ fragment.diagnose(:error, :malformed, 'invalid IP fragment size or offset')
44
+ remove(key)
45
+ return nil
46
+ end
47
+ if network.diagnostics.any? { |diagnostic| diagnostic.code == :truncated }
48
+ fragment.diagnose(:note, :reassembly_gap, 'truncated IP fragment cannot be reassembled')
49
+ return nil
50
+ end
51
+ data = packet.data.byteslice(fragment.payload_offset, length) #: String
52
+ state = remove(key) || { parts: Array.new, frames: Array.new, header: nil, next_offset: 6, next: fragment[:next], end: nil } #: untyped
53
+ if key[0] == 6 && state[:next] != fragment[:next]
54
+ fragment.diagnose(:error, :malformed, 'IPv6 fragments have inconsistent next headers')
55
+ return nil
56
+ end
57
+ state[:time] = @clock
58
+ old_parts = state[:parts]
59
+ overlap = old_parts.any? { |pos, bytes| offset < pos + bytes.bytesize && offset + length > pos }
60
+ if overlap
61
+ fragment.diagnose(:warning, :fragment_overlap, 'overlapping IP fragments')
62
+ if key[0] == 6
63
+ @discarded[key] = @clock
64
+ @bytes += 128
65
+ while @discarded.size > [4096, @max_bytes / 128].min
66
+ @discarded.shift
67
+ @bytes -= 128
68
+ @evicted += 1
69
+ end
70
+ while bytesize > @max_bytes && remove_oldest
71
+ @evicted += 1
72
+ end
73
+ return nil
74
+ end
75
+ end
76
+ pieces = [[offset, data]]
77
+ old_parts.each do |pos, bytes|
78
+ replacement = [] #: Array[untyped]
79
+ pieces.each do |start, payload|
80
+ ending, old_end = start + payload.bytesize, pos + bytes.bytesize
81
+ if start < old_end && ending > pos
82
+ from, to = [start, pos].max, [ending, old_end].min
83
+ fragment.diagnose(:warning, :overlap_mismatch) if payload.byteslice(from - start, to - from) != bytes.byteslice(from - pos, to - from)
84
+ replacement << [start, payload.byteslice(0, pos - start)] if start < pos
85
+ replacement << [old_end, payload.byteslice(old_end - start..)] if ending > old_end
86
+ else
87
+ replacement << [start, payload]
88
+ end
89
+ end
90
+ pieces = replacement
91
+ end
92
+ first_final = !more && !state[:end]
93
+ unless more
94
+ if state[:end] && state[:end] != offset + length
95
+ fragment.diagnose(:error, :malformed, 'inconsistent final IP fragment length')
96
+ return nil
97
+ end
98
+ state[:end] = offset + length
99
+ end
100
+ state[:frames] << packet.number if !pieces.empty? || first_final
101
+ state[:parts].concat(pieces).sort_by!(&:first)
102
+ if offset.zero? && !state[:header]
103
+ state[:header] = packet.data.byteslice(network.offset, fragment.payload_offset - network.offset - (key[0] == 6 ? 8 : 0))
104
+ if key[0] == 6
105
+ position = packet.layers.index(fragment) #: Integer
106
+ previous = packet.layers.take(position).last #: Layer
107
+ state[:next_offset] = previous == network ? 6 : previous.offset - network.offset
108
+ end
109
+ end
110
+ @entries[key] = state
111
+ @bytes += state_bytesize(state)
112
+ if complete?(state)
113
+ remove(key)
114
+ return virtual_packet(packet, state, key[0])
115
+ end
116
+ while bytesize > @max_bytes
117
+ removed = remove_oldest
118
+ break unless removed
119
+ @evicted += 1
120
+ fragment.diagnose(:warning, :reassembly_gap, 'IP reassembly memory limit exceeded')
121
+ end
122
+ nil
123
+ end
124
+ # @rbs (untyped state) -> bool
125
+ def complete?(state)
126
+ return false unless state[:header] && state[:end]
127
+ expected = 0
128
+ state[:parts].each do |offset, bytes|
129
+ return false unless offset == expected
130
+ expected += bytes.bytesize
131
+ end
132
+ expected == state[:end]
133
+ end
134
+ # @rbs (Packet packet, untyped state, Integer version) -> Packet?
135
+ def virtual_packet(packet, state, version)
136
+ payload = state[:parts].map(&:last).join.b
137
+ header = state[:header].dup
138
+ if header.bytesize + payload.bytesize > 65_535
139
+ packet.layers.last.diagnose(:error, :bad_length, 'reassembled IP datagram exceeds 65535 bytes')
140
+ return nil
141
+ end
142
+ if version == 4
143
+ header[2, 2] = [header.bytesize + payload.bytesize].pack('n')
144
+ header[6, 2] = [header.unpack1('n', offset: 6) & 0x4000].pack('n')
145
+ header[10, 2] = "\0\0"
146
+ header[10, 2] = [Protocols::Checksum.value(header)].pack('n')
147
+ else
148
+ header.setbyte(state[:next_offset], state[:next])
149
+ header[4, 2] = [header.bytesize - 40 + payload.bytesize].pack('n')
150
+ end
151
+ result = Packet.new(header + payload, linktype: 101, timestamp_ns: packet.timestamp_ns, number: packet.number,
152
+ interface: packet.interface, direction: packet.direction,
153
+ meta: packet.meta.merge(reassembled_from: state[:frames].uniq.sort))
154
+ result.engine = packet.engine
155
+ result
156
+ end
157
+ # @rbs (Integer now_ns) -> void
158
+ def advance(now_ns)
159
+ @clock = [@clock, now_ns].max
160
+ expire(@clock) if @clock >= @next_sweep
161
+ end
162
+ # @rbs (Integer now_ns) -> void
163
+ def expire(now_ns)
164
+ @next_sweep = now_ns + 1_000_000_000
165
+ @entries.delete_if do |_key, state|
166
+ stale = now_ns - state[:time] >= @timeout_ns
167
+ if stale
168
+ @expired += 1
169
+ @bytes -= state_bytesize(state)
170
+ end
171
+ stale
172
+ end
173
+ @discarded.delete_if do |_key, time|
174
+ stale = now_ns - time >= @timeout_ns
175
+ @bytes -= 128 if stale
176
+ stale
177
+ end
178
+ end
179
+ # @rbs () -> Integer
180
+ def bytesize = @bytes
181
+ # @rbs (untyped state) -> Integer
182
+ def state_bytesize(state)
183
+ 512 + (state[:header]&.bytesize || 0) * 2 + state[:frames].size * 32 + state[:parts].sum { |_pos, bytes| bytes.bytesize * 2 + 168 }
184
+ end
185
+ # @rbs (untyped key) -> untyped
186
+ def remove(key)
187
+ state = @entries.delete(key)
188
+ @bytes -= state_bytesize(state) if state
189
+ state
190
+ end
191
+ # @rbs () -> untyped
192
+ def remove_oldest
193
+ pair = @entries.shift
194
+ @bytes -= state_bytesize(pair[1]) if pair
195
+ pair
196
+ end
197
+ # @rbs () -> Integer
198
+ def finish
199
+ incomplete = @entries.size
200
+ @entries.clear
201
+ @discarded.clear
202
+ @bytes = 0
203
+ incomplete
204
+ end
205
+ # @rbs () -> Integer
206
+ def size = @entries.size
207
+ end
208
+ end
209
+ end