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,150 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Analysis
7
+ # @api private
8
+ module Stats
9
+ # @api private
10
+ class Table
11
+ attr_reader :evicted
12
+ # @rbs (?max_rows: Integer) -> void
13
+ def initialize(max_rows: 100_000)
14
+ @max_rows, @evicted = max_rows, 0
15
+ @rows = {} #: Hash[untyped, untyped]
16
+ end
17
+ # @rbs (untyped key) { () -> untyped } -> untyped
18
+ def row(key)
19
+ item = @rows.delete(key)
20
+ unless item
21
+ if @rows.size >= @max_rows
22
+ @rows.shift
23
+ @evicted += 1
24
+ end
25
+ item = yield
26
+ end
27
+ @rows[key] = item
28
+ item
29
+ end
30
+ # @rbs () -> Integer
31
+ def bytesize = @rows.size * 1024
32
+ # @rbs () -> void
33
+ def evict
34
+ @evicted += 1 if @rows.shift
35
+ end
36
+ # @rbs (untyped io) -> void
37
+ def format(io)
38
+ result = to_h
39
+ io.puts("#{result[:kind]}#{result[:type] ? ",#{result[:type]}" : ''}")
40
+ result[:rows].each { |item| io.puts(item.map { |name, value| "#{name}=#{value.is_a?(Array) ? value.join(':') : value}" }.join(' ')) }
41
+ io.puts("#{result[:omitted_intervals]} earlier IO intervals omitted") if result[:omitted_intervals] && result[:omitted_intervals].positive?
42
+ io.puts("#{@evicted} statistics rows evicted") if @evicted.positive?
43
+ end
44
+ # @rbs () -> Hash[Symbol, untyped]
45
+ def to_h = { kind: :stats, rows: @rows.values }
46
+ end
47
+
48
+ # @api private
49
+ class Io < Table
50
+ # @rbs ((Integer | Float) seconds, ?max_rows: Integer) -> void
51
+ def initialize(seconds, max_rows: 100_000)
52
+ super(max_rows: max_rows)
53
+ @interval_ns = (seconds * 1_000_000_000).to_i
54
+ raise ArgumentError, 'statistics interval must be positive' unless @interval_ns.positive?
55
+ @start_ns = nil # @rbs Integer?
56
+ end
57
+ # @rbs (Packet packet) -> void
58
+ def update(packet)
59
+ @start_ns ||= packet.timestamp_ns
60
+ start = @start_ns #: Integer
61
+ interval = [(packet.timestamp_ns - start) / @interval_ns, 0].max
62
+ item = row(interval) { interval_row(interval) }
63
+ item[:packets] += 1
64
+ item[:bytes] += packet.original_length
65
+ end
66
+ # @rbs () -> Hash[Symbol, untyped]
67
+ def to_h
68
+ return { kind: :io, interval_ns: @interval_ns, rows: [] } if @rows.empty?
69
+ last = @rows.keys.max #: Integer
70
+ first = [@rows.keys.min, last - @max_rows + 1].max #: Integer
71
+ rows = (first..last).map { |interval| @rows[interval] || interval_row(interval) }
72
+ { kind: :io, interval_ns: @interval_ns, omitted_intervals: first, rows: rows }
73
+ end
74
+ # @rbs (Integer interval) -> Hash[Symbol, Integer]
75
+ def interval_row(interval)
76
+ start = @start_ns || 0
77
+ { interval: interval, start_ns: start + interval * @interval_ns, end_ns: start + (interval + 1) * @interval_ns, packets: 0, bytes: 0 }
78
+ end
79
+ end
80
+
81
+ # @api private
82
+ class Conversations < Table
83
+ # @rbs (Symbol type, ?max_rows: Integer) -> void
84
+ def initialize(type, max_rows: 100_000)
85
+ super(max_rows: max_rows)
86
+ @type = type
87
+ end
88
+ # @rbs (Packet packet) -> void
89
+ def update(packet)
90
+ result = FlowKey.from(packet, @type)
91
+ return unless result
92
+ key, direction = result
93
+ item = row(key) do
94
+ { addr_a: key[1], port_a: key[2], addr_b: key[3], port_b: key[4],
95
+ packets_ab: 0, packets_ba: 0, bytes_ab: 0, bytes_ba: 0,
96
+ start_ns: packet.timestamp_ns, end_ns: packet.timestamp_ns, duration_ns: 0 }
97
+ end
98
+ suffix = direction.zero? ? 'ab' : 'ba'
99
+ item["packets_#{suffix}".to_sym] += 1
100
+ item["bytes_#{suffix}".to_sym] += packet.original_length
101
+ item[:start_ns] = [item[:start_ns], packet.timestamp_ns].min
102
+ item[:end_ns] = [item[:end_ns], packet.timestamp_ns].max
103
+ item[:duration_ns] = item[:end_ns] - item[:start_ns]
104
+ end
105
+ # @rbs () -> Hash[Symbol, untyped]
106
+ def to_h = { kind: :conv, type: @type, rows: @rows.values }
107
+ end
108
+
109
+ # @api private
110
+ class Endpoints < Table
111
+ # @rbs (Symbol type, ?max_rows: Integer) -> void
112
+ def initialize(type, max_rows: 100_000)
113
+ super(max_rows: max_rows)
114
+ @type = type
115
+ end
116
+ # @rbs (Packet packet) -> void
117
+ def update(packet)
118
+ result = FlowKey.from(packet, @type)
119
+ return unless result
120
+ key, direction = result
121
+ endpoints = [[key[1], key[2]], [key[3], key[4]]]
122
+ endpoints.each_with_index do |endpoint, index|
123
+ item = row(endpoint) { { address: endpoint[0], port: endpoint[1], packets_tx: 0, packets_rx: 0, bytes_tx: 0, bytes_rx: 0 } }
124
+ suffix = index == direction ? 'tx' : 'rx'
125
+ item["packets_#{suffix}".to_sym] += 1
126
+ item["bytes_#{suffix}".to_sym] += packet.original_length
127
+ end
128
+ end
129
+ # @rbs () -> Hash[Symbol, untyped]
130
+ def to_h = { kind: :endpoints, type: @type, rows: @rows.values }
131
+ end
132
+
133
+ # @api private
134
+ class Hierarchy < Table
135
+ # @rbs (Packet packet) -> void
136
+ def update(packet)
137
+ path = [] #: Array[Symbol]
138
+ packet.layers.reject(&:embedded).each do |layer|
139
+ path << layer.protocol
140
+ item = row(path.dup) { { path: path.dup, packets: 0, bytes: 0 } }
141
+ item[:packets] += 1
142
+ item[:bytes] += packet.original_length
143
+ end
144
+ end
145
+ # @rbs () -> Hash[Symbol, untyped]
146
+ def to_h = { kind: :phs, rows: @rows.values }
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,43 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Analysis
7
+ # @api private
8
+ module TcpAnalysis
9
+ # @rbs (Flow flow, Integer direction, TcpStream stream, Layer layer, Integer timestamp_ns) -> void
10
+ def self.update(flow, direction, stream, layer, timestamp_ns)
11
+ flags, seq, ack, length = layer[:flags], layer[:seq], layer[:ack], layer[:length]
12
+ layer.add(:stream, 'tcp.stream', flow.id)
13
+ layer.add(:seq_relative, 'tcp.seq_relative', Util::Seq.diff(seq, stream.base_seq || seq))
14
+ peer = flow.streams[1 - direction]
15
+ layer.add(:ack_relative, 'tcp.ack_relative', Util::Seq.diff(ack, peer.base_seq)) if peer && peer.base_seq && flags & 0x10 != 0
16
+ keep_alive = stream.base_seq && flags & 7 == 0 && length <= 1 && seq == (stream.next_sequence - 1) % Util::Seq::MOD
17
+ keep_alive_ack = flow.keep_alives[1 - direction] && peer && flags & 0x17 == 0x10 && length.zero? && ack == peer.next_sequence
18
+ if flags & 0x10 != 0
19
+ flag(layer, :duplicate_ack) if flags & 7 == 0 && length.zero? && layer[:window].positive? && flow.acks[direction] == [ack, layer[:window], 0] && !keep_alive_ack
20
+ flow.acks[direction] = [ack, layer[:window], length]
21
+ flow.keep_alives[1 - direction] = false
22
+ end
23
+ flag(layer, :zero_window) if layer[:window].zero? && flags & 6 == 0
24
+ if keep_alive
25
+ flag(layer, :keep_alive)
26
+ flow.keep_alives[direction] = true
27
+ end
28
+ if flags & 2 != 0 && flags & 0x10 == 0
29
+ flow.syn_ns ||= timestamp_ns
30
+ flow.syn_direction ||= direction
31
+ elsif flags & 0x12 == 0x12 && flow.syn_ns && flow.syn_direction != direction
32
+ flow.synack_ns ||= timestamp_ns
33
+ flow.synack_seq ||= seq
34
+ elsif flags & 0x10 != 0 && flow.synack_ns && flow.syn_direction == direction && !flow.initial_rtt && ack == (flow.synack_seq + 1) % Util::Seq::MOD
35
+ flow.initial_rtt = [timestamp_ns - flow.syn_ns, 0].max.fdiv(1_000_000_000)
36
+ end
37
+ layer.add(:initial_rtt, 'tcp.analysis.initial_rtt', flow.initial_rtt) if flow.initial_rtt && flags & 0x17 == 0x10
38
+ end
39
+ # @rbs (Layer layer, Symbol name) -> void
40
+ def self.flag(layer, name) = layer.add("analysis_#{name}".to_sym, "tcp.analysis.#{name}", true, type: :boolean)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,176 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Analysis
7
+ # @api private
8
+ class TcpReassembler
9
+ # @rbs (?registry: Registry, ?max_bytes: Integer, ?stream_bytes: Integer, ?protocol_streams: bool, ?follow: untyped) -> void
10
+ def initialize(registry: Registry.default, max_bytes: 64 << 20, stream_bytes: 1 << 20, protocol_streams: true, follow: nil)
11
+ @registry, @max_bytes, @stream_bytes, @protocol_streams, @follow = registry, max_bytes, stream_bytes, protocol_streams, follow
12
+ end
13
+ # @rbs (Packet packet, Flow flow, Integer direction) -> void
14
+ def update(packet, flow, direction)
15
+ layer = packet.layers.reverse.find { |item| item.protocol == :tcp && !item.embedded }
16
+ return unless layer && !layer.error? && layer[:seq] && layer[:flags]
17
+ stream = flow.streams[direction] ||= TcpStream.new(max_bytes: @stream_bytes)
18
+ application = flow.applications[direction]
19
+ if !application && @protocol_streams
20
+ klass = @registry.by_port('tcp.port', layer[:srcport], layer[:dstport])
21
+ unless klass
22
+ recognized = packet.layers.find { |item| (candidate = @registry.protocols[item.protocol]) && candidate <= StreamDissector }
23
+ klass = recognized ? @registry.protocols[recognized.protocol] : flow.applications[1 - direction]&.class
24
+ end
25
+ flow.applications[direction] = create_application(klass, flow, direction, layer) if klass && klass <= StreamDissector
26
+ application = flow.applications[direction]
27
+ end
28
+ TcpAnalysis.update(flow, direction, stream, layer, packet.timestamp_ns)
29
+ flags = layer[:flags]
30
+ data = packet.data.byteslice(layer.payload_offset, layer.payload_end - layer.payload_offset) #: String
31
+ stream.push(layer[:seq], data, syn: flags & 2 != 0, fin: flags & 1 != 0, frame: packet.number)
32
+ # Packet-local application parsing cannot frame a multi-segment PDU.
33
+ if application
34
+ index = packet.layers.index(layer) #: Integer
35
+ packet.layers.slice!(index + 1, packet.layers.length)
36
+ end
37
+ stream.diagnostics.uniq.each do |code|
38
+ if %i[retransmission out_of_order lost_segment].include?(code)
39
+ TcpAnalysis.flag(layer, code) unless layer.field_value('tcp.analysis.keep_alive')
40
+ else
41
+ layer.diagnose(:warning, code)
42
+ end
43
+ end
44
+ deliver(flow, direction, stream, application, packet)
45
+ if flags & 4 != 0
46
+ finish_flow(flow, packet)
47
+ elsif flow.streams.compact.size == 2 && flow.streams.compact.all?(&:fin_seen?)
48
+ finish_flow(flow, packet)
49
+ elsif stream.fin
50
+ attach(packet, invoke(flow, direction, application, :on_close)) if application
51
+ flow.closed_ns ||= packet.timestamp_ns if flow.streams.compact.size == 2 && flow.streams.compact.all?(&:fin)
52
+ end
53
+ end
54
+ # @rbs (Flow flow, Integer direction, TcpStream stream, untyped application, Packet? packet) -> Array[Layer]
55
+ def deliver(flow, direction, stream, application, packet)
56
+ completed = [] #: Array[Layer]
57
+ stream.deliveries.each do |data, origin|
58
+ unless data
59
+ gap = Layer.new(:tcp, 0, 0, 0).diagnose(:warning, :reassembly_gap, "#{origin} TCP bytes missing")
60
+ packet&.[](:tcp)&.diagnose(:warning, :reassembly_gap, gap.diagnostics.first.message)
61
+ completed << gap unless packet
62
+ flow.probes[direction].clear
63
+ flow.probe_disabled[direction] = false
64
+ application = flow.applications[direction]
65
+ layers = application ? invoke(flow, direction, application, :on_gap, origin) : Array.new #: Array[Layer]
66
+ completed.concat(layers)
67
+ attach(packet, layers) if packet
68
+ next
69
+ end
70
+ @follow&.write(flow, direction, data)
71
+ layers = application_data(flow, direction, data, origin, packet)
72
+ attach(packet, layers) if packet
73
+ completed.concat(layers)
74
+ end
75
+ completed
76
+ ensure
77
+ stream.clear_deliveries
78
+ end
79
+ # @rbs (Flow flow, Integer direction, String data, Integer origin, Packet? packet) -> Array[Layer]
80
+ def application_data(flow, direction, data, origin, packet)
81
+ application = flow.applications[direction]
82
+ probe = flow.probes[direction]
83
+ unless application
84
+ return [] unless @protocol_streams && !flow.probe_disabled[direction]
85
+ sample = (probe.map(&:first).join + data.byteslice(0, 128)).byteslice(0, 128) #: String
86
+ ctx = Context.new(Packet.new(sample), registry: @registry)
87
+ ctx.layers << Layer.new(:tcp, 0, 0, sample.bytesize)
88
+ klass = @registry.heuristic(:tcp, ctx, ctx.cursor)
89
+ if klass && klass <= StreamDissector
90
+ diagnostic_layer = packet&.[](:tcp) || Layer.new(:tcp, 0, 0, 0)
91
+ application = flow.applications[direction] = create_application(klass, flow, direction, diagnostic_layer)
92
+ return packet ? [] : [diagnostic_layer] unless application
93
+ if packet
94
+ position = packet.layers.index { |layer| layer.protocol == :tcp && !layer.embedded }
95
+ packet.layers.slice!(position + 1, packet.layers.size) if position
96
+ end
97
+ else
98
+ if sample.bytesize < 128 && probe.sum { |bytes, _frame| bytes.bytesize * 2 + 128 } + data.bytesize * 2 + 128 <= @stream_bytes
99
+ probe << [data.dup, origin]
100
+ else
101
+ probe.clear
102
+ flow.probe_disabled[direction] = true
103
+ end
104
+ return []
105
+ end
106
+ end
107
+ chunks = probe + [[data, origin]]
108
+ probe.clear
109
+ chunks.flat_map { |bytes, frame| invoke(flow, direction, application, :on_data, bytes, frame) }
110
+ rescue StandardError => error
111
+ [application_error(flow, direction, :tcp, error)]
112
+ end
113
+ # @rbs (untyped klass, Flow flow, Integer direction, Layer layer) -> untyped
114
+ def create_application(klass, flow, direction, layer)
115
+ klass.new(registry: @registry, max_bytes: @stream_bytes)
116
+ rescue StandardError => error
117
+ layer.diagnostics.concat(application_error(flow, direction, klass.protocol_id, error).diagnostics)
118
+ nil
119
+ end
120
+ # @rbs (Flow flow, Integer direction, untyped application, Symbol method, *untyped arguments) -> Array[Layer]
121
+ def invoke(flow, direction, application, method, *arguments)
122
+ layers = application.public_send(method, flow, direction, *arguments)
123
+ raise TypeError, 'stream callback must return an array of Layers' unless layers.is_a?(Array) && layers.all? { |layer| layer.is_a?(Layer) }
124
+ layers
125
+ rescue StandardError => error
126
+ [application_error(flow, direction, application.class.protocol_id, error)]
127
+ end
128
+ # @rbs (Flow flow, Integer direction, Symbol protocol, StandardError error) -> Layer
129
+ def application_error(flow, direction, protocol, error)
130
+ raise error if ENV['REDHOUND_STRICT'] == '1'
131
+ flow.applications[direction] = nil
132
+ flow.probes[direction].clear
133
+ flow.probe_disabled[direction] = true
134
+ Layer.new(protocol, 0, 0, 0).diagnose(:error, :dissector_bug, "#{error.class}: #{error.message}")
135
+ end
136
+ # @rbs (Packet packet, Array[Layer] layers) -> void
137
+ def attach(packet, layers)
138
+ layers.each do |layer|
139
+ layer.add(:reassembled_in, 'tcp.reassembled_in', packet.number)
140
+ packet.layers << layer
141
+ end
142
+ end
143
+ # @rbs (Flow flow, ?Packet? packet) -> Array[Layer]
144
+ def finish_flow(flow, packet = nil)
145
+ completed = [] #: Array[Layer]
146
+ flow.streams.each_with_index do |stream, direction|
147
+ next unless stream
148
+ application = flow.applications[direction]
149
+ while stream.pending?
150
+ stream.flush_gap
151
+ completed.concat(deliver(flow, direction, stream, application, packet))
152
+ end
153
+ application = flow.applications[direction]
154
+ if application
155
+ layers = invoke(flow, direction, application, :on_close)
156
+ attach(packet, layers) if packet
157
+ completed.concat(layers)
158
+ end
159
+ stream.release
160
+ end
161
+ flow.http_methods.each(&:clear)
162
+ flow.probes.each(&:clear)
163
+ flow.closed_ns ||= packet ? packet.timestamp_ns : flow.last_ns
164
+ completed
165
+ end
166
+ # @rbs (FlowTable flows, Packet packet) -> void
167
+ def enforce_limit(flows, packet)
168
+ while flows.tcp_bytesize > @max_bytes
169
+ flow = flows.evict(:tcp)
170
+ break unless flow
171
+ packet.layers.last.diagnose(:warning, :reassembly_gap, 'TCP reassembly memory limit exceeded')
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,156 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Analysis
7
+ # @api private
8
+ class TcpStream
9
+ attr_reader :base_seq, :expected, :diagnostics, :deliveries, :gap_lengths, :fin
10
+ # @rbs (?max_bytes: Integer) -> void
11
+ def initialize(max_bytes: 1 << 20)
12
+ raise ArgumentError, 'max_bytes must be positive' unless max_bytes.positive?
13
+ @max_bytes, @expected, @history_start = max_bytes, 0, 0
14
+ @highest = 0
15
+ @base_seq = nil # @rbs Integer?
16
+ @history = ''.b
17
+ @queue = [] #: Array[untyped]
18
+ @diagnostics, @deliveries, @gap_lengths = [], [], [] # @rbs untyped
19
+ @fin, @fin_offset = false, nil # @rbs untyped
20
+ end
21
+ # @rbs (Integer sequence, String data, ?syn: bool, ?fin: bool, ?frame: Integer) -> Array[String]
22
+ def push(sequence, data, syn: false, fin: false, frame: 0)
23
+ @diagnostics, @deliveries, @gap_lengths = [], [], []
24
+ if @base_seq && ((syn && sequence == @base_seq) || (fin && Util::Seq.lt((sequence + data.bytesize) % Util::Seq::MOD, next_sequence)))
25
+ @diagnostics << :retransmission
26
+ end
27
+ unless @base_seq
28
+ @base_seq = sequence
29
+ @expected = syn ? 1 : 0
30
+ @history_start = @expected
31
+ @highest = @expected
32
+ end
33
+ base = @base_seq #: Integer
34
+ start = @expected + Util::Seq.signed_diff(sequence, (base + @expected) % Util::Seq::MOD) + (syn ? 1 : 0)
35
+ @fin_offset = start + data.bytesize if fin
36
+ @diagnostics << :out_of_order if !data.empty? && start >= @expected && start < @highest
37
+ @diagnostics << :lost_segment if (!data.empty? || fin) && start > @highest
38
+ @diagnostics << :retransmission if !data.empty? && start < @expected
39
+ @highest = [@highest, start + data.bytesize + (fin ? 1 : 0)].max
40
+ pieces = [[start, data, frame]]
41
+ if start < @expected
42
+ length = [data.bytesize, @expected - start].min
43
+ compare_overlap(start, data, @history_start, @history)
44
+ pieces = length >= data.bytesize ? Array.new : [[start + length, data.byteslice(length..), frame]] #: Array[untyped]
45
+ end
46
+ @queue.each do |old_start, old_data, _old_frame|
47
+ replacement = [] #: Array[untyped]
48
+ pieces.each do |pos, bytes, origin|
49
+ compare_overlap(pos, bytes, old_start, old_data)
50
+ ending, old_end = pos + bytes.bytesize, old_start + old_data.bytesize
51
+ if pos < old_end && ending > old_start
52
+ replacement << [pos, bytes.byteslice(0, old_start - pos), origin] if pos < old_start
53
+ replacement << [old_end, bytes.byteslice(old_end - pos..), origin] if ending > old_end
54
+ else
55
+ replacement << [pos, bytes, origin]
56
+ end
57
+ end
58
+ pieces = replacement
59
+ end
60
+ if !data.empty? && pieces.empty?
61
+ @diagnostics.delete(:out_of_order)
62
+ @diagnostics.delete(:lost_segment)
63
+ @diagnostics << :retransmission unless @diagnostics.include?(:retransmission)
64
+ end
65
+ pieces.each do |piece|
66
+ next if piece[1].empty?
67
+ index = @queue.bsearch_index { |entry| entry[0] >= piece[0] } || @queue.length
68
+ @queue.insert(index, piece)
69
+ end
70
+ drain
71
+ while bytesize > @max_bytes || (@queue.first && @queue.first[0] - @expected > @max_bytes)
72
+ break if @queue.empty?
73
+ flush_gap(false)
74
+ end
75
+ trim_history
76
+ consume_fin
77
+ @deliveries.filter_map(&:first)
78
+ end
79
+
80
+ # @rbs (Integer start, String data, Integer old_start, String old_data) -> void
81
+ def compare_overlap(start, data, old_start, old_data)
82
+ from = [start, old_start].max
83
+ to = [start + data.bytesize, old_start + old_data.bytesize].min
84
+ return unless from < to
85
+ if data.byteslice(from - start, to - from) != old_data.byteslice(from - old_start, to - from) && !@diagnostics.include?(:overlap_mismatch)
86
+ @diagnostics << :overlap_mismatch
87
+ end
88
+ end
89
+ # @rbs () -> void
90
+ def drain
91
+ while @queue.first && @queue.first[0] == @expected
92
+ pos, data, frame = @queue.shift
93
+ @deliveries << [data, frame]
94
+ @history_start = pos if @history.empty?
95
+ @history << data
96
+ @expected += data.bytesize
97
+ end
98
+ end
99
+ # @rbs (?bool clear) -> Array[String]
100
+ def flush_gap(clear = true)
101
+ @deliveries, @gap_lengths = [], [] if clear
102
+ target = @queue.empty? ? @fin_offset : @queue.first[0]
103
+ return [] unless target
104
+ gap = target - @expected
105
+ if gap.positive?
106
+ @diagnostics << :reassembly_gap
107
+ @gap_lengths << gap
108
+ @deliveries << [nil, gap]
109
+ @expected += gap
110
+ @history.clear
111
+ @history_start = @expected
112
+ end
113
+ drain
114
+ consume_fin
115
+ trim_history
116
+ @deliveries.filter_map(&:first)
117
+ end
118
+ # @rbs () -> void
119
+ def trim_history
120
+ # Previously delivered bytes share the stream's bounded budget with holes.
121
+ # Ruby grows mutable String buffers geometrically. Reserve twice their
122
+ # length plus object storage so capacity also stays inside the budget.
123
+ limit = [(@max_bytes - @queue.sum { |item| item[1].bytesize * 2 + 168 } - 40) / 2, 0].max
124
+ excess = @history.bytesize - limit
125
+ if excess.positive?
126
+ @history = @history.byteslice(excess..) #: String
127
+ @history_start += excess
128
+ end
129
+ end
130
+ # @rbs () -> Integer
131
+ def bytesize = (@history.empty? ? 0 : @history.bytesize * 2 + 40) + @queue.sum { |item| item[1].bytesize * 2 + 168 }
132
+ # @rbs () -> bool
133
+ def pending? = !@queue.empty? || (!@fin && !@fin_offset.nil?)
134
+ # @rbs () -> bool
135
+ def fin_seen? = !@fin_offset.nil?
136
+ # @rbs () -> void
137
+ def consume_fin
138
+ if !@fin && @fin_offset && @expected >= @fin_offset
139
+ @fin = true
140
+ @expected += 1
141
+ end
142
+ end
143
+ # @rbs () -> Integer
144
+ def next_sequence = ((@base_seq || 0) + @expected) % Util::Seq::MOD
145
+ # @rbs () -> void
146
+ def clear_deliveries = @deliveries.clear
147
+ # @rbs () -> void
148
+ def release
149
+ @history.clear
150
+ @queue.clear
151
+ @deliveries.clear
152
+ @gap_lengths.clear
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,157 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'util/seq'
5
+ require_relative 'stream_dissector'
6
+ require_relative 'analysis/flow_key'
7
+ require_relative 'analysis/tcp_stream'
8
+ require_relative 'analysis/flow_table'
9
+ require_relative 'analysis/ip_reassembler'
10
+ require_relative 'analysis/tcp_analysis'
11
+ require_relative 'analysis/tcp_reassembler'
12
+ require_relative 'analysis/stats'
13
+ require_relative 'output/follow'
14
+
15
+ module Redhound
16
+ # @api private
17
+ module Analysis
18
+ # @api private
19
+ class Session
20
+ attr_reader :flows, :ip_reassembler, :tcp_reassembler, :statistics
21
+ # @rbs (?stats: Array[String], ?follow: String?, ?registry: Registry, ?max_state_bytes: Integer, ?max_flows: Integer, ?ip_bytes: Integer, ?tcp_bytes: Integer, ?stream_bytes: Integer, ?protocol_streams: bool) -> void
22
+ def initialize(stats: [], follow: nil, registry: Registry.default, max_state_bytes: 256 << 20, max_flows: 100_000,
23
+ ip_bytes: 16 << 20, tcp_bytes: 64 << 20, stream_bytes: 1 << 20, protocol_streams: true)
24
+ raise ArgumentError, 'state limit must be positive' unless max_state_bytes.positive?
25
+ @max_state_bytes, @finished = max_state_bytes, false
26
+ @flows = FlowTable.new(max_flows: max_flows)
27
+ @ip_reassembler = IpReassembler.new(max_bytes: [ip_bytes, max_state_bytes].min)
28
+ @follow = follow ? Output::Follow.new(follow) : nil
29
+ @tcp_reassembler = TcpReassembler.new(registry: registry, max_bytes: [tcp_bytes, max_state_bytes].min,
30
+ stream_bytes: stream_bytes, protocol_streams: protocol_streams, follow: @follow)
31
+ @removed_gaps, @removed_incomplete, @removed_bugs = 0, 0, 0
32
+ @flows.on_remove = lambda do |flow|
33
+ @tcp_reassembler.finish_flow(flow).each do |layer|
34
+ layer.diagnostics.each do |diagnostic|
35
+ @removed_gaps += 1 if diagnostic.code == :reassembly_gap
36
+ @removed_incomplete += 1 if diagnostic.code == :truncated
37
+ @removed_bugs += 1 if diagnostic.code == :dissector_bug
38
+ end
39
+ end
40
+ end
41
+ specs = stats.uniq
42
+ raise ConfigurationError, 'too many statistics specifications (maximum 64)' if specs.size > 64
43
+ row_limit = [100_000, [((128 << 20) / [specs.size, 1].max) / 1024, 1].max].min
44
+ @statistics = specs.map { |spec| statistic(spec, row_limit) }
45
+ end
46
+ # @rbs (String specification, Integer max_rows) -> untyped
47
+ def statistic(specification, max_rows)
48
+ kind, argument = specification.split(',', 2)
49
+ case kind
50
+ when 'io'
51
+ raise ConfigurationError, 'io statistics requires an interval of at least one nanosecond' unless argument && argument.match?(/\A(?:\d+(?:\.\d*)?|\.\d+)\z/) && argument.to_f.finite? && argument.to_f >= 0.000_000_001
52
+ Stats::Io.new(argument.to_f, max_rows: max_rows)
53
+ when 'conv', 'endpoints'
54
+ raise ConfigurationError, 'statistics type must be eth, ip, ipv6, tcp or udp' unless argument && %w[eth ip ipv6 tcp udp].include?(argument)
55
+ klass = kind == 'conv' ? Stats::Conversations : Stats::Endpoints
56
+ klass.new(argument.to_sym, max_rows: max_rows)
57
+ when 'phs'
58
+ raise ConfigurationError, 'phs does not accept an argument' if argument
59
+ Stats::Hierarchy.new(max_rows: max_rows)
60
+ else raise ConfigurationError, "unknown statistics: #{specification}"
61
+ end
62
+ end
63
+ # @rbs (Packet packet) -> void
64
+ def update(packet)
65
+ return if @finished
66
+ @flows.advance(packet.timestamp_ns)
67
+ @ip_reassembler.advance(packet.timestamp_ns)
68
+ fragment = @ip_reassembler.descriptor(packet)
69
+ analyzed = packet
70
+ if fragment
71
+ boundary = packet.layers.index(fragment[1]) #: Integer
72
+ packet.layers.slice!(boundary + 1, packet.layers.length)
73
+ virtual = @ip_reassembler.update(packet)
74
+ if virtual
75
+ packet.meta[:reassembled_from] = virtual.meta[:reassembled_from]
76
+ packet.meta[:reassembled_packet] = virtual
77
+ analyzed = virtual
78
+ else
79
+ @statistics.each { |stat| stat.update(packet) }
80
+ enforce_limit(packet)
81
+ return
82
+ end
83
+ end
84
+ result = @flows.update(analyzed)
85
+ if result
86
+ flow, direction = result
87
+ @tcp_reassembler.update(analyzed, flow, direction) if flow.key[0] == :tcp
88
+ @flows.account(flow)
89
+ end
90
+ if analyzed != packet
91
+ packet.layers.concat(analyzed.layers.reject { |layer| %i[ipv4 ipv6 ipv6_ext].include?(layer.protocol) })
92
+ packet.layers.last.add(:ip_reassembled_from, 'ip.reassembled_from', packet.meta[:reassembled_from])
93
+ end
94
+ @statistics.each { |stat| stat.update(packet) }
95
+ @tcp_reassembler.enforce_limit(@flows, packet)
96
+ enforce_limit(packet)
97
+ end
98
+ # @rbs (Packet packet) -> void
99
+ def enforce_limit(packet)
100
+ while bytesize > @max_state_bytes
101
+ flow = @flows.evict
102
+ unless flow
103
+ statistic = @statistics.max_by(&:bytesize)
104
+ break unless statistic && statistic.bytesize.positive?
105
+ statistic.evict
106
+ end
107
+ packet.layers.last.diagnose(:warning, :state_evicted, 'analysis state memory limit exceeded')
108
+ end
109
+ end
110
+ # @rbs () -> Integer
111
+ def bytesize = @flows.bytesize + @ip_reassembler.bytesize + @statistics.sum(&:bytesize)
112
+ # @rbs (untyped err) -> void
113
+ def snapshot(err)
114
+ @statistics.each { |stat| stat.format(err) }
115
+ err.puts("#{@removed_gaps} TCP reassembly_gap events during flow removal") if @removed_gaps.positive?
116
+ err.puts("#{@removed_incomplete} truncated application PDUs during flow removal") if @removed_incomplete.positive?
117
+ err.puts("#{@removed_bugs} dissector_bug events during flow removal") if @removed_bugs.positive?
118
+ err.puts("#{@flows.evicted} flows evicted; #{@flows.expired} flows expired") if @flows.evicted.positive? || @flows.expired.positive?
119
+ err.puts("#{@ip_reassembler.evicted} fragmented datagrams evicted; #{@ip_reassembler.expired} expired") if @ip_reassembler.evicted.positive? || @ip_reassembler.expired.positive?
120
+ end
121
+ # @rbs (untyped out, untyped err) -> void
122
+ def finish(out, err)
123
+ return if @finished
124
+ @flows.values.each do |flow|
125
+ @tcp_reassembler.finish_flow(flow).each do |layer|
126
+ layer.diagnostics.each do |diagnostic|
127
+ message = diagnostic.message.b.gsub(/[^\x20-\x7e]/n) { |byte| format('\\x%02x', byte.getbyte(0)) }
128
+ err.puts("stream #{flow.id} #{layer.protocol}: #{diagnostic.code}: #{message}")
129
+ end
130
+ end
131
+ @flows.account(flow)
132
+ end
133
+ incomplete = @ip_reassembler.finish
134
+ err.puts("#{incomplete} incomplete IP datagrams: reassembly_gap at EOF") if incomplete.positive?
135
+ @follow&.format(out)
136
+ snapshot(err)
137
+ ensure
138
+ original_error = $!
139
+ @finished = true
140
+ @flows.values.each do |flow|
141
+ flow.streams.compact.each(&:release)
142
+ flow.applications.fill(nil)
143
+ flow.http_methods.each(&:clear)
144
+ flow.probes.each(&:clear)
145
+ flow.closed_ns ||= flow.last_ns
146
+ @flows.account(flow)
147
+ end
148
+ @ip_reassembler.finish
149
+ begin
150
+ @follow&.close
151
+ rescue StandardError
152
+ raise unless original_error
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end