redhound 1.0.0 → 2.0.0.rc1
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 +35 -0
- data/README.md +97 -31
- data/docs/API.md +76 -0
- data/docs/FILTERS.md +37 -0
- data/docs/MIGRATION.md +23 -0
- data/docs/PROTOCOLS.md +45 -0
- data/docs/USAGE.md +82 -0
- data/docs/VALIDATION.md +59 -0
- data/docs/json-schema.json +64 -0
- data/exe/redhound +2 -1
- data/lib/redhound/analysis/flow_key.rb +53 -0
- data/lib/redhound/analysis/flow_table.rb +117 -0
- data/lib/redhound/analysis/ip_reassembler.rb +176 -0
- data/lib/redhound/analysis/stats.rb +138 -0
- data/lib/redhound/analysis/tcp_analysis.rb +43 -0
- data/lib/redhound/analysis/tcp_reassembler.rb +124 -0
- data/lib/redhound/analysis/tcp_stream.rb +151 -0
- data/lib/redhound/analysis.rb +129 -0
- data/lib/redhound/capture/bsd/bpf_device.rb +152 -0
- data/lib/redhound/capture/bsd/constants.rb +29 -0
- data/lib/redhound/capture/file_source.rb +83 -0
- data/lib/redhound/capture/interface.rb +66 -0
- data/lib/redhound/capture/linktype.rb +30 -0
- data/lib/redhound/capture/linux/constants.rb +37 -0
- data/lib/redhound/capture/linux/cooked.rb +18 -0
- data/lib/redhound/capture/linux/ifreq.rb +42 -0
- data/lib/redhound/capture/linux/packet_socket.rb +180 -0
- data/lib/redhound/capture/linux/sockaddr_ll.rb +33 -0
- data/lib/redhound/capture/linux/tpacket_v3.rb +132 -0
- data/lib/redhound/capture/source.rb +79 -0
- data/lib/redhound/capture/stats.rb +22 -0
- data/lib/redhound/capture.rb +69 -0
- data/lib/redhound/cli/command.rb +42 -0
- data/lib/redhound/cli/options.rb +133 -0
- data/lib/redhound/cli/privileges.rb +29 -0
- data/lib/redhound/cli/runner.rb +129 -0
- data/lib/redhound/context.rb +20 -0
- data/lib/redhound/cursor.rb +69 -0
- data/lib/redhound/diagnostic.rb +7 -0
- data/lib/redhound/dissector.rb +138 -0
- data/lib/redhound/engine.rb +66 -0
- data/lib/redhound/errors.rb +38 -0
- data/lib/redhound/field.rb +62 -0
- data/lib/redhound/file/format.rb +56 -0
- data/lib/redhound/file/pcap_reader.rb +43 -0
- data/lib/redhound/file/pcap_writer.rb +72 -0
- data/lib/redhound/file/pcapng_reader.rb +209 -0
- data/lib/redhound/file/pcapng_writer.rb +143 -0
- data/lib/redhound/file/rotating_writer.rb +98 -0
- data/lib/redhound/filter/analyzer.rb +197 -0
- data/lib/redhound/filter/bpf/assembler.rb +121 -0
- data/lib/redhound/filter/bpf/disassembler.rb +58 -0
- data/lib/redhound/filter/bpf/validator.rb +77 -0
- data/lib/redhound/filter/bpf/vm.rb +108 -0
- data/lib/redhound/filter/codegen.rb +413 -0
- data/lib/redhound/filter/lexer.rb +46 -0
- data/lib/redhound/filter/parser.rb +206 -0
- data/lib/redhound/filter/program.rb +55 -0
- data/lib/redhound/filter.rb +30 -0
- data/lib/redhound/layer.rb +75 -0
- data/lib/redhound/output/fields.rb +20 -0
- data/lib/redhound/output/follow.rb +59 -0
- data/lib/redhound/output/hexdump.rb +29 -0
- data/lib/redhound/output/json.rb +32 -0
- data/lib/redhound/output/summary.rb +59 -0
- data/lib/redhound/output/timestamp.rb +34 -0
- data/lib/redhound/output/tree.rb +24 -0
- data/lib/redhound/packet.rb +63 -0
- data/lib/redhound/protocols/arp.rb +39 -0
- data/lib/redhound/protocols/checksum.rb +38 -0
- data/lib/redhound/protocols/data.rb +21 -0
- data/lib/redhound/protocols/dhcp.rb +124 -0
- data/lib/redhound/protocols/dns.rb +268 -0
- data/lib/redhound/protocols/ethernet.rb +24 -0
- data/lib/redhound/protocols/http.rb +183 -0
- data/lib/redhound/protocols/icmp.rb +191 -0
- data/lib/redhound/protocols/ipv4.rb +56 -0
- data/lib/redhound/protocols/ipv6.rb +65 -0
- data/lib/redhound/protocols/link.rb +70 -0
- data/lib/redhound/protocols/llc.rb +37 -0
- data/lib/redhound/protocols/ntp.rb +47 -0
- data/lib/redhound/protocols/tcp.rb +80 -0
- data/lib/redhound/protocols/tls.rb +218 -0
- data/lib/redhound/protocols/tunnel.rb +48 -0
- data/lib/redhound/protocols/udp.rb +32 -0
- data/lib/redhound/protocols/vlan.rb +23 -0
- data/lib/redhound/reader.rb +8 -0
- data/lib/redhound/registry.rb +64 -0
- data/lib/redhound/stream_dissector.rb +168 -0
- data/lib/redhound/util/seq.rb +21 -0
- data/lib/redhound/version.rb +2 -1
- data/lib/redhound/writer.rb +25 -43
- data/lib/redhound.rb +81 -9
- data/sig/generated/redhound/analysis/flow_key.rbs +15 -0
- data/sig/generated/redhound/analysis/flow_table.rbs +98 -0
- data/sig/generated/redhound/analysis/ip_reassembler.rbs +40 -0
- data/sig/generated/redhound/analysis/stats.rbs +77 -0
- data/sig/generated/redhound/analysis/tcp_analysis.rbs +15 -0
- data/sig/generated/redhound/analysis/tcp_reassembler.rbs +27 -0
- data/sig/generated/redhound/analysis/tcp_stream.rbs +60 -0
- data/sig/generated/redhound/analysis.rbs +38 -0
- data/sig/generated/redhound/capture/bsd/bpf_device.rbs +39 -0
- data/sig/generated/redhound/capture/bsd/constants.rbs +40 -0
- data/sig/generated/redhound/capture/file_source.rbs +34 -0
- data/sig/generated/redhound/capture/interface.rbs +53 -0
- data/sig/generated/redhound/capture/linktype.rbs +20 -0
- data/sig/generated/redhound/capture/linux/constants.rbs +58 -0
- data/sig/generated/redhound/capture/linux/cooked.rbs +15 -0
- data/sig/generated/redhound/capture/linux/ifreq.rbs +18 -0
- data/sig/generated/redhound/capture/linux/packet_socket.rbs +57 -0
- data/sig/generated/redhound/capture/linux/sockaddr_ll.rbs +31 -0
- data/sig/generated/redhound/capture/linux/tpacket_v3.rbs +35 -0
- data/sig/generated/redhound/capture/source.rbs +44 -0
- data/sig/generated/redhound/capture/stats.rbs +25 -0
- data/sig/generated/redhound/capture.rbs +14 -0
- data/sig/generated/redhound/cli/command.rbs +12 -0
- data/sig/generated/redhound/cli/options.rbs +25 -0
- data/sig/generated/redhound/cli/privileges.rbs +12 -0
- data/sig/generated/redhound/cli/runner.rbs +27 -0
- data/sig/generated/redhound/context.rbs +27 -0
- data/sig/generated/redhound/cursor.rbs +59 -0
- data/sig/generated/redhound/diagnostic.rbs +21 -0
- data/sig/generated/redhound/dissector.rbs +113 -0
- data/sig/generated/redhound/engine.rbs +23 -0
- data/sig/generated/redhound/errors.rbs +56 -0
- data/sig/generated/redhound/field.rbs +74 -0
- data/sig/generated/redhound/file/format.rbs +33 -0
- data/sig/generated/redhound/file/pcap_reader.rbs +19 -0
- data/sig/generated/redhound/file/pcap_writer.rbs +38 -0
- data/sig/generated/redhound/file/pcapng_reader.rbs +54 -0
- data/sig/generated/redhound/file/pcapng_writer.rbs +50 -0
- data/sig/generated/redhound/file/rotating_writer.rbs +42 -0
- data/sig/generated/redhound/filter/analyzer.rbs +42 -0
- data/sig/generated/redhound/filter/bpf/assembler.rbs +39 -0
- data/sig/generated/redhound/filter/bpf/disassembler.rbs +18 -0
- data/sig/generated/redhound/filter/bpf/validator.rbs +30 -0
- data/sig/generated/redhound/filter/bpf/vm.rbs +26 -0
- data/sig/generated/redhound/filter/codegen.rbs +101 -0
- data/sig/generated/redhound/filter/lexer.rbs +15 -0
- data/sig/generated/redhound/filter/parser.rbs +65 -0
- data/sig/generated/redhound/filter/program.rbs +48 -0
- data/sig/generated/redhound/filter.rbs +10 -0
- data/sig/generated/redhound/layer.rbs +81 -0
- data/sig/generated/redhound/output/fields.rbs +15 -0
- data/sig/generated/redhound/output/follow.rbs +21 -0
- data/sig/generated/redhound/output/hexdump.rbs +15 -0
- data/sig/generated/redhound/output/json.rbs +18 -0
- data/sig/generated/redhound/output/summary.rbs +21 -0
- data/sig/generated/redhound/output/timestamp.rbs +21 -0
- data/sig/generated/redhound/output/tree.rbs +15 -0
- data/sig/generated/redhound/packet.rbs +77 -0
- data/sig/generated/redhound/protocols/arp.rbs +15 -0
- data/sig/generated/redhound/protocols/checksum.rbs +15 -0
- data/sig/generated/redhound/protocols/data.rbs +15 -0
- data/sig/generated/redhound/protocols/dhcp.rbs +28 -0
- data/sig/generated/redhound/protocols/dns.rbs +42 -0
- data/sig/generated/redhound/protocols/ethernet.rbs +15 -0
- data/sig/generated/redhound/protocols/http.rbs +36 -0
- data/sig/generated/redhound/protocols/icmp.rbs +41 -0
- data/sig/generated/redhound/protocols/ipv4.rbs +15 -0
- data/sig/generated/redhound/protocols/ipv6.rbs +24 -0
- data/sig/generated/redhound/protocols/link.rbs +33 -0
- data/sig/generated/redhound/protocols/llc.rbs +15 -0
- data/sig/generated/redhound/protocols/ntp.rbs +17 -0
- data/sig/generated/redhound/protocols/tcp.rbs +25 -0
- data/sig/generated/redhound/protocols/tls.rbs +51 -0
- data/sig/generated/redhound/protocols/tunnel.rbs +24 -0
- data/sig/generated/redhound/protocols/udp.rbs +18 -0
- data/sig/generated/redhound/protocols/vlan.rbs +12 -0
- data/sig/generated/redhound/reader.rbs +7 -0
- data/sig/generated/redhound/registry.rbs +45 -0
- data/sig/generated/redhound/stream_dissector.rbs +44 -0
- data/sig/generated/redhound/util/seq.rbs +20 -0
- data/sig/generated/redhound/version.rbs +1 -0
- data/sig/generated/redhound/writer.rbs +6 -19
- data/sig/generated/redhound.rbs +18 -0
- data/sig/shims/io.rbs +7 -0
- data/sig/shims/process.rbs +3 -0
- metadata +175 -55
- data/CODE_OF_CONDUCT.md +0 -132
- data/Dockerfile +0 -7
- data/Rakefile +0 -15
- data/Steepfile +0 -4
- data/docker-compose.yml +0 -9
- data/lib/redhound/analyzer.rb +0 -34
- data/lib/redhound/builder/packet_mreq.rb +0 -56
- data/lib/redhound/builder/socket.rb +0 -35
- data/lib/redhound/builder.rb +0 -4
- data/lib/redhound/command.rb +0 -67
- data/lib/redhound/l2/ether.rb +0 -68
- data/lib/redhound/l2/protocol.rb +0 -33
- data/lib/redhound/l2.rb +0 -4
- data/lib/redhound/l3/arp.rb +0 -114
- data/lib/redhound/l3/base.rb +0 -49
- data/lib/redhound/l3/ipv4.rb +0 -95
- data/lib/redhound/l3/ipv6.rb +0 -69
- data/lib/redhound/l3/protocol.rb +0 -173
- data/lib/redhound/l3/resolver.rb +0 -30
- data/lib/redhound/l3.rb +0 -8
- data/lib/redhound/l4/base.rb +0 -31
- data/lib/redhound/l4/icmp.rb +0 -71
- data/lib/redhound/l4/resolver.rb +0 -28
- data/lib/redhound/l4/udp.rb +0 -63
- data/lib/redhound/l4.rb +0 -6
- data/lib/redhound/receiver.rb +0 -45
- data/lib/redhound/resolver.rb +0 -22
- data/lib/redhound/source/socket.rb +0 -18
- data/lib/redhound/source.rb +0 -3
- data/rbs_collection.lock.yaml +0 -20
- data/rbs_collection.yaml +0 -17
- data/sig/generated/redhound/analyzer.rbs +0 -14
- data/sig/generated/redhound/builder/packet_mreq.rbs +0 -39
- data/sig/generated/redhound/builder/socket.rbs +0 -24
- data/sig/generated/redhound/command.rbs +0 -19
- data/sig/generated/redhound/l2/ether.rbs +0 -41
- data/sig/generated/redhound/l2/protocol.rbs +0 -15
- data/sig/generated/redhound/l3/arp.rbs +0 -57
- data/sig/generated/redhound/l3/base.rbs +0 -28
- data/sig/generated/redhound/l3/ipv4.rbs +0 -53
- data/sig/generated/redhound/l3/ipv6.rbs +0 -38
- data/sig/generated/redhound/l3/protocol.rbs +0 -16
- data/sig/generated/redhound/l3/resolver.rbs +0 -16
- data/sig/generated/redhound/l4/base.rbs +0 -19
- data/sig/generated/redhound/l4/icmp.rbs +0 -33
- data/sig/generated/redhound/l4/resolver.rbs +0 -16
- data/sig/generated/redhound/l4/udp.rbs +0 -36
- data/sig/generated/redhound/receiver.rbs +0 -19
- data/sig/generated/redhound/resolver.rbs +0 -11
- data/sig/generated/redhound/source/socket.rbs +0 -13
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'optparse'
|
|
5
|
+
|
|
6
|
+
module Redhound
|
|
7
|
+
# @api private
|
|
8
|
+
module CLI
|
|
9
|
+
# @api private
|
|
10
|
+
class Options
|
|
11
|
+
attr_reader :values, :parser
|
|
12
|
+
# @rbs (Array[String] argv) -> void
|
|
13
|
+
def initialize(argv)
|
|
14
|
+
@values = { interface: nil, read: nil, write: nil, count: nil, snaplen: 262144, promiscuous: true,
|
|
15
|
+
buffer_size: nil, direction: :inout, backend: :auto, filter: nil, format: nil,
|
|
16
|
+
packet_buffered: false, output: :summary, output_explicit: false, fields: Array.new, verbosity: 0,
|
|
17
|
+
link_layer: false, quick: false, timestamp: :clock, precision: :micro, resolve_names: false,
|
|
18
|
+
stats: Array.new, follow: nil, decode_as: Array.new, plugins: Array.new, dump: 0, debug: false, no_yjit: false }
|
|
19
|
+
args = normalize(argv.dup)
|
|
20
|
+
@parser = build_parser
|
|
21
|
+
@parser.order!(args)
|
|
22
|
+
filter = args.join(' ')
|
|
23
|
+
if @values[:filter_file]
|
|
24
|
+
raise ConfigurationError, 'cannot combine filter file and expression' unless filter.empty?
|
|
25
|
+
filter = ::File.read(@values[:filter_file])
|
|
26
|
+
end
|
|
27
|
+
@values[:filter] = filter unless filter.strip.empty?
|
|
28
|
+
validate
|
|
29
|
+
rescue OptionParser::ParseError, ArgumentError => e
|
|
30
|
+
raise ConfigurationError, e.message
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @rbs (Array[String] args) -> Array[String]
|
|
34
|
+
def normalize(args)
|
|
35
|
+
fields = args.each_cons(2).any? { |flag, value| %w[-T --output-format].include?(flag) && value == 'fields' } || args.include?('--output-format=fields')
|
|
36
|
+
result = [] #: Array[String]
|
|
37
|
+
while (arg = args.shift)
|
|
38
|
+
if /\A-v{1,3}\z/.match?(arg)
|
|
39
|
+
@values[:verbosity] += arg.length - 1
|
|
40
|
+
elsif /\A-t{1,5}\z/.match?(arg)
|
|
41
|
+
@values[:timestamp] = %i[none epoch delta date elapsed][arg.length - 2]
|
|
42
|
+
elsif %w[-x -xx -X -XX].include?(arg)
|
|
43
|
+
@values[:hex] = { ascii: arg.include?('X'), link_layer: arg.length == 3 }
|
|
44
|
+
elsif arg == '-e'
|
|
45
|
+
if fields
|
|
46
|
+
field = args.shift
|
|
47
|
+
raise ConfigurationError, '-e needs a field with -T fields' if !field || field.start_with?('-')
|
|
48
|
+
@values[:fields] << field
|
|
49
|
+
else
|
|
50
|
+
@values[:link_layer] = true
|
|
51
|
+
end
|
|
52
|
+
else
|
|
53
|
+
result << arg
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
result
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @rbs () -> OptionParser
|
|
60
|
+
def build_parser
|
|
61
|
+
OptionParser.new do |o|
|
|
62
|
+
o.banner = 'Usage: redhound [options] [filter expression]'
|
|
63
|
+
o.on('-i', '--interface IF', 'interface name, index or any') { |v| @values[:interface] = v }
|
|
64
|
+
o.on('-D', '--list-interfaces', 'list interfaces and exit') { @values[:list_interfaces] = true }
|
|
65
|
+
o.on('-r', '--read FILE', 'read pcap or pcapng (- for stdin)') { |v| @values[:read] = v }
|
|
66
|
+
o.on('-c', '--count N', Integer, 'stop after N packets') { |v| @values[:count] = v }
|
|
67
|
+
o.on('-s', '--snaplen N', Integer, 'capture length (default 262144)') { |v| @values[:snaplen] = v.zero? ? 262144 : v }
|
|
68
|
+
o.on('-p', '--no-promiscuous', 'disable promiscuous capture') { @values[:promiscuous] = false }
|
|
69
|
+
o.on('-B', '--buffer-size KiB', Integer, 'kernel buffer size') { |v| @values[:buffer_size] = v * 1024 }
|
|
70
|
+
o.on('-Q', '--direction DIR', %w[in out inout], 'capture direction') { |v| @values[:direction] = v.to_sym }
|
|
71
|
+
o.on('-F', '--filter-file FILE', 'read a capture filter') { |v| @values[:filter_file] = v }
|
|
72
|
+
o.on('--capture-backend BACKEND', %w[auto ring socket bpf], 'capture backend') { |v| @values[:backend] = v.to_sym }
|
|
73
|
+
o.on('-w', '--write FILE', 'write capture (- for stdout)') { |v| @values[:write] = v }
|
|
74
|
+
o.on('--format FORMAT', %w[pcap pcapng], 'capture file format') { |v| @values[:format] = v.to_sym }
|
|
75
|
+
o.on('-C MB', Float, 'rotate after MB (decimal)') { |v| @values[:max_bytes] = (v * 1_000_000).to_i }
|
|
76
|
+
o.on('-G SECONDS', Float, 'rotate at this interval') { |v| @values[:interval] = v }
|
|
77
|
+
o.on('-W N', Integer, 'maximum rotation file count') { |v| @values[:file_count] = v }
|
|
78
|
+
o.on('--post-rotate-command CMD', 'command to run after closing each file') { |v| @values[:post_rotate_command] = v }
|
|
79
|
+
o.on('-U', '--packet-buffered', 'flush each packet') { @values[:packet_buffered] = true }
|
|
80
|
+
o.on('-T', '--output-format FORMAT', %w[summary tree json ndjson fields], 'packet output format') do |v|
|
|
81
|
+
@values[:output], @values[:output_explicit] = v.to_sym, true
|
|
82
|
+
end
|
|
83
|
+
o.on('-V', 'show packet details') { @values[:output], @values[:output_explicit] = :tree, true }
|
|
84
|
+
o.on('-q', 'quick output and no capture statistics') { @values[:quick] = true }
|
|
85
|
+
o.on('--time-stamp-precision PRECISION', %w[micro nano], 'timestamp digits') { |v| @values[:precision] = v.to_sym }
|
|
86
|
+
o.on('-N', '--resolve-names', 'resolve addresses') { @values[:resolve_names] = true }
|
|
87
|
+
o.on('-n', 'disable address resolution (default)') { @values[:resolve_names] = false }
|
|
88
|
+
o.on('--stats SPEC', 'io,N / conv,TYPE / endpoints,TYPE / phs') { |v| @values[:stats] << v }
|
|
89
|
+
o.on('--follow SPEC', 'tcp,ascii|hex|raw,N') { |v| @values[:follow] = v }
|
|
90
|
+
o.on('--decode-as RULE', 'e.g. udp.port==8443,dns') { |v| @values[:decode_as] << v }
|
|
91
|
+
o.on('-I', '--require FILE', 'load a custom dissector') { |v| @values[:plugins] << v }
|
|
92
|
+
o.on('-d', 'dump cBPF instructions') { @values[:dump] += 1 }
|
|
93
|
+
o.on('-Z', '--relinquish-privileges USER', 'drop capture privileges') { |v| @values[:user] = v }
|
|
94
|
+
o.on('--list-protocols', 'list protocols and fields') { @values[:list_protocols] = true }
|
|
95
|
+
o.on('--debug', 'print error backtraces') { @values[:debug] = true }
|
|
96
|
+
o.on('--no-yjit', 'disable automatic YJIT activation') { @values[:no_yjit] = true }
|
|
97
|
+
o.on('-h', '--help', 'print help') { @values[:help] = true }
|
|
98
|
+
o.on('--version', 'print version') { @values[:version] = true }
|
|
99
|
+
o.separator ' -e [-T fields: FIELD] link header or selected field (repeatable)'
|
|
100
|
+
o.separator ' -v / -vv / -vvv verbosity and checksum verification'
|
|
101
|
+
o.separator ' -t / -tt / -ttt / -tttt / -ttttt timestamp style'
|
|
102
|
+
o.separator ' -x / -xx / -X / -XX hex dump, with link header / ASCII'
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# @rbs () -> void
|
|
107
|
+
def validate
|
|
108
|
+
%i[count snaplen buffer_size max_bytes interval file_count].each do |key|
|
|
109
|
+
value = @values[key]
|
|
110
|
+
raise ConfigurationError, "#{key} must be positive" if value && (!value.positive? || (value.respond_to?(:finite?) && !value.finite?))
|
|
111
|
+
end
|
|
112
|
+
raise ConfigurationError, 'snaplen exceeds 16 MiB' if @values[:snaplen] > 16 * 1024 * 1024
|
|
113
|
+
raise ConfigurationError, 'choose either -r or -i' if @values[:read] && @values[:interface]
|
|
114
|
+
raise ConfigurationError, 'rotation requires -w' if !@values[:write] && %i[max_bytes interval file_count post_rotate_command].any? { |k| @values[k] }
|
|
115
|
+
raise ConfigurationError, '-W and post-rotate-command require -C or -G' if (@values[:file_count] || @values[:post_rotate_command]) && !@values[:max_bytes] && !@values[:interval]
|
|
116
|
+
raise ConfigurationError, '-T fields requires at least one -e' if @values[:output] == :fields && @values[:fields].empty?
|
|
117
|
+
raise ConfigurationError, 'cannot combine JSON/fields output and hex dump' if @values[:hex] && !%i[summary tree].include?(@values[:output])
|
|
118
|
+
raise ConfigurationError, 'cannot display packets while writing capture to stdout' if @values[:write] == '-' && (@values[:output_explicit] || @values[:hex])
|
|
119
|
+
if @values[:read] && @values[:write] && @values[:read] != '-' && @values[:write] != '-'
|
|
120
|
+
input, output = @values[:read], @values[:write]
|
|
121
|
+
if ::File.expand_path(input) == ::File.expand_path(output) || (::File.exist?(input) && ::File.exist?(output) && ::File.identical?(input, output))
|
|
122
|
+
raise ConfigurationError, 'input and output must be different files'
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
@values[:stats].each do |spec|
|
|
126
|
+
valid = spec == 'phs' || /\A(?:conv|endpoints),(?:eth|ip|ipv6|tcp|udp)\z/.match?(spec) || /\Aio,(?:\d+(?:\.\d+)?)\z/.match?(spec) && spec.split(',').last.to_f.positive?
|
|
127
|
+
raise ConfigurationError, "invalid statistics: #{spec}" unless valid
|
|
128
|
+
end
|
|
129
|
+
raise ConfigurationError, 'invalid TCP follow specification' if @values[:follow] && !/\Atcp,(?:ascii|hex|raw),\d+\z/.match?(@values[:follow])
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'etc'
|
|
5
|
+
|
|
6
|
+
module Redhound
|
|
7
|
+
# @api private
|
|
8
|
+
module CLI
|
|
9
|
+
# @api private
|
|
10
|
+
module Privileges
|
|
11
|
+
# @rbs (String user) -> void
|
|
12
|
+
def self.drop(user)
|
|
13
|
+
account = Etc.getpwnam(user)
|
|
14
|
+
raise ConfigurationError, 'privilege target must not be root' if account.uid.zero?
|
|
15
|
+
Process.initgroups(account.name, account.gid)
|
|
16
|
+
Process::Sys.setgid(account.gid)
|
|
17
|
+
Process::Sys.setuid(account.uid)
|
|
18
|
+
begin
|
|
19
|
+
Process::Sys.setuid(0)
|
|
20
|
+
rescue Errno::EPERM
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
raise CaptureError, 'failed to drop root privileges permanently'
|
|
24
|
+
rescue ArgumentError => e
|
|
25
|
+
raise ConfigurationError, e.message
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module CLI
|
|
7
|
+
# @api private
|
|
8
|
+
class Runner
|
|
9
|
+
# @rbs (Hash[Symbol, untyped] options, ?out: untyped, ?err: untyped) -> void
|
|
10
|
+
def initialize(options, out: $stdout, err: $stderr)
|
|
11
|
+
@options, @out, @err, @count = options, out, err, 0
|
|
12
|
+
@source, @writer, @formatter, @analysis = nil, nil, nil, nil
|
|
13
|
+
@handlers = {} #: Hash[String, untyped]
|
|
14
|
+
end
|
|
15
|
+
# @rbs () -> void
|
|
16
|
+
def run
|
|
17
|
+
opts = @options
|
|
18
|
+
install_signals
|
|
19
|
+
if opts[:dump].positive?
|
|
20
|
+
if opts[:read]
|
|
21
|
+
@source = read_capture { Redhound.open(opts[:read]) }
|
|
22
|
+
linktype = read_capture { @source.next_packet }&.linktype || @source.linktype
|
|
23
|
+
else
|
|
24
|
+
linktype = opts[:interface] ? Capture::Interface.find(opts[:interface]).linktype : 1
|
|
25
|
+
end
|
|
26
|
+
program = Filter.compile(opts[:filter] || '', linktype: linktype, live: !opts[:read])
|
|
27
|
+
@out.puts(program.disassemble(format: %i[text ruby decimal][[opts[:dump], 3].min - 1]))
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
@source = if opts[:read]
|
|
31
|
+
read_capture { Redhound.open(opts[:read], filter: opts[:filter]) }
|
|
32
|
+
else
|
|
33
|
+
raise ConfigurationError, 'specify an interface with -i or capture file with -r' unless opts[:interface]
|
|
34
|
+
Capture.open(interface: opts[:interface], backend: opts[:backend], snaplen: opts[:snaplen],
|
|
35
|
+
promiscuous: opts[:promiscuous], buffer_size: opts[:buffer_size], direction: opts[:direction], filter: opts[:filter])
|
|
36
|
+
end
|
|
37
|
+
if opts[:write]
|
|
38
|
+
@writer = Writer.open(opts[:write], linktype: @source.linktype, format: opts[:format], snaplen: opts[:snaplen],
|
|
39
|
+
precision: opts[:precision], packet_buffered: opts[:packet_buffered],
|
|
40
|
+
max_bytes: opts[:max_bytes], interval: opts[:interval], file_count: opts[:file_count],
|
|
41
|
+
post_rotate_command: opts[:post_rotate_command], forbidden_input: opts[:read], filter: opts[:filter])
|
|
42
|
+
end
|
|
43
|
+
display = !opts[:write] || opts[:output_explicit] || opts[:hex]
|
|
44
|
+
@formatter = formatter if display && !opts[:follow]
|
|
45
|
+
hex = opts[:hex] && Output::Hexdump.new(**opts[:hex])
|
|
46
|
+
registry = Registry.default.copy
|
|
47
|
+
opts[:decode_as].each { |rule| registry.decode_as(rule) }
|
|
48
|
+
engine = Engine.new(registry: registry, verify_checksums: opts[:verbosity].positive?)
|
|
49
|
+
if display || !opts[:stats].empty? || opts[:follow]
|
|
50
|
+
@analysis = Analysis::Session.new(stats: opts[:stats], follow: opts[:follow], registry: registry)
|
|
51
|
+
end
|
|
52
|
+
Privileges.drop(opts[:user]) if opts[:user]
|
|
53
|
+
until @stopping || @source.stopped?
|
|
54
|
+
if @report_requested
|
|
55
|
+
statistics
|
|
56
|
+
@analysis&.snapshot(@err)
|
|
57
|
+
@report_requested = false
|
|
58
|
+
end
|
|
59
|
+
packet = read_capture { @source.next_packet(timeout: 0.1) }
|
|
60
|
+
next unless packet
|
|
61
|
+
@writer&.write(packet)
|
|
62
|
+
packet.engine = engine
|
|
63
|
+
@analysis&.update(packet)
|
|
64
|
+
@formatter&.format(packet, @out)
|
|
65
|
+
hex&.format(packet, @out) if display
|
|
66
|
+
@out.flush if opts[:packet_buffered] || @out.tty?
|
|
67
|
+
@count += 1
|
|
68
|
+
break if @stopping || (opts[:count] && @count >= opts[:count])
|
|
69
|
+
end
|
|
70
|
+
rescue RotationComplete, Interrupt
|
|
71
|
+
nil
|
|
72
|
+
ensure
|
|
73
|
+
begin
|
|
74
|
+
begin
|
|
75
|
+
@formatter.finish(@out) if @formatter.respond_to?(:finish)
|
|
76
|
+
@analysis&.finish(@out, @err)
|
|
77
|
+
statistics if @source && !@options[:quick] && @options[:dump].zero?
|
|
78
|
+
@writer.write_stats(@source.stats) if @writer && @source && @writer.respond_to?(:write_stats)
|
|
79
|
+
ensure
|
|
80
|
+
@writer&.close
|
|
81
|
+
end
|
|
82
|
+
ensure
|
|
83
|
+
begin
|
|
84
|
+
@source&.close
|
|
85
|
+
ensure
|
|
86
|
+
@handlers.each { |signal, handler| Signal.trap(signal, handler) }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
# @rbs () -> untyped
|
|
91
|
+
def formatter
|
|
92
|
+
opts = @options
|
|
93
|
+
case opts[:output]
|
|
94
|
+
when :tree then Output::Tree.new
|
|
95
|
+
when :json then Output::Json.new
|
|
96
|
+
when :ndjson then Output::Json.new(ndjson: true)
|
|
97
|
+
when :fields then Output::Fields.new(opts[:fields])
|
|
98
|
+
else Output::Summary.new(timestamp: opts[:timestamp], precision: opts[:precision], link_layer: opts[:link_layer],
|
|
99
|
+
quick: opts[:quick], verbosity: opts[:verbosity], resolve_names: opts[:resolve_names])
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
# @rbs () -> void
|
|
103
|
+
def install_signals
|
|
104
|
+
%w[INT TERM].each do |signal|
|
|
105
|
+
@handlers[signal] = Signal.trap(signal) do
|
|
106
|
+
@stopping = true
|
|
107
|
+
@source&.stop
|
|
108
|
+
raise Interrupt if @options[:read] && @reading_capture
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
%w[USR1 INFO].select { |s| Signal.list.key?(s) }.each do |signal|
|
|
112
|
+
@handlers[signal] = Signal.trap(signal) { @report_requested = true }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
# @rbs [T] () { () -> T } -> T
|
|
116
|
+
def read_capture
|
|
117
|
+
@reading_capture = true
|
|
118
|
+
yield
|
|
119
|
+
ensure
|
|
120
|
+
@reading_capture = false
|
|
121
|
+
end
|
|
122
|
+
# @rbs () -> void
|
|
123
|
+
def statistics
|
|
124
|
+
stats = @source.stats
|
|
125
|
+
@err.puts("#{@count} packets captured", "#{stats.received} packets received by filter", "#{stats.dropped} packets dropped by kernel")
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
class Context
|
|
7
|
+
attr_reader :packet, :registry, :layers
|
|
8
|
+
attr_accessor :cursor, :embedded, :extension_count
|
|
9
|
+
# @rbs (Packet packet, registry: Registry, ?verify_checksums: bool) -> void
|
|
10
|
+
def initialize(packet, registry:, verify_checksums: false)
|
|
11
|
+
@packet, @registry, @verify_checksums = packet, registry, verify_checksums
|
|
12
|
+
@cursor = Cursor.new(packet.data)
|
|
13
|
+
@embedded, @extension_count, @layers = false, 0, []
|
|
14
|
+
end
|
|
15
|
+
# @rbs () -> bool
|
|
16
|
+
def verify_checksums? = @verify_checksums
|
|
17
|
+
# @rbs () -> Layer?
|
|
18
|
+
def network = @layers.reverse.find { |l| %i[ipv4 ipv6].include?(l.protocol) && l.embedded == @embedded }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# Checked binary reads within an absolute packet slice.
|
|
6
|
+
class Cursor
|
|
7
|
+
# Internal exception for a checked read beyond the captured slice.
|
|
8
|
+
class Truncated < StandardError; end
|
|
9
|
+
# Underlying data and absolute start/end bounds.
|
|
10
|
+
attr_reader :data, :start, :limit
|
|
11
|
+
|
|
12
|
+
# @rbs (String data, ?Integer start, ?Integer? limit) -> void
|
|
13
|
+
# Create a bounded view; reject invalid ranges.
|
|
14
|
+
def initialize(data, start = 0, limit = nil)
|
|
15
|
+
@data, @start, @limit = data, start, limit || data.bytesize
|
|
16
|
+
raise ArgumentError, 'invalid cursor bounds' if start.negative? || @limit < start || @limit > data.bytesize
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @rbs () -> Integer
|
|
20
|
+
# Remaining slice length in bytes.
|
|
21
|
+
def remaining = @limit - @start
|
|
22
|
+
# @rbs (Integer off) -> Integer
|
|
23
|
+
# Read an unsigned byte.
|
|
24
|
+
def u8(off)
|
|
25
|
+
check(off, 1)
|
|
26
|
+
@data.getbyte(@start + off) #: Integer
|
|
27
|
+
end
|
|
28
|
+
# @rbs (Integer off) -> Integer
|
|
29
|
+
# Read an unsigned network-order 16-bit integer.
|
|
30
|
+
def u16(off)
|
|
31
|
+
check(off, 2)
|
|
32
|
+
@data.unpack1('n', offset: @start + off) #: Integer
|
|
33
|
+
end
|
|
34
|
+
# @rbs (Integer off) -> Integer
|
|
35
|
+
# Read an unsigned network-order 32-bit integer.
|
|
36
|
+
def u32(off)
|
|
37
|
+
check(off, 4)
|
|
38
|
+
@data.unpack1('N', offset: @start + off) #: Integer
|
|
39
|
+
end
|
|
40
|
+
# @rbs (Integer off) -> Integer
|
|
41
|
+
# Read an unsigned network-order 64-bit integer.
|
|
42
|
+
def u64(off)
|
|
43
|
+
check(off, 8)
|
|
44
|
+
@data.unpack1('Q>', offset: @start + off) #: Integer
|
|
45
|
+
end
|
|
46
|
+
# @rbs (String template, Integer size, ?Integer off) -> Array[untyped]
|
|
47
|
+
# Unpack a checked fixed-width header.
|
|
48
|
+
def unpack(template, size, off = 0) = (check(off, size); @data.unpack(template, offset: @start + off))
|
|
49
|
+
# @rbs (Integer off, Integer len) -> String
|
|
50
|
+
# Read a checked binary substring.
|
|
51
|
+
def bytes(off, len)
|
|
52
|
+
check(off, len)
|
|
53
|
+
@data.byteslice(@start + off, len) #: String
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @rbs (Integer from, Integer to) -> Cursor
|
|
57
|
+
# Create a child slice that cannot escape the parent bounds.
|
|
58
|
+
def sub(from, to)
|
|
59
|
+
raise ArgumentError, 'child cursor escapes parent' if from < @start || from > @limit || to < from
|
|
60
|
+
Cursor.new(@data, from, [to, @limit].min)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @rbs (Integer off, Integer len) -> void
|
|
64
|
+
# Validate a relative offset and read length.
|
|
65
|
+
def check(off, len)
|
|
66
|
+
raise Truncated, "need #{len} bytes at #{@start + off}, limit #{@limit}" if off.negative? || len.negative? || off + len > remaining
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# Base class and declarative fixed-header DSL for custom protocols.
|
|
6
|
+
class Dissector
|
|
7
|
+
# Unpack formats and byte lengths supported by the header DSL.
|
|
8
|
+
TYPES = { uint8: ['C', 1], uint16: ['n', 2], uint32: ['N', 4], uint64: ['Q>', 8],
|
|
9
|
+
mac: ['a6', 6], ipv4: ['N', 4], ipv6: ['a16', 16] }.freeze
|
|
10
|
+
|
|
11
|
+
# Build a single unpack template and its field descriptions.
|
|
12
|
+
class Header
|
|
13
|
+
# Compiled unpack template, header byte length and field definitions.
|
|
14
|
+
attr_reader :template, :length, :definitions
|
|
15
|
+
# @rbs () -> void
|
|
16
|
+
# Start an empty fixed-header definition.
|
|
17
|
+
def initialize
|
|
18
|
+
@template, @length, @index, @definitions = +'', 0, 0, []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @rbs (Symbol type, Symbol key, String name, **untyped opts) -> void
|
|
22
|
+
# Append a typed fixed-width field with optional format/enum metadata.
|
|
23
|
+
def field(type, key, name, **opts)
|
|
24
|
+
fmt, size = TYPES.fetch(type)
|
|
25
|
+
@definitions << FieldDefinition.new(key, name, type, @length, size, @index, nil, nil, 1, opts[:format], opts[:enum])
|
|
26
|
+
@template << fmt
|
|
27
|
+
@length += size
|
|
28
|
+
@index += 1
|
|
29
|
+
end
|
|
30
|
+
# @rbs (Symbol key, String name, **untyped opts) -> void
|
|
31
|
+
# Append an unsigned 8-bit field.
|
|
32
|
+
def uint8(key, name, **opts) = field(:uint8, key, name, **opts)
|
|
33
|
+
# @rbs (Symbol key, String name, **untyped opts) -> void
|
|
34
|
+
# Append a network-order unsigned 16-bit field.
|
|
35
|
+
def uint16(key, name, **opts) = field(:uint16, key, name, **opts)
|
|
36
|
+
# @rbs (Symbol key, String name, **untyped opts) -> void
|
|
37
|
+
# Append a network-order unsigned 32-bit field.
|
|
38
|
+
def uint32(key, name, **opts) = field(:uint32, key, name, **opts)
|
|
39
|
+
# @rbs (Symbol key, String name, **untyped opts) -> void
|
|
40
|
+
# Append a network-order unsigned 64-bit field.
|
|
41
|
+
def uint64(key, name, **opts) = field(:uint64, key, name, **opts)
|
|
42
|
+
# @rbs (Symbol key, String name, **untyped opts) -> void
|
|
43
|
+
# Append a six-byte MAC address.
|
|
44
|
+
def mac(key, name, **opts) = field(:mac, key, name, **opts)
|
|
45
|
+
# @rbs (Symbol key, String name, **untyped opts) -> void
|
|
46
|
+
# Append a four-byte IPv4 address.
|
|
47
|
+
def ipv4(key, name, **opts) = field(:ipv4, key, name, **opts)
|
|
48
|
+
# @rbs (Symbol key, String name, **untyped opts) -> void
|
|
49
|
+
# Append a sixteen-byte IPv6 address.
|
|
50
|
+
def ipv6(key, name, **opts) = field(:ipv6, key, name, **opts)
|
|
51
|
+
|
|
52
|
+
# @rbs (Integer width) { (Header) [self: Header] -> void } -> void
|
|
53
|
+
# Define bit fields filling an 8/16/32/64-bit network-order word.
|
|
54
|
+
def bits(width, &block)
|
|
55
|
+
raise ArgumentError, 'bit width must be 8, 16, 32 or 64' unless [8, 16, 32, 64].include?(width)
|
|
56
|
+
@bit_width, @bit_left = width, width
|
|
57
|
+
instance_eval(&block)
|
|
58
|
+
raise ArgumentError, 'bit fields must fill the word' unless @bit_left.zero?
|
|
59
|
+
@template << TYPES.fetch("uint#{width}".to_sym).first
|
|
60
|
+
@length += width / 8
|
|
61
|
+
@index += 1
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @rbs (Symbol key, String name, Integer width, **untyped opts) -> void
|
|
65
|
+
# Append a bit field inside bits, optionally scaled/formatted.
|
|
66
|
+
def bit(key, name, width, **opts)
|
|
67
|
+
raise ArgumentError, 'invalid bit field width' unless width.positive? && width <= @bit_left
|
|
68
|
+
@bit_left -= width
|
|
69
|
+
@definitions << FieldDefinition.new(key, name, :bits, @length, @bit_width / 8, @index,
|
|
70
|
+
@bit_left, (1 << width) - 1, opts.fetch(:scale, 1), opts[:format], opts[:enum])
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class << self
|
|
75
|
+
# @rbs () -> Symbol
|
|
76
|
+
# Registered protocol symbol.
|
|
77
|
+
def protocol_id = @protocol_id
|
|
78
|
+
# @rbs () -> String
|
|
79
|
+
# Human-readable protocol name.
|
|
80
|
+
def protocol_name = @protocol_name
|
|
81
|
+
# @rbs () -> String
|
|
82
|
+
# Short output label.
|
|
83
|
+
def short_name = @short_name
|
|
84
|
+
# @rbs () -> untyped
|
|
85
|
+
# Compiled header definition, if one was declared.
|
|
86
|
+
def compiled_header = @compiled_header
|
|
87
|
+
# @rbs (Symbol id, name: String, short: String) -> void
|
|
88
|
+
# Declare protocol identity and register the dissector.
|
|
89
|
+
def protocol(id, name:, short:)
|
|
90
|
+
@protocol_id, @protocol_name, @short_name = id, name, short
|
|
91
|
+
Registry.default.register_protocol(self)
|
|
92
|
+
end
|
|
93
|
+
# @rbs (String key, *Integer values) -> void
|
|
94
|
+
# Register this protocol under one or more dispatch values.
|
|
95
|
+
def dissects_on(key, *values)
|
|
96
|
+
values.each { |value| Registry.default.register(key, value, self) }
|
|
97
|
+
end
|
|
98
|
+
# @rbs () { (Header) [self: Header] -> void } -> void
|
|
99
|
+
# Compile a fixed header from the DSL block.
|
|
100
|
+
def header(&block)
|
|
101
|
+
@compiled_header = Header.new
|
|
102
|
+
@compiled_header.instance_eval(&block)
|
|
103
|
+
@compiled_header.template.freeze
|
|
104
|
+
end
|
|
105
|
+
# @rbs () -> String
|
|
106
|
+
# Compiled unpack template, or an empty template.
|
|
107
|
+
def header_template = @compiled_header&.template || ''
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @rbs (Context ctx, Cursor cursor) -> Layer
|
|
111
|
+
# Decode fixed fields then invoke the variable-header hook.
|
|
112
|
+
def parse(ctx, cursor)
|
|
113
|
+
header = self.class.compiled_header
|
|
114
|
+
length = header&.length || 0
|
|
115
|
+
raw = cursor.unpack(self.class.header_template, length)
|
|
116
|
+
defs = header ? header.definitions.dup : Array.new
|
|
117
|
+
values = {} #: Hash[Symbol, untyped]
|
|
118
|
+
defs.each do |d|
|
|
119
|
+
val = raw[d.index]
|
|
120
|
+
val = ((val >> d.shift) & d.mask) * d.scale if d.shift
|
|
121
|
+
values[d.key] = val
|
|
122
|
+
end
|
|
123
|
+
layer = Layer.new(self.class.protocol_id, cursor.start, length, cursor.limit,
|
|
124
|
+
definitions: defs, values: values, embedded: ctx.embedded)
|
|
125
|
+
dissect(ctx, layer)
|
|
126
|
+
layer
|
|
127
|
+
end
|
|
128
|
+
# @rbs (Context ctx, Layer layer) -> void
|
|
129
|
+
# Override to parse variable fields using the bounded Context cursor.
|
|
130
|
+
def dissect(ctx, layer); end
|
|
131
|
+
# @rbs (Context ctx, Layer layer) -> untyped
|
|
132
|
+
# Override to return the next protocol class or nil.
|
|
133
|
+
def next_dissector(ctx, layer) = nil
|
|
134
|
+
# @rbs (Layer layer) -> String
|
|
135
|
+
# Override to format the layer summary.
|
|
136
|
+
def summary(layer) = self.class.short_name || layer.protocol.to_s.upcase
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
class Engine
|
|
7
|
+
MAX_DEPTH = 32
|
|
8
|
+
# @rbs () -> Engine
|
|
9
|
+
def self.default = (@default ||= new)
|
|
10
|
+
# @rbs (?registry: Registry, ?verify_checksums: bool) -> void
|
|
11
|
+
def initialize(registry: Registry.default, verify_checksums: false)
|
|
12
|
+
@registry, @verify_checksums = registry, verify_checksums
|
|
13
|
+
end
|
|
14
|
+
# @rbs (Packet packet) -> Array[Layer]
|
|
15
|
+
def dissect(packet)
|
|
16
|
+
ctx = Context.new(packet, registry: @registry, verify_checksums: @verify_checksums)
|
|
17
|
+
klass = @registry.lookup('linktype', packet.linktype) || Protocols::Data
|
|
18
|
+
loop do
|
|
19
|
+
cursor = ctx.cursor
|
|
20
|
+
layer = safely(klass, ctx, cursor)
|
|
21
|
+
ctx.layers << layer
|
|
22
|
+
break if layer.error? || layer.payload_empty? || klass == Protocols::Data
|
|
23
|
+
if ctx.layers.length >= MAX_DEPTH
|
|
24
|
+
layer.diagnose(:error, :depth_exceeded)
|
|
25
|
+
break
|
|
26
|
+
end
|
|
27
|
+
ctx.cursor = cursor.sub(layer.payload_offset, layer.payload_end)
|
|
28
|
+
next_klass = next_dissector(klass, ctx, layer)
|
|
29
|
+
break if layer.error?
|
|
30
|
+
klass = next_klass || Protocols::Data
|
|
31
|
+
end
|
|
32
|
+
ctx.layers
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @rbs (untyped klass, Context ctx, Layer layer) -> untyped
|
|
36
|
+
def next_dissector(klass, ctx, layer)
|
|
37
|
+
klass.new.next_dissector(ctx, layer) || @registry.heuristic(layer.protocol, ctx, ctx.cursor)
|
|
38
|
+
rescue Cursor::Truncated => e
|
|
39
|
+
layer.diagnose(:note, :truncated, e.message)
|
|
40
|
+
nil
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
raise if ENV['REDHOUND_STRICT'] == '1'
|
|
43
|
+
layer.diagnose(:error, :dissector_bug, "#{e.class}: #{e.message}")
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @rbs (untyped klass, Context ctx, Cursor cursor) -> Layer
|
|
48
|
+
def safely(klass, ctx, cursor)
|
|
49
|
+
layer = klass.new.parse(ctx, cursor)
|
|
50
|
+
if layer.payload_offset < cursor.start || layer.payload_end < layer.payload_offset
|
|
51
|
+
layer.diagnose(:error, :bad_length)
|
|
52
|
+
elsif layer.payload_offset > cursor.limit || layer.payload_end > cursor.limit
|
|
53
|
+
layer.diagnose(:note, :truncated)
|
|
54
|
+
layer.payload_end = cursor.limit
|
|
55
|
+
end
|
|
56
|
+
layer
|
|
57
|
+
rescue Cursor::Truncated => e
|
|
58
|
+
Layer.new(klass.protocol_id, cursor.start, cursor.remaining, cursor.limit, embedded: ctx.embedded)
|
|
59
|
+
.diagnose(:note, :truncated, e.message)
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
raise if ENV['REDHOUND_STRICT'] == '1'
|
|
62
|
+
Layer.new(klass.protocol_id, cursor.start, 0, cursor.limit, embedded: ctx.embedded)
|
|
63
|
+
.diagnose(:error, :dissector_bug, "#{e.class}: #{e.message}")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# Base exception for Redhound operational and configuration errors.
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
# Capture-device or live-operation failure.
|
|
8
|
+
class CaptureError < Error; end
|
|
9
|
+
# @api private
|
|
10
|
+
class RotationComplete < CaptureError; end
|
|
11
|
+
# Insufficient permission to access the capture device.
|
|
12
|
+
class PermissionDenied < CaptureError; end
|
|
13
|
+
# Requested capture interface does not exist.
|
|
14
|
+
class InterfaceNotFound < CaptureError; end
|
|
15
|
+
# Requested platform or capture capability is unavailable.
|
|
16
|
+
class UnsupportedPlatform < CaptureError; end
|
|
17
|
+
# Malformed or unsupported capture-file structure.
|
|
18
|
+
class FileFormatError < Error; end
|
|
19
|
+
# Invalid command-line or library configuration.
|
|
20
|
+
class ConfigurationError < Error; end
|
|
21
|
+
# Invalid cBPF program or unsupported filter operation.
|
|
22
|
+
class FilterError < Error; end
|
|
23
|
+
# Filter exceeds the target platform instruction limit.
|
|
24
|
+
class FilterTooLarge < FilterError; end
|
|
25
|
+
# Capture-filter syntax error with original expression and byte position.
|
|
26
|
+
class FilterSyntaxError < FilterError
|
|
27
|
+
# Original filter expression and zero-based error position.
|
|
28
|
+
attr_reader :expression, :position
|
|
29
|
+
|
|
30
|
+
# @rbs (String message, ?expression: String?, ?position: Integer?) -> void
|
|
31
|
+
# Create a syntax diagnostic suitable for a caret display.
|
|
32
|
+
def initialize(message, expression: nil, position: nil)
|
|
33
|
+
@expression = expression
|
|
34
|
+
@position = position
|
|
35
|
+
super(message)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|