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,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "redhound"
|
|
5
|
+
require "stringio"
|
|
6
|
+
require "io/wait"
|
|
7
|
+
|
|
8
|
+
module Vanken
|
|
9
|
+
module Gateway
|
|
10
|
+
class FileReader
|
|
11
|
+
include Enumerable #[Core::Frame]
|
|
12
|
+
QUEUED_BYTE_LIMIT = 64 << 20 #: Integer
|
|
13
|
+
class BufferedInput
|
|
14
|
+
# @rbs (IO input) -> void
|
|
15
|
+
def initialize(input)
|
|
16
|
+
@input, @buffer, @stopped = input, StringIO.new("".b), false
|
|
17
|
+
@input.binmode
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @rbs (Integer length) -> String?
|
|
21
|
+
def read(length)
|
|
22
|
+
return "".b if length.zero?
|
|
23
|
+
until @stopped
|
|
24
|
+
part = @buffer.read(length)
|
|
25
|
+
return part if part && !part.empty?
|
|
26
|
+
|
|
27
|
+
part = @input.read_nonblock(65_536, exception: false)
|
|
28
|
+
case part
|
|
29
|
+
when String then @buffer.string = part
|
|
30
|
+
when :wait_readable then @input.wait_readable(0.05)
|
|
31
|
+
else return nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @rbs () -> void
|
|
38
|
+
def stop = (@stopped = true)
|
|
39
|
+
end
|
|
40
|
+
private_constant :BufferedInput, :QUEUED_BYTE_LIMIT
|
|
41
|
+
|
|
42
|
+
# @rbs (String | IO | StringIO input) -> void
|
|
43
|
+
def initialize(input)
|
|
44
|
+
io = input == "-" ? $stdin : input
|
|
45
|
+
@buffered_input = nil #: BufferedInput?
|
|
46
|
+
@packets = nil #: Thread::SizedQueue[Redhound::Packet | StandardError]?
|
|
47
|
+
@pump = nil #: Thread?
|
|
48
|
+
@budget_mutex, @budget_available, @queued_bytes = Mutex.new, ConditionVariable.new, 0
|
|
49
|
+
@stopped = false
|
|
50
|
+
@buffered_input = BufferedInput.new(io) if io.is_a?(IO) && !io.stat.file?
|
|
51
|
+
@reader = Redhound.open(@buffered_input || input)
|
|
52
|
+
@number = 0
|
|
53
|
+
if @buffered_input
|
|
54
|
+
# One parsed packet waiting for the byte budget can retain at most another 16 MiB.
|
|
55
|
+
@packets = SizedQueue.new(256)
|
|
56
|
+
@pump = Thread.new { pump_packets }
|
|
57
|
+
end
|
|
58
|
+
rescue Redhound::Error, IOError, SystemCallError => error
|
|
59
|
+
raise Vanken::FileError, error.message
|
|
60
|
+
end
|
|
61
|
+
# @rbs (?timeout: Numeric?) -> Core::Frame?
|
|
62
|
+
def next_frame(timeout: nil)
|
|
63
|
+
return nil if @stopped
|
|
64
|
+
raise ArgumentError, "timeout must be nonnegative" if timeout && timeout.negative?
|
|
65
|
+
|
|
66
|
+
packet = @packets ? @packets.pop(timeout: timeout && Float(timeout)) : @reader.next_packet(timeout: timeout)
|
|
67
|
+
raise packet if packet.is_a?(StandardError)
|
|
68
|
+
return nil unless packet
|
|
69
|
+
if @packets
|
|
70
|
+
@budget_mutex.synchronize do
|
|
71
|
+
@queued_bytes -= packet.data.bytesize
|
|
72
|
+
@budget_available.signal
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
@number += 1
|
|
76
|
+
interface = packet.interface && %i[name index linktype snaplen flags mtu mac description filter].to_h { |key| [key.to_s, packet.interface.public_send(key)] }
|
|
77
|
+
# Omit absent optional values; keep the declared interface metadata for saving.
|
|
78
|
+
interface&.compact!
|
|
79
|
+
Core::Frame.new(bytes: packet.data, timestamp_ns: packet.timestamp_ns, original_length: packet.original_length,
|
|
80
|
+
linktype: packet.linktype, interface: interface, direction: packet.direction, number: @number)
|
|
81
|
+
rescue Redhound::Error, IOError, SystemCallError => error
|
|
82
|
+
raise Vanken::FileError, error.message
|
|
83
|
+
end
|
|
84
|
+
# @rbs () { (Core::Frame) -> void } -> void
|
|
85
|
+
# @rbs () -> Enumerator[Core::Frame, void]
|
|
86
|
+
def each
|
|
87
|
+
return enum_for(:each) unless block_given?
|
|
88
|
+
loop do
|
|
89
|
+
packet = next_frame(timeout: 0.1)
|
|
90
|
+
yield packet if packet
|
|
91
|
+
break if eof?
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
# @rbs () -> bool
|
|
95
|
+
def eof? = @stopped || (@packets ? @packets.closed? && @packets.empty? : @reader.stopped?)
|
|
96
|
+
# @rbs () -> void
|
|
97
|
+
def stop
|
|
98
|
+
@budget_mutex.synchronize do
|
|
99
|
+
@stopped = true
|
|
100
|
+
@budget_available.broadcast
|
|
101
|
+
end
|
|
102
|
+
@buffered_input&.stop
|
|
103
|
+
@packets&.close
|
|
104
|
+
@reader.stop
|
|
105
|
+
@pump&.join
|
|
106
|
+
end
|
|
107
|
+
# @rbs () -> void
|
|
108
|
+
def close
|
|
109
|
+
stop
|
|
110
|
+
@reader.close
|
|
111
|
+
end
|
|
112
|
+
# @rbs () -> Redhound::Capture::Stats
|
|
113
|
+
def stats = @reader.stats
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
# @rbs () -> void
|
|
118
|
+
def pump_packets
|
|
119
|
+
queue = @packets
|
|
120
|
+
return unless queue
|
|
121
|
+
until @stopped
|
|
122
|
+
packet = @reader.next_packet
|
|
123
|
+
break unless packet
|
|
124
|
+
break unless reserve_bytes(packet)
|
|
125
|
+
|
|
126
|
+
queue << packet
|
|
127
|
+
end
|
|
128
|
+
rescue ClosedQueueError
|
|
129
|
+
nil
|
|
130
|
+
rescue StandardError => error
|
|
131
|
+
begin
|
|
132
|
+
queue << error unless @stopped
|
|
133
|
+
rescue ClosedQueueError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
ensure
|
|
137
|
+
queue&.close
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @rbs (Redhound::Packet packet) -> bool
|
|
141
|
+
def reserve_bytes(packet)
|
|
142
|
+
size = packet.data.bytesize
|
|
143
|
+
@budget_mutex.synchronize do
|
|
144
|
+
@budget_available.wait(@budget_mutex) until @stopped || @queued_bytes + size <= QUEUED_BYTE_LIMIT
|
|
145
|
+
return false if @stopped
|
|
146
|
+
|
|
147
|
+
@queued_bytes += size
|
|
148
|
+
end
|
|
149
|
+
true
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "redhound"
|
|
5
|
+
require_relative "../errors"
|
|
6
|
+
|
|
7
|
+
module Vanken
|
|
8
|
+
module Gateway
|
|
9
|
+
class FileWriter
|
|
10
|
+
# @rbs (String | IO output, **untyped options) -> FileWriter
|
|
11
|
+
# @rbs [T] (String | IO output, **untyped options) { (FileWriter) -> T } -> T
|
|
12
|
+
def self.open(output, **options)
|
|
13
|
+
writer = new(output, **options)
|
|
14
|
+
return writer unless block_given?
|
|
15
|
+
begin
|
|
16
|
+
yield writer
|
|
17
|
+
ensure
|
|
18
|
+
writer.close
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
# @rbs (String | IO output, ?format: Symbol | String, **untyped options) -> void
|
|
22
|
+
def initialize(output, format: :pcapng, **options)
|
|
23
|
+
@format = format.to_sym
|
|
24
|
+
@interfaces = {} #: Hash[Hash[String, untyped], Redhound::Capture::Interface]
|
|
25
|
+
if output.is_a?(String)
|
|
26
|
+
@owned = File.open(output, "wb", 0o600)
|
|
27
|
+
@owned.chmod(0o600)
|
|
28
|
+
end
|
|
29
|
+
@writer = Redhound::Writer.open(@owned || output, format: @format, **options)
|
|
30
|
+
rescue Redhound::Error, IOError, SystemCallError => error
|
|
31
|
+
@owned&.close
|
|
32
|
+
raise Vanken::FileError, error.message
|
|
33
|
+
end
|
|
34
|
+
# @rbs (Core::Frame | Redhound::Packet frame) -> self
|
|
35
|
+
def <<(frame)
|
|
36
|
+
packet = defined?(Core::Frame) && frame.is_a?(Core::Frame) ? Dissector.packet(frame, interfaces: @interfaces) : frame
|
|
37
|
+
if @format == :pcap
|
|
38
|
+
@linktype ||= packet.linktype
|
|
39
|
+
raise Vanken::FileError, "pcap requires one link type; save as pcapng" unless packet.linktype == @linktype
|
|
40
|
+
end
|
|
41
|
+
@writer << packet
|
|
42
|
+
self
|
|
43
|
+
rescue Redhound::Error, ArgumentError, IOError, SystemCallError => error
|
|
44
|
+
raise Vanken::FileError, error.message
|
|
45
|
+
end
|
|
46
|
+
# @rbs () -> void
|
|
47
|
+
def flush = @writer.flush
|
|
48
|
+
# @rbs (Redhound::Capture::Stats | Hash[Symbol | String, Integer] stats, ?interface: Redhound::Capture::Interface?) -> void
|
|
49
|
+
def write_stats(stats, interface: nil)
|
|
50
|
+
if stats.is_a?(Hash)
|
|
51
|
+
values = stats.transform_keys(&:to_sym)
|
|
52
|
+
stats = Redhound::Capture::Stats.new(received: values.fetch(:received, 0), dropped: values.fetch(:dropped, 0), if_dropped: values.fetch(:if_dropped, 0))
|
|
53
|
+
end
|
|
54
|
+
@writer.write_stats(stats, interface: interface)
|
|
55
|
+
end
|
|
56
|
+
# @rbs () -> void
|
|
57
|
+
def close
|
|
58
|
+
@writer&.close
|
|
59
|
+
ensure
|
|
60
|
+
@owned&.close unless @owned&.closed?
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "redhound"
|
|
4
|
+
|
|
5
|
+
module Vanken
|
|
6
|
+
module Gateway
|
|
7
|
+
module Interfaces
|
|
8
|
+
class NotFound < StandardError; end
|
|
9
|
+
|
|
10
|
+
# @rbs () -> Array[Hash[Symbol, untyped]]
|
|
11
|
+
def self.list
|
|
12
|
+
result = Redhound::Capture.interfaces.map do |interface|
|
|
13
|
+
{name: interface.name, index: interface.index, linktype: interface.linktype,
|
|
14
|
+
snaplen: interface.snaplen, description: interface.description, mac: interface.mac,
|
|
15
|
+
mtu: interface.mtu, up: interface.up?, running: interface.running?, loopback: interface.loopback?}
|
|
16
|
+
end #: Array[Hash[Symbol, untyped]]
|
|
17
|
+
if RUBY_PLATFORM.include?("linux")
|
|
18
|
+
result << {name: "any", index: 0, linktype: 276, snaplen: 262_144,
|
|
19
|
+
description: "All interfaces", mac: nil, mtu: nil, up: true, running: true, loopback: false}
|
|
20
|
+
end
|
|
21
|
+
result
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @rbs (String name) -> Hash[Symbol, untyped]
|
|
25
|
+
def self.find(name)
|
|
26
|
+
list.find { |interface| interface[:name] == name } || raise(NotFound, "interface #{name.inspect} not found")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "redhound"
|
|
4
|
+
require_relative "interfaces"
|
|
5
|
+
require_relative "capture_filter"
|
|
6
|
+
|
|
7
|
+
module Vanken
|
|
8
|
+
module Gateway
|
|
9
|
+
class LiveCapture
|
|
10
|
+
class Error < StandardError
|
|
11
|
+
attr_reader :code #: String
|
|
12
|
+
attr_reader :exit_code #: Integer
|
|
13
|
+
|
|
14
|
+
# @rbs (String message, code: String, ?exit_code: Integer) -> void
|
|
15
|
+
def initialize(message, code:, exit_code: 1)
|
|
16
|
+
@code, @exit_code = code, exit_code
|
|
17
|
+
super(message)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
attr_reader :backend #: Symbol
|
|
22
|
+
|
|
23
|
+
# @rbs (interface: String, ?backend: Symbol | String, ?snaplen: Integer, ?promiscuous: bool, ?buffer_size: Integer?, ?direction: Symbol, ?filter: String?) -> LiveCapture
|
|
24
|
+
def self.open(interface:, backend: :auto, snaplen: 262_144, promiscuous: true, buffer_size: nil, direction: :inout, filter: nil)
|
|
25
|
+
metadata = Interfaces.find(interface)
|
|
26
|
+
if filter && !filter.empty?
|
|
27
|
+
CaptureFilter.compile(filter, linktype: metadata[:linktype], snaplen: snaplen, live: RUBY_PLATFORM.include?("linux"))
|
|
28
|
+
end
|
|
29
|
+
new(Redhound::Capture.open(interface: interface, backend: backend, snaplen: snaplen, promiscuous: promiscuous,
|
|
30
|
+
buffer_size: buffer_size, direction: direction, filter: filter))
|
|
31
|
+
rescue Interfaces::NotFound, Redhound::InterfaceNotFound => e
|
|
32
|
+
raise Error.new(e.message, code: "interface_not_found")
|
|
33
|
+
rescue CaptureFilter::Error, Redhound::FilterError => e
|
|
34
|
+
raise Error.new(e.message, code: "invalid_filter", exit_code: 2)
|
|
35
|
+
rescue Redhound::PermissionDenied, Errno::EACCES, Errno::EPERM => e
|
|
36
|
+
raise Error.new(e.message, code: "permission_denied", exit_code: 3)
|
|
37
|
+
rescue Redhound::UnsupportedPlatform => e
|
|
38
|
+
raise Error.new(e.message, code: "unsupported_platform")
|
|
39
|
+
rescue Redhound::CaptureError, NotImplementedError, ArgumentError => e
|
|
40
|
+
raise Error.new(e.message, code: "backend_unavailable")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @rbs (Redhound::Capture::Source source) -> void
|
|
44
|
+
def initialize(source)
|
|
45
|
+
@source = source
|
|
46
|
+
@backend = case source
|
|
47
|
+
when Redhound::Capture::Linux::TPacketV3 then :ring
|
|
48
|
+
when Redhound::Capture::Linux::PacketSocket then :socket
|
|
49
|
+
when Redhound::Capture::Bsd::BpfDevice then :bpf
|
|
50
|
+
else :unknown
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @rbs (?timeout: Numeric?) -> Redhound::Packet?
|
|
55
|
+
def next_packet(timeout: 0.05) = @source.next_packet(timeout: timeout)
|
|
56
|
+
# @rbs () -> bool
|
|
57
|
+
def stopped? = @source.stopped?
|
|
58
|
+
# @rbs () -> Hash[Symbol, Integer]
|
|
59
|
+
def stats = @source.stats.to_h
|
|
60
|
+
# @rbs () -> Integer
|
|
61
|
+
def linktype = @source.linktype
|
|
62
|
+
# @rbs () -> Array[Hash[Symbol, untyped]]
|
|
63
|
+
def interfaces
|
|
64
|
+
@source.interfaces.map do |interface|
|
|
65
|
+
{name: interface.name, linktype: interface.linktype, snaplen: interface.snaplen,
|
|
66
|
+
description: interface.description, mac: interface.mac, mtu: interface.mtu}
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @rbs () -> void
|
|
71
|
+
def stop = @source.stop
|
|
72
|
+
# @rbs () -> void
|
|
73
|
+
def close = @source.close
|
|
74
|
+
|
|
75
|
+
# @rbs () -> String
|
|
76
|
+
def self.version = Redhound::VERSION
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vanken
|
|
4
|
+
module Gateway
|
|
5
|
+
module FieldValues
|
|
6
|
+
ALIASES = {"ip.addr" => %w[ip.src ip.dst], "ipv6.addr" => %w[ipv6.src ipv6.dst],
|
|
7
|
+
"eth.addr" => %w[eth.src eth.dst], "tcp.port" => %w[tcp.srcport tcp.dstport],
|
|
8
|
+
"udp.port" => %w[udp.srcport udp.dstport]}.freeze
|
|
9
|
+
TCP_FLAGS = {"fin" => 1, "syn" => 2, "rst" => 4, "psh" => 8, "ack" => 16, "urg" => 32}.freeze
|
|
10
|
+
|
|
11
|
+
# @rbs (String name) { (String) -> Array[untyped] } -> Array[untyped]
|
|
12
|
+
def self.read(name)
|
|
13
|
+
if name.start_with?("tcp.flags.") && (bit = TCP_FLAGS[name.delete_prefix("tcp.flags.")])
|
|
14
|
+
return yield("tcp.flags").map { |flags| flags & bit != 0 }
|
|
15
|
+
end
|
|
16
|
+
ALIASES.fetch(name, [name]).flat_map { |field| yield field }.compact
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# A field-only copy of stateful dissection, with an explicit binary JSON representation.
|
|
21
|
+
class PacketSnapshot
|
|
22
|
+
attr_reader :columns #: Core::column_values
|
|
23
|
+
attr_reader :annotations #: Core::annotation_values
|
|
24
|
+
|
|
25
|
+
# @rbs (fields: Hash[String, Array[untyped]], types: Hash[String, Symbol], layers: Array[String], columns: Core::column_values, annotations: Core::annotation_values) -> void
|
|
26
|
+
def initialize(fields:, types:, layers:, columns:, annotations:)
|
|
27
|
+
@fields, @types, @layers = fields, types, layers
|
|
28
|
+
@columns, @annotations = columns, annotations
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @rbs (PacketView packet) -> PacketSnapshot
|
|
32
|
+
def self.from_packet(packet)
|
|
33
|
+
groups = packet.fields.group_by(&:name)
|
|
34
|
+
new(fields: groups.transform_values { |fields| fields.map(&:value) },
|
|
35
|
+
types: groups.transform_values { |fields| fields.first.type }, layers: packet.columns[:layers],
|
|
36
|
+
columns: packet.columns, annotations: packet.annotations)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @rbs (Hash[String, untyped] value) -> PacketSnapshot
|
|
40
|
+
def self.from_h(value)
|
|
41
|
+
new(fields: decode(value.fetch("fields")), types: value.fetch("types").transform_values(&:to_sym),
|
|
42
|
+
layers: value.fetch("layers"), columns: decode(value.fetch("columns")), annotations: decode(value.fetch("annotations")))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @rbs () -> Hash[String, untyped]
|
|
46
|
+
def to_h
|
|
47
|
+
{"fields" => self.class.encode(@fields), "types" => @types.transform_values(&:to_s),
|
|
48
|
+
"layers" => @layers, "columns" => self.class.encode(@columns), "annotations" => self.class.encode(@annotations)}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @rbs (String name) -> Array[untyped]
|
|
52
|
+
def values(name) = FieldValues.read(name) { |field| Array(@fields[field]) }
|
|
53
|
+
# @rbs (String name) -> Symbol?
|
|
54
|
+
def field_type(name) = @types[name]
|
|
55
|
+
# @rbs (String name) -> bool
|
|
56
|
+
def layer?(name) = @layers.include?(name)
|
|
57
|
+
|
|
58
|
+
# @rbs (untyped value) -> untyped
|
|
59
|
+
def self.encode(value)
|
|
60
|
+
case value
|
|
61
|
+
when String
|
|
62
|
+
value.encoding == Encoding::UTF_8 && value.valid_encoding? ? value :
|
|
63
|
+
{"$bytes" => value.unpack1("H*"), "$encoding" => value.encoding.name}
|
|
64
|
+
when Symbol then {"$symbol" => value.to_s}
|
|
65
|
+
when Array then value.map { |item| encode(item) }
|
|
66
|
+
when Hash then {"$hash" => value.map { |key, item| [encode(key), encode(item)] }}
|
|
67
|
+
when Integer, Float, TrueClass, FalseClass, NilClass then value
|
|
68
|
+
else raise TypeError, "unsupported snapshot value: #{value.class}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @rbs (untyped value) -> untyped
|
|
73
|
+
def self.decode(value)
|
|
74
|
+
case value
|
|
75
|
+
when Array then value.map { |item| decode(item) }
|
|
76
|
+
when Hash
|
|
77
|
+
if value.key?("$bytes")
|
|
78
|
+
[value.fetch("$bytes")].pack("H*").force_encoding(value.fetch("$encoding"))
|
|
79
|
+
elsif value.key?("$symbol")
|
|
80
|
+
value.fetch("$symbol").to_sym
|
|
81
|
+
else
|
|
82
|
+
value.fetch("$hash").to_h { |key, item| [decode(key), decode(item)] }
|
|
83
|
+
end
|
|
84
|
+
else value
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vanken
|
|
4
|
+
module UI
|
|
5
|
+
module Actions
|
|
6
|
+
def self.install(ui)
|
|
7
|
+
commands = {
|
|
8
|
+
open: ["開く", "cmd-o", -> { ui.open_dialog }], save: ["名前を付けて保存", "cmd-shift-s", -> { ui.save_dialog }],
|
|
9
|
+
close_file: ["ファイルを閉じる", "cmd-w", -> { ui.close_document }], reload: ["再読み込み", "cmd-r", -> { ui.reload_file }],
|
|
10
|
+
quit: ["終了", "cmd-q", -> { ui.quit }], preferences: ["設定", "cmd-,", -> { ui.preferences_dialog }],
|
|
11
|
+
capture: ["キャプチャ開始/停止", "cmd-e", -> { ui.capture_toggle }], restart: ["キャプチャ再開", nil, -> { ui.restart_capture }],
|
|
12
|
+
apply_filter: ["フィルタを適用", "enter", -> { ui.apply_filter }], clear_filter: ["フィルタをクリア", nil, -> { ui.clear_filter }],
|
|
13
|
+
filter_history: ["フィルタ履歴", nil, -> { ui.filter_history }], bookmark: ["フィルタを保存", nil, -> { ui.bookmark_filter }],
|
|
14
|
+
filter_selected: ["選択した項目でフィルタ", nil, -> { ui.filter_from_selection }],
|
|
15
|
+
prepare_selected: ["選択した項目のフィルタを準備", nil, -> { ui.filter_from_selection(prepare: true) }],
|
|
16
|
+
columns: ["表示する列", nil, -> { ui.columns_dialog }], autoscroll: ["末尾追従", nil, -> { ui.toggle_autoscroll }],
|
|
17
|
+
zoom_in: ["拡大", "cmd-+", -> { ui.zoom(1) }], zoom_out: ["縮小", "cmd--", -> { ui.zoom(-1) }], zoom_reset: ["標準サイズ", "cmd-0", -> { ui.zoom }],
|
|
18
|
+
previous_packet: ["前のパケット", "alt-up", -> { ui.navigate(-1) }], next_packet: ["次のパケット", "alt-down", -> { ui.navigate(1) }],
|
|
19
|
+
first_packet: ["最初のパケット", nil, -> { ui.first_packet }], last_packet: ["最後のパケット", nil, -> { ui.last_packet }],
|
|
20
|
+
go_to_packet: ["パケットへ移動", "cmd-g", -> { ui.go_to_packet }], copy_filter: ["項目をフィルタとしてコピー", nil, -> { ui.copy_text(ui.selected_node.filter) if ui.selected_node&.filter }]
|
|
21
|
+
}
|
|
22
|
+
%i[selected not and and_not or or_not].zip(["選択項目", "選択項目を除外", "AND", "AND NOT", "OR", "OR NOT"]).each do |mode, label|
|
|
23
|
+
commands[:"filter_#{mode}"] = ["適用: #{label}", nil, -> { ui.filter_from_selection(mode) }]
|
|
24
|
+
commands[:"prepare_#{mode}"] = ["準備: #{label}", nil, -> { ui.filter_from_selection(mode, prepare: true) }]
|
|
25
|
+
end
|
|
26
|
+
{hex: "16 進数", text: "テキスト", dump: "ダンプ", escaped: "エスケープした文字列"}.each do |format, label|
|
|
27
|
+
commands[:"copy_#{format}"] = ["バイトをコピー: #{label}", nil, -> { ui.copy_bytes(format) }]
|
|
28
|
+
end
|
|
29
|
+
commands.each do |name, (title, keys, operation)|
|
|
30
|
+
ui.app.actions.register(name, title: title) { |_| operation.call }
|
|
31
|
+
next unless keys
|
|
32
|
+
key = RUBY_PLATFORM.include?("darwin") ? keys : keys.sub("cmd", "ctrl")
|
|
33
|
+
ui.window.dispatcher.keymap.bind(key, name, context: name == :apply_filter ? "in_display_filter && !in_completion" : "")
|
|
34
|
+
end
|
|
35
|
+
ui.app.menu_bar = Zaniah::Menu.build do
|
|
36
|
+
submenu("ファイル") { %i[open save close_file reload quit].each { |action| item(action) } }
|
|
37
|
+
submenu("編集") { %i[copy_filter copy_hex copy_text copy_dump copy_escaped preferences].each { |action| item(action) } }
|
|
38
|
+
submenu("表示") { %i[columns autoscroll zoom_in zoom_out zoom_reset].each { |action| item(action) } }
|
|
39
|
+
submenu("移動") { %i[previous_packet next_packet first_packet last_packet go_to_packet].each { |action| item(action) } }
|
|
40
|
+
submenu("キャプチャ") { %i[capture restart].each { |action| item(action) } }
|
|
41
|
+
submenu("フィルタ") do
|
|
42
|
+
%i[apply_filter clear_filter filter_history bookmark].each { |action| item(action) }
|
|
43
|
+
submenu("選択項目を適用") { %i[selected not and and_not or or_not].each { |mode| item(:"filter_#{mode}") } }
|
|
44
|
+
submenu("選択項目から準備") { %i[selected not and and_not or or_not].each { |mode| item(:"prepare_#{mode}") } }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "vanken"
|
|
4
|
+
require "zaniah/ui"
|
|
5
|
+
require "zaniah/process_pool"
|
|
6
|
+
require "uri"
|
|
7
|
+
require_relative "main_view"
|
|
8
|
+
require_relative "file_operations"
|
|
9
|
+
require_relative "filter_operations"
|
|
10
|
+
require_relative "selection"
|
|
11
|
+
require_relative "dialogs"
|
|
12
|
+
require_relative "actions"
|
|
13
|
+
require_relative "packet_source"
|
|
14
|
+
require_relative "../gateway/interfaces"
|
|
15
|
+
require_relative "../gateway/capture_filter"
|
|
16
|
+
require_relative "../app/capture_controller"
|
|
17
|
+
require_relative "../config/log"
|
|
18
|
+
|
|
19
|
+
module Vanken
|
|
20
|
+
module UI
|
|
21
|
+
class Application
|
|
22
|
+
include FileOperations, FilterOperations, Selection, Dialogs
|
|
23
|
+
attr_reader :app, :window, :preferences, :table, :tree, :hex, :filter_field, :capture,
|
|
24
|
+
:dialog_kind, :packet_source, :interface_infos
|
|
25
|
+
|
|
26
|
+
def initialize(backend: nil, preferences: Config::Preferences.new, debug: false)
|
|
27
|
+
@preferences = preferences
|
|
28
|
+
@log = Config::Log.open(debug: debug, directory: File.join(preferences.directory, "logs"))
|
|
29
|
+
@app = Zaniah::App.new
|
|
30
|
+
@entity = @app.new_entity { {document: nil, number: nil, node: nil, details: [], bytes: "".b, error: nil} }
|
|
31
|
+
@selection_generation, @file_generation, @filter_generation = 0, 0, 0
|
|
32
|
+
@pool_mutex = Mutex.new
|
|
33
|
+
@autoscroll = false
|
|
34
|
+
@backend = backend || (RUBY_PLATFORM.match?(/darwin/) ? :mac : RUBY_PLATFORM.match?(/mingw|mswin/) ? :windows : :linux)
|
|
35
|
+
@filter_field = Zaniah::UI::TextField.new("", placeholder: "表示フィルタ", clearable: false).test_id("vk.filter.input")
|
|
36
|
+
@filter_field.completion(CompletionProvider.new(self)).on_change { |text, _| validate_filter(text) }
|
|
37
|
+
@packet_source = PacketSource.new(self)
|
|
38
|
+
@capture = App::CaptureController.new(preferences: preferences,
|
|
39
|
+
on_document: ->(document) { @app.executor.post { attach_document(document) unless @closing } },
|
|
40
|
+
on_update: ->(*) { @app.executor.post { changed unless @closing } })
|
|
41
|
+
@window = @app.open_window(backend: @backend, width: preferences.get("layout.width"), height: preferences.get("layout.height"), title: "Vanken")
|
|
42
|
+
unless @backend == :tui
|
|
43
|
+
@window.text_system = Zaniah::TextSystem::Renderer.new
|
|
44
|
+
@window.text_system.scale_factor = @window.scale_factor
|
|
45
|
+
end
|
|
46
|
+
@window.on_close do
|
|
47
|
+
if !@closing && (@capture.running? || document&.dirty?)
|
|
48
|
+
request_destructive { @closing = true; @window.close }
|
|
49
|
+
false
|
|
50
|
+
else
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
@window.on_state_change do |state|
|
|
55
|
+
@preferences.set("layout.width", state.frame.width.to_i.clamp(320, 65_536))
|
|
56
|
+
@preferences.set("layout.height", state.frame.height.to_i.clamp(240, 65_536))
|
|
57
|
+
end
|
|
58
|
+
Actions.install(self)
|
|
59
|
+
change_theme(preferences.get("appearance.theme"), persist: false)
|
|
60
|
+
@main_view = MainView.new(self).test_id("vk.main")
|
|
61
|
+
@window.draw { @main_view }
|
|
62
|
+
show_error(Vanken::ConfigError.new(preferences.warning)) if preferences.warning
|
|
63
|
+
@app.executor.background do
|
|
64
|
+
infos = Gateway::Interfaces.list
|
|
65
|
+
@app.executor.post do
|
|
66
|
+
next if @closing
|
|
67
|
+
@interface_infos = infos
|
|
68
|
+
changed
|
|
69
|
+
capture_options if @dialog_kind == :capture_options
|
|
70
|
+
end
|
|
71
|
+
rescue StandardError => error
|
|
72
|
+
@app.executor.post { @interface_error = error.message; changed }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def document = @app.read(@entity)[:document]
|
|
77
|
+
def selected_number = @app.read(@entity)[:number]
|
|
78
|
+
def selected_node = @app.read(@entity)[:node]
|
|
79
|
+
def detail_nodes = @app.read(@entity)[:details]
|
|
80
|
+
def selected_bytes = @app.read(@entity)[:bytes]
|
|
81
|
+
def update
|
|
82
|
+
@app.update(@entity) { |state, cx| yield(state); cx.notify }
|
|
83
|
+
end
|
|
84
|
+
def changed
|
|
85
|
+
return if @closing
|
|
86
|
+
@packet_source.refresh do
|
|
87
|
+
next if @closing
|
|
88
|
+
error = document&.error || @capture.error
|
|
89
|
+
@app.update(@entity) { |state, cx| state[:error] = error; cx.notify }
|
|
90
|
+
show_error(error) if error && @last_error != error
|
|
91
|
+
if document && @pending_filter && document.complete?
|
|
92
|
+
@pending_filter = false
|
|
93
|
+
apply_filter
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
def attach_document(value)
|
|
98
|
+
return if @closing
|
|
99
|
+
value.scanner = ->(payload) { filter_pool.submit(payload) } if value
|
|
100
|
+
@selection_generation += 1
|
|
101
|
+
update { |state| state.merge!(document: value, number: nil, node: nil, details: [], bytes: "".b) }
|
|
102
|
+
@packet_source.reset
|
|
103
|
+
@tree&.replace([])
|
|
104
|
+
@hex.bytes = "".b if @hex
|
|
105
|
+
@table&.selection&.clear
|
|
106
|
+
@autoscroll = !!(value && (value.source == :live || value.equal?(@capture.document)) && @preferences.get("packet_list.autoscroll"))
|
|
107
|
+
@table.follow_tail = @autoscroll if @table
|
|
108
|
+
@table.scroll_to(0) if @table && value&.displayed_count&.positive? && !@autoscroll
|
|
109
|
+
@window.title = "Vanken#{value&.path ? " — #{File.basename(value.path)}" : ""}" if @window.respond_to?(:title=)
|
|
110
|
+
end
|
|
111
|
+
def run = @app.run
|
|
112
|
+
def native? = !%i[headless tui].include?(@backend)
|
|
113
|
+
def monospace_font = @window.text_system&.font
|
|
114
|
+
def install_panes(table, tree, hex) = (@table, @tree, @hex = table, tree, hex)
|
|
115
|
+
def filter_pool
|
|
116
|
+
@pool_mutex.synchronize do
|
|
117
|
+
@pool ||= Zaniah::ProcessPool.new(workers: @preferences.get("analysis.workers"),
|
|
118
|
+
handler: "Vanken::Capture::FilterWorker", requires: [File.expand_path("../capture/filter_worker.rb", __dir__)],
|
|
119
|
+
load_paths: $LOAD_PATH.select { |path| File.directory?(path) })
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
def smoke
|
|
123
|
+
3.times { @app.executor.drain; @window.tick }
|
|
124
|
+
end
|
|
125
|
+
def close
|
|
126
|
+
return if @closed
|
|
127
|
+
@closing = true
|
|
128
|
+
@packet_source.reset
|
|
129
|
+
@selection_generation += 1
|
|
130
|
+
@filter_generation += 1
|
|
131
|
+
@file_generation += 1
|
|
132
|
+
@validation_task&.cancel
|
|
133
|
+
@capture.close
|
|
134
|
+
document&.close
|
|
135
|
+
@pool&.shutdown
|
|
136
|
+
@app.executor.shutdown
|
|
137
|
+
@app.executor.drain
|
|
138
|
+
@window.close
|
|
139
|
+
@log.close
|
|
140
|
+
@closed = true
|
|
141
|
+
end
|
|
142
|
+
def quit = request_destructive { @closing = true; @window.close }
|
|
143
|
+
|
|
144
|
+
def change_theme(name, persist: true)
|
|
145
|
+
theme = name == "system" ? Zaniah::Theme.for(@window.appearance) : Zaniah::Theme.public_send(name)
|
|
146
|
+
size = @preferences.get("appearance.font_size")
|
|
147
|
+
theme = theme.with(typography: theme.typography.with(size_md: size, size_sm: size - 1, size_xs: size - 2))
|
|
148
|
+
@app.global(:theme, theme)
|
|
149
|
+
@preferences.set("appearance.theme", name) if persist
|
|
150
|
+
@window.request_frame
|
|
151
|
+
end
|
|
152
|
+
def zoom(delta = nil)
|
|
153
|
+
size = delta ? (@preferences.get("appearance.font_size") + delta).clamp(8, 32) : 13
|
|
154
|
+
@preferences.set("appearance.font_size", size)
|
|
155
|
+
change_theme(@preferences.get("appearance.theme"), persist: false)
|
|
156
|
+
end
|
|
157
|
+
def time_format(name)
|
|
158
|
+
@preferences.set("packet_list.time_format", name.to_s)
|
|
159
|
+
@packet_source.reset
|
|
160
|
+
@window.request_frame
|
|
161
|
+
end
|
|
162
|
+
def toggle_autoscroll
|
|
163
|
+
@autoscroll = !@autoscroll
|
|
164
|
+
@preferences.set("packet_list.autoscroll", @autoscroll)
|
|
165
|
+
@table.follow_tail = @autoscroll if @table
|
|
166
|
+
@table.scroll_to(@packet_source.count - 1, align: :end) if @autoscroll && @packet_source.count.positive?
|
|
167
|
+
@window.request_frame
|
|
168
|
+
end
|
|
169
|
+
def autoscroll? = @autoscroll && (!@table || @table.following_tail?)
|
|
170
|
+
def capture_toggle
|
|
171
|
+
return @capture.stop if @capture.running?
|
|
172
|
+
capture_options
|
|
173
|
+
end
|
|
174
|
+
def start_capture(options)
|
|
175
|
+
request_destructive do
|
|
176
|
+
dismiss_dialog
|
|
177
|
+
old = document
|
|
178
|
+
@app.executor.background { old&.close; @capture.start(options) unless @closing }
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
def restart_capture
|
|
182
|
+
request_destructive do
|
|
183
|
+
old = document
|
|
184
|
+
@capture.stop
|
|
185
|
+
@app.executor.background { @capture.wait; old&.close; @capture.start(@capture.options) unless @closing }
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
def status_text
|
|
189
|
+
doc = document
|
|
190
|
+
return "キャプチャファイルを開くか、インタフェースを選んで開始してください" unless doc
|
|
191
|
+
dropped = @capture.stats.fetch("dropped", @capture.stats.fetch(:dropped, 0))
|
|
192
|
+
total = doc.store.durable_count
|
|
193
|
+
"#{doc.loading? ? '読み込み中' : 'パケット'} #{@packet_source.packet_count} / #{total} 表示 #{@packet_source.count} ドロップ #{dropped}#{doc.progress ? " #{(doc.progress * 100).round}%" : ''}"
|
|
194
|
+
end
|
|
195
|
+
def copy_text(text) = @window.write_clipboard([Zaniah::Clipboard::Item.new({"text/plain" => text})])
|
|
196
|
+
def show_error(error)
|
|
197
|
+
@log.error("#{error.class}: #{error.message}")
|
|
198
|
+
@last_error = error
|
|
199
|
+
@dialog_kind = :error
|
|
200
|
+
@dialog = Zaniah::UI::Dialog.new(Zaniah::UI::Label.new(error.message, wrap: :word), title: "操作を完了できません").test_id("vk.error")
|
|
201
|
+
@window.request_frame
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|