redhound 1.0.1 → 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 +51 -0
- data/README.md +97 -31
- data/docs/API.md +104 -0
- data/docs/FILTERS.md +37 -0
- data/docs/MIGRATION.md +23 -0
- data/docs/PROTOCOLS.md +54 -0
- data/docs/USAGE.md +86 -0
- data/docs/VALIDATION.md +175 -0
- data/docs/json-schema.json +64 -0
- data/exe/redhound +2 -1
- data/lib/redhound/analysis/flow_key.rb +59 -0
- data/lib/redhound/analysis/flow_table.rb +124 -0
- data/lib/redhound/analysis/ip_reassembler.rb +209 -0
- data/lib/redhound/analysis/stats.rb +150 -0
- data/lib/redhound/analysis/tcp_analysis.rb +43 -0
- data/lib/redhound/analysis/tcp_reassembler.rb +176 -0
- data/lib/redhound/analysis/tcp_stream.rb +156 -0
- data/lib/redhound/analysis.rb +157 -0
- data/lib/redhound/capture/bsd/bpf_device.rb +155 -0
- data/lib/redhound/capture/bsd/constants.rb +29 -0
- data/lib/redhound/capture/bsd/ifreq.rb +41 -0
- data/lib/redhound/capture/file_source.rb +214 -0
- data/lib/redhound/capture/interface.rb +70 -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 +224 -0
- data/lib/redhound/capture/linux/sockaddr_ll.rb +33 -0
- data/lib/redhound/capture/linux/tpacket_v3.rb +137 -0
- data/lib/redhound/capture/source.rb +79 -0
- data/lib/redhound/capture/stats.rb +22 -0
- data/lib/redhound/capture.rb +70 -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 +152 -0
- data/lib/redhound/engine.rb +69 -0
- data/lib/redhound/errors.rb +38 -0
- data/lib/redhound/field.rb +62 -0
- data/lib/redhound/file/format.rb +63 -0
- data/lib/redhound/file/pcap_reader.rb +43 -0
- data/lib/redhound/file/pcap_writer.rb +79 -0
- data/lib/redhound/file/pcapng_reader.rb +221 -0
- data/lib/redhound/file/pcapng_writer.rb +151 -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 +109 -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 +64 -0
- data/lib/redhound/output/hexdump.rb +29 -0
- data/lib/redhound/output/json.rb +32 -0
- data/lib/redhound/output/summary.rb +84 -0
- data/lib/redhound/output/timestamp.rb +34 -0
- data/lib/redhound/output/tree.rb +25 -0
- data/lib/redhound/packet.rb +77 -0
- data/lib/redhound/protocols/arp.rb +39 -0
- data/lib/redhound/protocols/checksum.rb +38 -0
- data/lib/redhound/protocols/data.rb +23 -0
- data/lib/redhound/protocols/dhcp.rb +140 -0
- data/lib/redhound/protocols/dns.rb +268 -0
- data/lib/redhound/protocols/ethernet.rb +24 -0
- data/lib/redhound/protocols/http.rb +185 -0
- data/lib/redhound/protocols/icmp.rb +205 -0
- data/lib/redhound/protocols/ipv4.rb +56 -0
- data/lib/redhound/protocols/ipv6.rb +68 -0
- data/lib/redhound/protocols/link.rb +70 -0
- data/lib/redhound/protocols/llc.rb +37 -0
- data/lib/redhound/protocols/ntp.rb +45 -0
- data/lib/redhound/protocols/tcp.rb +88 -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 +102 -0
- data/sig/generated/redhound/analysis/ip_reassembler.rbs +52 -0
- data/sig/generated/redhound/analysis/stats.rbs +80 -0
- data/sig/generated/redhound/analysis/tcp_analysis.rbs +15 -0
- data/sig/generated/redhound/analysis/tcp_reassembler.rbs +39 -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/bsd/ifreq.rbs +15 -0
- data/sig/generated/redhound/capture/file_source.rbs +67 -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 +64 -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 +117 -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 +39 -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 +28 -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 +82 -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 +17 -0
- data/sig/generated/redhound/protocols/dhcp.rbs +31 -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 +177 -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 -9
- 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,140 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Protocols
|
|
7
|
+
# @api private
|
|
8
|
+
class Dhcp < Dissector
|
|
9
|
+
MESSAGE_TYPES = { 1 => 'Discover', 2 => 'Offer', 3 => 'Request', 4 => 'Decline', 5 => 'ACK',
|
|
10
|
+
6 => 'NAK', 7 => 'Release', 8 => 'Inform' }.freeze
|
|
11
|
+
ADDRESS_OPTIONS = { 1 => 'subnet_mask', 3 => 'router', 6 => 'domain_name_server',
|
|
12
|
+
50 => 'requested_ip_address', 54 => 'dhcp_server_id' }.freeze
|
|
13
|
+
protocol :dhcp, name: 'Dynamic Host Configuration Protocol', short: 'DHCP'
|
|
14
|
+
dissects_on 'udp.port', 67, 68
|
|
15
|
+
header do
|
|
16
|
+
uint8 :type, 'dhcp.type'
|
|
17
|
+
uint8 :hw_type, 'dhcp.hw.type'
|
|
18
|
+
uint8 :hw_len, 'dhcp.hw.len'
|
|
19
|
+
uint8 :hops, 'dhcp.hops'
|
|
20
|
+
uint32 :xid, 'dhcp.id', format: :hex
|
|
21
|
+
uint16 :secs, 'dhcp.secs'
|
|
22
|
+
uint16 :flags, 'dhcp.flags', format: :hex
|
|
23
|
+
ipv4 :client, 'dhcp.ip.client'
|
|
24
|
+
ipv4 :your, 'dhcp.ip.your'
|
|
25
|
+
ipv4 :server, 'dhcp.ip.server'
|
|
26
|
+
ipv4 :relay, 'dhcp.ip.relay'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @rbs (Context ctx, Layer layer) -> void
|
|
30
|
+
def dissect(ctx, layer)
|
|
31
|
+
cursor = ctx.cursor
|
|
32
|
+
cursor.check(0, 236)
|
|
33
|
+
return layer.diagnose(:error, :bad_length, 'hardware address is longer than 16 bytes') if layer[:hw_len] > 16
|
|
34
|
+
put(layer, 'dhcp.hw.mac_addr', cursor.bytes(28, layer[:hw_len]),
|
|
35
|
+
layer[:hw_type] == 1 && layer[:hw_len] == 6 ? :mac : :bytes, 28, layer[:hw_len])
|
|
36
|
+
put(layer, 'dhcp.server', cursor.bytes(44, 64).split("\0", 2).first || '', :string, 44, 64)
|
|
37
|
+
put(layer, 'dhcp.file', cursor.bytes(108, 128).split("\0", 2).first || '', :string, 108, 128)
|
|
38
|
+
return if cursor.remaining == 236
|
|
39
|
+
cursor.check(236, 4)
|
|
40
|
+
unless cursor.u32(236) == 0x63825363
|
|
41
|
+
put(layer, 'dhcp.vendor', cursor.bytes(236, cursor.remaining - 236), :bytes, 236, cursor.remaining - 236)
|
|
42
|
+
return
|
|
43
|
+
end
|
|
44
|
+
put(layer, 'dhcp.cookie', 0x63825363, :uint, 236, 4)
|
|
45
|
+
# @type var options: Hash[Integer, untyped]
|
|
46
|
+
options = Hash.new
|
|
47
|
+
read_options(cursor, layer, 240, cursor.remaining, options)
|
|
48
|
+
overload = options[52]&.map { |entry| entry[0] }&.join
|
|
49
|
+
if overload
|
|
50
|
+
return layer.diagnose(:error, :bad_length, 'invalid DHCP overload option') unless overload.bytesize == 1 && overload.getbyte(0).between?(1, 3)
|
|
51
|
+
read_options(cursor, layer, 108, 236, options) if overload.getbyte(0) & 1 != 0
|
|
52
|
+
read_options(cursor, layer, 44, 108, options) if overload.getbyte(0) & 2 != 0
|
|
53
|
+
end
|
|
54
|
+
options.each do |code, entries|
|
|
55
|
+
value = entries.map { |entry| entry[0] }.join
|
|
56
|
+
decode_option(layer, code, value, entries)
|
|
57
|
+
end
|
|
58
|
+
rescue Cursor::Truncated => e
|
|
59
|
+
layer.diagnose(:note, :truncated, e.message)
|
|
60
|
+
ensure
|
|
61
|
+
layer.header_length = cursor.remaining
|
|
62
|
+
layer.payload_offset = layer.payload_end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @rbs (Cursor cursor, Layer layer, Integer pos, Integer ending, untyped options) -> void
|
|
66
|
+
def read_options(cursor, layer, pos, ending, options)
|
|
67
|
+
while pos < ending
|
|
68
|
+
code = cursor.u8(pos)
|
|
69
|
+
pos += 1
|
|
70
|
+
next if code.zero?
|
|
71
|
+
break if code == 255
|
|
72
|
+
raise Cursor::Truncated, 'truncated DHCP option length' if pos >= ending
|
|
73
|
+
len = cursor.u8(pos)
|
|
74
|
+
pos += 1
|
|
75
|
+
raise Cursor::Truncated, 'DHCP option exceeds available bytes' if pos + len > ending
|
|
76
|
+
value = cursor.bytes(pos, len)
|
|
77
|
+
put(layer, 'dhcp.option.type', code, :uint, pos - 2, 1)
|
|
78
|
+
put(layer, 'dhcp.option.length', len, :uint, pos - 1, 1)
|
|
79
|
+
put(layer, 'dhcp.option.value', value, :bytes, pos, len)
|
|
80
|
+
options[code] = Array.new unless options.key?(code)
|
|
81
|
+
options[code] << [value, pos]
|
|
82
|
+
pos += len
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @rbs (Layer layer, Integer code, String value, untyped entries) -> void
|
|
87
|
+
def decode_option(layer, code, value, entries)
|
|
88
|
+
offset = entries.first[1]
|
|
89
|
+
length = value.bytesize
|
|
90
|
+
if ADDRESS_OPTIONS.key?(code)
|
|
91
|
+
valid = length.positive? && length % 4 == 0 && (![1, 50, 54].include?(code) || length == 4)
|
|
92
|
+
return layer.diagnose(:error, :bad_length, "invalid DHCP option #{code} length") unless valid
|
|
93
|
+
(length / 4).times do |i|
|
|
94
|
+
put(layer, "dhcp.option.#{ADDRESS_OPTIONS[code]}", value.unpack1('N', offset: i * 4), :ipv4, offset + i * 4, 4, entries)
|
|
95
|
+
end
|
|
96
|
+
else
|
|
97
|
+
case code
|
|
98
|
+
when 53, 52
|
|
99
|
+
return layer.diagnose(:error, :bad_length, "invalid DHCP option #{code} length") unless length == 1
|
|
100
|
+
put(layer, code == 53 ? 'dhcp.option.dhcp' : 'dhcp.option.overload', value.getbyte(0), :uint, offset, 1, entries)
|
|
101
|
+
when 51
|
|
102
|
+
return layer.diagnose(:error, :bad_length, 'invalid DHCP lease length') unless length == 4
|
|
103
|
+
put(layer, 'dhcp.option.ip_address_lease_time', value.unpack1('N'), :uint, offset, 4, entries)
|
|
104
|
+
when 55
|
|
105
|
+
length.times { |i| put(layer, 'dhcp.option.request_list_item', value.getbyte(i), :uint, offset + i, 1, entries) }
|
|
106
|
+
when 61
|
|
107
|
+
return layer.diagnose(:error, :bad_length, 'DHCP client ID must include type and identifier') if length < 2
|
|
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)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
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
|
|
133
|
+
layer.add("#{name}_#{layer.definitions.length}".to_sym, name, value, type: type, offset: offset, length: length)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @rbs (Layer layer) -> String
|
|
137
|
+
def summary(layer) = "DHCP #{MESSAGE_TYPES[layer.field_value('dhcp.option.dhcp')] || 'BOOTP'}, xid #{format('0x%x', layer[:xid] || 0)}"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Protocols
|
|
7
|
+
# @api private
|
|
8
|
+
class Dns < StreamDissector
|
|
9
|
+
# @api private
|
|
10
|
+
class Malformed < StandardError; end
|
|
11
|
+
TYPES = { 1 => 'A', 2 => 'NS', 5 => 'CNAME', 6 => 'SOA', 12 => 'PTR', 15 => 'MX',
|
|
12
|
+
16 => 'TXT', 28 => 'AAAA', 33 => 'SRV', 41 => 'OPT', 64 => 'SVCB', 65 => 'HTTPS' }.freeze
|
|
13
|
+
protocol :dns, name: 'Domain Name System', short: 'DNS'
|
|
14
|
+
dissects_on 'udp.port', 53, 5353, 5355
|
|
15
|
+
dissects_on 'tcp.port', 53, 5353, 5355
|
|
16
|
+
|
|
17
|
+
# @rbs (Context ctx, Layer layer) -> void
|
|
18
|
+
def dissect(ctx, layer)
|
|
19
|
+
cursor = ctx.cursor
|
|
20
|
+
pos = 0
|
|
21
|
+
transport = ctx.layers.last
|
|
22
|
+
tcp = transport&.protocol == :tcp
|
|
23
|
+
port = [transport&.[](:srcport), transport&.[](:dstport)].compact.sort.find { |value| [53, 5353, 5355].include?(value) }
|
|
24
|
+
variant = { 5353 => :mdns, 5355 => :llmnr }.fetch(port, :dns)
|
|
25
|
+
layer.values[:variant] = variant
|
|
26
|
+
loop do
|
|
27
|
+
if tcp
|
|
28
|
+
len = cursor.u16(pos)
|
|
29
|
+
put(layer, 'dns.length', len, :uint, pos, 2)
|
|
30
|
+
raise Malformed, 'DNS message shorter than header' if len < 12
|
|
31
|
+
pos += 2
|
|
32
|
+
ending = pos + len
|
|
33
|
+
layer.diagnose(:note, :truncated) if ending > cursor.remaining
|
|
34
|
+
message(cursor.sub(cursor.start + pos, cursor.start + ending), layer, variant)
|
|
35
|
+
pos = ending
|
|
36
|
+
else
|
|
37
|
+
message(cursor, layer, variant)
|
|
38
|
+
pos = cursor.remaining
|
|
39
|
+
end
|
|
40
|
+
break unless tcp && pos < cursor.remaining
|
|
41
|
+
end
|
|
42
|
+
rescue Cursor::Truncated => e
|
|
43
|
+
layer.diagnose(:note, :truncated, e.message)
|
|
44
|
+
rescue Malformed => e
|
|
45
|
+
layer.diagnose(:error, :malformed, e.message)
|
|
46
|
+
ensure
|
|
47
|
+
layer.header_length = cursor.remaining
|
|
48
|
+
layer.payload_offset = layer.payload_end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @rbs (Cursor cursor, Layer layer, Symbol variant) -> void
|
|
52
|
+
def message(cursor, layer, variant)
|
|
53
|
+
cursor.check(0, 12)
|
|
54
|
+
base = cursor.start - layer.offset
|
|
55
|
+
put(layer, 'dns.id', cursor.u16(0), :uint, base, 2)
|
|
56
|
+
flags = cursor.u16(2)
|
|
57
|
+
put(layer, 'dns.flags', flags, :uint, base + 2, 2)
|
|
58
|
+
flag_bits = { response: [15, 1], opcode: [11, 15], authoritative: [10, 1], truncated: [9, 1],
|
|
59
|
+
recdesired: [8, 1], recavail: [7, 1], authenticated: [5, 1], checkdisable: [4, 1], rcode: [0, 15] }
|
|
60
|
+
if variant == :llmnr
|
|
61
|
+
%i[authoritative recdesired recavail authenticated checkdisable].each { |key| flag_bits.delete(key) }
|
|
62
|
+
flag_bits.merge!(conflict: [10, 1], tentative: [8, 1])
|
|
63
|
+
end
|
|
64
|
+
flag_bits.each do |key, pair|
|
|
65
|
+
next if key == :rcode && flags & 0x8000 == 0
|
|
66
|
+
shift, mask = pair.fetch(0), pair.fetch(1)
|
|
67
|
+
value = (flags >> shift) & mask
|
|
68
|
+
put(layer, "dns.flags.#{key}", mask == 1 ? value == 1 : value, mask == 1 ? :boolean : :uint, base + 2, 2)
|
|
69
|
+
end
|
|
70
|
+
counts = cursor.unpack('n4', 8, 4)
|
|
71
|
+
%w[queries answers auth_rr additional_rr].each_with_index do |name, i|
|
|
72
|
+
put(layer, "dns.count.#{name}", counts[i], :uint, base + 4 + i * 2, 2)
|
|
73
|
+
end
|
|
74
|
+
pos = 12
|
|
75
|
+
counts[0].times do
|
|
76
|
+
name, after = read_name(cursor, pos)
|
|
77
|
+
put(layer, 'dns.qry.name', name, :string, base + pos, after - pos)
|
|
78
|
+
cursor.check(after, 4)
|
|
79
|
+
put(layer, 'dns.qry.type', cursor.u16(after), :uint, base + after, 2)
|
|
80
|
+
klass = cursor.u16(after + 2)
|
|
81
|
+
put(layer, 'dns.qry.class', variant == :mdns ? klass & 0x7fff : klass, :uint, base + after + 2, 2)
|
|
82
|
+
put(layer, 'dns.qry.qu', klass & 0x8000 != 0, :boolean, base + after + 2, 2) if variant == :mdns
|
|
83
|
+
pos = after + 4
|
|
84
|
+
end
|
|
85
|
+
counts.drop(1).sum.times { pos = record(cursor, layer, pos, base, variant) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @rbs (Cursor cursor, Integer pos, ?Integer? limit, ?bool compression) -> [String, Integer]
|
|
89
|
+
def read_name(cursor, pos, limit = nil, compression = true)
|
|
90
|
+
limit ||= cursor.remaining
|
|
91
|
+
labels, seen, wire_length, jumps, ending = [], {}, 1, 0, nil
|
|
92
|
+
loop do
|
|
93
|
+
raise Malformed, 'DNS name compression loop' if seen[pos]
|
|
94
|
+
seen[pos] = true
|
|
95
|
+
len = cursor.u8(pos)
|
|
96
|
+
raise Malformed, 'DNS name exceeds record length' if ending.nil? && pos >= limit
|
|
97
|
+
if len & 0xc0 == 0xc0
|
|
98
|
+
raise Malformed, 'compressed target name is forbidden' unless compression
|
|
99
|
+
raise Malformed, 'DNS pointer exceeds record length' if ending.nil? && pos + 2 > limit
|
|
100
|
+
target = cursor.u16(pos) & 0x3fff
|
|
101
|
+
raise Malformed, 'DNS pointer is not backward' unless target < pos
|
|
102
|
+
jumps += 1
|
|
103
|
+
raise Malformed, 'DNS compression exceeds 128 jumps' if jumps > 128
|
|
104
|
+
ending ||= pos + 2
|
|
105
|
+
pos = target
|
|
106
|
+
else
|
|
107
|
+
raise Malformed, 'invalid DNS label type' unless len & 0xc0 == 0
|
|
108
|
+
pos += 1
|
|
109
|
+
break if len.zero?
|
|
110
|
+
wire_length += len + 1
|
|
111
|
+
raise Malformed, 'DNS name exceeds 255 bytes' if wire_length > 255
|
|
112
|
+
cursor.check(pos, len)
|
|
113
|
+
raise Malformed, 'DNS label exceeds record length' if ending.nil? && pos + len > limit
|
|
114
|
+
labels << cursor.bytes(pos, len)
|
|
115
|
+
pos += len
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
[labels.join('.'), ending || pos]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @rbs (Cursor cursor, Layer layer, Integer pos, Integer base, Symbol variant) -> Integer
|
|
122
|
+
def record(cursor, layer, pos, base, variant)
|
|
123
|
+
name, after = read_name(cursor, pos)
|
|
124
|
+
cursor.check(after, 10)
|
|
125
|
+
type, klass, ttl, length = cursor.unpack('nnNn', 10, after)
|
|
126
|
+
first, ending = after + 10, after + 10 + length
|
|
127
|
+
cursor.check(first, length)
|
|
128
|
+
if type == 33
|
|
129
|
+
service, protocol, domain = name.split('.', 3)
|
|
130
|
+
put(layer, 'dns.srv.service', service || '', :string, base + pos, after - pos)
|
|
131
|
+
put(layer, 'dns.srv.proto', protocol || '', :string, base + pos, after - pos)
|
|
132
|
+
put(layer, 'dns.srv.name', domain, :string, base + pos, after - pos) if domain
|
|
133
|
+
else
|
|
134
|
+
put(layer, 'dns.resp.name', name.empty? ? '<Root>' : name, :string, base + pos, after - pos)
|
|
135
|
+
end
|
|
136
|
+
put(layer, 'dns.resp.type', type, :uint, base + after, 2)
|
|
137
|
+
put(layer, 'dns.resp.len', length, :uint, base + after + 8, 2)
|
|
138
|
+
if type == 41
|
|
139
|
+
put(layer, 'dns.rr.udp_payload_size', klass, :uint, base + after + 2, 2)
|
|
140
|
+
put(layer, 'dns.resp.ext_rcode', ttl >> 24, :uint, base + after + 4, 1)
|
|
141
|
+
put(layer, 'dns.resp.edns0_version', (ttl >> 16) & 255, :uint, base + after + 5, 1)
|
|
142
|
+
put(layer, 'dns.resp.z', ttl & 0xffff, :uint, base + after + 6, 2)
|
|
143
|
+
put(layer, 'dns.resp.z.do', ttl & 0x8000 != 0, :boolean, base + after + 6, 2)
|
|
144
|
+
edns(cursor, layer, first, ending, base)
|
|
145
|
+
else
|
|
146
|
+
put(layer, 'dns.resp.class', variant == :mdns ? klass & 0x7fff : klass, :uint, base + after + 2, 2)
|
|
147
|
+
put(layer, 'dns.resp.ttl', ttl, :uint, base + after + 4, 4)
|
|
148
|
+
put(layer, 'dns.resp.cache_flush', klass & 0x8000 != 0, :boolean, base + after + 2, 2) if variant == :mdns
|
|
149
|
+
rdata(cursor, layer, type, first, ending, base)
|
|
150
|
+
end
|
|
151
|
+
ending
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# @rbs (Cursor cursor, Layer layer, Integer type, Integer pos, Integer ending, Integer base) -> void
|
|
155
|
+
def rdata(cursor, layer, type, pos, ending, base)
|
|
156
|
+
length = ending - pos
|
|
157
|
+
case type
|
|
158
|
+
when 1, 28
|
|
159
|
+
expected = type == 1 ? 4 : 16
|
|
160
|
+
return layer.diagnose(:error, :bad_length, 'invalid DNS address length') unless length == expected
|
|
161
|
+
value = type == 1 ? cursor.u32(pos) : cursor.bytes(pos, 16)
|
|
162
|
+
put(layer, type == 1 ? 'dns.a' : 'dns.aaaa', value, type == 1 ? :ipv4 : :ipv6, base + pos, length)
|
|
163
|
+
when 2, 5, 12
|
|
164
|
+
name, after = read_name(cursor, pos, ending)
|
|
165
|
+
put(layer, { 2 => 'dns.ns', 5 => 'dns.cname', 12 => 'dns.ptr.domain_name' }.fetch(type), name, :string, base + pos, after - pos)
|
|
166
|
+
raise Malformed, 'DNS name has trailing record bytes' unless after == ending
|
|
167
|
+
when 15, 33
|
|
168
|
+
width = type == 15 ? 2 : 6
|
|
169
|
+
raise Malformed, 'DNS service record is too short' if length < width + 1
|
|
170
|
+
names = type == 15 ? ['dns.mx.preference'] : %w[dns.srv.priority dns.srv.weight dns.srv.port]
|
|
171
|
+
names.each_with_index { |name, i| put(layer, name, cursor.u16(pos + i * 2), :uint, base + pos + i * 2, 2) }
|
|
172
|
+
name, after = read_name(cursor, pos + width, ending)
|
|
173
|
+
put(layer, type == 15 ? 'dns.mx.mail_exchange' : 'dns.srv.target', name, :string, base + pos + width, after - pos - width)
|
|
174
|
+
raise Malformed, 'DNS service record has trailing bytes' unless after == ending
|
|
175
|
+
when 16
|
|
176
|
+
while pos < ending
|
|
177
|
+
len = cursor.u8(pos)
|
|
178
|
+
raise Malformed, 'DNS TXT string exceeds record' if pos + 1 + len > ending
|
|
179
|
+
put(layer, 'dns.txt', cursor.bytes(pos + 1, len), :string, base + pos + 1, len)
|
|
180
|
+
pos += len + 1
|
|
181
|
+
end
|
|
182
|
+
when 6
|
|
183
|
+
%w[mname rname].each do |key|
|
|
184
|
+
name, after = read_name(cursor, pos, ending)
|
|
185
|
+
put(layer, "dns.soa.#{key}", name, :string, base + pos, after - pos)
|
|
186
|
+
pos = after
|
|
187
|
+
end
|
|
188
|
+
raise Malformed, 'invalid SOA integer section' unless ending - pos == 20
|
|
189
|
+
%w[serial_number refresh_interval retry_interval expire_limit minimum_ttl].each_with_index do |key, i|
|
|
190
|
+
put(layer, "dns.soa.#{key}", cursor.u32(pos + i * 4), :uint, base + pos + i * 4, 4)
|
|
191
|
+
end
|
|
192
|
+
when 64, 65 then service_binding(cursor, layer, pos, ending, base)
|
|
193
|
+
else put(layer, 'dns.data', cursor.bytes(pos, length), :bytes, base + pos, length)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# @rbs (Cursor cursor, Layer layer, Integer pos, Integer ending, Integer base) -> void
|
|
198
|
+
def service_binding(cursor, layer, pos, ending, base)
|
|
199
|
+
raise Malformed, 'short SVCB record' if ending - pos < 3
|
|
200
|
+
priority = cursor.u16(pos)
|
|
201
|
+
put(layer, 'dns.svcb.svcpriority', priority, :uint, base + pos, 2)
|
|
202
|
+
name, after = read_name(cursor, pos + 2, ending, false)
|
|
203
|
+
put(layer, 'dns.svcb.targetname', name, :string, base + pos + 2, after - pos - 2)
|
|
204
|
+
pos, previous = after, -1
|
|
205
|
+
while pos < ending
|
|
206
|
+
raise Malformed, 'short SVCB parameter header' if ending - pos < 4
|
|
207
|
+
key, len = cursor.u16(pos), cursor.u16(pos + 2)
|
|
208
|
+
raise Malformed, 'invalid SVCB parameter order or length' if key <= previous || pos + 4 + len > ending
|
|
209
|
+
previous = key
|
|
210
|
+
put(layer, 'dns.svcb.svcparam.key', key, :uint, base + pos, 2)
|
|
211
|
+
put(layer, 'dns.svcb.svcparam.value.length', len, :uint, base + pos + 2, 2)
|
|
212
|
+
first = pos + 4
|
|
213
|
+
case key
|
|
214
|
+
when 0, 3, 4, 6
|
|
215
|
+
size = { 0 => 2, 3 => 2, 4 => 4, 6 => 16 }.fetch(key)
|
|
216
|
+
raise Malformed, 'invalid SVCB parameter size' if len.zero? || len % size != 0 || (key == 3 && len != 2)
|
|
217
|
+
(len / size).times do |i|
|
|
218
|
+
value = size == 16 ? cursor.bytes(first + i * size, size) : (size == 4 ? cursor.u32(first + i * size) : cursor.u16(first + i * size))
|
|
219
|
+
name = { 0 => 'mandatory.key', 3 => 'port', 4 => 'ipv4hint.ip', 6 => 'ipv6hint.ip' }.fetch(key)
|
|
220
|
+
put(layer, "dns.svcb.svcparam.#{name}", value, { 4 => :ipv4, 6 => :ipv6 }.fetch(key, :uint), base + first + i * size, size)
|
|
221
|
+
end
|
|
222
|
+
when 1
|
|
223
|
+
while first < pos + 4 + len
|
|
224
|
+
size = cursor.u8(first)
|
|
225
|
+
raise Malformed, 'invalid SVCB ALPN size' if size.zero? || first + size + 1 > pos + 4 + len
|
|
226
|
+
put(layer, 'dns.svcb.svcparam.alpn', cursor.bytes(first + 1, size), :string, base + first + 1, size)
|
|
227
|
+
first += size + 1
|
|
228
|
+
end
|
|
229
|
+
when 2 then raise Malformed, 'no-default-alpn must be empty' unless len.zero?
|
|
230
|
+
else put(layer, key == 7 ? 'dns.svcb.svcparam.dohpath' : 'dns.svcb.svcparam.value', cursor.bytes(first, len), key == 7 ? :string : :bytes, base + first, len)
|
|
231
|
+
end
|
|
232
|
+
pos += len + 4
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# @rbs (Cursor cursor, Layer layer, Integer pos, Integer ending, Integer base) -> void
|
|
237
|
+
def edns(cursor, layer, pos, ending, base)
|
|
238
|
+
while pos < ending
|
|
239
|
+
raise Malformed, 'short EDNS option header' if ending - pos < 4
|
|
240
|
+
code, len = cursor.u16(pos), cursor.u16(pos + 2)
|
|
241
|
+
raise Malformed, 'EDNS option exceeds record' if pos + 4 + len > ending
|
|
242
|
+
put(layer, 'dns.opt.code', code, :uint, base + pos, 2)
|
|
243
|
+
put(layer, 'dns.opt.len', len, :uint, base + pos + 2, 2)
|
|
244
|
+
put(layer, 'dns.opt.data', cursor.bytes(pos + 4, len), :bytes, base + pos + 4, len)
|
|
245
|
+
if code == 10
|
|
246
|
+
raise Malformed, 'invalid EDNS cookie length' unless len == 8 || len.between?(16, 40)
|
|
247
|
+
put(layer, 'dns.opt.cookie.client', cursor.bytes(pos + 4, 8), :bytes, base + pos + 4, 8)
|
|
248
|
+
put(layer, 'dns.opt.cookie.server', cursor.bytes(pos + 12, len - 8), :bytes, base + pos + 12, len - 8) if len > 8
|
|
249
|
+
end
|
|
250
|
+
pos += len + 4
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# @rbs (Layer layer, String name, untyped value, Symbol type, Integer offset, Integer length) -> void
|
|
255
|
+
def put(layer, name, value, type, offset, length)
|
|
256
|
+
layer.add("#{name}_#{layer.definitions.length}".to_sym, name, value, type: type, offset: offset, length: length)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# @rbs (Layer layer) -> String
|
|
260
|
+
def summary(layer)
|
|
261
|
+
name = layer.fields.find { |field| field.name == 'dns.qry.name' }&.display || ''
|
|
262
|
+
type = TYPES[layer.field_value('dns.qry.type')] || layer.field_value('dns.qry.type')
|
|
263
|
+
label = { mdns: 'mDNS', llmnr: 'LLMNR' }.fetch(layer[:variant], 'DNS')
|
|
264
|
+
"#{label} #{layer.field_value('dns.flags.response') ? 'response' : 'query'} #{type} #{name}"
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Protocols
|
|
7
|
+
# @api private
|
|
8
|
+
class Ethernet < Dissector
|
|
9
|
+
protocol :eth, name: 'Ethernet', short: 'ETH'
|
|
10
|
+
dissects_on 'linktype', 1
|
|
11
|
+
header do
|
|
12
|
+
mac :dst, 'eth.dst'
|
|
13
|
+
mac :src, 'eth.src'
|
|
14
|
+
uint16 :type, 'eth.type', format: :hex
|
|
15
|
+
end
|
|
16
|
+
# @rbs (Context ctx, Layer layer) -> void
|
|
17
|
+
def dissect(ctx, layer)
|
|
18
|
+
layer.payload_end = layer.payload_offset + layer[:type] if layer[:type] <= 1500
|
|
19
|
+
end
|
|
20
|
+
# @rbs (Context ctx, Layer layer) -> untyped
|
|
21
|
+
def next_dissector(ctx, layer) = layer[:type] <= 1500 ? Llc : ctx.registry.lookup('ethertype', layer[:type])
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Protocols
|
|
7
|
+
# @api private
|
|
8
|
+
class Http < StreamDissector
|
|
9
|
+
HEADER_LIMIT = 65_536
|
|
10
|
+
TOKEN = /\A[!#$%&'*+\-.^_`|~0-9A-Za-z]+\z/n
|
|
11
|
+
HEADERS = %w[host user_agent accept content_type content_length transfer_encoding connection
|
|
12
|
+
server location referer cookie set_cookie authorization cache_control].freeze
|
|
13
|
+
protocol :http, name: 'Hypertext Transfer Protocol', short: 'HTTP'
|
|
14
|
+
dissects_on 'tcp.port', 80, 8080, 8000
|
|
15
|
+
|
|
16
|
+
# @rbs (Context ctx, Cursor cursor) -> bool
|
|
17
|
+
def self.heuristic?(ctx, cursor)
|
|
18
|
+
sample = cursor.bytes(0, [cursor.remaining, 128].min)
|
|
19
|
+
sample.match?(/\A(?:HTTP\/1\.[01] |[A-Z]{3,20} \S+ HTTP\/1\.[01](?:\r\n|\z))/n)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @rbs (Context ctx, Layer layer) -> void
|
|
23
|
+
def dissect(ctx, layer)
|
|
24
|
+
bytes = ctx.cursor.bytes(0, ctx.cursor.remaining)
|
|
25
|
+
pos = 0
|
|
26
|
+
while pos < bytes.bytesize
|
|
27
|
+
first = bytes.index("\r\n", pos)
|
|
28
|
+
return layer.diagnose(:note, :truncated, 'incomplete HTTP start line') unless first
|
|
29
|
+
return layer.diagnose(:error, :malformed, 'HTTP start line is too long') if first - pos > HEADER_LIMIT
|
|
30
|
+
line = bytes.byteslice(pos, first - pos)
|
|
31
|
+
response = line.start_with?('HTTP/')
|
|
32
|
+
status = start_line(layer, line, pos, response)
|
|
33
|
+
return if layer.error?
|
|
34
|
+
ending = bytes.index("\r\n\r\n", pos)
|
|
35
|
+
return layer.diagnose(:note, :truncated, 'incomplete HTTP headers') unless ending
|
|
36
|
+
return layer.diagnose(:error, :malformed, 'HTTP headers exceed 64 KiB') if ending - pos > HEADER_LIMIT
|
|
37
|
+
headers = parse_headers(layer, bytes, first + 2, ending + 2)
|
|
38
|
+
return if layer.error?
|
|
39
|
+
body_start = ending + 4
|
|
40
|
+
length_values = headers.fetch('content-length', []).flat_map { |value| value.empty? ? [''] : value.split(',', -1).map(&:strip) }
|
|
41
|
+
unless length_values.all? { |value| value.match?(/\A[0-9]+\z/n) } && length_values.uniq.length <= 1
|
|
42
|
+
return layer.diagnose(:error, :malformed, 'conflicting or invalid Content-Length')
|
|
43
|
+
end
|
|
44
|
+
content_length = length_values.first&.to_i
|
|
45
|
+
transfer = headers.fetch('transfer-encoding', []).join(',').downcase.split(',').map(&:strip)
|
|
46
|
+
if !transfer.empty? && content_length
|
|
47
|
+
return layer.diagnose(:error, :malformed, 'Transfer-Encoding and Content-Length conflict')
|
|
48
|
+
end
|
|
49
|
+
no_body = response && (ctx.packet.meta[:http_head_response] == true || status.between?(100, 199) || [204, 304].include?(status))
|
|
50
|
+
if no_body
|
|
51
|
+
pos = body_start
|
|
52
|
+
elsif transfer.last == 'chunked'
|
|
53
|
+
body, pos = chunked(layer, bytes, body_start)
|
|
54
|
+
put(layer, 'http.file_data', body, :bytes, body_start, pos - body_start) unless body.empty?
|
|
55
|
+
elsif !transfer.empty?
|
|
56
|
+
return layer.diagnose(:error, :malformed, 'request transfer coding must end in chunked') unless response
|
|
57
|
+
put(layer, 'http.file_data', bytes.byteslice(body_start..), :bytes, body_start, bytes.bytesize - body_start)
|
|
58
|
+
pos = bytes.bytesize
|
|
59
|
+
elsif content_length
|
|
60
|
+
available = [content_length, bytes.bytesize - body_start].min
|
|
61
|
+
put(layer, 'http.file_data', bytes.byteslice(body_start, available), :bytes, body_start, available) if available.positive?
|
|
62
|
+
pos = body_start + available
|
|
63
|
+
layer.diagnose(:note, :truncated, 'incomplete HTTP body') if available < content_length
|
|
64
|
+
elsif response
|
|
65
|
+
put(layer, 'http.file_data', bytes.byteslice(body_start..), :bytes, body_start, bytes.bytesize - body_start) if body_start < bytes.bytesize
|
|
66
|
+
pos = bytes.bytesize
|
|
67
|
+
else
|
|
68
|
+
pos = body_start
|
|
69
|
+
end
|
|
70
|
+
break if layer.error?
|
|
71
|
+
end
|
|
72
|
+
ensure
|
|
73
|
+
layer.header_length = ctx.cursor.remaining
|
|
74
|
+
layer.payload_offset = layer.payload_end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @rbs (Layer layer, String line, Integer offset, bool response) -> Integer
|
|
78
|
+
def start_line(layer, line, offset, response)
|
|
79
|
+
if response
|
|
80
|
+
match = /\A(HTTP\/1\.[01]) ([0-9]{3})(?: (.*))?\z/n.match(line)
|
|
81
|
+
return layer.diagnose(:error, :malformed, 'invalid HTTP status line') && 0 unless match
|
|
82
|
+
put(layer, 'http.response.version', match[1], :string, offset, match[1].bytesize)
|
|
83
|
+
put(layer, 'http.response.code', match[2].to_i, :uint, offset + match[1].bytesize + 1, 3)
|
|
84
|
+
put(layer, 'http.response.phrase', match[3] || '', :string, offset + match[1].bytesize + 5, (match[3] || '').bytesize)
|
|
85
|
+
match[2].to_i
|
|
86
|
+
else
|
|
87
|
+
match = /\A([^ ]+) ([^ ]+) (HTTP\/1\.[01])\z/n.match(line)
|
|
88
|
+
return layer.diagnose(:error, :malformed, 'invalid HTTP request line') && 0 unless match && match[1].match?(TOKEN)
|
|
89
|
+
put(layer, 'http.request.method', match[1], :string, offset, match[1].bytesize)
|
|
90
|
+
put(layer, 'http.request.uri', match[2], :string, offset + match[1].bytesize + 1, match[2].bytesize)
|
|
91
|
+
put(layer, 'http.request.version', match[3], :string, offset + line.bytesize - match[3].bytesize, match[3].bytesize)
|
|
92
|
+
0
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @rbs (Layer layer, String bytes, Integer pos, Integer ending) -> Hash[String, Array[String]]
|
|
97
|
+
def parse_headers(layer, bytes, pos, ending)
|
|
98
|
+
# @type var headers: Hash[String, Array[String]]
|
|
99
|
+
headers = {}
|
|
100
|
+
while pos < ending - 2
|
|
101
|
+
after = bytes.index("\r\n", pos) #: Integer
|
|
102
|
+
line = bytes.byteslice(pos, after - pos) #: String
|
|
103
|
+
colon = line.index(':')
|
|
104
|
+
name = colon ? line.byteslice(0, colon) : '' #: String
|
|
105
|
+
unless colon && name.match?(TOKEN)
|
|
106
|
+
layer.diagnose(:error, :malformed, 'invalid HTTP header name')
|
|
107
|
+
return headers
|
|
108
|
+
end
|
|
109
|
+
name = name.downcase
|
|
110
|
+
value = line.byteslice(colon + 1..) #: String
|
|
111
|
+
leading = (value[/\A[ \t]*/n] || '').bytesize
|
|
112
|
+
trailing = value.rindex(/[^ \t]/n)
|
|
113
|
+
value = value.byteslice(leading, trailing ? trailing + 1 - leading : 0) #: String
|
|
114
|
+
(headers[name] ||= []) << value
|
|
115
|
+
key = name.tr('-', '_')
|
|
116
|
+
if HEADERS.include?(key)
|
|
117
|
+
numeric = key == 'content_length' && value.match?(/\A[0-9]+\z/n)
|
|
118
|
+
put(layer, "http.#{key}", numeric ? value.to_i : value, numeric ? :uint : :string,
|
|
119
|
+
pos + colon + 1 + leading, value.bytesize)
|
|
120
|
+
else
|
|
121
|
+
put(layer, 'http.request.line', line, :string, pos, line.bytesize)
|
|
122
|
+
end
|
|
123
|
+
layer.diagnose(:error, :malformed, 'control byte in HTTP header') if value.match?(/[\x00-\x08\x0a-\x1f\x7f]/n)
|
|
124
|
+
pos = after + 2
|
|
125
|
+
end
|
|
126
|
+
headers
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @rbs (Layer layer, String bytes, Integer pos) -> [String, Integer]
|
|
130
|
+
def chunked(layer, bytes, pos)
|
|
131
|
+
body = ''.b
|
|
132
|
+
loop do
|
|
133
|
+
ending = bytes.index("\r\n", pos)
|
|
134
|
+
unless ending
|
|
135
|
+
layer.diagnose(:note, :truncated, 'incomplete HTTP chunk size')
|
|
136
|
+
return [body, bytes.bytesize]
|
|
137
|
+
end
|
|
138
|
+
line = bytes.byteslice(pos, ending - pos) #: String
|
|
139
|
+
size = line.split(';', 2).first || ''
|
|
140
|
+
unless size.match?(/\A[0-9a-fA-F]{1,16}\z/n)
|
|
141
|
+
layer.diagnose(:error, :malformed, 'invalid HTTP chunk size')
|
|
142
|
+
return [body, ending + 2]
|
|
143
|
+
end
|
|
144
|
+
length = size.to_i(16)
|
|
145
|
+
pos = ending + 2
|
|
146
|
+
if length.zero?
|
|
147
|
+
loop do
|
|
148
|
+
ending = bytes.index("\r\n", pos)
|
|
149
|
+
unless ending
|
|
150
|
+
layer.diagnose(:note, :truncated, 'incomplete HTTP trailers')
|
|
151
|
+
return [body, bytes.bytesize]
|
|
152
|
+
end
|
|
153
|
+
return [body, ending + 2] if ending == pos
|
|
154
|
+
put(layer, 'http.chunked.trailer', bytes.byteslice(pos, ending - pos), :string, pos, ending - pos)
|
|
155
|
+
pos = ending + 2
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
available = [length, bytes.bytesize - pos].min
|
|
159
|
+
fragment = bytes.byteslice(pos, available) #: String
|
|
160
|
+
body << fragment
|
|
161
|
+
if available < length || pos + length + 2 > bytes.bytesize
|
|
162
|
+
layer.diagnose(:note, :truncated, 'incomplete HTTP chunk')
|
|
163
|
+
return [body, bytes.bytesize]
|
|
164
|
+
end
|
|
165
|
+
unless bytes.byteslice(pos + length, 2) == "\r\n"
|
|
166
|
+
layer.diagnose(:error, :malformed, 'HTTP chunk lacks CRLF')
|
|
167
|
+
return [body, pos + length]
|
|
168
|
+
end
|
|
169
|
+
pos += length + 2
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# @rbs (Layer layer, String name, untyped value, Symbol type, Integer offset, Integer length) -> void
|
|
174
|
+
def put(layer, name, value, type, offset, length)
|
|
175
|
+
layer.add("#{name}_#{layer.definitions.length}".to_sym, name, value, type: type, offset: offset, length: length)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# @rbs (Layer layer) -> String
|
|
179
|
+
def summary(layer)
|
|
180
|
+
names = layer.field_value('http.request.method') ? %w[http.request.method http.request.uri] : %w[http.response.code http.response.phrase]
|
|
181
|
+
'HTTP ' + names.filter_map { |name| layer.fields.find { |field| field.name == name }&.display }.join(' ')
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|