vanken 0.1.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +84 -0
- data/docs/filters.md +25 -0
- data/docs/performance.md +113 -0
- data/docs/releases.md +15 -0
- data/docs/upstream.md +32 -0
- data/exe/vanken +5 -0
- data/exe/vanken-capture +6 -0
- data/lib/vanken/app/capture_controller.rb +197 -0
- data/lib/vanken/app/document.rb +305 -0
- data/lib/vanken/app/document_jobs.rb +199 -0
- data/lib/vanken/capture/analyzer.rb +44 -0
- data/lib/vanken/capture/analyzer_process.rb +91 -0
- data/lib/vanken/capture/analyzer_wire.rb +42 -0
- data/lib/vanken/capture/analyzer_worker.rb +153 -0
- data/lib/vanken/capture/control_protocol.rb +32 -0
- data/lib/vanken/capture/filter_worker.rb +180 -0
- data/lib/vanken/capture/helper_main.rb +137 -0
- data/lib/vanken/capture/helper_options.rb +70 -0
- data/lib/vanken/capture/launcher.rb +181 -0
- data/lib/vanken/capture/privileges.rb +24 -0
- data/lib/vanken/capture/receiver.rb +39 -0
- data/lib/vanken/cli.rb +68 -0
- data/lib/vanken/config/log.rb +19 -0
- data/lib/vanken/config/preferences.rb +126 -0
- data/lib/vanken/core/display_filter/compiler.rb +108 -0
- data/lib/vanken/core/display_filter/errors.rb +19 -0
- data/lib/vanken/core/display_filter/field_resolver.rb +86 -0
- data/lib/vanken/core/display_filter/lexer.rb +124 -0
- data/lib/vanken/core/display_filter/parser.rb +157 -0
- data/lib/vanken/core/display_filter/types.rb +176 -0
- data/lib/vanken/core/frame.rb +33 -0
- data/lib/vanken/core/frame_store.rb +179 -0
- data/lib/vanken/core/frame_view.rb +68 -0
- data/lib/vanken/core/stores.rb +192 -0
- data/lib/vanken/errors.rb +9 -0
- data/lib/vanken/gateway/capture_filter.rb +30 -0
- data/lib/vanken/gateway/detail_builder.rb +63 -0
- data/lib/vanken/gateway/dissector.rb +176 -0
- data/lib/vanken/gateway/file_reader.rb +153 -0
- data/lib/vanken/gateway/file_writer.rb +64 -0
- data/lib/vanken/gateway/interfaces.rb +30 -0
- data/lib/vanken/gateway/live_capture.rb +79 -0
- data/lib/vanken/gateway/packet_snapshot.rb +89 -0
- data/lib/vanken/ui/actions.rb +50 -0
- data/lib/vanken/ui/application.rb +205 -0
- data/lib/vanken/ui/dialogs.rb +89 -0
- data/lib/vanken/ui/file_operations.rb +113 -0
- data/lib/vanken/ui/filter_operations.rb +120 -0
- data/lib/vanken/ui/main_view.rb +113 -0
- data/lib/vanken/ui/packet_source.rb +161 -0
- data/lib/vanken/ui/selection.rb +91 -0
- data/lib/vanken/version.rb +5 -0
- data/lib/vanken.rb +17 -0
- data/packaging/README.md +33 -0
- data/sig/generated/vanken/app/capture_controller.rbs +58 -0
- data/sig/generated/vanken/app/document.rbs +117 -0
- data/sig/generated/vanken/app/document_jobs.rbs +27 -0
- data/sig/generated/vanken/capture/analyzer.rbs +11 -0
- data/sig/generated/vanken/capture/analyzer_process.rbs +17 -0
- data/sig/generated/vanken/capture/analyzer_wire.rbs +19 -0
- data/sig/generated/vanken/capture/analyzer_worker.rbs +45 -0
- data/sig/generated/vanken/capture/control_protocol.rbs +17 -0
- data/sig/generated/vanken/capture/filter_worker.rbs +51 -0
- data/sig/generated/vanken/capture/helper_main.rbs +23 -0
- data/sig/generated/vanken/capture/helper_options.rbs +27 -0
- data/sig/generated/vanken/capture/launcher.rbs +60 -0
- data/sig/generated/vanken/capture/privileges.rbs +12 -0
- data/sig/generated/vanken/capture/receiver.rbs +15 -0
- data/sig/generated/vanken/cli.rbs +9 -0
- data/sig/generated/vanken/config/log.rbs +9 -0
- data/sig/generated/vanken/config/preferences.rbs +49 -0
- data/sig/generated/vanken/core/display_filter/compiler.rbs +45 -0
- data/sig/generated/vanken/core/display_filter/errors.rbs +16 -0
- data/sig/generated/vanken/core/display_filter/field_resolver.rbs +48 -0
- data/sig/generated/vanken/core/display_filter/lexer.rbs +49 -0
- data/sig/generated/vanken/core/display_filter/parser.rbs +95 -0
- data/sig/generated/vanken/core/display_filter/types.rbs +45 -0
- data/sig/generated/vanken/core/frame.rbs +63 -0
- data/sig/generated/vanken/core/frame_store.rbs +65 -0
- data/sig/generated/vanken/core/frame_view.rbs +36 -0
- data/sig/generated/vanken/core/stores.rbs +110 -0
- data/sig/generated/vanken/errors.rbs +18 -0
- data/sig/generated/vanken/gateway/capture_filter.rbs +17 -0
- data/sig/generated/vanken/gateway/detail_builder.rbs +21 -0
- data/sig/generated/vanken/gateway/dissector.rbs +113 -0
- data/sig/generated/vanken/gateway/file_reader.rbs +53 -0
- data/sig/generated/vanken/gateway/file_writer.rbs +27 -0
- data/sig/generated/vanken/gateway/interfaces.rbs +16 -0
- data/sig/generated/vanken/gateway/live_capture.rbs +48 -0
- data/sig/generated/vanken/gateway/packet_snapshot.rbs +48 -0
- data/sig/generated/vanken/ui/actions.rbs +9 -0
- data/sig/generated/vanken/ui/application.rbs +85 -0
- data/sig/generated/vanken/ui/dialogs.rbs +21 -0
- data/sig/generated/vanken/ui/file_operations.rbs +25 -0
- data/sig/generated/vanken/ui/filter_operations.rbs +33 -0
- data/sig/generated/vanken/ui/main_view.rbs +21 -0
- data/sig/generated/vanken/ui/packet_source.rbs +40 -0
- data/sig/generated/vanken/ui/selection.rbs +23 -0
- data/sig/generated/vanken/version.rbs +5 -0
- data/sig/vanken.rbs +25 -0
- metadata +192 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "tempfile"
|
|
5
|
+
require "json"
|
|
6
|
+
require_relative "../core/frame_view"
|
|
7
|
+
require_relative "../core/display_filter/compiler"
|
|
8
|
+
require_relative "../capture/receiver"
|
|
9
|
+
require_relative "../capture/analyzer"
|
|
10
|
+
require_relative "../capture/analyzer_process"
|
|
11
|
+
require_relative "document_jobs"
|
|
12
|
+
|
|
13
|
+
module Vanken
|
|
14
|
+
module App
|
|
15
|
+
class Document
|
|
16
|
+
include DocumentJobs
|
|
17
|
+
attr_reader :store, :columns, :annotations, :error, :path, :source, :progress, :filter,
|
|
18
|
+
:marked, :ignored, :time_references, :catalog, :analysis_options
|
|
19
|
+
attr_accessor :frame_latency, :scanner
|
|
20
|
+
|
|
21
|
+
def initialize(on_update: nil, preferences: nil, store: nil, scanner: nil, process_analysis: true)
|
|
22
|
+
@store = store || Core::FrameStore.new
|
|
23
|
+
@columns = Core::ColumnStore.new
|
|
24
|
+
@annotations = Core::AnnotationStore.new(@store.directory)
|
|
25
|
+
@cache = Core::RowCache.new(limit: preferences&.get("packet_list.row_cache_rows") || 20_000)
|
|
26
|
+
@verify_checksums = preferences&.get("analysis.verify_checksums") || false
|
|
27
|
+
@dissector = Gateway::Dissector.new(verify_checksums: @verify_checksums)
|
|
28
|
+
@process_analysis = process_analysis
|
|
29
|
+
@analysis_options = {max_state_bytes: (preferences&.get("analysis.max_state_mib") || 256) << 20,
|
|
30
|
+
max_flows: preferences&.get("analysis.max_flows") || 100_000}.freeze
|
|
31
|
+
@catalog = Gateway::FieldCatalog.new
|
|
32
|
+
@mutex, @condition = Mutex.new, ConditionVariable.new
|
|
33
|
+
@jobs, @count, @generation = [], 0, 0
|
|
34
|
+
@display = nil
|
|
35
|
+
@marked, @ignored, @time_references = Set.new, Set.new, Set.new
|
|
36
|
+
@received, @analyzed, @cancelled, @dirty = false, false, false, false
|
|
37
|
+
@on_update, @last_notify = on_update, 0.0
|
|
38
|
+
@scanner = scanner
|
|
39
|
+
@scan_concurrency = preferences&.get("analysis.workers") || 4
|
|
40
|
+
@filter_tasks = []
|
|
41
|
+
@details = File.open(File.join(@store.directory, "details.jsonl"), "w+b", 0o600)
|
|
42
|
+
@detail_reader = File.open(File.join(@store.directory, "details.jsonl"), "rb")
|
|
43
|
+
@detail_index = File.open(File.join(@store.directory, "reassembled.idx"), "w+b", 0o600)
|
|
44
|
+
@detail_offsets = {}
|
|
45
|
+
@summaries = File.open(File.join(@store.directory, "summaries.bin"), "w+b", 0o600)
|
|
46
|
+
@summary_index = File.open(File.join(@store.directory, "summaries.idx"), "w+b", 0o600)
|
|
47
|
+
@summary_reader = File.open(File.join(@store.directory, "summaries.bin"), "rb")
|
|
48
|
+
@summary_index_reader = File.open(File.join(@store.directory, "summaries.idx"), "rb")
|
|
49
|
+
@summary_count = 0
|
|
50
|
+
@frame_latency = 0.0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def open(path)
|
|
54
|
+
@path, @source = File.expand_path(path), :file
|
|
55
|
+
ingest(Gateway::FileReader.new(@path))
|
|
56
|
+
rescue Vanken::FileError => error
|
|
57
|
+
fail(error)
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def ingest(source, live: false)
|
|
62
|
+
@source ||= live ? :live : :file
|
|
63
|
+
@dirty = live
|
|
64
|
+
@reader = source
|
|
65
|
+
@analyzer = if @process_analysis
|
|
66
|
+
Capture::AnalyzerProcess.new(self, verify_checksums: @verify_checksums)
|
|
67
|
+
else
|
|
68
|
+
Capture::Analyzer.new(self, @dissector)
|
|
69
|
+
end
|
|
70
|
+
@jobs << thread { @analyzer.run }
|
|
71
|
+
@jobs << thread { Capture::Receiver.new(self, source).run }
|
|
72
|
+
self
|
|
73
|
+
end
|
|
74
|
+
def count = @mutex.synchronize { @count }
|
|
75
|
+
def displayed_count = @mutex.synchronize { @display ? @display.size : @count }
|
|
76
|
+
def number_at(index) = @mutex.synchronize { @display ? @display.fetch(index) : (index + 1).tap { |number| raise IndexError unless number.between?(1, @count) } }
|
|
77
|
+
def display_numbers = @mutex.synchronize { @display ? @display.dup : (1..@count).to_a }
|
|
78
|
+
def dirty? = @dirty
|
|
79
|
+
def loading? = !@analyzed && !@cancelled
|
|
80
|
+
def cancelled? = @cancelled
|
|
81
|
+
def received? = @received
|
|
82
|
+
def complete? = @analyzed
|
|
83
|
+
def closing? = !!@closing
|
|
84
|
+
def stream_frames(stream) = @mutex.synchronize { @annotations.streams[stream].dup }
|
|
85
|
+
def packet(number) = @dissector.dissect(@store.read(number))
|
|
86
|
+
# @rbs (Integer number) -> Gateway::PacketSnapshot?
|
|
87
|
+
def packet_snapshot(number)
|
|
88
|
+
saved = persisted_details(number)
|
|
89
|
+
saved && Gateway::PacketSnapshot.from_h(saved.fetch("packet"))
|
|
90
|
+
end
|
|
91
|
+
def view(number, packet: nil, snapshot: nil) = Core::FrameView.new(self, number, packet: packet, snapshot: snapshot)
|
|
92
|
+
def row(number)
|
|
93
|
+
base = @mutex.synchronize { @columns[number] }
|
|
94
|
+
frame = @store.metadata(number)
|
|
95
|
+
info = @cache.fetch(number) { persisted_summary(number) || persisted_details(number)&.fetch("info", nil) || packet(number).info }
|
|
96
|
+
base.merge(no: number, number: number, timestamp_ns: frame[:timestamp_ns], length: frame[:original_length], info: info)
|
|
97
|
+
end
|
|
98
|
+
def details(number)
|
|
99
|
+
saved = persisted_details(number)
|
|
100
|
+
return restore_nodes(saved.fetch("nodes")) if saved
|
|
101
|
+
|
|
102
|
+
Gateway::DetailBuilder.new.build(packet(number), annotations: @annotations[number])
|
|
103
|
+
end
|
|
104
|
+
def publish(number, packet)
|
|
105
|
+
committing = false
|
|
106
|
+
begin
|
|
107
|
+
columns, annotation = packet.columns, packet.annotations
|
|
108
|
+
@catalog.observe(packet)
|
|
109
|
+
saved = JSON.generate("nodes" => Gateway::DetailBuilder.new.build(packet).map(&:to_h), "info" => packet.info,
|
|
110
|
+
"packet" => Gateway::PacketSnapshot.from_packet(packet).to_h) + "\n" if packet.reassembled?
|
|
111
|
+
loop do
|
|
112
|
+
program, generation, snapshot = @mutex.synchronize { [@filter, @generation, @filter_context] }
|
|
113
|
+
matched = !program || program.match?(view(number, packet: packet, snapshot: snapshot))
|
|
114
|
+
committed = @mutex.synchronize do
|
|
115
|
+
next false if generation != @generation
|
|
116
|
+
committing = true
|
|
117
|
+
@columns.append(columns)
|
|
118
|
+
@annotations.append(number, annotation)
|
|
119
|
+
if saved
|
|
120
|
+
@detail_offsets[number] = @details.pos
|
|
121
|
+
@details.write(saved)
|
|
122
|
+
@details.flush
|
|
123
|
+
@detail_index.write([number, @detail_offsets[number]].pack("Q<Q<"))
|
|
124
|
+
@detail_index.flush
|
|
125
|
+
end
|
|
126
|
+
@count = number
|
|
127
|
+
@dirty = true if @source == :live
|
|
128
|
+
@display << number if @display && matched
|
|
129
|
+
true
|
|
130
|
+
end
|
|
131
|
+
break if committed
|
|
132
|
+
end
|
|
133
|
+
rescue StandardError => error
|
|
134
|
+
# Storage errors must abort, rather than appending another record.
|
|
135
|
+
raise if committing
|
|
136
|
+
return publish_failure(number, error)
|
|
137
|
+
end
|
|
138
|
+
notify
|
|
139
|
+
end
|
|
140
|
+
def publish_failure(number, error)
|
|
141
|
+
@mutex.synchronize do
|
|
142
|
+
@columns.append(source: "", destination: "", protocol: "Malformed", src_port: -1, dst_port: -1, ip_proto: -1, layers: [])
|
|
143
|
+
@annotations.append(number, tcp_stream: -1, seq_rel: -1, ack_rel: -1, analysis_flags: [], expert_max: 3,
|
|
144
|
+
expert_items: [{severity: :error, code: "vanken.internal_error", protocol: "vanken", message: error.message}], extra: {})
|
|
145
|
+
@count = number
|
|
146
|
+
@display << number if @display && !@filter
|
|
147
|
+
end
|
|
148
|
+
notify
|
|
149
|
+
end
|
|
150
|
+
# Only immutable context for this unpublished range crosses the worker pipe.
|
|
151
|
+
def analysis_configuration(first, last)
|
|
152
|
+
@mutex.synchronize do
|
|
153
|
+
historical = !!@filter_context
|
|
154
|
+
current = @filter_context || {marked: @marked, ignored: @ignored, references: @time_references,
|
|
155
|
+
predecessors: nil, limit: @count, last_displayed: @display ? @display.last : @count}
|
|
156
|
+
snapshot = current.merge(marked: current[:marked].select { |n| n.between?(first, last) }.to_set,
|
|
157
|
+
ignored: current[:ignored].select { |n| n.between?(first, last) }.to_set,
|
|
158
|
+
references: current[:references].dup, predecessors: nil,
|
|
159
|
+
unfiltered_predecessor: historical && current[:predecessors].nil?)
|
|
160
|
+
{expression: @filter&.expression || "", generation: @generation, snapshot: snapshot,
|
|
161
|
+
historical_context: historical, context_token: @filter_context&.object_id}
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
def publish_analysis_batch(batch, analyzer, generation, context_token: nil)
|
|
165
|
+
first, last = batch.values_at(:first, :last)
|
|
166
|
+
length = last - first + 1
|
|
167
|
+
raise Vanken::Error, "invalid analysis batch" unless length.between?(1, 256) &&
|
|
168
|
+
batch[:columns][:data].bytesize == length * Core::ColumnStore::SIZE &&
|
|
169
|
+
batch[:annotations][:data].bytesize == length * Core::AnnotationStore::SIZE && batch[:summary_index].bytesize == length * 8
|
|
170
|
+
loop do
|
|
171
|
+
committed = @mutex.synchronize do
|
|
172
|
+
raise Vanken::Error, "nonsequential analysis publication" unless first == @count + 1
|
|
173
|
+
next false if generation != @generation || context_token != @filter_context&.object_id
|
|
174
|
+
raise Vanken::Error, "invalid filter matches" unless batch[:matches].all? { |number| number.between?(first, last) } && batch[:matches] == batch[:matches].sort.uniq
|
|
175
|
+
@columns.import(batch[:columns])
|
|
176
|
+
@annotations.import(batch[:annotations])
|
|
177
|
+
batch[:details].each do |number, saved|
|
|
178
|
+
@detail_offsets[number] = @details.pos
|
|
179
|
+
@details.write(saved)
|
|
180
|
+
@detail_index.write([number, @detail_offsets[number]].pack("Q<Q<"))
|
|
181
|
+
end
|
|
182
|
+
base = @summaries.pos
|
|
183
|
+
offsets = batch[:summary_index].unpack("Q<*").map { |offset| base + offset }.pack("Q<*")
|
|
184
|
+
@summaries.write(batch[:summaries])
|
|
185
|
+
@summary_index.write(offsets)
|
|
186
|
+
[@annotations, @details, @detail_index, @summaries, @summary_index].each(&:flush)
|
|
187
|
+
@catalog.merge(batch[:fields])
|
|
188
|
+
@summary_count = @count = last
|
|
189
|
+
@dirty = true if @source == :live
|
|
190
|
+
@display.concat(batch[:matches]) if @display
|
|
191
|
+
true
|
|
192
|
+
end
|
|
193
|
+
break if committed
|
|
194
|
+
configuration = analysis_configuration(first, last)
|
|
195
|
+
batch[:matches] = analyzer.request(configuration.merge(command: :match)).fetch(:matches)
|
|
196
|
+
generation = configuration[:generation]
|
|
197
|
+
context_token = configuration[:context_token]
|
|
198
|
+
end
|
|
199
|
+
notify
|
|
200
|
+
end
|
|
201
|
+
def receiving_done = (@received = true; signal)
|
|
202
|
+
def analyzing_done = (@annotations.flush; @analyzed = true; notify(force: true))
|
|
203
|
+
def signal = @mutex.synchronize { @condition.broadcast }
|
|
204
|
+
def wait_for_frames = @mutex.synchronize { @condition.wait(@mutex, 0.05) }
|
|
205
|
+
def fail(error) = (@error = error; notify(force: true))
|
|
206
|
+
|
|
207
|
+
def time_value(number, format = :relative, references: nil)
|
|
208
|
+
current = @store.metadata(number)[:timestamp_ns]
|
|
209
|
+
base = case format.to_sym
|
|
210
|
+
when :epoch, :absolute then 0
|
|
211
|
+
when :delta then number == 1 ? current : @store.metadata(number - 1)[:timestamp_ns]
|
|
212
|
+
when :delta_displayed
|
|
213
|
+
displayed = display_numbers
|
|
214
|
+
index = displayed.index(number)
|
|
215
|
+
previous = index && index.positive? ? displayed[index - 1] : (number > count ? displayed.last : nil)
|
|
216
|
+
previous ? @store.metadata(previous)[:timestamp_ns] : current
|
|
217
|
+
else
|
|
218
|
+
reference = (references || @time_references).select { |value| value <= number }.max || 1
|
|
219
|
+
@store.metadata(reference)[:timestamp_ns]
|
|
220
|
+
end
|
|
221
|
+
(current - base) / 1e9
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def cancel
|
|
225
|
+
@cancelled = true
|
|
226
|
+
@reader.stop if @reader.respond_to?(:stop)
|
|
227
|
+
signal
|
|
228
|
+
self
|
|
229
|
+
end
|
|
230
|
+
def wait(timeout = nil)
|
|
231
|
+
deadline = timeout && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout)
|
|
232
|
+
@jobs.each do |job|
|
|
233
|
+
remaining = deadline && (deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC))
|
|
234
|
+
raise Vanken::Error, "background operation did not stop" if remaining && remaining <= 0 && job.alive?
|
|
235
|
+
raise Vanken::Error, "background operation did not stop" unless job.join(remaining && [remaining, 0].max)
|
|
236
|
+
end
|
|
237
|
+
self
|
|
238
|
+
end
|
|
239
|
+
def close
|
|
240
|
+
return self if @closed
|
|
241
|
+
@closing = true
|
|
242
|
+
cancel
|
|
243
|
+
cancel_scan
|
|
244
|
+
wait(3)
|
|
245
|
+
@annotations.close
|
|
246
|
+
@details.close
|
|
247
|
+
@detail_reader.close
|
|
248
|
+
@detail_index.close
|
|
249
|
+
@summaries.close
|
|
250
|
+
@summary_index.close
|
|
251
|
+
@summary_reader.close
|
|
252
|
+
@summary_index_reader.close
|
|
253
|
+
@store.close
|
|
254
|
+
@closed = true
|
|
255
|
+
self
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
private
|
|
259
|
+
def persisted_summary(number)
|
|
260
|
+
return nil if number > @summary_count
|
|
261
|
+
if @summary_index_reader.respond_to?(:pread)
|
|
262
|
+
offset = @summary_index_reader.pread(8, (number - 1) * 8).unpack1("Q<")
|
|
263
|
+
length = @summary_reader.pread(4, offset).unpack1("L<")
|
|
264
|
+
length.zero? ? "" : @summary_reader.pread(length, offset + 4)
|
|
265
|
+
else
|
|
266
|
+
@mutex.synchronize do
|
|
267
|
+
@summary_index_reader.seek((number - 1) * 8)
|
|
268
|
+
offset = @summary_index_reader.read(8).unpack1("Q<")
|
|
269
|
+
@summary_reader.seek(offset)
|
|
270
|
+
length = @summary_reader.read(4).unpack1("L<")
|
|
271
|
+
length.zero? ? "" : @summary_reader.read(length)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
def persisted_details(number)
|
|
276
|
+
@mutex.synchronize do
|
|
277
|
+
offset = @detail_offsets[number]
|
|
278
|
+
next nil unless offset
|
|
279
|
+
@detail_reader.seek(offset)
|
|
280
|
+
JSON.parse(@detail_reader.gets)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
def thread(&block)
|
|
284
|
+
Thread.new do
|
|
285
|
+
Thread.current.report_on_exception = false
|
|
286
|
+
block.call
|
|
287
|
+
rescue StandardError => error
|
|
288
|
+
fail(error)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
def notify(force: false)
|
|
292
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
293
|
+
return unless force || now - @last_notify >= 0.1
|
|
294
|
+
@last_notify = now
|
|
295
|
+
@on_update&.call(self)
|
|
296
|
+
end
|
|
297
|
+
def restore_nodes(nodes)
|
|
298
|
+
nodes.map do |value|
|
|
299
|
+
node = value.transform_keys(&:to_sym)
|
|
300
|
+
Core::DetailNode.new(**node.merge(source: node[:source].to_sym, severity: node[:severity]&.to_sym, children: restore_nodes(node[:children])))
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Vanken
|
|
5
|
+
module App
|
|
6
|
+
module DocumentJobs
|
|
7
|
+
def apply_filter(expression)
|
|
8
|
+
program = Core::DisplayFilter.compile(expression, catalog: @catalog)
|
|
9
|
+
cancel_scan
|
|
10
|
+
if expression.empty?
|
|
11
|
+
@mutex.synchronize do
|
|
12
|
+
@filter = @filter_context = @display = @progress = @filter_job = nil
|
|
13
|
+
end
|
|
14
|
+
notify(force: true)
|
|
15
|
+
return self
|
|
16
|
+
end
|
|
17
|
+
generation, limit, snapshot = @mutex.synchronize do
|
|
18
|
+
context = filter_snapshot(displayed_delta: program.fields.include?("frame.time_delta_displayed"))
|
|
19
|
+
@filter = program
|
|
20
|
+
@filter_context = context
|
|
21
|
+
# Reserve the history's capacity before publishing matches without placeholder rows.
|
|
22
|
+
@display = Array.new(@count)
|
|
23
|
+
@display[0, @count] = []
|
|
24
|
+
@progress = 0.0
|
|
25
|
+
[@generation, @count, context]
|
|
26
|
+
end
|
|
27
|
+
@filter_job = thread do
|
|
28
|
+
if @scanner && !program.fast?
|
|
29
|
+
path = @mutex.synchronize { @annotations.snapshot(File.join(@store.directory, "annotations-#{generation}.json")) }
|
|
30
|
+
snapshot = snapshot.merge(annotations_json: path).freeze
|
|
31
|
+
end
|
|
32
|
+
historical_cursor = 0
|
|
33
|
+
ranges = (1..limit).step(10_000).map { |first| [first, [first + 10_000, limit + 1].min] }
|
|
34
|
+
ranges.each_slice(@scanner && !program.fast? ? @scan_concurrency : 1) do |window|
|
|
35
|
+
break if stale_job?(generation)
|
|
36
|
+
pending = window.map do |first, last|
|
|
37
|
+
if @scanner && !program.fast?
|
|
38
|
+
task = @scanner.call(filter_payload(first, last, expression: expression, snapshot: snapshot))
|
|
39
|
+
stale = @mutex.synchronize do
|
|
40
|
+
@filter_tasks << task if generation == @generation && task.respond_to?(:cancel)
|
|
41
|
+
generation != @generation
|
|
42
|
+
end
|
|
43
|
+
task.cancel if stale && task.respond_to?(:cancel)
|
|
44
|
+
[last, task]
|
|
45
|
+
else
|
|
46
|
+
[last, {"matches" => (first...last).select { |number| !stale_job?(generation) && program.match?(view(number, snapshot: snapshot)) }}]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
pending.each do |last, task|
|
|
50
|
+
break if stale_job?(generation)
|
|
51
|
+
value = task.respond_to?(:await) ? task.await : task
|
|
52
|
+
break if stale_job?(generation)
|
|
53
|
+
matches = value.fetch("matches")
|
|
54
|
+
@mutex.synchronize do
|
|
55
|
+
next if generation != @generation
|
|
56
|
+
@display[historical_cursor, 0] = matches
|
|
57
|
+
historical_cursor += matches.length
|
|
58
|
+
@progress = (last - 1).fdiv([limit, 1].max)
|
|
59
|
+
end
|
|
60
|
+
notify
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
@mutex.synchronize do
|
|
64
|
+
next if generation != @generation
|
|
65
|
+
@filter_context = nil
|
|
66
|
+
@progress = nil
|
|
67
|
+
@filter_tasks.clear
|
|
68
|
+
end
|
|
69
|
+
notify(force: true) unless stale_job?(generation)
|
|
70
|
+
rescue StandardError => error
|
|
71
|
+
fail(error) unless stale_job?(generation)
|
|
72
|
+
ensure
|
|
73
|
+
tasks = @mutex.synchronize do
|
|
74
|
+
if generation == @generation
|
|
75
|
+
@progress = nil
|
|
76
|
+
@filter_context = nil
|
|
77
|
+
remaining, @filter_tasks = @filter_tasks, []
|
|
78
|
+
remaining
|
|
79
|
+
else
|
|
80
|
+
[]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
tasks.each(&:cancel)
|
|
84
|
+
File.unlink(path) if path && File.exist?(path)
|
|
85
|
+
end
|
|
86
|
+
@jobs << @filter_job
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def cancel_scan
|
|
91
|
+
tasks = @mutex.synchronize do
|
|
92
|
+
@generation += 1
|
|
93
|
+
@progress = nil
|
|
94
|
+
@filter_context = nil
|
|
95
|
+
saved, @filter_tasks = @filter_tasks, []
|
|
96
|
+
saved
|
|
97
|
+
end
|
|
98
|
+
tasks.each(&:cancel)
|
|
99
|
+
self
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def filter_payload(first, last, expression: @filter&.expression || "", snapshot: nil)
|
|
103
|
+
unless snapshot
|
|
104
|
+
displayed_delta = Core::DisplayFilter.compile(expression, catalog: @catalog).fields.include?("frame.time_delta_displayed")
|
|
105
|
+
snapshot = @mutex.synchronize do
|
|
106
|
+
@snapshot_sequence = (@snapshot_sequence || 0) + 1
|
|
107
|
+
filter_snapshot(displayed_delta: displayed_delta).merge(annotations_json: @annotations.snapshot(File.join(@store.directory, "annotations-#{@generation}-#{@snapshot_sequence}.json")))
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
predecessors = (first...last).to_h { |number| [number.to_s, displayed_predecessor(number, snapshot)] }
|
|
111
|
+
{"spool" => @store.directory, "expr" => expression, "from" => first, "to" => last,
|
|
112
|
+
"decode_as" => [], "plugins" => [], "annotations_json" => snapshot[:annotations_json],
|
|
113
|
+
"marked" => snapshot[:marked].select { |number| number >= first && number < last },
|
|
114
|
+
"ignored" => snapshot[:ignored].select { |number| number >= first && number < last },
|
|
115
|
+
"time_references_ns" => snapshot[:references].to_h { |number| [number.to_s, @store.metadata(number)[:timestamp_ns]] },
|
|
116
|
+
"time_reference_ns" => snapshot[:limit].zero? ? 0 : @store.metadata(1)[:timestamp_ns],
|
|
117
|
+
"displayed_predecessors" => predecessors}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def displayed_predecessor(number, snapshot)
|
|
121
|
+
return number - 1 unless snapshot[:predecessors]
|
|
122
|
+
return snapshot[:last_displayed] || 0 if number > snapshot[:limit]
|
|
123
|
+
snapshot[:predecessors].fetch(number, 0)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def sort(key, direction = :asc)
|
|
127
|
+
raise ArgumentError, "invalid sort direction" unless %i[asc desc].include?(direction)
|
|
128
|
+
generation, sort_generation, filter_job = @mutex.synchronize do
|
|
129
|
+
@sort_generation = (@sort_generation || 0) + 1
|
|
130
|
+
[@generation, @sort_generation, @filter_job]
|
|
131
|
+
end
|
|
132
|
+
@jobs << thread do
|
|
133
|
+
filter_job&.join
|
|
134
|
+
next if generation != @generation || sort_generation != @sort_generation || @closing
|
|
135
|
+
numbers = display_numbers
|
|
136
|
+
values = numbers.to_h do |number|
|
|
137
|
+
value = case key.to_sym
|
|
138
|
+
when :no, :number then number
|
|
139
|
+
when :time then @store.metadata(number)[:timestamp_ns]
|
|
140
|
+
when :length then @store.metadata(number)[:original_length]
|
|
141
|
+
else row(number).fetch(key.to_sym)
|
|
142
|
+
end
|
|
143
|
+
[number, value]
|
|
144
|
+
end
|
|
145
|
+
numbers.sort! { |a, b| comparison = values[a] <=> values[b]; comparison = -comparison if direction == :desc; comparison.zero? ? a <=> b : comparison }
|
|
146
|
+
@mutex.synchronize do
|
|
147
|
+
next if generation != @generation || sort_generation != @sort_generation || @closing
|
|
148
|
+
@display = numbers + (@display ? @display - numbers : ((1..@count).to_a - numbers))
|
|
149
|
+
end
|
|
150
|
+
notify(force: true)
|
|
151
|
+
end
|
|
152
|
+
self
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def save(path, format: nil, numbers: nil)
|
|
156
|
+
@error = nil
|
|
157
|
+
format ||= File.extname(path) == ".pcap" ? :pcap : :pcapng
|
|
158
|
+
limit = count
|
|
159
|
+
@save_job = thread do
|
|
160
|
+
temporary = Tempfile.create([".vanken-", ".tmp"], File.dirname(File.expand_path(path)))
|
|
161
|
+
begin
|
|
162
|
+
linktype = limit.zero? ? 1 : @store.metadata(1)[:linktype]
|
|
163
|
+
Gateway::FileWriter.open(temporary, format: format, linktype: linktype) do |writer|
|
|
164
|
+
(numbers || (1..limit)).each { |number| writer << @store.read(number) }
|
|
165
|
+
end
|
|
166
|
+
temporary.flush
|
|
167
|
+
temporary.fsync
|
|
168
|
+
temporary.close
|
|
169
|
+
File.rename(temporary.path, File.expand_path(path))
|
|
170
|
+
@mutex.synchronize do
|
|
171
|
+
@dirty = false if !numbers && @count == limit && @store.count == limit
|
|
172
|
+
@path = File.expand_path(path) unless numbers
|
|
173
|
+
end
|
|
174
|
+
notify(force: true)
|
|
175
|
+
ensure
|
|
176
|
+
temporary.close unless temporary.closed?
|
|
177
|
+
FileUtils.rm_f(temporary.path)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
@jobs << @save_job
|
|
181
|
+
self
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def wait_for_save(timeout = nil)
|
|
185
|
+
raise Vanken::Error, "save operation did not stop" if @save_job && !@save_job.join(timeout)
|
|
186
|
+
self
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
def stale_job?(generation) = @closing || generation != @generation
|
|
191
|
+
def filter_snapshot(displayed_delta: true)
|
|
192
|
+
predecessors = @display.each_with_index.to_h { |number, index| [number, index.zero? ? 0 : @display[index - 1]] }.freeze if displayed_delta && @display
|
|
193
|
+
{marked: @marked.dup.freeze, ignored: @ignored.dup.freeze, references: @time_references.dup.freeze,
|
|
194
|
+
predecessors: predecessors,
|
|
195
|
+
limit: @count, last_displayed: @display ? @display.last : @count}.freeze
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Vanken
|
|
5
|
+
module Capture
|
|
6
|
+
class Analyzer
|
|
7
|
+
def initialize(document, dissector)
|
|
8
|
+
@document, @dissector = document, dissector
|
|
9
|
+
@analysis = Gateway::Analysis.new(registry: dissector.registry, **document.analysis_options)
|
|
10
|
+
end
|
|
11
|
+
def run
|
|
12
|
+
number = 1
|
|
13
|
+
loop do
|
|
14
|
+
break if @document.respond_to?(:closing?) && @document.closing?
|
|
15
|
+
if number > @document.store.durable_count
|
|
16
|
+
break if @document.received? && number > @document.store.durable_count
|
|
17
|
+
@document.wait_for_frames
|
|
18
|
+
next
|
|
19
|
+
end
|
|
20
|
+
frame = @document.store.read(number)
|
|
21
|
+
begin
|
|
22
|
+
packet = @dissector.dissect(frame)
|
|
23
|
+
@analysis.update(packet)
|
|
24
|
+
rescue StandardError => error
|
|
25
|
+
@document.publish_failure(number, error)
|
|
26
|
+
packet = nil
|
|
27
|
+
end
|
|
28
|
+
@document.publish(number, packet) if packet
|
|
29
|
+
number += 1
|
|
30
|
+
if number % 256 == 0
|
|
31
|
+
Thread.pass
|
|
32
|
+
sleep(0.002) if @document.frame_latency > 0.033
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
ensure
|
|
36
|
+
begin
|
|
37
|
+
@analysis.close
|
|
38
|
+
ensure
|
|
39
|
+
@document.analyzing_done
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
require_relative "analyzer_wire"
|
|
5
|
+
|
|
6
|
+
module Vanken
|
|
7
|
+
module Capture
|
|
8
|
+
class AnalyzerProcess
|
|
9
|
+
attr_reader :pid
|
|
10
|
+
def initialize(document, verify_checksums: false)
|
|
11
|
+
@document, @verify_checksums = document, verify_checksums
|
|
12
|
+
end
|
|
13
|
+
def run
|
|
14
|
+
unless Gem.win_platform?
|
|
15
|
+
raise Vanken::Error, "packet analysis must run without root privileges" if Process.uid.zero? || Process.euid.zero?
|
|
16
|
+
end
|
|
17
|
+
child_in, @input = IO.pipe
|
|
18
|
+
@output, child_out = IO.pipe
|
|
19
|
+
[child_in, @input, @output, child_out].each(&:binmode)
|
|
20
|
+
arguments = [RbConfig.ruby]
|
|
21
|
+
arguments << "--yjit" if defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?
|
|
22
|
+
arguments << File.expand_path("analyzer_worker.rb", __dir__)
|
|
23
|
+
@pid = Process.spawn(*arguments, in: child_in, out: child_out, close_others: true)
|
|
24
|
+
child_in.close
|
|
25
|
+
child_out.close
|
|
26
|
+
request(command: :start, spool: @document.store.directory, verify_checksums: @verify_checksums, analysis_options: @document.analysis_options)
|
|
27
|
+
number = 1
|
|
28
|
+
loop do
|
|
29
|
+
break if @document.closing?
|
|
30
|
+
limit = @document.store.durable_count
|
|
31
|
+
if number > limit
|
|
32
|
+
break if @document.received? && number > @document.store.durable_count
|
|
33
|
+
@document.wait_for_frames
|
|
34
|
+
next
|
|
35
|
+
end
|
|
36
|
+
last = [number + 255, limit].min
|
|
37
|
+
configuration = @document.analysis_configuration(number, last)
|
|
38
|
+
batch = request(configuration.merge(command: :analyze, first: number, last: last, interfaces: @document.store.interfaces))
|
|
39
|
+
raise Vanken::Error, "analysis response range mismatch" unless batch[:first] == number && batch[:last] == last
|
|
40
|
+
@document.publish_analysis_batch(batch, self, configuration[:generation], context_token: configuration[:context_token])
|
|
41
|
+
number = last + 1
|
|
42
|
+
end
|
|
43
|
+
rescue StandardError
|
|
44
|
+
@document.cancel
|
|
45
|
+
raise unless @document.closing?
|
|
46
|
+
ensure
|
|
47
|
+
child_in&.close unless child_in&.closed?
|
|
48
|
+
child_out&.close unless child_out&.closed?
|
|
49
|
+
begin
|
|
50
|
+
shutdown
|
|
51
|
+
ensure
|
|
52
|
+
@document.analyzing_done
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
def request(value)
|
|
56
|
+
AnalyzerWire.write(@input, value)
|
|
57
|
+
response = AnalyzerWire.read(@output, cancelled: -> { @document.closing? })
|
|
58
|
+
raise Vanken::Error, response[:error] if response[:error]
|
|
59
|
+
response
|
|
60
|
+
end
|
|
61
|
+
def shutdown
|
|
62
|
+
begin
|
|
63
|
+
AnalyzerWire.write(@input, {command: :stop}) if @pid && @input && !@input.closed?
|
|
64
|
+
rescue IOError, SystemCallError
|
|
65
|
+
nil
|
|
66
|
+
ensure
|
|
67
|
+
@input&.close unless @input&.closed?
|
|
68
|
+
@output&.close unless @output&.closed?
|
|
69
|
+
end
|
|
70
|
+
return unless @pid
|
|
71
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 1
|
|
72
|
+
status = nil
|
|
73
|
+
loop do
|
|
74
|
+
if (result = Process.waitpid2(@pid, Process::WNOHANG))
|
|
75
|
+
status = result.last
|
|
76
|
+
break
|
|
77
|
+
end
|
|
78
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
79
|
+
Process.kill("KILL", @pid)
|
|
80
|
+
status = Process.waitpid2(@pid).last
|
|
81
|
+
break
|
|
82
|
+
end
|
|
83
|
+
sleep(0.005)
|
|
84
|
+
end
|
|
85
|
+
raise Vanken::Error, "analysis worker failed while stopping" unless status.success? || @document.closing? || $!
|
|
86
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/wait"
|
|
4
|
+
|
|
5
|
+
module Vanken
|
|
6
|
+
module Capture
|
|
7
|
+
# These pipes are private to the same-user worker. Marshal.load only receives
|
|
8
|
+
# protocol messages serialized by our child, never an external serialized object.
|
|
9
|
+
module AnalyzerWire
|
|
10
|
+
VERSION = 1
|
|
11
|
+
MAX_BYTES = 64 << 20
|
|
12
|
+
def self.write(io, value)
|
|
13
|
+
bytes = Marshal.dump(value.merge(version: VERSION))
|
|
14
|
+
raise Vanken::Error, "analysis message exceeds limit" if bytes.bytesize > MAX_BYTES
|
|
15
|
+
io.write([bytes.bytesize].pack("L>"))
|
|
16
|
+
io.write(bytes)
|
|
17
|
+
io.flush
|
|
18
|
+
end
|
|
19
|
+
def self.read(io, cancelled: nil)
|
|
20
|
+
header = exact(io, 4, cancelled)
|
|
21
|
+
length = header.unpack1("L>")
|
|
22
|
+
raise Vanken::Error, "invalid analysis message size" unless length.between?(1, MAX_BYTES)
|
|
23
|
+
value = Marshal.load(exact(io, length, cancelled))
|
|
24
|
+
raise Vanken::Error, "invalid analysis protocol" unless value.is_a?(Hash) && value[:version] == VERSION
|
|
25
|
+
value
|
|
26
|
+
end
|
|
27
|
+
def self.exact(io, length, cancelled)
|
|
28
|
+
bytes = String.new(capacity: length, encoding: Encoding::BINARY)
|
|
29
|
+
fragment = String.new(capacity: [length, 64 << 10].min, encoding: Encoding::BINARY)
|
|
30
|
+
until bytes.bytesize == length
|
|
31
|
+
raise Vanken::Error, "analysis stopped" if cancelled&.call
|
|
32
|
+
next unless io.wait_readable(0.05)
|
|
33
|
+
part = io.read_nonblock([length - bytes.bytesize, 64 << 10].min, fragment, exception: false)
|
|
34
|
+
next if part == :wait_readable
|
|
35
|
+
raise Vanken::Error, "analysis worker closed its pipe" unless part
|
|
36
|
+
bytes << part
|
|
37
|
+
end
|
|
38
|
+
bytes
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|