redhound 2.0.0.rc1 → 2.0.0.rc2
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 +20 -0
- data/README.md +1 -1
- data/docs/API.md +29 -1
- data/docs/MIGRATION.md +1 -1
- data/docs/PROTOCOLS.md +10 -1
- data/docs/USAGE.md +5 -1
- data/docs/VALIDATION.md +119 -3
- data/docs/json-schema.json +2 -2
- data/lib/redhound/analysis/flow_key.rb +7 -1
- data/lib/redhound/analysis/flow_table.rb +12 -5
- data/lib/redhound/analysis/ip_reassembler.rb +44 -11
- data/lib/redhound/analysis/stats.rb +14 -2
- data/lib/redhound/analysis/tcp_reassembler.rb +68 -16
- data/lib/redhound/analysis/tcp_stream.rb +7 -2
- data/lib/redhound/analysis.rb +30 -2
- data/lib/redhound/capture/bsd/bpf_device.rb +3 -0
- data/lib/redhound/capture/bsd/ifreq.rb +41 -0
- data/lib/redhound/capture/file_source.rb +135 -4
- data/lib/redhound/capture/interface.rb +5 -1
- data/lib/redhound/capture/linux/packet_socket.rb +53 -9
- data/lib/redhound/capture/linux/tpacket_v3.rb +5 -0
- data/lib/redhound/capture.rb +3 -2
- data/lib/redhound/cli/command.rb +1 -1
- data/lib/redhound/dissector.rb +18 -4
- data/lib/redhound/engine.rb +3 -0
- data/lib/redhound/field.rb +1 -1
- data/lib/redhound/file/format.rb +8 -1
- data/lib/redhound/file/pcap_writer.rb +10 -3
- data/lib/redhound/file/pcapng_reader.rb +19 -7
- data/lib/redhound/file/pcapng_writer.rb +22 -14
- data/lib/redhound/filter/bpf/vm.rb +3 -2
- data/lib/redhound/output/follow.rb +6 -1
- data/lib/redhound/output/summary.rb +29 -4
- data/lib/redhound/output/timestamp.rb +2 -2
- data/lib/redhound/output/tree.rb +2 -1
- data/lib/redhound/packet.rb +18 -4
- data/lib/redhound/protocols/data.rb +3 -1
- data/lib/redhound/protocols/dhcp.rb +29 -13
- data/lib/redhound/protocols/http.rb +5 -3
- data/lib/redhound/protocols/icmp.rb +55 -41
- data/lib/redhound/protocols/ipv6.rb +5 -2
- data/lib/redhound/protocols/ntp.rb +2 -4
- data/lib/redhound/protocols/tcp.rb +17 -9
- data/lib/redhound/protocols/tunnel.rb +2 -2
- data/lib/redhound/stream_dissector.rb +2 -2
- data/lib/redhound/version.rb +1 -1
- data/sig/generated/redhound/analysis/flow_table.rbs +6 -2
- data/sig/generated/redhound/analysis/ip_reassembler.rbs +12 -0
- data/sig/generated/redhound/analysis/stats.rbs +3 -0
- data/sig/generated/redhound/analysis/tcp_reassembler.rbs +12 -0
- data/sig/generated/redhound/capture/bsd/ifreq.rbs +15 -0
- data/sig/generated/redhound/capture/file_source.rbs +33 -0
- data/sig/generated/redhound/capture/linux/packet_socket.rbs +7 -0
- data/sig/generated/redhound/dissector.rbs +4 -0
- data/sig/generated/redhound/file/format.rbs +6 -0
- data/sig/generated/redhound/file/pcapng_reader.rbs +2 -2
- data/sig/generated/redhound/filter/bpf/vm.rbs +2 -0
- data/sig/generated/redhound/packet.rbs +5 -0
- data/sig/generated/redhound/protocols/data.rbs +2 -0
- data/sig/generated/redhound/protocols/dhcp.rbs +7 -4
- metadata +3 -1
data/lib/redhound/dissector.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Redhound
|
|
|
5
5
|
# Base class and declarative fixed-header DSL for custom protocols.
|
|
6
6
|
class Dissector
|
|
7
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],
|
|
8
|
+
TYPES = { int8: ['c', 1], uint8: ['C', 1], uint16: ['n', 2], uint32: ['N', 4], uint64: ['Q>', 8],
|
|
9
9
|
mac: ['a6', 6], ipv4: ['N', 4], ipv6: ['a16', 16] }.freeze
|
|
10
10
|
|
|
11
11
|
# Build a single unpack template and its field descriptions.
|
|
@@ -28,6 +28,9 @@ module Redhound
|
|
|
28
28
|
@index += 1
|
|
29
29
|
end
|
|
30
30
|
# @rbs (Symbol key, String name, **untyped opts) -> void
|
|
31
|
+
# Append a signed 8-bit field.
|
|
32
|
+
def int8(key, name, **opts) = field(:int8, key, name, **opts)
|
|
33
|
+
# @rbs (Symbol key, String name, **untyped opts) -> void
|
|
31
34
|
# Append an unsigned 8-bit field.
|
|
32
35
|
def uint8(key, name, **opts) = field(:uint8, key, name, **opts)
|
|
33
36
|
# @rbs (Symbol key, String name, **untyped opts) -> void
|
|
@@ -112,17 +115,28 @@ module Redhound
|
|
|
112
115
|
def parse(ctx, cursor)
|
|
113
116
|
header = self.class.compiled_header
|
|
114
117
|
length = header&.length || 0
|
|
115
|
-
|
|
118
|
+
truncated = cursor.remaining < length
|
|
119
|
+
raw = if truncated
|
|
120
|
+
cursor.bytes(0, cursor.remaining).unpack(self.class.header_template)
|
|
121
|
+
else
|
|
122
|
+
cursor.unpack(self.class.header_template, length)
|
|
123
|
+
end
|
|
116
124
|
defs = header ? header.definitions.dup : Array.new
|
|
125
|
+
defs.select! { |d| d.offset + d.length <= cursor.remaining } if truncated
|
|
117
126
|
values = {} #: Hash[Symbol, untyped]
|
|
118
127
|
defs.each do |d|
|
|
119
128
|
val = raw[d.index]
|
|
120
129
|
val = ((val >> d.shift) & d.mask) * d.scale if d.shift
|
|
121
130
|
values[d.key] = val
|
|
122
131
|
end
|
|
123
|
-
layer = Layer.new(self.class.protocol_id, cursor.start, length, cursor.limit,
|
|
132
|
+
layer = Layer.new(self.class.protocol_id, cursor.start, truncated ? cursor.remaining : length, cursor.limit,
|
|
124
133
|
definitions: defs, values: values, embedded: ctx.embedded)
|
|
125
|
-
|
|
134
|
+
return layer.diagnose(:note, :truncated, "need #{length} header bytes, have #{cursor.remaining}") if truncated
|
|
135
|
+
begin
|
|
136
|
+
dissect(ctx, layer)
|
|
137
|
+
rescue Cursor::Truncated => e
|
|
138
|
+
layer.diagnose(:note, :truncated, e.message)
|
|
139
|
+
end
|
|
126
140
|
layer
|
|
127
141
|
end
|
|
128
142
|
# @rbs (Context ctx, Layer layer) -> void
|
data/lib/redhound/engine.rb
CHANGED
|
@@ -29,6 +29,9 @@ module Redhound
|
|
|
29
29
|
break if layer.error?
|
|
30
30
|
klass = next_klass || Protocols::Data
|
|
31
31
|
end
|
|
32
|
+
if packet.meta[:possible_truncation]
|
|
33
|
+
ctx.layers.first.diagnose(:warn, :capture_truncated, 'receive buffer limit reached; original packet length is unknown')
|
|
34
|
+
end
|
|
32
35
|
ctx.layers
|
|
33
36
|
end
|
|
34
37
|
|
data/lib/redhound/field.rb
CHANGED
|
@@ -30,7 +30,7 @@ module Redhound
|
|
|
30
30
|
# Decoded value; addresses are returned in human-readable text.
|
|
31
31
|
def value
|
|
32
32
|
case type
|
|
33
|
-
when :ipv4 then
|
|
33
|
+
when :ipv4 then "#{(@raw_value >> 24) & 255}.#{(@raw_value >> 16) & 255}.#{(@raw_value >> 8) & 255}.#{@raw_value & 255}"
|
|
34
34
|
when :ipv6 then IPAddr.new_ntoh(@raw_value).to_s
|
|
35
35
|
when :mac then @raw_value.unpack('H2' * 6).join(':')
|
|
36
36
|
else @raw_value
|
data/lib/redhound/file/format.rb
CHANGED
|
@@ -7,6 +7,9 @@ module Redhound
|
|
|
7
7
|
# @api private
|
|
8
8
|
module Format
|
|
9
9
|
MAX_RECORD = 16 * 1024 * 1024
|
|
10
|
+
MAX_INTERFACES = 4096
|
|
11
|
+
MAX_INTERFACE_METADATA = 16 * 1024 * 1024
|
|
12
|
+
MAX_OPTIONS = 4096
|
|
10
13
|
PCAP_MAGICS = {
|
|
11
14
|
"\xd4\xc3\xb2\xa1".b => [:little, 1_000], "\xa1\xb2\xc3\xd4".b => [:big, 1_000],
|
|
12
15
|
"\x4d\x3c\xb2\xa1".b => [:little, 1], "\xa1\xb2\x3c\x4d".b => [:big, 1]
|
|
@@ -19,7 +22,11 @@ module Redhound
|
|
|
19
22
|
raise FileFormatError, "invalid record length #{length}" unless length.between?(0, MAX_RECORD)
|
|
20
23
|
return ''.b if length.zero?
|
|
21
24
|
|
|
22
|
-
result =
|
|
25
|
+
result = io.read(length)
|
|
26
|
+
return result if result && result.bytesize == length
|
|
27
|
+
return nil if eof && (!result || result.empty?)
|
|
28
|
+
|
|
29
|
+
result ||= ''.b
|
|
23
30
|
while result.bytesize < length
|
|
24
31
|
part = io.read(length - result.bytesize)
|
|
25
32
|
if !part || part.empty?
|
|
@@ -24,12 +24,16 @@ module Redhound
|
|
|
24
24
|
@closed = false
|
|
25
25
|
magic = precision == :nano ? 0xa1b23c4d : 0xa1b2c3d4
|
|
26
26
|
write_bytes([magic, 2, 4, 0, 0, snaplen, @linktype].pack('VvvV4'))
|
|
27
|
+
rescue StandardError, Interrupt
|
|
28
|
+
@io.close if @owned && @io && !@io.closed?
|
|
29
|
+
raise
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
# @rbs (Packet packet) -> self
|
|
30
33
|
def write(packet)
|
|
31
34
|
raise IOError, 'writer is closed' if @closed
|
|
32
35
|
raise FileFormatError, 'pcap cannot store multiple link types; use pcapng' unless packet.linktype == @linktype
|
|
36
|
+
raise FileFormatError, 'packet original length exceeds unsigned 32-bit range' unless packet.original_length <= 0xffff_ffff
|
|
33
37
|
|
|
34
38
|
seconds, nanos = packet.timestamp_ns.divmod(1_000_000_000)
|
|
35
39
|
raise FileFormatError, 'pcap timestamp exceeds unsigned 32-bit seconds' unless seconds.between?(0, 0xffff_ffff)
|
|
@@ -56,9 +60,12 @@ module Redhound
|
|
|
56
60
|
def close
|
|
57
61
|
return if @closed
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
begin
|
|
64
|
+
@io.flush
|
|
65
|
+
ensure
|
|
66
|
+
@closed = true
|
|
67
|
+
@io.close if @owned && !@io.closed?
|
|
68
|
+
end
|
|
62
69
|
end
|
|
63
70
|
|
|
64
71
|
private
|
|
@@ -17,6 +17,7 @@ module Redhound
|
|
|
17
17
|
@packet_counts = {} #: Hash[Integer, Integer]
|
|
18
18
|
@stats = Capture::Stats.new
|
|
19
19
|
@number = 0
|
|
20
|
+
@interface_metadata = 0
|
|
20
21
|
@magic = magic
|
|
21
22
|
@integer_format = 'V'
|
|
22
23
|
read_block
|
|
@@ -88,10 +89,11 @@ module Redhound
|
|
|
88
89
|
# @rbs (String body) -> void
|
|
89
90
|
def read_interface(body)
|
|
90
91
|
require_size(body, 8)
|
|
92
|
+
raise FileFormatError, 'pcapng interface limit exceeded' if @interfaces.size >= Format::MAX_INTERFACES
|
|
91
93
|
linktype, _reserved, snaplen = body.unpack(@integer_format == 'V' ? 'vvV' : 'nnN') #: [Integer, Integer, Integer]
|
|
92
94
|
raise FileFormatError, 'invalid pcapng snaplen' if snaplen > Format::MAX_RECORD
|
|
93
95
|
|
|
94
|
-
opts = options(body, 8)
|
|
96
|
+
opts = options(body, 8, [2, 3, 9, 11, 14])
|
|
95
97
|
resolution = opts.fetch(9, ["\x06".b]).fetch(0)
|
|
96
98
|
raise FileFormatError, 'invalid pcapng timestamp resolution' unless resolution.bytesize == 1
|
|
97
99
|
|
|
@@ -105,6 +107,11 @@ module Redhound
|
|
|
105
107
|
interface = Capture::Interface.new(name: opts.fetch(2, ["interface#{index}"]).fetch(0), index:, linktype:, snaplen:,
|
|
106
108
|
description: opts[3]&.first, filter: filter_option && filter_option.getbyte(0) == 0 ? filter_option.byteslice(1..) : nil,
|
|
107
109
|
meta: { timestamp_units: units, timestamp_offset: offset.unpack1(@integer_format == 'V' ? 'q<' : 'q>') })
|
|
110
|
+
metadata_size = 512 + interface.name.bytesize + (interface.description&.bytesize || 0) + (interface.filter&.bytesize || 0)
|
|
111
|
+
if @interface_metadata + metadata_size > Format::MAX_INTERFACE_METADATA
|
|
112
|
+
raise FileFormatError, 'pcapng interface metadata limit exceeded'
|
|
113
|
+
end
|
|
114
|
+
@interface_metadata += metadata_size
|
|
108
115
|
@section_interfaces << interface
|
|
109
116
|
@interfaces << interface
|
|
110
117
|
end
|
|
@@ -116,7 +123,7 @@ module Redhound
|
|
|
116
123
|
interface = interface_at(index)
|
|
117
124
|
Format.check_lengths(caplen, original_length, interface.snaplen)
|
|
118
125
|
require_size(body, 20 + Format.padded(caplen))
|
|
119
|
-
opts = options(body, 20 + Format.padded(caplen))
|
|
126
|
+
opts = options(body, 20 + Format.padded(caplen), [1, 2])
|
|
120
127
|
flags_option = opts[2]&.first
|
|
121
128
|
raise FileFormatError, 'invalid pcapng packet flags' if flags_option && flags_option.bytesize != 4
|
|
122
129
|
|
|
@@ -150,7 +157,7 @@ module Redhound
|
|
|
150
157
|
require_size(body, 12)
|
|
151
158
|
index = body.unpack1(@integer_format) #: Integer
|
|
152
159
|
interface = interface_at(index)
|
|
153
|
-
opts = options(body, 12)
|
|
160
|
+
opts = options(body, 12, [4, 5, 7, 8])
|
|
154
161
|
values = {} #: Hash[Symbol, Integer]
|
|
155
162
|
{ received: 4, dropped: 5, if_dropped: 7, captured: 8 }.each do |name, code|
|
|
156
163
|
option = opts[code]&.first
|
|
@@ -181,9 +188,10 @@ module Redhound
|
|
|
181
188
|
@section_interfaces.fetch(index) { raise FileFormatError, "pcapng references unknown interface #{index}" }
|
|
182
189
|
end
|
|
183
190
|
|
|
184
|
-
# @rbs (String body, Integer offset) -> Hash[Integer, Array[String]]
|
|
185
|
-
def options(body, offset)
|
|
191
|
+
# @rbs (String body, Integer offset, Array[Integer] allowed) -> Hash[Integer, Array[String]]
|
|
192
|
+
def options(body, offset, allowed)
|
|
186
193
|
result = {} #: Hash[Integer, Array[String]]
|
|
194
|
+
count = 0
|
|
187
195
|
while offset < body.bytesize
|
|
188
196
|
require_size(body, offset + 4)
|
|
189
197
|
code, length = body.unpack(@integer_format == 'V' ? 'vv' : 'nn', offset:) #: [Integer, Integer]
|
|
@@ -192,9 +200,13 @@ module Redhound
|
|
|
192
200
|
raise FileFormatError, 'invalid pcapng end of options' unless length.zero?
|
|
193
201
|
break
|
|
194
202
|
end
|
|
203
|
+
count += 1
|
|
204
|
+
raise FileFormatError, 'pcapng option count limit exceeded' if count > Format::MAX_OPTIONS
|
|
195
205
|
require_size(body, offset + Format.padded(length))
|
|
196
|
-
|
|
197
|
-
|
|
206
|
+
if allowed.include?(code)
|
|
207
|
+
value = body.byteslice(offset, length) #: String
|
|
208
|
+
(result[code] ||= []) << value
|
|
209
|
+
end
|
|
198
210
|
offset += Format.padded(length)
|
|
199
211
|
end
|
|
200
212
|
result
|
|
@@ -26,12 +26,16 @@ module Redhound
|
|
|
26
26
|
opts = { 3 => RUBY_PLATFORM, 4 => "Redhound #{VERSION}" }
|
|
27
27
|
opts[1] = comment if comment
|
|
28
28
|
write_block(0x0a0d0d0a, [0x1a2b3c4d, 1, 0, 0xffff_ffff_ffff_ffff].pack('VvvQ<') + options(opts))
|
|
29
|
+
rescue StandardError, Interrupt
|
|
30
|
+
@io.close if @owned && @io && !@io.closed?
|
|
31
|
+
raise
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
# @rbs (Packet packet) -> self
|
|
32
35
|
def write(packet)
|
|
33
36
|
raise IOError, 'writer is closed' if @closed
|
|
34
37
|
raise FileFormatError, 'pcapng timestamp must be nonnegative' if packet.timestamp_ns.negative?
|
|
38
|
+
raise FileFormatError, 'packet original length exceeds unsigned 32-bit range' unless packet.original_length <= 0xffff_ffff
|
|
35
39
|
|
|
36
40
|
index = ensure_interface(packet)
|
|
37
41
|
data = packet.data.byteslice(0, @snaplen)
|
|
@@ -76,20 +80,23 @@ module Redhound
|
|
|
76
80
|
def close
|
|
77
81
|
return if @closed
|
|
78
82
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
begin
|
|
84
|
+
@interfaces.each_value do |index|
|
|
85
|
+
stats = @statistics[index]
|
|
86
|
+
start_time, end_time = @start_times.fetch(index, 0), @end_times.fetch(index, 0)
|
|
87
|
+
opts = { 2 => timestamp_words(start_time), 3 => timestamp_words(end_time),
|
|
88
|
+
8 => [@counts.fetch(index, 0)].pack('Q<') }
|
|
89
|
+
if stats
|
|
90
|
+
opts[4], opts[5], opts[7] = [stats.received].pack('Q<'), [stats.dropped].pack('Q<'), [stats.if_dropped].pack('Q<')
|
|
91
|
+
opts[6] = [stats.captured].pack('Q<')
|
|
92
|
+
end
|
|
93
|
+
write_block(5, [index].pack('V') + timestamp_words(end_time) + options(opts))
|
|
87
94
|
end
|
|
88
|
-
|
|
95
|
+
@io.flush
|
|
96
|
+
ensure
|
|
97
|
+
@closed = true
|
|
98
|
+
@io.close if @owned && !@io.closed?
|
|
89
99
|
end
|
|
90
|
-
@io.flush
|
|
91
|
-
@io.close if @owned
|
|
92
|
-
@closed = true
|
|
93
100
|
end
|
|
94
101
|
|
|
95
102
|
private
|
|
@@ -99,14 +106,15 @@ module Redhound
|
|
|
99
106
|
interface = packet.interface
|
|
100
107
|
key = [interface&.object_id || 0, packet.linktype]
|
|
101
108
|
return @interfaces[key] if @interfaces.key?(key)
|
|
109
|
+
raise FileFormatError, 'pcapng interface limit exceeded' if @interfaces.size >= Format::MAX_INTERFACES
|
|
102
110
|
|
|
103
111
|
index = @interfaces.size
|
|
104
|
-
@interfaces[key] = index
|
|
105
112
|
opts = { 2 => interface&.name || "interface#{index}", 9 => "\x09".b, 12 => RUBY_PLATFORM }
|
|
106
113
|
opts[3] = interface.description if interface&.description
|
|
107
|
-
expression =
|
|
114
|
+
expression = @filter || interface&.filter
|
|
108
115
|
opts[11] = "\0" + expression if expression
|
|
109
116
|
write_block(1, [packet.linktype, 0, @snaplen].pack('vvV') + options(opts))
|
|
117
|
+
@interfaces[key] = index
|
|
110
118
|
index
|
|
111
119
|
end
|
|
112
120
|
|
|
@@ -8,6 +8,7 @@ module Redhound
|
|
|
8
8
|
module BPF
|
|
9
9
|
# @api private
|
|
10
10
|
class VM
|
|
11
|
+
LOAD_FORMATS = { 0x00 => 'N', 0x08 => 'n', 0x10 => 'C' }.freeze
|
|
11
12
|
# @rbs (Array[[Integer, Integer, Integer, Integer]] instructions) -> void
|
|
12
13
|
def initialize(instructions)
|
|
13
14
|
Validator.validate!(instructions)
|
|
@@ -34,9 +35,9 @@ module Redhound
|
|
|
34
35
|
if k >= 0xfffff000 && (code & 0xe0) == 0x20
|
|
35
36
|
ancillary(k, packet.meta)
|
|
36
37
|
else
|
|
37
|
-
size =
|
|
38
|
+
size = 4 >> ((code & 0x18) >> 3)
|
|
38
39
|
return 0 if offset.negative? || offset + size > packet.data.bytesize
|
|
39
|
-
packet.data.unpack1(
|
|
40
|
+
packet.data.unpack1(LOAD_FORMATS.fetch(code & 0x18), offset: offset)
|
|
40
41
|
end
|
|
41
42
|
end
|
|
42
43
|
if code == 0xb1
|
|
@@ -15,18 +15,33 @@ module Redhound
|
|
|
15
15
|
# @rbs (Packet packet) -> String
|
|
16
16
|
def line(packet)
|
|
17
17
|
parts = [@timestamp.format(packet)]
|
|
18
|
+
if packet.interface
|
|
19
|
+
parts << [packet.interface.name, packet.direction&.to_s&.capitalize].compact.join(' ')
|
|
20
|
+
end
|
|
18
21
|
if @link_layer && (eth = packet[:eth])
|
|
19
22
|
parts << "#{eth.display(:src)} > #{eth.display(:dst)}, ethertype #{eth.display(:type)}, length #{packet.original_length}:"
|
|
20
23
|
end
|
|
21
|
-
network =
|
|
22
|
-
transport =
|
|
24
|
+
network = nil #: Layer?
|
|
25
|
+
transport = nil #: Layer?
|
|
26
|
+
layer = nil #: Layer?
|
|
27
|
+
packet.layers.each do |current|
|
|
28
|
+
next if current.embedded
|
|
29
|
+
case current.protocol
|
|
30
|
+
when :ipv4, :ipv6
|
|
31
|
+
network, transport, layer = current, nil, nil
|
|
32
|
+
when :tcp, :udp
|
|
33
|
+
transport = layer = current
|
|
34
|
+
when :data, :raw, :eth, :vlan, :ipv6_ext, :sll, :sll2, :null
|
|
35
|
+
else layer = current
|
|
36
|
+
end
|
|
37
|
+
end
|
|
23
38
|
if network
|
|
24
39
|
src, dst = address(network.display(:src)), address(network.display(:dst))
|
|
25
40
|
src += ".#{transport[:srcport]}" if transport
|
|
26
41
|
dst += ".#{transport[:dstport]}" if transport
|
|
27
42
|
parts << "#{network.protocol == :ipv4 ? 'IP' : 'IP6'} #{src} > #{dst}:"
|
|
28
43
|
end
|
|
29
|
-
layer
|
|
44
|
+
layer ||= packet.layers.last
|
|
30
45
|
klass = layer && Registry.default.protocols[layer.protocol]
|
|
31
46
|
parts << if @quick && transport
|
|
32
47
|
"#{transport.protocol.to_s.upcase}, length #{[transport.payload_end - transport.payload_offset, 0].max}"
|
|
@@ -37,11 +52,21 @@ module Redhound
|
|
|
37
52
|
end
|
|
38
53
|
if @verbosity.positive? && network
|
|
39
54
|
parts << "ttl #{network[:ttl] || network[:hlim]}"
|
|
55
|
+
parts << "id #{network.display(:id)}, length #{network[:len]}" if network.protocol == :ipv4
|
|
56
|
+
parts << "checksum #{network.display(:checksum)}" if network[:checksum]
|
|
57
|
+
if transport
|
|
58
|
+
parts << "#{transport.protocol} checksum #{transport.display(:checksum)}"
|
|
59
|
+
options = transport.fields.select { |field| field.name.start_with?('tcp.options.') }
|
|
60
|
+
unless options.empty?
|
|
61
|
+
parts << 'options [' + options.map { |field| "#{field.name.delete_prefix('tcp.options.')} #{field.display}" }.join(', ') + ']'
|
|
62
|
+
end
|
|
63
|
+
end
|
|
40
64
|
end
|
|
41
65
|
diagnostics = packet.layers.flat_map(&:diagnostics)
|
|
42
66
|
parts << diagnostics.map { |d| "[#{d.code}]" }.join(' ') unless diagnostics.empty?
|
|
43
67
|
# All protocol summaries and interface names pass this terminal boundary.
|
|
44
|
-
parts.reject(&:empty?).join(' ').b
|
|
68
|
+
text = parts.reject(&:empty?).join(' ').b
|
|
69
|
+
text.match?(/[^\x20-\x7e]/n) ? text.gsub(/[^\x20-\x7e]/n) { |c| Kernel.format('\\x%02x', c.getbyte(0)) } : text
|
|
45
70
|
end
|
|
46
71
|
# @rbs (Packet packet, untyped io) -> void
|
|
47
72
|
def format(packet, io) = io.write(line(packet) + "\n")
|
|
@@ -26,9 +26,9 @@ module Redhound
|
|
|
26
26
|
value
|
|
27
27
|
end
|
|
28
28
|
# @rbs (Integer ns) -> String
|
|
29
|
-
def fraction(ns) = Kernel.format(@precision == :nano ? '%09d' : '%06d', @precision == :nano ? ns
|
|
29
|
+
def fraction(ns) = Kernel.format(@precision == :nano ? '%09d' : '%06d', @precision == :nano ? ns % 1_000_000_000 : (ns % 1_000_000_000) / 1000)
|
|
30
30
|
# @rbs (Integer ns) -> String
|
|
31
|
-
def seconds(ns) = "#{ns.negative? ? '-' : ''}#{ns.abs / 1_000_000_000}.#{fraction(ns)}"
|
|
31
|
+
def seconds(ns) = "#{ns.negative? ? '-' : ''}#{ns.abs / 1_000_000_000}.#{fraction(ns.abs)}"
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
end
|
data/lib/redhound/output/tree.rb
CHANGED
|
@@ -8,7 +8,8 @@ module Redhound
|
|
|
8
8
|
class Tree
|
|
9
9
|
# @rbs (Packet packet, untyped io) -> void
|
|
10
10
|
def format(packet, io)
|
|
11
|
-
|
|
11
|
+
interface = packet.interface ? safe([packet.interface.name, packet.direction&.to_s&.capitalize].compact.join(' ')) + ', ' : ''
|
|
12
|
+
io.write("Frame #{packet.number}: #{packet.original_length} bytes on wire, #{packet.caplen} captured, #{interface}#{packet.time.utc.strftime('%Y-%m-%d %H:%M:%S')}.#{Kernel.format('%09d', packet.time.nsec)}\n")
|
|
12
13
|
packet.layers.each_with_index do |layer, depth|
|
|
13
14
|
klass = Registry.default.protocols[layer.protocol]
|
|
14
15
|
indent = ' ' * depth
|
data/lib/redhound/packet.rb
CHANGED
|
@@ -12,9 +12,13 @@ module Redhound
|
|
|
12
12
|
# Copy binary data and preserve capture metadata; reject impossible original lengths.
|
|
13
13
|
def initialize(data, timestamp_ns: 0, original_length: nil, linktype: 1, interface: nil, direction: nil, number: 1, meta: {})
|
|
14
14
|
@data = data.b.freeze
|
|
15
|
-
|
|
15
|
+
raise ArgumentError, 'timestamp_ns must be an Integer' unless timestamp_ns.is_a?(Integer)
|
|
16
|
+
raise ArgumentError, 'number must be a positive Integer' unless number.is_a?(Integer) && number.positive?
|
|
17
|
+
raise ArgumentError, 'direction must be in, out or nil' unless direction.nil? || direction == :in || direction == :out
|
|
18
|
+
@timestamp_ns, @original_length = timestamp_ns, original_length.nil? ? data.bytesize : original_length
|
|
19
|
+
raise ArgumentError, 'original length must be an Integer' unless @original_length.is_a?(Integer)
|
|
16
20
|
raise ArgumentError, 'original length is shorter than captured length' if @original_length < data.bytesize
|
|
17
|
-
@linktype =
|
|
21
|
+
@linktype = Capture::Linktype.resolve(linktype)
|
|
18
22
|
@interface, @direction, @number, @meta = interface, direction, number, meta.dup
|
|
19
23
|
@engine = nil # @rbs Engine?
|
|
20
24
|
end
|
|
@@ -56,8 +60,18 @@ module Redhound
|
|
|
56
60
|
# Return a JSON-compatible packet matching docs/json-schema.json.
|
|
57
61
|
def to_h
|
|
58
62
|
{ frame: { number: @number, time_epoch_ns: @timestamp_ns, time: time.utc.strftime('%Y-%m-%dT%H:%M:%S.') + format('%09dZ', time.nsec),
|
|
59
|
-
caplen: caplen, len: @original_length, interface: @interface&.name
|
|
60
|
-
layers: layers.map(&:to_h), diagnostics: layers.flat_map { |l| l.diagnostics.map { |d| { layer: l.protocol, severity: d.severity, code: d.code, message: d.message, field: d.field } } } }
|
|
63
|
+
caplen: caplen, len: @original_length, interface: json_text(@interface&.name), direction: @direction, linktype: @linktype },
|
|
64
|
+
layers: layers.map(&:to_h), diagnostics: layers.flat_map { |l| l.diagnostics.map { |d| { layer: l.protocol, severity: d.severity, code: d.code, message: json_text(d.message), field: json_text(d.field) } } } }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# @rbs (String? text) -> String?
|
|
70
|
+
def json_text(text)
|
|
71
|
+
return nil unless text
|
|
72
|
+
utf8 = text.dup.force_encoding(Encoding::UTF_8)
|
|
73
|
+
return utf8 if utf8.valid_encoding?
|
|
74
|
+
text.unpack1('H*') #: String
|
|
61
75
|
end
|
|
62
76
|
end
|
|
63
77
|
end
|
|
@@ -6,11 +6,13 @@ module Redhound
|
|
|
6
6
|
module Protocols
|
|
7
7
|
# @api private
|
|
8
8
|
class Data < Dissector
|
|
9
|
+
LENGTH_FIELD = FieldDefinition.new(:length, 'data.len', :uint, 0, 0, 0, nil, nil, 1, nil, nil)
|
|
9
10
|
protocol :data, name: 'Data', short: 'DATA'
|
|
10
11
|
# @rbs (Context ctx, Layer layer) -> void
|
|
11
12
|
def dissect(ctx, layer)
|
|
12
13
|
length = ctx.cursor.remaining
|
|
13
|
-
layer.
|
|
14
|
+
layer.definitions << LENGTH_FIELD
|
|
15
|
+
layer.values[:length] = length
|
|
14
16
|
layer.add(:data, 'data.data', ctx.cursor.bytes(0, length), type: :bytes, length: length)
|
|
15
17
|
layer.payload_offset = layer.payload_end
|
|
16
18
|
end
|
|
@@ -53,7 +53,7 @@ module Redhound
|
|
|
53
53
|
end
|
|
54
54
|
options.each do |code, entries|
|
|
55
55
|
value = entries.map { |entry| entry[0] }.join
|
|
56
|
-
decode_option(layer, code, value, entries
|
|
56
|
+
decode_option(layer, code, value, entries)
|
|
57
57
|
end
|
|
58
58
|
rescue Cursor::Truncated => e
|
|
59
59
|
layer.diagnose(:note, :truncated, e.message)
|
|
@@ -83,37 +83,53 @@ module Redhound
|
|
|
83
83
|
end
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
-
# @rbs (Layer layer, Integer code, String value,
|
|
87
|
-
def decode_option(layer, code, value,
|
|
86
|
+
# @rbs (Layer layer, Integer code, String value, untyped entries) -> void
|
|
87
|
+
def decode_option(layer, code, value, entries)
|
|
88
|
+
offset = entries.first[1]
|
|
88
89
|
length = value.bytesize
|
|
89
90
|
if ADDRESS_OPTIONS.key?(code)
|
|
90
91
|
valid = length.positive? && length % 4 == 0 && (![1, 50, 54].include?(code) || length == 4)
|
|
91
92
|
return layer.diagnose(:error, :bad_length, "invalid DHCP option #{code} length") unless valid
|
|
92
93
|
(length / 4).times do |i|
|
|
93
|
-
put(layer, "dhcp.option.#{ADDRESS_OPTIONS[code]}", value.unpack1('N', offset: i * 4), :ipv4, offset + i * 4, 4)
|
|
94
|
+
put(layer, "dhcp.option.#{ADDRESS_OPTIONS[code]}", value.unpack1('N', offset: i * 4), :ipv4, offset + i * 4, 4, entries)
|
|
94
95
|
end
|
|
95
96
|
else
|
|
96
97
|
case code
|
|
97
98
|
when 53, 52
|
|
98
99
|
return layer.diagnose(:error, :bad_length, "invalid DHCP option #{code} length") unless length == 1
|
|
99
|
-
put(layer, code == 53 ? 'dhcp.option.dhcp' : 'dhcp.option.overload', value.getbyte(0), :uint, offset, 1)
|
|
100
|
+
put(layer, code == 53 ? 'dhcp.option.dhcp' : 'dhcp.option.overload', value.getbyte(0), :uint, offset, 1, entries)
|
|
100
101
|
when 51
|
|
101
102
|
return layer.diagnose(:error, :bad_length, 'invalid DHCP lease length') unless length == 4
|
|
102
|
-
put(layer, 'dhcp.option.ip_address_lease_time', value.unpack1('N'), :uint, offset, 4)
|
|
103
|
+
put(layer, 'dhcp.option.ip_address_lease_time', value.unpack1('N'), :uint, offset, 4, entries)
|
|
103
104
|
when 55
|
|
104
|
-
length.times { |i| put(layer, 'dhcp.option.request_list_item', value.getbyte(i), :uint, offset + i, 1) }
|
|
105
|
+
length.times { |i| put(layer, 'dhcp.option.request_list_item', value.getbyte(i), :uint, offset + i, 1, entries) }
|
|
105
106
|
when 61
|
|
106
107
|
return layer.diagnose(:error, :bad_length, 'DHCP client ID must include type and identifier') if length < 2
|
|
107
|
-
put(layer, 'dhcp.option.client_id', value, :bytes, offset, length)
|
|
108
|
-
put(layer, 'dhcp.option.client_id.type', value.getbyte(0), :uint, offset, 1)
|
|
109
|
-
put(layer, 'dhcp.option.client_id.hw_mac_addr', value.byteslice(1, 6), :mac, offset + 1, 6) if length == 7 && value.getbyte(0) == 1
|
|
110
|
-
when 12 then put(layer, 'dhcp.option.hostname', value, :string, offset, length)
|
|
108
|
+
put(layer, 'dhcp.option.client_id', value, :bytes, offset, length, entries)
|
|
109
|
+
put(layer, 'dhcp.option.client_id.type', value.getbyte(0), :uint, offset, 1, entries)
|
|
110
|
+
put(layer, 'dhcp.option.client_id.hw_mac_addr', value.byteslice(1, 6), :mac, offset + 1, 6, entries) if length == 7 && value.getbyte(0) == 1
|
|
111
|
+
when 12 then put(layer, 'dhcp.option.hostname', value, :string, offset, length, entries)
|
|
111
112
|
end
|
|
112
113
|
end
|
|
113
114
|
end
|
|
114
115
|
|
|
115
|
-
# @rbs (
|
|
116
|
-
def
|
|
116
|
+
# @rbs (untyped entries, Integer logical) -> Integer
|
|
117
|
+
def wire_offset(entries, logical)
|
|
118
|
+
entries.each do |bytes, pos|
|
|
119
|
+
return pos + logical if logical < bytes.bytesize
|
|
120
|
+
logical -= bytes.bytesize
|
|
121
|
+
end
|
|
122
|
+
entries.last[1] + entries.last[0].bytesize
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# @rbs (Layer layer, String name, untyped value, Symbol type, Integer offset, Integer length, ?untyped entries) -> void
|
|
126
|
+
def put(layer, name, value, type, offset, length, entries = nil)
|
|
127
|
+
if entries
|
|
128
|
+
logical = offset - entries.first[1]
|
|
129
|
+
first = wire_offset(entries, logical)
|
|
130
|
+
ending = length.zero? ? first : wire_offset(entries, logical + length - 1) + 1
|
|
131
|
+
offset, length = first, ending - first
|
|
132
|
+
end
|
|
117
133
|
layer.add("#{name}_#{layer.definitions.length}".to_sym, name, value, type: type, offset: offset, length: length)
|
|
118
134
|
end
|
|
119
135
|
|
|
@@ -37,7 +37,7 @@ module Redhound
|
|
|
37
37
|
headers = parse_headers(layer, bytes, first + 2, ending + 2)
|
|
38
38
|
return if layer.error?
|
|
39
39
|
body_start = ending + 4
|
|
40
|
-
length_values = headers.fetch('content-length', []).flat_map { |value| value.split(',').map(&:strip) }
|
|
40
|
+
length_values = headers.fetch('content-length', []).flat_map { |value| value.empty? ? [''] : value.split(',', -1).map(&:strip) }
|
|
41
41
|
unless length_values.all? { |value| value.match?(/\A[0-9]+\z/n) } && length_values.uniq.length <= 1
|
|
42
42
|
return layer.diagnose(:error, :malformed, 'conflicting or invalid Content-Length')
|
|
43
43
|
end
|
|
@@ -108,13 +108,15 @@ module Redhound
|
|
|
108
108
|
end
|
|
109
109
|
name = name.downcase
|
|
110
110
|
value = line.byteslice(colon + 1..) #: String
|
|
111
|
-
|
|
111
|
+
leading = (value[/\A[ \t]*/n] || '').bytesize
|
|
112
|
+
trailing = value.rindex(/[^ \t]/n)
|
|
113
|
+
value = value.byteslice(leading, trailing ? trailing + 1 - leading : 0) #: String
|
|
112
114
|
(headers[name] ||= []) << value
|
|
113
115
|
key = name.tr('-', '_')
|
|
114
116
|
if HEADERS.include?(key)
|
|
115
117
|
numeric = key == 'content_length' && value.match?(/\A[0-9]+\z/n)
|
|
116
118
|
put(layer, "http.#{key}", numeric ? value.to_i : value, numeric ? :uint : :string,
|
|
117
|
-
pos + colon + 1,
|
|
119
|
+
pos + colon + 1 + leading, value.bytesize)
|
|
118
120
|
else
|
|
119
121
|
put(layer, 'http.request.line', line, :string, pos, line.bytesize)
|
|
120
122
|
end
|