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,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Vanken
|
|
5
|
+
module Core
|
|
6
|
+
class FrameView
|
|
7
|
+
# @rbs (App::Document document, Integer number, ?packet: Gateway::PacketView?, ?snapshot: Hash[Symbol, untyped]?) -> void
|
|
8
|
+
def initialize(document, number, packet: nil, snapshot: nil)
|
|
9
|
+
@document, @number, @packet = document, number, packet
|
|
10
|
+
@snapshot = snapshot
|
|
11
|
+
@metadata = nil #: frame_metadata?
|
|
12
|
+
end
|
|
13
|
+
# @rbs (String name) -> bool
|
|
14
|
+
def layer?(name) = @packet ? @packet.layer?(name) : !!@document.columns.layer?(@number, name)
|
|
15
|
+
# @rbs (String name) -> Symbol?
|
|
16
|
+
def field_type(name) = packet.field_type(name)
|
|
17
|
+
# @rbs (String name) -> Array[untyped]
|
|
18
|
+
def values(name)
|
|
19
|
+
case name
|
|
20
|
+
when "frame.number" then [@number]
|
|
21
|
+
when "frame.len" then [metadata[:original_length]]
|
|
22
|
+
when "frame.cap_len" then [metadata[:caplen]]
|
|
23
|
+
when "frame.time_epoch" then [metadata[:timestamp_ns] / 1e9]
|
|
24
|
+
when "frame.time_relative" then [@document.time_value(@number, :relative, references: @snapshot&.fetch(:references))]
|
|
25
|
+
when "frame.time_delta" then [@number == 1 ? 0.0 : (metadata[:timestamp_ns] - @document.store.metadata(@number - 1)[:timestamp_ns]) / 1e9]
|
|
26
|
+
when "frame.time_delta_displayed" then [displayed_delta]
|
|
27
|
+
when "frame.interface_name"
|
|
28
|
+
interface = metadata[:interface]
|
|
29
|
+
interface ? [interface["name"]] : []
|
|
30
|
+
when "frame.direction" then metadata[:direction] ? [metadata[:direction].to_s] : []
|
|
31
|
+
when "frame.marked" then [(@snapshot ? @snapshot[:marked] : @document.marked).include?(@number)]
|
|
32
|
+
when "frame.ignored" then [(@snapshot ? @snapshot[:ignored] : @document.ignored).include?(@number)]
|
|
33
|
+
when "frame.protocols" then [column[:layers].join(":")]
|
|
34
|
+
when "tcp.port", "udp.port", "tcp.srcport", "tcp.dstport", "udp.srcport", "udp.dstport"
|
|
35
|
+
@packet ? @packet.values(name) : @document.columns.port_values(@number, name)
|
|
36
|
+
when "tcp.stream" then annotation[:tcp_stream] >= 0 ? [annotation[:tcp_stream]] : []
|
|
37
|
+
when "tcp.seq_relative" then annotation[:seq_rel] >= 0 ? [annotation[:seq_rel]] : []
|
|
38
|
+
when "tcp.ack_relative" then annotation[:ack_rel] >= 0 ? [annotation[:ack_rel]] : []
|
|
39
|
+
when "tcp.analysis.flags" then annotation[:analysis_flags].empty? ? [] : [true]
|
|
40
|
+
when "expert.severity" then annotation[:expert_items].map { |item| item[:severity].to_s }
|
|
41
|
+
when "expert.code" then annotation[:expert_items].map { |item| item[:code] }
|
|
42
|
+
else
|
|
43
|
+
if name.start_with?("tcp.analysis.")
|
|
44
|
+
return [true] if annotation[:analysis_flags].include?(name.delete_prefix("tcp.analysis."))
|
|
45
|
+
return Array(annotation[:extra][name]).compact
|
|
46
|
+
end
|
|
47
|
+
packet.values(name)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
private
|
|
51
|
+
# @rbs () -> frame_metadata
|
|
52
|
+
def metadata = @metadata ||= @document.store.metadata(@number)
|
|
53
|
+
# @rbs () -> column_values
|
|
54
|
+
def column = @column ||= @packet ? @packet.columns : @document.columns[@number]
|
|
55
|
+
# @rbs () -> annotation_values
|
|
56
|
+
def annotation = @annotation ||= @packet ? @packet.annotations : @document.annotations[@number]
|
|
57
|
+
# @rbs () -> Gateway::PacketView | Gateway::PacketSnapshot
|
|
58
|
+
def packet = @packet ||= @document.packet_snapshot(@number) || @document.packet(@number)
|
|
59
|
+
# @rbs () -> Float
|
|
60
|
+
def displayed_delta
|
|
61
|
+
return @document.time_value(@number, :delta_displayed) unless @snapshot
|
|
62
|
+
|
|
63
|
+
previous = @document.displayed_predecessor(@number, @snapshot)
|
|
64
|
+
previous > 0 ? (metadata[:timestamp_ns] - @document.store.metadata(previous)[:timestamp_ns]) / 1e9 : 0.0
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "set"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Vanken
|
|
8
|
+
module Core
|
|
9
|
+
class InternTable
|
|
10
|
+
# @rbs! @values: Array[String]
|
|
11
|
+
# @rbs! @ids: Hash[String, Integer]
|
|
12
|
+
# @rbs () -> void
|
|
13
|
+
def initialize
|
|
14
|
+
@values = [] #: Array[String]
|
|
15
|
+
@ids = {} #: Hash[String, Integer]
|
|
16
|
+
end
|
|
17
|
+
# @rbs (String value) -> Integer
|
|
18
|
+
def intern(value) = @ids.fetch(value) { @ids[value] = @values.length; @values << value.freeze; @values.length - 1 }
|
|
19
|
+
# @rbs (Integer id) -> String
|
|
20
|
+
def [](id) = @values.fetch(id)
|
|
21
|
+
# @rbs () -> Integer
|
|
22
|
+
def size = @values.size
|
|
23
|
+
# @rbs (Integer index) -> Array[String]
|
|
24
|
+
def since(index) = @values.drop(index)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class ColumnStore
|
|
28
|
+
FORMAT = "L<L<L<l<l<l<Q<"
|
|
29
|
+
SIZE = 32
|
|
30
|
+
attr_reader :strings #: InternTable
|
|
31
|
+
# @rbs! @protocol_ids: Hash[String, Integer]
|
|
32
|
+
# @rbs! @protocol_names: Array[String]
|
|
33
|
+
# @rbs! @transport_values: Hash[Integer, Hash[String, Array[Integer]]]
|
|
34
|
+
# @rbs! @base: Integer
|
|
35
|
+
# @rbs! @string_cursor: Integer
|
|
36
|
+
# @rbs! @protocol_cursor: Integer
|
|
37
|
+
# @rbs () -> void
|
|
38
|
+
def initialize
|
|
39
|
+
@strings = InternTable.new
|
|
40
|
+
@data = +"".b
|
|
41
|
+
@protocol_ids = {} #: Hash[String, Integer]
|
|
42
|
+
@protocol_names = [] #: Array[String]
|
|
43
|
+
@transport_values = {} #: Hash[Integer, Hash[String, Array[Integer]]]
|
|
44
|
+
@base = @string_cursor = @protocol_cursor = 0
|
|
45
|
+
end
|
|
46
|
+
# @rbs (column_values values) -> void
|
|
47
|
+
def append(values)
|
|
48
|
+
mask = values[:layers].reduce(0) do |bits, name|
|
|
49
|
+
id = @protocol_ids.fetch(name) { @protocol_ids[name] = @protocol_names.length; @protocol_names << name; @protocol_names.length - 1 }
|
|
50
|
+
raise Vanken::Error, "too many protocols in compact index" if id >= 64
|
|
51
|
+
bits | (1 << id)
|
|
52
|
+
end
|
|
53
|
+
@data << [@strings.intern(values[:source]), @strings.intern(values[:destination]), @strings.intern(values[:protocol]),
|
|
54
|
+
values[:src_port], values[:dst_port], values[:ip_proto], mask].pack(FORMAT)
|
|
55
|
+
@transport_values[@base + (@data.bytesize / SIZE)] = values[:transport_values] if values[:transport_values]
|
|
56
|
+
end
|
|
57
|
+
# Transfer a batch without retaining a second copy of every frame in the worker.
|
|
58
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
59
|
+
def drain
|
|
60
|
+
batch = {data: @data, strings: @strings.since(@string_cursor), protocols: @protocol_names.drop(@protocol_cursor), transport: @transport_values}
|
|
61
|
+
@base += @data.bytesize / SIZE
|
|
62
|
+
@string_cursor, @protocol_cursor = @strings.size, @protocol_names.size
|
|
63
|
+
@data, @transport_values = +"".b, {}
|
|
64
|
+
batch
|
|
65
|
+
end
|
|
66
|
+
# @rbs (Hash[Symbol, untyped] batch) -> void
|
|
67
|
+
def import(batch)
|
|
68
|
+
raise Vanken::Error, "invalid column batch" unless batch[:data].bytesize % SIZE == 0
|
|
69
|
+
batch[:strings].each { |value| @strings.intern(value) }
|
|
70
|
+
batch[:protocols].each do |name|
|
|
71
|
+
raise Vanken::Error, "invalid protocol dictionary" if @protocol_ids.key?(name) || @protocol_names.size >= 64
|
|
72
|
+
@protocol_ids[name] = @protocol_names.length
|
|
73
|
+
@protocol_names << name
|
|
74
|
+
end
|
|
75
|
+
@data << batch[:data]
|
|
76
|
+
@transport_values.merge!(batch[:transport])
|
|
77
|
+
end
|
|
78
|
+
# @rbs (Integer number) -> column_values
|
|
79
|
+
def [](number)
|
|
80
|
+
src, dst, proto, sport, dport, ipproto, mask = @data.byteslice((number - 1) * SIZE, SIZE).unpack(FORMAT)
|
|
81
|
+
{source: @strings[src], destination: @strings[dst], protocol: @strings[proto], src_port: sport, dst_port: dport, ip_proto: ipproto, layers: @protocol_names.select { |name| mask & (1 << @protocol_ids[name]) != 0 }}
|
|
82
|
+
end
|
|
83
|
+
# @rbs (Integer number, String name) -> bool?
|
|
84
|
+
def layer?(number, name)
|
|
85
|
+
id = @protocol_ids[name]
|
|
86
|
+
id && (@data.unpack1("Q<", offset: ((number - 1) * SIZE) + 24) & (1 << id)) != 0
|
|
87
|
+
end
|
|
88
|
+
# @rbs (Integer number, String name) -> Array[Integer]
|
|
89
|
+
def port_values(number, name)
|
|
90
|
+
if (all = @transport_values[number])
|
|
91
|
+
names = name.end_with?(".port") ? [name.sub(".port", ".srcport"), name.sub(".port", ".dstport")] : [name]
|
|
92
|
+
return names.flat_map { |field| Array(all[field]) }
|
|
93
|
+
end
|
|
94
|
+
protocol = if name.start_with?("tcp.")
|
|
95
|
+
"tcp"
|
|
96
|
+
elsif name.start_with?("udp.")
|
|
97
|
+
"udp"
|
|
98
|
+
else
|
|
99
|
+
name.split(".").first
|
|
100
|
+
end
|
|
101
|
+
return [] unless layer?(number, protocol)
|
|
102
|
+
|
|
103
|
+
offset = ((number - 1) * SIZE) + 12
|
|
104
|
+
if name.end_with?(".port")
|
|
105
|
+
[@data.unpack1("l<", offset: offset), @data.unpack1("l<", offset: offset + 4)].reject(&:negative?)
|
|
106
|
+
else
|
|
107
|
+
offset += 4 unless name.end_with?("srcport")
|
|
108
|
+
[@data.unpack1("l<", offset: offset)].reject(&:negative?)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class AnnotationStore
|
|
114
|
+
FLAGS = %w[retransmission out_of_order lost_segment duplicate_ack zero_window keep_alive].freeze
|
|
115
|
+
FORMAT = "q<q<q<L<Cx3"
|
|
116
|
+
SIZE = 32
|
|
117
|
+
attr_reader :streams #: Hash[Integer, Array[Integer]]
|
|
118
|
+
attr_reader :experts #: Hash[Integer, Array[Hash[Symbol, untyped]]]
|
|
119
|
+
# @rbs! @extra: Hash[Integer, Hash[String, untyped]]
|
|
120
|
+
# @rbs! @io: File?
|
|
121
|
+
# @rbs (String directory, ?persist: bool) -> void
|
|
122
|
+
def initialize(directory, persist: true)
|
|
123
|
+
@data = +"".b
|
|
124
|
+
@extra, @streams, @experts = {}, Hash.new { |hash, key| hash[key] = [] }, {}
|
|
125
|
+
@io = File.open(File.join(directory, "annotations.bin"), "wb", 0o600) if persist
|
|
126
|
+
end
|
|
127
|
+
# @rbs (Integer number, annotation_values annotation) -> void
|
|
128
|
+
def append(number, annotation)
|
|
129
|
+
flags = FLAGS.each_with_index.sum { |name, index| annotation[:analysis_flags].include?(name) ? 1 << index : 0 }
|
|
130
|
+
record = [annotation[:tcp_stream], annotation[:seq_rel], annotation[:ack_rel], flags, annotation[:expert_max]].pack(FORMAT)
|
|
131
|
+
@data << record
|
|
132
|
+
@io&.write(record)
|
|
133
|
+
@streams[annotation[:tcp_stream]] << number if annotation[:tcp_stream] >= 0
|
|
134
|
+
@experts[number] = annotation[:expert_items] unless annotation[:expert_items].empty?
|
|
135
|
+
@extra[number] = annotation[:extra] unless annotation[:extra].empty?
|
|
136
|
+
end
|
|
137
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
138
|
+
def drain
|
|
139
|
+
batch = {data: @data, extras: @extra, streams: @streams.transform_values { |numbers| numbers }, experts: @experts}
|
|
140
|
+
@data = +"".b
|
|
141
|
+
@extra, @streams, @experts = {}, Hash.new { |hash, key| hash[key] = [] }, {}
|
|
142
|
+
batch
|
|
143
|
+
end
|
|
144
|
+
# @rbs (Hash[Symbol, untyped] batch) -> void
|
|
145
|
+
def import(batch)
|
|
146
|
+
raise Vanken::Error, "invalid annotation batch" unless batch[:data].bytesize % SIZE == 0
|
|
147
|
+
@io&.write(batch[:data])
|
|
148
|
+
@data << batch[:data]
|
|
149
|
+
@extra.merge!(batch[:extras])
|
|
150
|
+
@experts.merge!(batch[:experts])
|
|
151
|
+
batch[:streams].each { |stream, numbers| @streams[stream].concat(numbers) }
|
|
152
|
+
end
|
|
153
|
+
# @rbs (Integer number) -> annotation_values
|
|
154
|
+
def [](number)
|
|
155
|
+
stream, seq, ack, flags, expert = @data.byteslice((number - 1) * SIZE, SIZE).unpack(FORMAT)
|
|
156
|
+
{tcp_stream: stream, seq_rel: seq, ack_rel: ack, expert_max: expert,
|
|
157
|
+
analysis_flags: FLAGS.each_with_index.filter_map { |name, index| name if flags & (1 << index) != 0 },
|
|
158
|
+
expert_items: @experts.fetch(number, []), extra: @extra.fetch(number) { Hash.new }}
|
|
159
|
+
end
|
|
160
|
+
# @rbs () -> void
|
|
161
|
+
def flush = @io&.flush
|
|
162
|
+
# @rbs (String path) -> String
|
|
163
|
+
def snapshot(path)
|
|
164
|
+
flush
|
|
165
|
+
File.open(path, "w", 0o600) { |io| io.write(JSON.generate("experts" => @experts, "extras" => @extra)) }
|
|
166
|
+
path
|
|
167
|
+
end
|
|
168
|
+
# @rbs () -> void
|
|
169
|
+
def close = @io&.close
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
class RowCache
|
|
173
|
+
# @rbs! @entries: Hash[untyped, untyped]
|
|
174
|
+
# @rbs (?limit: Integer) -> void
|
|
175
|
+
def initialize(limit: 20_000)
|
|
176
|
+
@limit = limit
|
|
177
|
+
@entries = {} #: Hash[untyped, untyped]
|
|
178
|
+
@mutex = Mutex.new
|
|
179
|
+
end
|
|
180
|
+
# @rbs [T] (untyped key) { () -> T } -> T
|
|
181
|
+
def fetch(key)
|
|
182
|
+
cached = @mutex.synchronize { value = @entries.delete(key); @entries[key] = value if value; value }
|
|
183
|
+
return cached if cached
|
|
184
|
+
value = yield
|
|
185
|
+
@mutex.synchronize { @entries[key] = value; @entries.shift while @entries.size > @limit }
|
|
186
|
+
value
|
|
187
|
+
end
|
|
188
|
+
# @rbs () -> void
|
|
189
|
+
def clear = @mutex.synchronize { @entries.clear }
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "redhound"
|
|
4
|
+
|
|
5
|
+
module Vanken
|
|
6
|
+
module Gateway
|
|
7
|
+
module CaptureFilter
|
|
8
|
+
class Error < StandardError
|
|
9
|
+
attr_reader :position #: Integer?
|
|
10
|
+
|
|
11
|
+
# @rbs (String message, ?position: Integer?) -> void
|
|
12
|
+
def initialize(message, position: nil)
|
|
13
|
+
@position = position
|
|
14
|
+
super(message)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @rbs (String expression, ?linktype: Integer, ?snaplen: Integer, ?live: bool) -> Redhound::Filter::Program
|
|
19
|
+
def self.compile(expression, linktype: 1, snaplen: 262_144, live: false)
|
|
20
|
+
raise Error, "capture filter exceeds 8192 bytes" unless expression.is_a?(String) && expression.bytesize <= 8192
|
|
21
|
+
|
|
22
|
+
program = Redhound::Filter.compile(expression, linktype: linktype, snaplen: snaplen, live: live)
|
|
23
|
+
Redhound::Filter::BPF::Validator.validate!(program.instructions)
|
|
24
|
+
program
|
|
25
|
+
rescue Redhound::FilterError => e
|
|
26
|
+
raise Error.new(e.message, position: e.is_a?(Redhound::FilterSyntaxError) ? e.position : nil)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Vanken
|
|
7
|
+
module Gateway
|
|
8
|
+
class DetailBuilder
|
|
9
|
+
# @rbs (PacketView packet, ?annotations: Core::annotation_values?) -> Array[Core::DetailNode]
|
|
10
|
+
def build(packet, annotations: nil)
|
|
11
|
+
raw = packet.raw_packet
|
|
12
|
+
frame = node("frame", "Frame #{raw.number}: #{raw.original_length} bytes on wire, #{raw.caplen} captured", offset: 0, length: raw.caplen)
|
|
13
|
+
nodes = packet.layers.each_with_index.map do |layer, index|
|
|
14
|
+
prefix = packet.layers.count { |item| item.protocol == layer.protocol } > 1 ? "#{layer.protocol}[#{index}]" : layer.protocol.to_s
|
|
15
|
+
klass = Redhound::Registry.default.protocols[layer.protocol]
|
|
16
|
+
source = packet.layer_source(layer)
|
|
17
|
+
children = layer.fields.map do |field|
|
|
18
|
+
value = field.value
|
|
19
|
+
node("#{prefix}/#{field.name}", "#{field.name.split('.').last.tr('_', ' ').capitalize}: #{field.display}",
|
|
20
|
+
field: field.name, offset: field.length.positive? ? field.offset : nil, length: field.length,
|
|
21
|
+
source: source,
|
|
22
|
+
filter: "#{field.name} == #{literal(value, field.type)}")
|
|
23
|
+
end
|
|
24
|
+
layer.diagnostics.each_with_index do |item, position|
|
|
25
|
+
children << node("#{prefix}/expert/#{position}", "[#{Severity.normalize(item.severity)}: #{item.code}] #{escape(item.message)}", source: source, severity: Severity.normalize(item.severity))
|
|
26
|
+
end
|
|
27
|
+
node(prefix, klass&.protocol_name || layer.protocol.to_s.upcase, offset: layer.offset,
|
|
28
|
+
length: layer.payload_end - layer.offset, source: source, children: children)
|
|
29
|
+
end
|
|
30
|
+
if annotations
|
|
31
|
+
annotations[:extra].each do |field, value|
|
|
32
|
+
next if packet.values(field).any?
|
|
33
|
+
nodes << node("annotation/#{field}", "[#{field}: #{escape(value)}]", field: field, filter: "#{field} == #{literal(value)}")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
[frame, *nodes]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
# @rbs (untyped value, ?Symbol? type) -> String
|
|
41
|
+
def literal(value, type = nil)
|
|
42
|
+
return value.to_s if %i[ipv4 ipv6 mac].include?(type) || !value.is_a?(String)
|
|
43
|
+
return "0x#{value.unpack1('H*')}" if type == :bytes && !value.empty?
|
|
44
|
+
|
|
45
|
+
escaped = value.b.bytes.map do |byte|
|
|
46
|
+
case byte
|
|
47
|
+
when 34 then '\\"'
|
|
48
|
+
when 92 then "\\\\"
|
|
49
|
+
when 32..126 then byte.chr
|
|
50
|
+
else format("\\x%02x", byte)
|
|
51
|
+
end
|
|
52
|
+
end.join
|
|
53
|
+
"\"#{escaped}\""
|
|
54
|
+
end
|
|
55
|
+
# @rbs (untyped value) -> String
|
|
56
|
+
def escape(value) = value.to_s.b.gsub(/[^\x20-\x7e]/n) { |byte| format("\\x%02x", byte.getbyte(0)) }
|
|
57
|
+
# @rbs (String id, String label, ?field: String?, ?offset: Integer?, ?length: Integer, ?source: Symbol, ?severity: Symbol?, ?filter: String?, ?children: Array[Core::DetailNode]) -> Core::DetailNode
|
|
58
|
+
def node(id, label, field: nil, offset: nil, length: 0, source: :frame, severity: nil, filter: nil, children: [])
|
|
59
|
+
Core::DetailNode.new(id: id, label: label, field: field, offset: offset, length: length, source: source, severity: severity, filter: filter, children: children)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "redhound"
|
|
5
|
+
require "stringio"
|
|
6
|
+
require_relative "packet_snapshot"
|
|
7
|
+
|
|
8
|
+
module Vanken
|
|
9
|
+
module Gateway
|
|
10
|
+
PROTOCOLS = {"ip" => :ipv4}.freeze
|
|
11
|
+
module Severity
|
|
12
|
+
# @rbs (Symbol? value) -> Symbol?
|
|
13
|
+
def self.normalize(value) = value == :warn ? :warning : value
|
|
14
|
+
# @rbs (Symbol? value) -> Integer
|
|
15
|
+
def self.rank(value) = {nil => 0, note: 1, warning: 2, warn: 2, error: 3}.fetch(value, 0)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Dissector
|
|
19
|
+
attr_reader :registry #: Redhound::Registry
|
|
20
|
+
# @rbs (?verify_checksums: bool, ?decode_as: Array[String], ?plugins: Array[String]) -> void
|
|
21
|
+
def initialize(verify_checksums: false, decode_as: [], plugins: [])
|
|
22
|
+
plugins.each { |path| load File.expand_path(path) }
|
|
23
|
+
@registry = Redhound::Registry.default.copy
|
|
24
|
+
decode_as.each { |rule| @registry.decode_as(rule) }
|
|
25
|
+
@engine = Redhound::Engine.new(registry: @registry, verify_checksums: verify_checksums)
|
|
26
|
+
end
|
|
27
|
+
# @rbs (Core::Frame frame) -> PacketView
|
|
28
|
+
def dissect(frame)
|
|
29
|
+
packet = self.class.packet(frame)
|
|
30
|
+
packet.engine = @engine
|
|
31
|
+
PacketView.new(packet, @registry)
|
|
32
|
+
rescue Redhound::Error, ArgumentError => error
|
|
33
|
+
raise Vanken::FileError, error.message
|
|
34
|
+
end
|
|
35
|
+
# @rbs (Core::Frame frame, ?interfaces: Hash[Hash[String, untyped], Redhound::Capture::Interface]) -> Redhound::Packet
|
|
36
|
+
def self.packet(frame, interfaces: {})
|
|
37
|
+
interface = frame.interface && interfaces.fetch(frame.interface) do
|
|
38
|
+
values = frame.interface.transform_keys(&:to_sym).select { |key, _| %i[name index linktype snaplen flags mtu mac description filter meta].include?(key) }
|
|
39
|
+
interfaces[frame.interface] = Redhound::Capture::Interface.new(name: values.fetch(:name), **values.reject { |key, _| key == :name })
|
|
40
|
+
end
|
|
41
|
+
Redhound.dissect(frame.bytes, linktype: frame.linktype, timestamp_ns: frame.timestamp_ns,
|
|
42
|
+
original_length: frame.original_length, interface: interface, direction: frame.direction, number: frame.number)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class PacketView
|
|
47
|
+
attr_reader :raw_packet #: Redhound::Packet
|
|
48
|
+
# @rbs (Redhound::Packet packet, Redhound::Registry registry) -> void
|
|
49
|
+
def initialize(packet, registry) = (@raw_packet = packet; @registry = registry)
|
|
50
|
+
# @rbs (String name) -> Array[untyped]
|
|
51
|
+
def values(name) = FieldValues.read(name) { |field| @raw_packet.field_values(field) }
|
|
52
|
+
# @rbs (String name) -> bool
|
|
53
|
+
def layer?(name) = @raw_packet.layers.any? { |layer| layer.protocol == PROTOCOLS.fetch(name, name.to_sym) }
|
|
54
|
+
# @rbs () -> Array[Redhound::Field]
|
|
55
|
+
def fields = @raw_packet.layers.flat_map(&:fields)
|
|
56
|
+
# @rbs (String name) -> Symbol?
|
|
57
|
+
def field_type(name) = fields.find { |field| field.name == name }&.type
|
|
58
|
+
# @rbs () -> Array[Redhound::Layer]
|
|
59
|
+
def layers = @raw_packet.layers
|
|
60
|
+
# @rbs () -> String
|
|
61
|
+
def bytes = @raw_packet.data
|
|
62
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
63
|
+
def to_h = @raw_packet.to_h
|
|
64
|
+
# @rbs () -> Array[Hash[Symbol, untyped]]
|
|
65
|
+
def diagnostics
|
|
66
|
+
layers.flat_map do |layer|
|
|
67
|
+
layer.diagnostics.map { |item| {severity: Severity.normalize(item.severity), code: item.code.to_s,
|
|
68
|
+
protocol: layer.protocol.to_s, message: safe_text(item.message)} }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
# @rbs () -> Core::column_values
|
|
72
|
+
def columns
|
|
73
|
+
relevant = layers.reject(&:embedded)
|
|
74
|
+
network = relevant.reverse.find { |layer| %i[ipv4 ipv6].include?(layer.protocol) }
|
|
75
|
+
address = network || relevant.find { |layer| layer.protocol == :arp } || relevant.find { |layer| layer.protocol == :eth }
|
|
76
|
+
transport = relevant.reverse.find { |layer| %i[tcp udp].include?(layer.protocol) }
|
|
77
|
+
meaning = relevant.reverse.find { |layer| !%i[data raw eth vlan ipv6_ext sll sll2 null].include?(layer.protocol) } || relevant.last
|
|
78
|
+
klass = meaning && @registry.protocols[meaning.protocol]
|
|
79
|
+
{source: address_text(address, :src), destination: address_text(address, :dst),
|
|
80
|
+
protocol: (klass&.short_name || meaning&.protocol.to_s.upcase),
|
|
81
|
+
src_port: transport&.[](:srcport) || -1, dst_port: transport&.[](:dstport) || -1,
|
|
82
|
+
ip_proto: network&.[](:proto) || network&.[](:nxt) || -1,
|
|
83
|
+
layers: layers.map { |layer| layer.protocol == :ipv4 ? "ip" : layer.protocol.to_s }.uniq,
|
|
84
|
+
transport_values: layers.count { |layer| %i[tcp udp].include?(layer.protocol) } > 1 ?
|
|
85
|
+
%w[tcp.srcport tcp.dstport udp.srcport udp.dstport].to_h { |name| [name, values(name)] } : nil}
|
|
86
|
+
end
|
|
87
|
+
# @rbs () -> String
|
|
88
|
+
def info
|
|
89
|
+
safe_text(@raw_packet.summary)
|
|
90
|
+
end
|
|
91
|
+
# @rbs () -> Core::annotation_values
|
|
92
|
+
def annotations
|
|
93
|
+
extra = fields.select { |field| field.name.start_with?("tcp.analysis.", "tcp.reassembled", "ip.reassembled") }.to_h { |field| [field.name, field.value] }
|
|
94
|
+
{tcp_stream: values("tcp.stream").first || -1, seq_rel: values("tcp.seq_relative").first || -1,
|
|
95
|
+
ack_rel: values("tcp.ack_relative").first || -1,
|
|
96
|
+
analysis_flags: Core::AnnotationStore::FLAGS.select { |flag| values("tcp.analysis.#{flag}").any? },
|
|
97
|
+
expert_max: diagnostics.map { |item| Severity.rank(item[:severity]) }.max || 0,
|
|
98
|
+
expert_items: diagnostics, extra: extra}
|
|
99
|
+
end
|
|
100
|
+
# @rbs () -> bool
|
|
101
|
+
def reassembled? = @raw_packet.meta.key?(:reassembled_packet) || fields.any? { |field| field.name == "tcp.reassembled_from" }
|
|
102
|
+
# @rbs (Redhound::Layer layer) -> (:frame | :reassembled)
|
|
103
|
+
def layer_source(layer)
|
|
104
|
+
virtual = @raw_packet.meta[:reassembled_packet]
|
|
105
|
+
return :reassembled if (virtual && virtual.layers.include?(layer)) || layer.field_value("tcp.reassembled_in")
|
|
106
|
+
|
|
107
|
+
:frame
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
# @rbs (untyped value) -> String
|
|
112
|
+
def safe_text(value) = value.to_s.b.gsub(/[^\x20-\x7e]/n) { |byte| format("\\x%02x", byte.getbyte(0)) }
|
|
113
|
+
# @rbs (Redhound::Layer? layer, Symbol direction) -> String
|
|
114
|
+
def address_text(layer, direction)
|
|
115
|
+
return "" unless layer
|
|
116
|
+
key = layer.protocol == :arp ? (direction == :src ? :spa : :tpa) : direction
|
|
117
|
+
layer.display(key)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
class Analysis
|
|
122
|
+
# @rbs (?registry: Redhound::Registry, **untyped options) -> void
|
|
123
|
+
def initialize(registry: Redhound::Registry.default, **options)
|
|
124
|
+
@session = Redhound::Analysis::Session.new(registry: registry, stats: [], **options)
|
|
125
|
+
end
|
|
126
|
+
# @rbs (PacketView packet) -> void
|
|
127
|
+
def update(packet) = @session.update(packet.raw_packet)
|
|
128
|
+
# @rbs () -> void
|
|
129
|
+
def close
|
|
130
|
+
@session.finish(StringIO.new, StringIO.new)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
class FieldCatalog
|
|
135
|
+
# @rbs () -> void
|
|
136
|
+
def initialize
|
|
137
|
+
@mutex = Mutex.new
|
|
138
|
+
@fields = {} #: Hash[String, Hash[Symbol, untyped]]
|
|
139
|
+
@protocols = Redhound::Registry.default.protocols.keys.map { |id| id == :ipv4 ? "ip" : id.to_s }
|
|
140
|
+
Redhound::Registry.default.protocols.each do |id, klass|
|
|
141
|
+
definitions = Array(klass.compiled_header&.definitions) #: Array[Redhound::FieldDefinition]
|
|
142
|
+
definitions.each do |field|
|
|
143
|
+
@fields[field.name] = {type: field.type, source: :dissect, protocol: id.to_s}.freeze
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
@names = (@fields.keys + @protocols).sort.freeze
|
|
147
|
+
end
|
|
148
|
+
# @rbs () -> Array[String]
|
|
149
|
+
def names = @mutex.synchronize { @names }
|
|
150
|
+
# @rbs (String name) -> Hash[Symbol, untyped]?
|
|
151
|
+
def lookup(name) = @mutex.synchronize { @fields[name] }
|
|
152
|
+
# @rbs (String name) -> bool
|
|
153
|
+
def protocol?(name) = @protocols.include?(name)
|
|
154
|
+
# @rbs (PacketView packet) -> void
|
|
155
|
+
def observe(packet)
|
|
156
|
+
fields = packet.fields
|
|
157
|
+
@mutex.synchronize do
|
|
158
|
+
changed = false
|
|
159
|
+
fields.each do |field|
|
|
160
|
+
next if @fields.key?(field.name)
|
|
161
|
+
@fields[field.name] = {type: field.type, source: :dissect, protocol: field.name.split(".").first}.freeze
|
|
162
|
+
changed = true
|
|
163
|
+
end
|
|
164
|
+
@names = (@fields.keys + @protocols).sort.freeze if changed
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
# @rbs (Hash[String, Hash[Symbol, untyped]] definitions) -> void
|
|
168
|
+
def merge(definitions)
|
|
169
|
+
@mutex.synchronize do
|
|
170
|
+
definitions.each { |name, definition| @fields[name] ||= definition.freeze }
|
|
171
|
+
@names = (@fields.keys + @protocols).sort.freeze unless definitions.empty?
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|