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.
Files changed (104) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +84 -0
  5. data/docs/filters.md +25 -0
  6. data/docs/performance.md +113 -0
  7. data/docs/releases.md +15 -0
  8. data/docs/upstream.md +32 -0
  9. data/exe/vanken +5 -0
  10. data/exe/vanken-capture +6 -0
  11. data/lib/vanken/app/capture_controller.rb +197 -0
  12. data/lib/vanken/app/document.rb +305 -0
  13. data/lib/vanken/app/document_jobs.rb +199 -0
  14. data/lib/vanken/capture/analyzer.rb +44 -0
  15. data/lib/vanken/capture/analyzer_process.rb +91 -0
  16. data/lib/vanken/capture/analyzer_wire.rb +42 -0
  17. data/lib/vanken/capture/analyzer_worker.rb +153 -0
  18. data/lib/vanken/capture/control_protocol.rb +32 -0
  19. data/lib/vanken/capture/filter_worker.rb +180 -0
  20. data/lib/vanken/capture/helper_main.rb +137 -0
  21. data/lib/vanken/capture/helper_options.rb +70 -0
  22. data/lib/vanken/capture/launcher.rb +181 -0
  23. data/lib/vanken/capture/privileges.rb +24 -0
  24. data/lib/vanken/capture/receiver.rb +39 -0
  25. data/lib/vanken/cli.rb +68 -0
  26. data/lib/vanken/config/log.rb +19 -0
  27. data/lib/vanken/config/preferences.rb +126 -0
  28. data/lib/vanken/core/display_filter/compiler.rb +108 -0
  29. data/lib/vanken/core/display_filter/errors.rb +19 -0
  30. data/lib/vanken/core/display_filter/field_resolver.rb +86 -0
  31. data/lib/vanken/core/display_filter/lexer.rb +124 -0
  32. data/lib/vanken/core/display_filter/parser.rb +157 -0
  33. data/lib/vanken/core/display_filter/types.rb +176 -0
  34. data/lib/vanken/core/frame.rb +33 -0
  35. data/lib/vanken/core/frame_store.rb +179 -0
  36. data/lib/vanken/core/frame_view.rb +68 -0
  37. data/lib/vanken/core/stores.rb +192 -0
  38. data/lib/vanken/errors.rb +9 -0
  39. data/lib/vanken/gateway/capture_filter.rb +30 -0
  40. data/lib/vanken/gateway/detail_builder.rb +63 -0
  41. data/lib/vanken/gateway/dissector.rb +176 -0
  42. data/lib/vanken/gateway/file_reader.rb +153 -0
  43. data/lib/vanken/gateway/file_writer.rb +64 -0
  44. data/lib/vanken/gateway/interfaces.rb +30 -0
  45. data/lib/vanken/gateway/live_capture.rb +79 -0
  46. data/lib/vanken/gateway/packet_snapshot.rb +89 -0
  47. data/lib/vanken/ui/actions.rb +50 -0
  48. data/lib/vanken/ui/application.rb +205 -0
  49. data/lib/vanken/ui/dialogs.rb +89 -0
  50. data/lib/vanken/ui/file_operations.rb +113 -0
  51. data/lib/vanken/ui/filter_operations.rb +120 -0
  52. data/lib/vanken/ui/main_view.rb +113 -0
  53. data/lib/vanken/ui/packet_source.rb +161 -0
  54. data/lib/vanken/ui/selection.rb +91 -0
  55. data/lib/vanken/version.rb +5 -0
  56. data/lib/vanken.rb +17 -0
  57. data/packaging/README.md +33 -0
  58. data/sig/generated/vanken/app/capture_controller.rbs +58 -0
  59. data/sig/generated/vanken/app/document.rbs +117 -0
  60. data/sig/generated/vanken/app/document_jobs.rbs +27 -0
  61. data/sig/generated/vanken/capture/analyzer.rbs +11 -0
  62. data/sig/generated/vanken/capture/analyzer_process.rbs +17 -0
  63. data/sig/generated/vanken/capture/analyzer_wire.rbs +19 -0
  64. data/sig/generated/vanken/capture/analyzer_worker.rbs +45 -0
  65. data/sig/generated/vanken/capture/control_protocol.rbs +17 -0
  66. data/sig/generated/vanken/capture/filter_worker.rbs +51 -0
  67. data/sig/generated/vanken/capture/helper_main.rbs +23 -0
  68. data/sig/generated/vanken/capture/helper_options.rbs +27 -0
  69. data/sig/generated/vanken/capture/launcher.rbs +60 -0
  70. data/sig/generated/vanken/capture/privileges.rbs +12 -0
  71. data/sig/generated/vanken/capture/receiver.rbs +15 -0
  72. data/sig/generated/vanken/cli.rbs +9 -0
  73. data/sig/generated/vanken/config/log.rbs +9 -0
  74. data/sig/generated/vanken/config/preferences.rbs +49 -0
  75. data/sig/generated/vanken/core/display_filter/compiler.rbs +45 -0
  76. data/sig/generated/vanken/core/display_filter/errors.rbs +16 -0
  77. data/sig/generated/vanken/core/display_filter/field_resolver.rbs +48 -0
  78. data/sig/generated/vanken/core/display_filter/lexer.rbs +49 -0
  79. data/sig/generated/vanken/core/display_filter/parser.rbs +95 -0
  80. data/sig/generated/vanken/core/display_filter/types.rbs +45 -0
  81. data/sig/generated/vanken/core/frame.rbs +63 -0
  82. data/sig/generated/vanken/core/frame_store.rbs +65 -0
  83. data/sig/generated/vanken/core/frame_view.rbs +36 -0
  84. data/sig/generated/vanken/core/stores.rbs +110 -0
  85. data/sig/generated/vanken/errors.rbs +18 -0
  86. data/sig/generated/vanken/gateway/capture_filter.rbs +17 -0
  87. data/sig/generated/vanken/gateway/detail_builder.rbs +21 -0
  88. data/sig/generated/vanken/gateway/dissector.rbs +113 -0
  89. data/sig/generated/vanken/gateway/file_reader.rbs +53 -0
  90. data/sig/generated/vanken/gateway/file_writer.rbs +27 -0
  91. data/sig/generated/vanken/gateway/interfaces.rbs +16 -0
  92. data/sig/generated/vanken/gateway/live_capture.rbs +48 -0
  93. data/sig/generated/vanken/gateway/packet_snapshot.rbs +48 -0
  94. data/sig/generated/vanken/ui/actions.rbs +9 -0
  95. data/sig/generated/vanken/ui/application.rbs +85 -0
  96. data/sig/generated/vanken/ui/dialogs.rbs +21 -0
  97. data/sig/generated/vanken/ui/file_operations.rbs +25 -0
  98. data/sig/generated/vanken/ui/filter_operations.rbs +33 -0
  99. data/sig/generated/vanken/ui/main_view.rbs +21 -0
  100. data/sig/generated/vanken/ui/packet_source.rbs +40 -0
  101. data/sig/generated/vanken/ui/selection.rbs +23 -0
  102. data/sig/generated/vanken/version.rbs +5 -0
  103. data/sig/vanken.rbs +25 -0
  104. 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vanken
4
+ class Error < StandardError; end
5
+ class FileError < Error; end
6
+ class CaptureError < Error; end
7
+ class ConfigError < Error; end
8
+ class FilterError < Error; end
9
+ 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