vanken 0.1.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +28 -11
- data/data/coloring_rules.yml +67 -0
- data/docs/performance.md +53 -0
- data/docs/releases.md +2 -4
- data/docs/usage.md +51 -0
- data/exe/vanken-setup-permissions +6 -0
- data/lib/vanken/app/capture_controller.rb +66 -1
- data/lib/vanken/app/document.rb +102 -39
- data/lib/vanken/app/document_analysis.rb +75 -0
- data/lib/vanken/app/document_jobs.rb +52 -9
- data/lib/vanken/app/expert_info.rb +26 -0
- data/lib/vanken/app/io_graph.rb +47 -0
- data/lib/vanken/app/navigation.rb +77 -0
- data/lib/vanken/app/search_job.rb +62 -0
- data/lib/vanken/app/stats_job.rb +45 -0
- data/lib/vanken/capture/analyzer.rb +3 -1
- data/lib/vanken/capture/analyzer_process.rb +9 -6
- data/lib/vanken/capture/analyzer_worker.rb +2 -2
- data/lib/vanken/capture/filter_worker.rb +8 -2
- data/lib/vanken/capture/helper_main.rb +10 -3
- data/lib/vanken/capture/helper_options.rb +6 -2
- data/lib/vanken/capture/launcher.rb +1 -0
- data/lib/vanken/capture/permission_setup.rb +201 -0
- data/lib/vanken/config/analysis_settings.rb +58 -0
- data/lib/vanken/config/columns.rb +138 -0
- data/lib/vanken/config/messages.rb +105 -0
- data/lib/vanken/config/preferences.rb +34 -8
- data/lib/vanken/config/profiles.rb +74 -0
- data/lib/vanken/config/sessions.rb +69 -0
- data/lib/vanken/config/yaml_file.rb +36 -0
- data/lib/vanken/core/coloring.rb +86 -0
- data/lib/vanken/core/frame_store.rb +2 -2
- data/lib/vanken/core/stores.rb +12 -0
- data/lib/vanken/gateway/detail_builder.rb +1 -1
- data/lib/vanken/gateway/display_capture_filter.rb +102 -0
- data/lib/vanken/gateway/dissector.rb +31 -6
- data/lib/vanken/gateway/exporter.rb +125 -0
- data/lib/vanken/gateway/file_writer.rb +26 -2
- data/lib/vanken/gateway/interfaces.rb +59 -0
- data/lib/vanken/gateway/resolver.rb +131 -0
- data/lib/vanken/gateway/statistics.rb +83 -0
- data/lib/vanken/gateway/stream.rb +159 -0
- data/lib/vanken/ui/actions.rb +54 -28
- data/lib/vanken/ui/analysis_dialogs.rb +371 -0
- data/lib/vanken/ui/application.rb +183 -9
- data/lib/vanken/ui/coloring_operations.rb +104 -0
- data/lib/vanken/ui/column_operations.rb +115 -0
- data/lib/vanken/ui/dialogs.rb +41 -26
- data/lib/vanken/ui/file_operations.rb +17 -12
- data/lib/vanken/ui/filter_operations.rb +43 -15
- data/lib/vanken/ui/main_view.rb +74 -29
- data/lib/vanken/ui/navigation_operations.rb +86 -0
- data/lib/vanken/ui/packet_source.rb +33 -1
- data/lib/vanken/ui/selection.rb +8 -2
- data/lib/vanken/ui/settings_operations.rb +233 -0
- data/lib/vanken/version.rb +1 -1
- data/packaging/README.md +41 -1
- data/packaging/linux/org.vanken.capture.policy +14 -0
- data/packaging/linux/vanken-capture.wrapper +6 -0
- data/packaging/linux/vanken.desktop +9 -0
- data/packaging/linux/vanken.sudoers +2 -0
- data/packaging/macos/chmod-bpf +6 -0
- data/packaging/macos/org.vanken.chmod-bpf.plist +8 -0
- data/sig/generated/vanken/app/capture_controller.rbs +16 -0
- data/sig/generated/vanken/app/document.rbs +23 -1
- data/sig/generated/vanken/app/document_analysis.rbs +21 -0
- data/sig/generated/vanken/app/document_jobs.rbs +6 -0
- data/sig/generated/vanken/app/expert_info.rbs +30 -0
- data/sig/generated/vanken/app/io_graph.rbs +45 -0
- data/sig/generated/vanken/app/navigation.rbs +31 -0
- data/sig/generated/vanken/app/search_job.rbs +21 -0
- data/sig/generated/vanken/app/stats_job.rbs +18 -0
- data/sig/generated/vanken/capture/helper_options.rbs +6 -0
- data/sig/generated/vanken/capture/permission_setup.rbs +43 -0
- data/sig/generated/vanken/config/analysis_settings.rbs +31 -0
- data/sig/generated/vanken/config/columns.rbs +37 -0
- data/sig/generated/vanken/config/messages.rbs +14 -0
- data/sig/generated/vanken/config/preferences.rbs +1 -1
- data/sig/generated/vanken/config/profiles.rbs +29 -0
- data/sig/generated/vanken/config/sessions.rbs +17 -0
- data/sig/generated/vanken/config/yaml_file.rbs +11 -0
- data/sig/generated/vanken/core/coloring.rbs +52 -0
- data/sig/generated/vanken/core/stores.rbs +4 -0
- data/sig/generated/vanken/gateway/display_capture_filter.rbs +37 -0
- data/sig/generated/vanken/gateway/dissector.rbs +15 -2
- data/sig/generated/vanken/gateway/exporter.rbs +27 -0
- data/sig/generated/vanken/gateway/file_writer.rbs +3 -0
- data/sig/generated/vanken/gateway/interfaces.rbs +12 -0
- data/sig/generated/vanken/gateway/resolver.rbs +52 -0
- data/sig/generated/vanken/gateway/statistics.rbs +104 -0
- data/sig/generated/vanken/gateway/stream.rbs +75 -0
- data/sig/generated/vanken/ui/analysis_dialogs.rbs +71 -0
- data/sig/generated/vanken/ui/application.rbs +23 -1
- data/sig/generated/vanken/ui/coloring_operations.rbs +29 -0
- data/sig/generated/vanken/ui/column_operations.rbs +29 -0
- data/sig/generated/vanken/ui/dialogs.rbs +5 -3
- data/sig/generated/vanken/ui/file_operations.rbs +2 -0
- data/sig/generated/vanken/ui/filter_operations.rbs +3 -1
- data/sig/generated/vanken/ui/main_view.rbs +14 -2
- data/sig/generated/vanken/ui/navigation_operations.rbs +43 -0
- data/sig/generated/vanken/ui/packet_source.rbs +4 -0
- data/sig/generated/vanken/ui/settings_operations.rbs +41 -0
- metadata +61 -3
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../gateway/statistics"
|
|
4
|
+
|
|
5
|
+
module Vanken
|
|
6
|
+
module App
|
|
7
|
+
# Invoke refresh on the executor; cancel joins its current frame before store close.
|
|
8
|
+
class StatsJob
|
|
9
|
+
def initialize(document, specs: ["phs"], displayed_only: false)
|
|
10
|
+
@document, @specs, @displayed_only = document, specs, displayed_only
|
|
11
|
+
@mutex, @numbers, @cancelled = Mutex.new, [], false
|
|
12
|
+
reset
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def refresh
|
|
16
|
+
@mutex.synchronize do
|
|
17
|
+
return [] if @cancelled || @document.closing?
|
|
18
|
+
numbers = @displayed_only ? @document.display_numbers.sort : (1..@document.count).to_a
|
|
19
|
+
reset unless numbers.take(@numbers.size) == @numbers
|
|
20
|
+
numbers.drop(@numbers.size).each do |number|
|
|
21
|
+
break if @cancelled || @document.closing?
|
|
22
|
+
@statistics.update(@dissector.dissect(@document.store.read(number)))
|
|
23
|
+
@numbers << number
|
|
24
|
+
end
|
|
25
|
+
@statistics.tables
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def cancel
|
|
30
|
+
@cancelled = true
|
|
31
|
+
@mutex.synchronize { @statistics.close }
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def reset
|
|
38
|
+
@statistics&.close
|
|
39
|
+
@numbers = []
|
|
40
|
+
@dissector = Gateway::Dissector.new(**@document.analysis_gateway_options)
|
|
41
|
+
@statistics = Gateway::Statistics.new(specs: @specs, registry: @dissector.registry, **@document.analysis_options)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -11,7 +11,7 @@ module Vanken
|
|
|
11
11
|
def run
|
|
12
12
|
number = 1
|
|
13
13
|
loop do
|
|
14
|
-
break if @document.
|
|
14
|
+
break if @document.analysis_stopped?
|
|
15
15
|
if number > @document.store.durable_count
|
|
16
16
|
break if @document.received? && number > @document.store.durable_count
|
|
17
17
|
@document.wait_for_frames
|
|
@@ -20,11 +20,13 @@ module Vanken
|
|
|
20
20
|
frame = @document.store.read(number)
|
|
21
21
|
begin
|
|
22
22
|
packet = @dissector.dissect(frame)
|
|
23
|
+
break if @document.analysis_stopped?
|
|
23
24
|
@analysis.update(packet)
|
|
24
25
|
rescue StandardError => error
|
|
25
26
|
@document.publish_failure(number, error)
|
|
26
27
|
packet = nil
|
|
27
28
|
end
|
|
29
|
+
break if @document.analysis_stopped?
|
|
28
30
|
@document.publish(number, packet) if packet
|
|
29
31
|
number += 1
|
|
30
32
|
if number % 256 == 0
|
|
@@ -11,6 +11,7 @@ module Vanken
|
|
|
11
11
|
@document, @verify_checksums = document, verify_checksums
|
|
12
12
|
end
|
|
13
13
|
def run
|
|
14
|
+
return if @document.analysis_stopped?
|
|
14
15
|
unless Gem.win_platform?
|
|
15
16
|
raise Vanken::Error, "packet analysis must run without root privileges" if Process.uid.zero? || Process.euid.zero?
|
|
16
17
|
end
|
|
@@ -23,10 +24,11 @@ module Vanken
|
|
|
23
24
|
@pid = Process.spawn(*arguments, in: child_in, out: child_out, close_others: true)
|
|
24
25
|
child_in.close
|
|
25
26
|
child_out.close
|
|
26
|
-
request(command: :start, spool: @document.store.directory, verify_checksums: @verify_checksums, analysis_options: @document.analysis_options
|
|
27
|
+
request(command: :start, spool: @document.store.directory, verify_checksums: @verify_checksums, analysis_options: @document.analysis_options,
|
|
28
|
+
gateway_options: @document.analysis_gateway_options)
|
|
27
29
|
number = 1
|
|
28
30
|
loop do
|
|
29
|
-
break if @document.
|
|
31
|
+
break if @document.analysis_stopped?
|
|
30
32
|
limit = @document.store.durable_count
|
|
31
33
|
if number > limit
|
|
32
34
|
break if @document.received? && number > @document.store.durable_count
|
|
@@ -36,13 +38,14 @@ module Vanken
|
|
|
36
38
|
last = [number + 255, limit].min
|
|
37
39
|
configuration = @document.analysis_configuration(number, last)
|
|
38
40
|
batch = request(configuration.merge(command: :analyze, first: number, last: last, interfaces: @document.store.interfaces))
|
|
41
|
+
break if @document.analysis_stopped?
|
|
39
42
|
raise Vanken::Error, "analysis response range mismatch" unless batch[:first] == number && batch[:last] == last
|
|
40
43
|
@document.publish_analysis_batch(batch, self, configuration[:generation], context_token: configuration[:context_token])
|
|
41
44
|
number = last + 1
|
|
42
45
|
end
|
|
43
46
|
rescue StandardError
|
|
44
|
-
@document.cancel
|
|
45
|
-
raise unless @document.
|
|
47
|
+
@document.cancel unless @document.analysis_stopped?
|
|
48
|
+
raise unless @document.analysis_stopped?
|
|
46
49
|
ensure
|
|
47
50
|
child_in&.close unless child_in&.closed?
|
|
48
51
|
child_out&.close unless child_out&.closed?
|
|
@@ -54,7 +57,7 @@ module Vanken
|
|
|
54
57
|
end
|
|
55
58
|
def request(value)
|
|
56
59
|
AnalyzerWire.write(@input, value)
|
|
57
|
-
response = AnalyzerWire.read(@output, cancelled: -> { @document.
|
|
60
|
+
response = AnalyzerWire.read(@output, cancelled: -> { @document.analysis_stopped? })
|
|
58
61
|
raise Vanken::Error, response[:error] if response[:error]
|
|
59
62
|
response
|
|
60
63
|
end
|
|
@@ -82,7 +85,7 @@ module Vanken
|
|
|
82
85
|
end
|
|
83
86
|
sleep(0.005)
|
|
84
87
|
end
|
|
85
|
-
raise Vanken::Error, "analysis worker failed while stopping" unless status.success? || @document.
|
|
88
|
+
raise Vanken::Error, "analysis worker failed while stopping" unless status.success? || @document.analysis_stopped? || $!
|
|
86
89
|
rescue Errno::ECHILD, Errno::ESRCH
|
|
87
90
|
nil
|
|
88
91
|
end
|
|
@@ -57,11 +57,11 @@ module Vanken
|
|
|
57
57
|
|
|
58
58
|
def initialize(request)
|
|
59
59
|
@reader = Reader.new(request.fetch(:spool))
|
|
60
|
-
@dissector = Gateway::Dissector.new(verify_checksums: request.fetch(:verify_checksums))
|
|
60
|
+
@dissector = Gateway::Dissector.new(**request.fetch(:gateway_options, {verify_checksums: request.fetch(:verify_checksums)}))
|
|
61
61
|
@analysis = Gateway::Analysis.new(registry: @dissector.registry, **request.fetch(:analysis_options))
|
|
62
62
|
@columns = Core::ColumnStore.new
|
|
63
63
|
@annotations = Core::AnnotationStore.new(request.fetch(:spool), persist: false)
|
|
64
|
-
@catalog = Gateway::FieldCatalog.new
|
|
64
|
+
@catalog = Gateway::FieldCatalog.new(registry: @dissector.registry)
|
|
65
65
|
@seen_fields = @catalog.names.to_set
|
|
66
66
|
@context = Context.new(@reader)
|
|
67
67
|
@next = 1
|
|
@@ -8,6 +8,7 @@ require_relative "../core/frame_store"
|
|
|
8
8
|
require_relative "../core/stores"
|
|
9
9
|
require_relative "../core/display_filter/compiler"
|
|
10
10
|
require_relative "../gateway/dissector"
|
|
11
|
+
require_relative "../gateway/display_capture_filter"
|
|
11
12
|
|
|
12
13
|
module Vanken
|
|
13
14
|
module Capture
|
|
@@ -153,12 +154,17 @@ module Vanken
|
|
|
153
154
|
raise ArgumentError, "filter chunk exceeds stored frames" unless last <= reader.count + 1
|
|
154
155
|
|
|
155
156
|
dissector = Gateway::Dissector.new(decode_as: payload.fetch("decode_as", []), plugins: payload.fetch("plugins", []))
|
|
156
|
-
program = Core::DisplayFilter.compile(payload.fetch("expr"), catalog: Gateway::FieldCatalog.new)
|
|
157
|
+
program = Core::DisplayFilter.compile(payload.fetch("expr"), catalog: Gateway::FieldCatalog.new(registry: dissector.registry))
|
|
158
|
+
capture = Gateway::DisplayCaptureFilter.new(payload.fetch("expr")) if payload.fetch("plugins", []).empty? && payload.fetch("decode_as", []).empty?
|
|
159
|
+
capture = nil unless capture&.expression
|
|
157
160
|
context = {marked: Set.new(payload.fetch("marked", [])), ignored: Set.new(payload.fetch("ignored", [])),
|
|
158
161
|
reference: payload.fetch("time_reference_ns") { reader.count.zero? ? 0 : reader.metadata(1)[:timestamp_ns] },
|
|
159
162
|
references: payload.fetch("time_references_ns", {}).map { |number, timestamp| [Integer(number), timestamp] }.sort_by(&:first),
|
|
160
163
|
predecessors: payload.fetch("displayed_predecessors", {})}
|
|
161
|
-
matches = (first...last).select
|
|
164
|
+
matches = (first...last).select do |number|
|
|
165
|
+
matched = capture&.match(reader.frame(number))
|
|
166
|
+
matched.nil? ? program.match?(View.new(reader, number, dissector, context)) : matched
|
|
167
|
+
end
|
|
162
168
|
{"matches" => matches}
|
|
163
169
|
ensure
|
|
164
170
|
reader&.close
|
|
@@ -23,7 +23,7 @@ module Vanken
|
|
|
23
23
|
when :list_interfaces then @stdout.puts(JSON.generate(Gateway::Interfaces.list)); return 0
|
|
24
24
|
when :check then return check(options)
|
|
25
25
|
when :version then @stdout.puts("Vanken #{VERSION}"); return 0
|
|
26
|
-
when :help then @stdout.puts("vanken-capture -i INTERFACE [--filter EXPR] [--snaplen N] [--no-promiscuous]\n [--buffer-size BYTES] [--direction in|out|inout] [--backend auto|ring|socket|bpf]\n [--drop-to UID:GID] [--stats-interval SEC] [--flush-interval SEC]\n --list-interfaces | --check [--interface IF] | --version"); return 0
|
|
26
|
+
when :help then @stdout.puts("vanken-capture -i INTERFACE [--filter EXPR] [--snaplen N] [--no-promiscuous]\n [--buffer-size BYTES] [--direction in|out|inout] [--backend auto|ring|socket|bpf]\n [--drop-to UID:GID] [--stats-interval SEC] [--flush-interval SEC]\n [--stop-count N] [--stop-duration SEC] [--stop-bytes BYTES]\n --list-interfaces | --check [--interface IF] | --version"); return 0
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
capture(options)
|
|
@@ -64,11 +64,18 @@ module Vanken
|
|
|
64
64
|
end
|
|
65
65
|
@control.write(:started, interface: options.capture[:interface], linktype: source.linktype,
|
|
66
66
|
snaplen: options.capture[:snaplen], backend: source.backend, filter: options.capture[:filter], privileges_dropped: dropped)
|
|
67
|
-
flushed_at = stats_at = monotonic
|
|
67
|
+
started_at = flushed_at = stats_at = monotonic
|
|
68
|
+
captured = 0
|
|
68
69
|
until @stop_reason || source.stopped?
|
|
69
70
|
packet = source.next_packet(timeout: 0.05)
|
|
70
|
-
|
|
71
|
+
if packet
|
|
72
|
+
writer << packet
|
|
73
|
+
captured += 1
|
|
74
|
+
end
|
|
71
75
|
now = monotonic
|
|
76
|
+
@stop_reason ||= "count_limit" if options.stop_count && captured >= options.stop_count
|
|
77
|
+
@stop_reason ||= "duration_limit" if options.stop_duration && now - started_at >= options.stop_duration
|
|
78
|
+
@stop_reason ||= "bytes_limit" if options.stop_bytes && writer.bytes_written >= options.stop_bytes
|
|
72
79
|
if now - flushed_at >= options.flush_interval
|
|
73
80
|
writer.flush
|
|
74
81
|
flushed_at = now
|
|
@@ -5,7 +5,8 @@ require "optparse"
|
|
|
5
5
|
module Vanken
|
|
6
6
|
module Capture
|
|
7
7
|
class HelperOptions
|
|
8
|
-
attr_reader :command, :capture, :drop_to, :stats_interval, :flush_interval
|
|
8
|
+
attr_reader :command, :capture, :drop_to, :stats_interval, :flush_interval,
|
|
9
|
+
:stop_count, :stop_duration, :stop_bytes
|
|
9
10
|
|
|
10
11
|
def self.parse(argv)
|
|
11
12
|
new.parse(argv)
|
|
@@ -36,13 +37,16 @@ module Vanken
|
|
|
36
37
|
end
|
|
37
38
|
opts.on("--stats-interval SEC", Float) { |v| @stats_interval = bounded(v, 0.01..60, "stats-interval") }
|
|
38
39
|
opts.on("--flush-interval SEC", Float) { |v| @flush_interval = bounded(v, 0.001..1, "flush-interval") }
|
|
40
|
+
opts.on("--stop-count N", Integer) { |v| @stop_count = bounded(v, 1..9_223_372_036_854_775_807, "stop-count") }
|
|
41
|
+
opts.on("--stop-duration SEC", Float) { |v| @stop_duration = bounded(v, 0.001..31_536_000, "stop-duration") }
|
|
42
|
+
opts.on("--stop-bytes N", Integer) { |v| @stop_bytes = bounded(v, 1..9_223_372_036_854_775_807, "stop-bytes") }
|
|
39
43
|
opts.on("--list-interfaces") { modes << :list_interfaces }
|
|
40
44
|
opts.on("--check") { modes << :check }
|
|
41
45
|
opts.on("--version") { modes << :version }
|
|
42
46
|
opts.on("-h", "--help") { modes << :help }
|
|
43
47
|
end
|
|
44
48
|
# OptionParser accepts long-option abbreviations by default; privileged inputs must be exact.
|
|
45
|
-
known = %w[--interface --filter --snaplen --no-promiscuous --buffer-size --direction --backend --drop-to --stats-interval --flush-interval --list-interfaces --check --version --help]
|
|
49
|
+
known = %w[--interface --filter --snaplen --no-promiscuous --buffer-size --direction --backend --drop-to --stats-interval --flush-interval --stop-count --stop-duration --stop-bytes --list-interfaces --check --version --help]
|
|
46
50
|
argv.each do |arg|
|
|
47
51
|
raise ArgumentError, "unknown option: #{arg}" if arg.start_with?("--") && !known.include?(arg.split("=", 2).first)
|
|
48
52
|
end
|
|
@@ -171,6 +171,7 @@ module Vanken
|
|
|
171
171
|
def capture_argv(options)
|
|
172
172
|
options.flat_map do |key, value|
|
|
173
173
|
next [] if value.nil?
|
|
174
|
+
next [] if %i[stop_count stop_duration stop_bytes].include?(key.to_sym) && value == 0
|
|
174
175
|
next value ? [] : ["--no-promiscuous"] if key.to_sym == :promiscuous
|
|
175
176
|
|
|
176
177
|
["--#{key.to_s.tr('_', '-')}", value.to_s]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require "json"
|
|
5
|
+
require "etc"
|
|
6
|
+
require "shellwords"
|
|
7
|
+
require "tempfile"
|
|
8
|
+
require "open3"
|
|
9
|
+
|
|
10
|
+
module Vanken
|
|
11
|
+
module Capture
|
|
12
|
+
class PermissionSetup
|
|
13
|
+
PREFIX = "/usr/local/libexec/vanken"
|
|
14
|
+
SOURCE = File.expand_path("../../..", __dir__)
|
|
15
|
+
|
|
16
|
+
def self.run(argv, stdout: $stdout, stderr: $stderr)
|
|
17
|
+
options = {}
|
|
18
|
+
dry_run = false
|
|
19
|
+
parser = OptionParser.new do |flags|
|
|
20
|
+
flags.banner = "Usage: vanken-setup-permissions [--dry-run] [--ruby PATH --gem-home PATH] [--policy polkit|sudoers] [--group GROUP]"
|
|
21
|
+
flags.on("--platform VALUE", %w[linux darwin]) { |value| options[:platform] = value.to_sym }
|
|
22
|
+
flags.on("--ruby PATH") { |value| options[:ruby] = value }
|
|
23
|
+
flags.on("--gem-home PATH") { |value| options[:gem_home] = value }
|
|
24
|
+
flags.on("--policy VALUE", %w[polkit sudoers]) { |value| options[:policy] = value.to_sym }
|
|
25
|
+
flags.on("--group GROUP") { |value| options[:group] = value }
|
|
26
|
+
flags.on("--root DIRECTORY", "Stage files without activating system permissions") { |value| options[:root] = value }
|
|
27
|
+
flags.on("--dry-run") { dry_run = true }
|
|
28
|
+
flags.on("-h", "--help") { stdout.puts(flags); return 0 }
|
|
29
|
+
end
|
|
30
|
+
known = %w[--platform --ruby --gem-home --policy --group --root --dry-run --help]
|
|
31
|
+
argv.each do |value|
|
|
32
|
+
raise ArgumentError, "unknown option: #{value}" if value.start_with?("--") && !known.include?(value.split("=", 2).first)
|
|
33
|
+
end
|
|
34
|
+
remaining = parser.parse(argv.dup)
|
|
35
|
+
raise ArgumentError, "unexpected arguments: #{remaining.join(' ')}" unless remaining.empty?
|
|
36
|
+
setup = new(**options)
|
|
37
|
+
setup.install unless dry_run
|
|
38
|
+
stdout.puts(JSON.pretty_generate(platform: setup.platform, dry_run: dry_run, staged: setup.staged?,
|
|
39
|
+
files: setup.plan.keys, next_steps: setup.next_steps))
|
|
40
|
+
0
|
|
41
|
+
rescue ArgumentError, OptionParser::ParseError => error
|
|
42
|
+
stderr.puts(error.message)
|
|
43
|
+
2
|
|
44
|
+
rescue SystemCallError, IOError => error
|
|
45
|
+
stderr.puts(error.message)
|
|
46
|
+
1
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
attr_reader :platform
|
|
50
|
+
|
|
51
|
+
def initialize(platform: nil,
|
|
52
|
+
ruby: nil, gem_home: nil, policy: :polkit, group: "vanken", root: "/")
|
|
53
|
+
platform ||= RUBY_PLATFORM.include?("darwin") ? :darwin : (RUBY_PLATFORM.include?("linux") ? :linux : :unsupported)
|
|
54
|
+
@platform, @ruby, @gem_home, @policy, @group = platform.to_sym, ruby, gem_home, policy.to_sym, group
|
|
55
|
+
raise ArgumentError, "unsupported platform" unless %i[linux darwin].include?(@platform)
|
|
56
|
+
raise ArgumentError, "invalid policy" unless %i[polkit sudoers].include?(@policy)
|
|
57
|
+
raise ArgumentError, "invalid capture group" unless @group.match?(/\A[a-z_][a-z0-9_-]{0,31}\z/)
|
|
58
|
+
if @platform == :linux
|
|
59
|
+
[@ruby, @gem_home].each do |path|
|
|
60
|
+
raise ArgumentError, "Linux setup requires absolute --ruby and --gem-home paths" unless path&.start_with?("/") && !path.include?("\n")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
raise ArgumentError, "staging root must be an existing absolute directory" unless root.start_with?("/") && File.directory?(root) && !File.symlink?(root)
|
|
64
|
+
@root = File.realpath(root)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def staged? = @root != "/"
|
|
68
|
+
|
|
69
|
+
def plan
|
|
70
|
+
if @platform == :darwin
|
|
71
|
+
return {"#{PREFIX}/chmod-bpf" => asset("macos/chmod-bpf", mode: 0o755),
|
|
72
|
+
"/Library/LaunchDaemons/org.vanken.chmod-bpf.plist" => asset("macos/org.vanken.chmod-bpf.plist")}
|
|
73
|
+
end
|
|
74
|
+
files = {"#{PREFIX}/vanken-capture" => asset("linux/vanken-capture.wrapper", mode: 0o755),
|
|
75
|
+
"#{PREFIX}/helper" => {contents: File.binread(File.join(SOURCE, "exe/vanken-capture")), mode: 0o755},
|
|
76
|
+
"/usr/share/applications/vanken.desktop" => asset("linux/vanken.desktop")}
|
|
77
|
+
if @policy == :polkit
|
|
78
|
+
files["/usr/share/polkit-1/actions/org.vanken.capture.policy"] = asset("linux/org.vanken.capture.policy")
|
|
79
|
+
else
|
|
80
|
+
files["/etc/sudoers.d/vanken"] = asset("linux/vanken.sudoers", mode: 0o440)
|
|
81
|
+
end
|
|
82
|
+
Dir.glob(File.join(SOURCE, "lib", "**", "*"), File::FNM_DOTMATCH).each do |path|
|
|
83
|
+
raise ArgumentError, "source library contains a symlink" if File.symlink?(path)
|
|
84
|
+
next if File.directory?(path)
|
|
85
|
+
files["#{PREFIX}/#{path.delete_prefix("#{SOURCE}/")}"] = {contents: File.binread(path), mode: 0o644}
|
|
86
|
+
end
|
|
87
|
+
files
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def install
|
|
91
|
+
raise ArgumentError, "system installation requires root; use --dry-run or --root for review" unless staged? || Process.euid.zero?
|
|
92
|
+
raise ArgumentError, "use --root or --dry-run to prepare files for another platform" unless staged? || RUBY_PLATFORM.include?(@platform.to_s)
|
|
93
|
+
validate_runtime! if @platform == :linux && !staged?
|
|
94
|
+
validate_group! if @platform == :darwin || @policy == :sudoers
|
|
95
|
+
plan.each do |absolute, item|
|
|
96
|
+
destination = File.join(@root, absolute.delete_prefix("/"))
|
|
97
|
+
secure_directory!(File.dirname(destination))
|
|
98
|
+
safe_entry!(destination) if File.exist?(destination) || File.symlink?(destination)
|
|
99
|
+
Tempfile.create([".vanken-", ".tmp"], File.dirname(destination)) do |file|
|
|
100
|
+
file.binmode
|
|
101
|
+
file.write(item[:contents])
|
|
102
|
+
file.chmod(item[:mode])
|
|
103
|
+
file.flush
|
|
104
|
+
validate_sudoers!(file.path) if absolute == "/etc/sudoers.d/vanken" && !staged?
|
|
105
|
+
File.rename(file.path, destination)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def validate_runtime!
|
|
112
|
+
trusted_tree!(@ruby)
|
|
113
|
+
raise ArgumentError, "Ruby interpreter must be executable" unless File.file?(@ruby) && File.executable?(@ruby)
|
|
114
|
+
runtime_prefix = File.dirname(File.dirname(File.realpath(@ruby)))
|
|
115
|
+
trusted_tree!(runtime_prefix) unless ["/", "/usr"].include?(runtime_prefix)
|
|
116
|
+
paths, status = Open3.capture2({"PATH" => "/usr/bin:/bin"}, @ruby, "--disable-gems", "-e", "$LOAD_PATH.each { |path| puts path }", unsetenv_others: true, chdir: "/")
|
|
117
|
+
raise ArgumentError, "could not inspect the Ruby runtime" unless status.success?
|
|
118
|
+
paths.each_line { |path| trusted_tree!(path.strip) }
|
|
119
|
+
trusted_tree!(@gem_home)
|
|
120
|
+
raise ArgumentError, "gem home must be a directory" unless File.directory?(@gem_home)
|
|
121
|
+
trusted_tree!(SOURCE)
|
|
122
|
+
self
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def validate_group!
|
|
126
|
+
Etc.getgrnam(@group)
|
|
127
|
+
rescue ArgumentError
|
|
128
|
+
raise ArgumentError, "create the #{@group} capture group and add the intended users before installing"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def next_steps
|
|
132
|
+
if @platform == :darwin
|
|
133
|
+
["sudo launchctl bootstrap system /Library/LaunchDaemons/org.vanken.chmod-bpf.plist",
|
|
134
|
+
"Log out and log in after changing group membership, then run vanken-capture --check --interface lo0."]
|
|
135
|
+
else
|
|
136
|
+
["Run vanken-capture --check --interface IF as a normal user, then start Vanken."]
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
def asset(relative, mode: 0o644)
|
|
143
|
+
contents = File.binread(File.join(SOURCE, "packaging", relative)).gsub("VANKEN_GROUP", @group)
|
|
144
|
+
contents = contents.gsub("VANKEN_RUBY", Shellwords.escape(@ruby.to_s)).gsub("VANKEN_GEM_HOME", Shellwords.escape(@gem_home.to_s))
|
|
145
|
+
{contents: contents, mode: mode}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def trusted_tree!(path, visited = {})
|
|
149
|
+
resolved = trusted_path!(path)
|
|
150
|
+
return if visited[resolved]
|
|
151
|
+
visited[resolved] = true
|
|
152
|
+
return unless File.directory?(resolved)
|
|
153
|
+
Dir.children(resolved).each { |name| trusted_tree!(File.join(resolved, name), visited) }
|
|
154
|
+
rescue SystemCallError => error
|
|
155
|
+
raise ArgumentError, "cannot verify root-owned privileged path: #{error.message}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def trusted_path!(path)
|
|
159
|
+
resolved = File.realpath(path)
|
|
160
|
+
current = path
|
|
161
|
+
loop do
|
|
162
|
+
stat = File.lstat(current)
|
|
163
|
+
unless stat.uid.zero? && (stat.symlink? || (stat.mode & 0o022).zero?) && (stat.file? || stat.directory? || stat.symlink?)
|
|
164
|
+
raise ArgumentError, "privileged paths must be root-owned without group or other write access"
|
|
165
|
+
end
|
|
166
|
+
if stat.symlink?
|
|
167
|
+
target = File.readlink(current)
|
|
168
|
+
trusted_path!(target.start_with?("/") ? target : File.join(File.dirname(current), target))
|
|
169
|
+
end
|
|
170
|
+
break if current == "/"
|
|
171
|
+
current = File.dirname(current)
|
|
172
|
+
end
|
|
173
|
+
trusted_path!(resolved) if path != resolved
|
|
174
|
+
resolved
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def safe_entry!(path)
|
|
178
|
+
stat = File.lstat(path)
|
|
179
|
+
raise ArgumentError, "unsafe installation path or symlink: #{path}" unless stat.uid == Process.euid && (stat.mode & 0o022).zero? && (stat.file? || stat.directory?)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def secure_directory!(path)
|
|
183
|
+
safe_entry!(@root)
|
|
184
|
+
current = @root
|
|
185
|
+
path.delete_prefix(@root).split("/").reject(&:empty?).each do |part|
|
|
186
|
+
current = File.join(current, part)
|
|
187
|
+
Dir.mkdir(current, 0o755) unless File.exist?(current) || File.symlink?(current)
|
|
188
|
+
safe_entry!(current)
|
|
189
|
+
raise ArgumentError, "installation parent is not a directory" unless File.directory?(current)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def validate_sudoers!(path)
|
|
194
|
+
executable = %w[/usr/sbin/visudo /sbin/visudo].find { |candidate| File.executable?(candidate) }
|
|
195
|
+
raise ArgumentError, "visudo is required to install sudoers permissions" unless executable
|
|
196
|
+
_, errors, status = Open3.capture3(executable, "-cf", path)
|
|
197
|
+
raise ArgumentError, "invalid sudoers configuration: #{errors}" unless status.success?
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "yaml_file"
|
|
4
|
+
|
|
5
|
+
module Vanken
|
|
6
|
+
module Config
|
|
7
|
+
class AnalysisSettings
|
|
8
|
+
def initialize(directory: Paths.config) = (@directory = directory)
|
|
9
|
+
|
|
10
|
+
def decode_as
|
|
11
|
+
value = YamlFile.read(File.join(@directory, "decode_as.yml"), default: {"rules" => []}).fetch("rules", [])
|
|
12
|
+
validate_rules(value)
|
|
13
|
+
value
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def decode_as=(rules)
|
|
17
|
+
validate_rules(rules)
|
|
18
|
+
YamlFile.write(File.join(@directory, "decode_as.yml"), "rules" => rules)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def plugin_entries
|
|
22
|
+
entries = YamlFile.read(File.join(@directory, "plugins.yml"), default: {"files" => []}).fetch("files", [])
|
|
23
|
+
raise ConfigError, "invalid plugin list" unless entries.is_a?(Array) && entries.all? do |entry|
|
|
24
|
+
entry.is_a?(Hash) && entry["path"].is_a?(String) && !entry["path"].include?("\0") &&
|
|
25
|
+
[true, false].include?(entry["trusted"]) && [true, false].include?(entry["enabled"])
|
|
26
|
+
end
|
|
27
|
+
entries
|
|
28
|
+
end
|
|
29
|
+
def plugins = plugin_entries.select { |entry| entry["enabled"] && entry["trusted"] }.map { |entry| entry["path"] }
|
|
30
|
+
def untrusted_plugins = plugin_entries.select { |entry| entry["enabled"] && !entry["trusted"] }.map { |entry| entry["path"] }
|
|
31
|
+
|
|
32
|
+
def register_plugin(path)
|
|
33
|
+
raise ConfigError, "invalid plugin path" unless path.is_a?(String) && !path.include?("\0") && File.file?(path)
|
|
34
|
+
absolute = File.expand_path(path)
|
|
35
|
+
entries = plugin_entries.reject { |entry| entry["path"] == absolute }
|
|
36
|
+
entries << {"path" => absolute, "enabled" => true, "trusted" => false}
|
|
37
|
+
save_plugins(entries)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def remove_plugin(path) = save_plugins(plugin_entries.reject { |entry| entry["path"] == File.expand_path(path) })
|
|
41
|
+
|
|
42
|
+
def trust_plugins(paths)
|
|
43
|
+
entries = plugin_entries
|
|
44
|
+
raise ConfigError, "plugin is not registered" unless paths.all? { |path| entries.any? { |entry| entry["path"] == path } }
|
|
45
|
+
entries.each { |entry| entry["trusted"] = true if paths.include?(entry["path"]) }
|
|
46
|
+
save_plugins(entries)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def save_plugins(entries) = YamlFile.write(File.join(@directory, "plugins.yml"), "files" => entries)
|
|
52
|
+
def validate_rules(rules)
|
|
53
|
+
valid = rules.is_a?(Array) && rules.all? { |rule| rule.is_a?(String) && rule.match?(/\A[a-z][a-z0-9_.]*==[^,\s]+,[a-z][a-z0-9_]*\z/i) }
|
|
54
|
+
raise ConfigError, "invalid Decode As rules" unless valid
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
require_relative "yaml_file"
|
|
5
|
+
|
|
6
|
+
module Vanken
|
|
7
|
+
module Config
|
|
8
|
+
class Columns
|
|
9
|
+
BUILTINS = [[:no, "No.", 64], [:time, "Time", 128], [:source, "Source", 164],
|
|
10
|
+
[:destination, "Destination", 164], [:protocol, "Protocol", 90], [:length, "Length", 76], [:info, "Info", 560]].freeze
|
|
11
|
+
|
|
12
|
+
def self.load(directory:, legacy: [])
|
|
13
|
+
path = File.join(directory, "columns.yml")
|
|
14
|
+
raise ConfigError, "columns configuration must not be a symlink" if File.symlink?(path)
|
|
15
|
+
return new(YamlFile.read(path)) if File.file?(path)
|
|
16
|
+
settings = new
|
|
17
|
+
unless legacy.empty?
|
|
18
|
+
ordered = settings.to_h["columns"].sort_by { |column| legacy.index { |item| item["key"] == column["key"] } || legacy.size }
|
|
19
|
+
ordered.each do |column|
|
|
20
|
+
saved = legacy.find { |item| item["key"] == column["key"] }
|
|
21
|
+
column.merge!(saved.slice("width", "visible")) if saved
|
|
22
|
+
end
|
|
23
|
+
settings.replace("schema_version" => 1, "columns" => ordered)
|
|
24
|
+
end
|
|
25
|
+
settings
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(payload = nil)
|
|
29
|
+
payload ||= {"schema_version" => 1, "columns" => BUILTINS.map do |key, label, width|
|
|
30
|
+
{"key" => key.to_s, "label" => label, "width" => width, "visible" => true, "field" => nil}
|
|
31
|
+
end}
|
|
32
|
+
replace(payload)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def replace(payload)
|
|
36
|
+
raise ConfigError, "unsupported columns schema" unless payload.is_a?(Hash) && payload["schema_version"] == 1
|
|
37
|
+
items = payload["columns"]
|
|
38
|
+
raise ConfigError, "columns must contain between 1 and 64 entries" unless items.is_a?(Array) && items.size.between?(1, 64)
|
|
39
|
+
rows = items.map do |item|
|
|
40
|
+
raise ConfigError, "invalid column" unless item.is_a?(Hash)
|
|
41
|
+
key, label, width, visible, field = item.values_at("key", "label", "width", "visible", "field")
|
|
42
|
+
custom = self.class.valid_field?(field) && key == "field:#{field}"
|
|
43
|
+
builtin = field.nil? && BUILTINS.any? { |entry| entry.first.to_s == key }
|
|
44
|
+
raise ConfigError, "invalid column key or field" unless key.is_a?(String) && (custom || builtin)
|
|
45
|
+
raise ConfigError, "invalid column label" unless label.is_a?(String) && !label.empty? && label.bytesize <= 256 && !label.include?("\0")
|
|
46
|
+
raise ConfigError, "invalid column width or visibility" unless width.is_a?(Numeric) && width.finite? && width.between?(40, 4096) && [true, false].include?(visible)
|
|
47
|
+
{"key" => key.dup.freeze, "label" => label.dup.freeze, "width" => width, "visible" => visible, "field" => field&.dup&.freeze}.freeze
|
|
48
|
+
end
|
|
49
|
+
raise ConfigError, "duplicate columns" unless rows.map { |item| item["key"] }.uniq.size == rows.size
|
|
50
|
+
@columns = rows.freeze
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.valid_field?(field)
|
|
55
|
+
field.is_a?(String) && field.bytesize <= 256 && /\A[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z0-9_]+)+\z/.match?(field)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_h = {"schema_version" => 1, "columns" => @columns.map(&:dup)}
|
|
59
|
+
def custom = @columns.select { |column| column["field"] }
|
|
60
|
+
def table_columns
|
|
61
|
+
@columns.map do |column|
|
|
62
|
+
key = column.fetch("key").to_sym
|
|
63
|
+
{key: key, label: column.fetch("label"), width: column.fetch("width"), visible: column.fetch("visible"),
|
|
64
|
+
align: %i[no time length].include?(key) ? :end : :start}
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def add(field, label: field, width: 120)
|
|
69
|
+
raise ConfigError, "invalid custom field name" unless self.class.valid_field?(field)
|
|
70
|
+
key = "field:#{field}"
|
|
71
|
+
rows = to_h["columns"]
|
|
72
|
+
existing = rows.find { |item| item["key"] == key }
|
|
73
|
+
if existing
|
|
74
|
+
existing["visible"] = true
|
|
75
|
+
else
|
|
76
|
+
rows << {"key" => key, "label" => label, "width" => width, "visible" => true, "field" => field}
|
|
77
|
+
end
|
|
78
|
+
replace("schema_version" => 1, "columns" => rows)
|
|
79
|
+
key
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def remove(key)
|
|
83
|
+
raise ConfigError, "hide builtin columns instead of removing them" unless custom.any? { |item| item["key"] == key.to_s }
|
|
84
|
+
replace("schema_version" => 1, "columns" => to_h["columns"].reject { |item| item["key"] == key.to_s })
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def move(key, index)
|
|
88
|
+
rows = to_h["columns"]
|
|
89
|
+
raise ConfigError, "column position is outside the list" unless index.is_a?(Integer) && index.between?(0, rows.size - 1)
|
|
90
|
+
item = rows.find { |column| column["key"] == key.to_s } || raise(ConfigError, "unknown column")
|
|
91
|
+
rows.delete(item)
|
|
92
|
+
rows.insert(index, item)
|
|
93
|
+
replace("schema_version" => 1, "columns" => rows)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def update_layout(items)
|
|
97
|
+
rows = items.map do |item|
|
|
98
|
+
key = item.fetch(:key).to_s
|
|
99
|
+
saved = @columns.find { |column| column["key"] == key } || raise(ConfigError, "unknown column")
|
|
100
|
+
saved.merge("width" => item.fetch(:width), "visible" => item.fetch(:visible))
|
|
101
|
+
end
|
|
102
|
+
raise ConfigError, "column layout is incomplete" unless rows.size == @columns.size
|
|
103
|
+
replace("schema_version" => 1, "columns" => rows)
|
|
104
|
+
rescue KeyError, NoMethodError
|
|
105
|
+
raise ConfigError, "invalid column layout"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def save(directory)
|
|
109
|
+
path = File.join(directory, "columns.yml")
|
|
110
|
+
raise ConfigError, "columns configuration must not be a symlink" if File.symlink?(directory) || File.symlink?(path)
|
|
111
|
+
YamlFile.write(path, to_h)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.display(values, type: nil)
|
|
115
|
+
# ponytail: cap a cell at 16 values and 4096 bytes; the details and byte panes retain full data.
|
|
116
|
+
text = values.first(16).map do |value|
|
|
117
|
+
text = type == :bytes && value.is_a?(String) ? value.byteslice(0, 1024).unpack1("H*") : value.to_s.byteslice(0, 1024)
|
|
118
|
+
escaped = text.b.gsub(/[^\x20-\x7e]/n) { |byte| format("\\x%02x", byte.getbyte(0)) }
|
|
119
|
+
escaped += "…" if value.to_s.bytesize > 1024
|
|
120
|
+
escaped
|
|
121
|
+
end.join(", ")
|
|
122
|
+
text = text.byteslice(0, 4096).force_encoding(Encoding::UTF_8).scrub + "…" if text.bytesize > 4096 || values.size > 16
|
|
123
|
+
text
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.sort_value(value, type: nil)
|
|
127
|
+
return [2, 0] if value.nil?
|
|
128
|
+
return [0, value] if value.is_a?(Numeric)
|
|
129
|
+
return [0, value ? 1 : 0] if [true, false].include?(value)
|
|
130
|
+
return [0, IPAddr.new(value.to_s).ipv4? ? 4 : 6, IPAddr.new(value.to_s).to_i] if type == :address
|
|
131
|
+
return [0, value.to_s.delete(".:-").to_i(16)] if type == :mac
|
|
132
|
+
[1, value.to_s.b]
|
|
133
|
+
rescue ArgumentError
|
|
134
|
+
[1, value.to_s.b]
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|