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,218 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Protocols
|
|
7
|
+
# @api private
|
|
8
|
+
class Tls < StreamDissector
|
|
9
|
+
# @type ivar @ranges: Array[untyped]
|
|
10
|
+
# @api private
|
|
11
|
+
class BadLength < StandardError; end
|
|
12
|
+
CONTENT_TYPES = { 20 => 'Change Cipher Spec', 21 => 'Alert', 22 => 'Handshake', 23 => 'Application Data', 24 => 'Heartbeat' }.freeze
|
|
13
|
+
HANDSHAKES = { 1 => 'Client Hello', 2 => 'Server Hello', 4 => 'New Session Ticket', 8 => 'Encrypted Extensions',
|
|
14
|
+
11 => 'Certificate', 12 => 'Server Key Exchange', 14 => 'Server Hello Done', 15 => 'Certificate Verify',
|
|
15
|
+
16 => 'Client Key Exchange', 20 => 'Finished', 24 => 'Key Update' }.freeze
|
|
16
|
+
protocol :tls, name: 'Transport Layer Security', short: 'TLS'
|
|
17
|
+
dissects_on 'tcp.port', 443
|
|
18
|
+
|
|
19
|
+
# @rbs (Context ctx, Cursor cursor) -> bool
|
|
20
|
+
def self.heuristic?(ctx, cursor)
|
|
21
|
+
cursor.remaining >= 5 && CONTENT_TYPES.key?(cursor.u8(0)) && cursor.u16(1).between?(0x0300, 0x0303) && cursor.u16(3) <= 18_432
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @rbs (Context ctx, Layer layer) -> void
|
|
25
|
+
def dissect(ctx, layer)
|
|
26
|
+
cursor = ctx.cursor
|
|
27
|
+
pos, encrypted = 0, ctx.packet.meta[:tls_encrypted] == true
|
|
28
|
+
handshake_data = ''.b
|
|
29
|
+
@ranges = Array.new
|
|
30
|
+
while pos < cursor.remaining
|
|
31
|
+
if pos + 5 > cursor.remaining
|
|
32
|
+
layer.diagnose(:note, :truncated, 'incomplete TLS record header')
|
|
33
|
+
break
|
|
34
|
+
end
|
|
35
|
+
type, version, length = cursor.unpack('Cnn', 5, pos)
|
|
36
|
+
put(layer, 'tls.record.content_type', type, :uint, pos, 1)
|
|
37
|
+
put(layer, 'tls.record.version', version, :uint, pos + 1, 2)
|
|
38
|
+
put(layer, 'tls.record.length', length, :uint, pos + 3, 2)
|
|
39
|
+
raise BadLength, 'invalid TLS record version, content type or length' unless CONTENT_TYPES.key?(type) && version.between?(0x0300, 0x0303) && length <= 18_432
|
|
40
|
+
available = [length, cursor.remaining - pos - 5].min
|
|
41
|
+
body = cursor.bytes(pos + 5, available)
|
|
42
|
+
if available < length && type != 22
|
|
43
|
+
layer.diagnose(:note, :truncated, 'incomplete TLS record')
|
|
44
|
+
break
|
|
45
|
+
end
|
|
46
|
+
if type == 22 && !encrypted
|
|
47
|
+
@ranges << [handshake_data.bytesize, pos + 5, available] if available.positive?
|
|
48
|
+
handshake_data << body
|
|
49
|
+
elsif type == 20
|
|
50
|
+
raise BadLength, 'invalid ChangeCipherSpec' unless body == "\1"
|
|
51
|
+
encrypted = true
|
|
52
|
+
elsif type == 21 && !encrypted
|
|
53
|
+
raise BadLength, 'invalid TLS alert length' unless length == 2
|
|
54
|
+
put(layer, 'tls.alert_message.level', cursor.u8(pos + 5), :uint, pos + 5, 1)
|
|
55
|
+
put(layer, 'tls.alert_message.desc', cursor.u8(pos + 6), :uint, pos + 6, 1)
|
|
56
|
+
else
|
|
57
|
+
put(layer, 'tls.app_data', body, :bytes, pos + 5, available)
|
|
58
|
+
end
|
|
59
|
+
if available < length
|
|
60
|
+
layer.diagnose(:note, :truncated, 'incomplete TLS record')
|
|
61
|
+
break
|
|
62
|
+
end
|
|
63
|
+
pos += length + 5
|
|
64
|
+
end
|
|
65
|
+
handshakes(layer, Cursor.new(handshake_data)) unless handshake_data.empty?
|
|
66
|
+
rescue Cursor::Truncated => e
|
|
67
|
+
layer.diagnose(:note, :truncated, e.message)
|
|
68
|
+
rescue BadLength => e
|
|
69
|
+
layer.diagnose(:error, :bad_length, e.message)
|
|
70
|
+
ensure
|
|
71
|
+
layer.header_length = cursor.remaining
|
|
72
|
+
layer.payload_offset = layer.payload_end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @rbs (Layer layer, Cursor cursor) -> void
|
|
76
|
+
def handshakes(layer, cursor)
|
|
77
|
+
pos = 0
|
|
78
|
+
while pos < cursor.remaining
|
|
79
|
+
cursor.check(pos, 4)
|
|
80
|
+
type = cursor.u8(pos)
|
|
81
|
+
length = (cursor.u8(pos + 1) << 16) | cursor.u16(pos + 2)
|
|
82
|
+
put(layer, 'tls.handshake.type', type, :uint, pos, 1, true)
|
|
83
|
+
put(layer, 'tls.handshake.length', length, :uint, pos + 1, 3, true)
|
|
84
|
+
cursor.check(pos + 4, length)
|
|
85
|
+
body = cursor.sub(cursor.start + pos + 4, cursor.start + pos + 4 + length)
|
|
86
|
+
hello(layer, body, type) if [1, 2].include?(type)
|
|
87
|
+
extensions(layer, body, 0, false) if type == 8
|
|
88
|
+
pos += length + 4
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @rbs (Layer layer, Cursor cursor, Integer type) -> void
|
|
93
|
+
def hello(layer, cursor, type)
|
|
94
|
+
cursor.check(0, 35)
|
|
95
|
+
base = cursor.start
|
|
96
|
+
put(layer, 'tls.handshake.version', cursor.u16(0), :uint, base, 2, true)
|
|
97
|
+
put(layer, 'tls.handshake.random', cursor.bytes(2, 32), :bytes, base + 2, 32, true)
|
|
98
|
+
session_length = cursor.u8(34)
|
|
99
|
+
raise BadLength, 'TLS session ID exceeds 32 bytes' if session_length > 32
|
|
100
|
+
cursor.check(35, session_length)
|
|
101
|
+
put(layer, 'tls.handshake.session_id', cursor.bytes(35, session_length), :bytes, base + 35, session_length, true)
|
|
102
|
+
pos = 35 + session_length
|
|
103
|
+
client = type == 1
|
|
104
|
+
if client
|
|
105
|
+
cipher_length = cursor.u16(pos)
|
|
106
|
+
raise BadLength, 'invalid TLS cipher suite vector' if cipher_length.zero? || cipher_length.odd?
|
|
107
|
+
pos += 2
|
|
108
|
+
cursor.check(pos, cipher_length)
|
|
109
|
+
(cipher_length / 2).times { |i| put(layer, 'tls.handshake.ciphersuite', cursor.u16(pos + i * 2), :uint, base + pos + i * 2, 2, true) }
|
|
110
|
+
pos += cipher_length
|
|
111
|
+
compression_length = cursor.u8(pos)
|
|
112
|
+
raise BadLength, 'empty TLS compression vector' if compression_length.zero?
|
|
113
|
+
cursor.check(pos + 1, compression_length)
|
|
114
|
+
put(layer, 'tls.handshake.comp_methods', cursor.bytes(pos + 1, compression_length), :bytes, base + pos + 1, compression_length, true)
|
|
115
|
+
pos += 1 + compression_length
|
|
116
|
+
else
|
|
117
|
+
put(layer, 'tls.handshake.ciphersuite', cursor.u16(pos), :uint, base + pos, 2, true)
|
|
118
|
+
put(layer, 'tls.handshake.comp_method', cursor.u8(pos + 2), :uint, base + pos + 2, 1, true)
|
|
119
|
+
pos += 3
|
|
120
|
+
end
|
|
121
|
+
extensions(layer, cursor, pos, client) if pos < cursor.remaining
|
|
122
|
+
rescue Cursor::Truncated => e
|
|
123
|
+
raise BadLength, e.message
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @rbs (Layer layer, Cursor cursor, Integer pos, bool client) -> void
|
|
127
|
+
def extensions(layer, cursor, pos, client)
|
|
128
|
+
length = cursor.u16(pos)
|
|
129
|
+
raise BadLength, 'TLS extensions length disagrees with handshake' unless pos + 2 + length == cursor.remaining
|
|
130
|
+
ending = pos + 2 + length
|
|
131
|
+
put(layer, 'tls.handshake.extensions_length', length, :uint, cursor.start + pos, 2, true)
|
|
132
|
+
pos += 2
|
|
133
|
+
# @type var seen: Hash[Integer, bool]
|
|
134
|
+
seen = {}
|
|
135
|
+
while pos < ending
|
|
136
|
+
raise BadLength, 'short TLS extension header' if pos + 4 > ending
|
|
137
|
+
type, size = cursor.u16(pos), cursor.u16(pos + 2)
|
|
138
|
+
raise BadLength, 'duplicate or oversized TLS extension' if seen[type] || pos + 4 + size > ending
|
|
139
|
+
seen[type] = true
|
|
140
|
+
put(layer, 'tls.handshake.extension.type', type, :uint, cursor.start + pos, 2, true)
|
|
141
|
+
put(layer, 'tls.handshake.extension.len', size, :uint, cursor.start + pos + 2, 2, true)
|
|
142
|
+
extension = cursor.sub(cursor.start + pos + 4, cursor.start + pos + 4 + size)
|
|
143
|
+
case type
|
|
144
|
+
when 0 then server_names(layer, extension) unless size.zero? && !client
|
|
145
|
+
when 16 then alpn(layer, extension)
|
|
146
|
+
when 43 then versions(layer, extension, client)
|
|
147
|
+
else put(layer, 'tls.handshake.extension.data', extension.bytes(0, size), :bytes, extension.start, size, true)
|
|
148
|
+
end
|
|
149
|
+
pos += size + 4
|
|
150
|
+
end
|
|
151
|
+
rescue Cursor::Truncated => e
|
|
152
|
+
raise BadLength, e.message
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @rbs (Layer layer, Cursor cursor) -> void
|
|
156
|
+
def server_names(layer, cursor)
|
|
157
|
+
length = cursor.u16(0)
|
|
158
|
+
raise BadLength, 'invalid TLS server name list length' unless length.positive? && length + 2 == cursor.remaining
|
|
159
|
+
pos, seen = 2, {}
|
|
160
|
+
while pos < cursor.remaining
|
|
161
|
+
raise BadLength, 'short TLS server name entry' if pos + 3 > cursor.remaining
|
|
162
|
+
type, length = cursor.u8(pos), cursor.u16(pos + 1)
|
|
163
|
+
raise BadLength, 'duplicate or invalid TLS server name entry' if seen[type] || length.zero? || pos + 3 + length > cursor.remaining
|
|
164
|
+
seen[type] = true
|
|
165
|
+
put(layer, 'tls.handshake.extensions_server_name_type', type, :uint, cursor.start + pos, 1, true)
|
|
166
|
+
put(layer, 'tls.handshake.extensions_server_name', cursor.bytes(pos + 3, length), :string, cursor.start + pos + 3, length, true) if type.zero?
|
|
167
|
+
pos += length + 3
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# @rbs (Layer layer, Cursor cursor) -> void
|
|
172
|
+
def alpn(layer, cursor)
|
|
173
|
+
length = cursor.u16(0)
|
|
174
|
+
raise BadLength, 'invalid TLS ALPN list length' unless length >= 2 && length + 2 == cursor.remaining
|
|
175
|
+
pos = 2
|
|
176
|
+
while pos < cursor.remaining
|
|
177
|
+
length = cursor.u8(pos)
|
|
178
|
+
raise BadLength, 'invalid TLS ALPN protocol length' if length.zero? || pos + 1 + length > cursor.remaining
|
|
179
|
+
put(layer, 'tls.handshake.extensions_alpn_str', cursor.bytes(pos + 1, length), :string, cursor.start + pos + 1, length, true)
|
|
180
|
+
pos += length + 1
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# @rbs (Layer layer, Cursor cursor, bool client) -> void
|
|
185
|
+
def versions(layer, cursor, client)
|
|
186
|
+
pos = client ? 1 : 0
|
|
187
|
+
length = client ? cursor.u8(0) : 2
|
|
188
|
+
raise BadLength, 'invalid TLS supported version vector' unless length.positive? && length.even? && pos + length == cursor.remaining
|
|
189
|
+
(length / 2).times do |i|
|
|
190
|
+
put(layer, 'tls.handshake.extensions.supported_version', cursor.u16(pos + i * 2), :uint, cursor.start + pos + i * 2, 2, true)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# @rbs (Integer logical) -> Integer
|
|
195
|
+
def wire_offset(logical)
|
|
196
|
+
range = @ranges.bsearch { |entry| entry[0] + entry[2] > logical } || @ranges.last
|
|
197
|
+
range ? range[1] + logical - range[0] : logical
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# @rbs (Layer layer, String name, untyped value, Symbol type, Integer offset, Integer length, ?bool handshake) -> void
|
|
201
|
+
def put(layer, name, value, type, offset, length, handshake = false)
|
|
202
|
+
if handshake
|
|
203
|
+
ending = length.positive? ? wire_offset(offset + length - 1) + 1 : wire_offset(offset)
|
|
204
|
+
offset = wire_offset(offset)
|
|
205
|
+
length = ending - offset
|
|
206
|
+
end
|
|
207
|
+
layer.add("#{name}_#{layer.definitions.length}".to_sym, name, value, type: type, offset: offset, length: length)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# @rbs (Layer layer) -> String
|
|
211
|
+
def summary(layer)
|
|
212
|
+
name = layer.fields.find { |field| field.name == 'tls.handshake.extensions_server_name' }&.display
|
|
213
|
+
label = HANDSHAKES[layer.field_value('tls.handshake.type')] || CONTENT_TYPES[layer.field_value('tls.record.content_type')]
|
|
214
|
+
"TLS #{label}#{name ? " #{name}" : ''}"
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Protocols
|
|
7
|
+
# @api private
|
|
8
|
+
class Gre < Dissector
|
|
9
|
+
protocol :gre, name: 'Generic Routing Encapsulation', short: 'GRE'
|
|
10
|
+
dissects_on 'ip.proto', 47
|
|
11
|
+
header do
|
|
12
|
+
uint16 :flags, 'gre.flags', format: :hex
|
|
13
|
+
uint16 :type, 'gre.proto', format: :hex
|
|
14
|
+
end
|
|
15
|
+
# @rbs (Context ctx, Layer layer) -> void
|
|
16
|
+
def dissect(ctx, layer)
|
|
17
|
+
flags, len = layer[:flags], 4
|
|
18
|
+
return layer.diagnose(:error, :malformed, 'unsupported GRE version or routing') unless (flags & 0x4007).zero?
|
|
19
|
+
len += 4 if flags & 0x8000 != 0
|
|
20
|
+
if flags & 0x2000 != 0
|
|
21
|
+
layer.add(:key, 'gre.key', ctx.cursor.u32(len), offset: len, length: 4)
|
|
22
|
+
len += 4
|
|
23
|
+
end
|
|
24
|
+
len += 4 if flags & 0x1000 != 0
|
|
25
|
+
ctx.cursor.check(0, len)
|
|
26
|
+
layer.header_length, layer.payload_offset = len, layer.offset + len
|
|
27
|
+
end
|
|
28
|
+
# @rbs (Context ctx, Layer layer) -> untyped
|
|
29
|
+
def next_dissector(ctx, layer) = layer[:type] == 0x6558 ? Ethernet : ctx.registry.lookup('ethertype', layer[:type])
|
|
30
|
+
end
|
|
31
|
+
# @api private
|
|
32
|
+
class Vxlan < Dissector
|
|
33
|
+
protocol :vxlan, name: 'Virtual eXtensible LAN', short: 'VXLAN'
|
|
34
|
+
dissects_on 'udp.port', 4789
|
|
35
|
+
header do
|
|
36
|
+
uint32 :flags, 'vxlan.flags', format: :hex
|
|
37
|
+
uint32 :vni_word, 'vxlan.vni_word'
|
|
38
|
+
end
|
|
39
|
+
# @rbs (Context ctx, Layer layer) -> void
|
|
40
|
+
def dissect(ctx, layer)
|
|
41
|
+
layer.add(:vni, 'vxlan.vni', layer[:vni_word] >> 8, offset: 4, length: 3)
|
|
42
|
+
layer.diagnose(:error, :malformed, 'VXLAN I flag is unset') if layer[:flags] & 0x08000000 == 0
|
|
43
|
+
end
|
|
44
|
+
# @rbs (Context ctx, Layer layer) -> untyped
|
|
45
|
+
def next_dissector(ctx, layer) = Ethernet
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Protocols
|
|
7
|
+
# @api private
|
|
8
|
+
class Udp < Dissector
|
|
9
|
+
protocol :udp, name: 'User Datagram Protocol', short: 'UDP'
|
|
10
|
+
dissects_on 'ip.proto', 17
|
|
11
|
+
header do
|
|
12
|
+
uint16 :srcport, 'udp.srcport'
|
|
13
|
+
uint16 :dstport, 'udp.dstport'
|
|
14
|
+
uint16 :length, 'udp.length'
|
|
15
|
+
uint16 :checksum, 'udp.checksum', format: :hex
|
|
16
|
+
end
|
|
17
|
+
# @rbs (Context ctx, Layer layer) -> void
|
|
18
|
+
def dissect(ctx, layer)
|
|
19
|
+
len = layer[:length]
|
|
20
|
+
return layer.diagnose(:error, :bad_length, 'UDP length is less than 8') if len < 8
|
|
21
|
+
layer.payload_end = layer.offset + len
|
|
22
|
+
return unless len <= ctx.cursor.remaining
|
|
23
|
+
return if layer[:checksum].zero? && ctx.network&.protocol == :ipv4
|
|
24
|
+
Checksum.verify(ctx, layer, ctx.cursor.bytes(0, len), 17)
|
|
25
|
+
end
|
|
26
|
+
# @rbs (Context ctx, Layer layer) -> untyped
|
|
27
|
+
def next_dissector(ctx, layer) = ctx.registry.by_port('udp.port', layer[:srcport], layer[:dstport])
|
|
28
|
+
# @rbs (Layer layer) -> String
|
|
29
|
+
def summary(layer) = "UDP, length #{layer[:length] - 8}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Protocols
|
|
7
|
+
# @api private
|
|
8
|
+
class Vlan < Dissector
|
|
9
|
+
protocol :vlan, name: '802.1Q VLAN', short: 'VLAN'
|
|
10
|
+
dissects_on 'ethertype', 0x8100, 0x88a8, 0x9100
|
|
11
|
+
header do
|
|
12
|
+
bits 16 do
|
|
13
|
+
bit :priority, 'vlan.priority', 3
|
|
14
|
+
bit :dei, 'vlan.dei', 1
|
|
15
|
+
bit :id, 'vlan.id', 12
|
|
16
|
+
end
|
|
17
|
+
uint16 :type, 'vlan.etype', format: :hex
|
|
18
|
+
end
|
|
19
|
+
# @rbs (Context ctx, Layer layer) -> untyped
|
|
20
|
+
def next_dissector(ctx, layer) = ctx.registry.lookup('ethertype', layer[:type])
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# Protocol dispatch and per-session decode-as rules.
|
|
6
|
+
class Registry
|
|
7
|
+
# @rbs () -> Registry
|
|
8
|
+
# Shared registry populated by built-in and custom dissectors.
|
|
9
|
+
def self.default = (@default ||= new)
|
|
10
|
+
# Registered protocol identities and dispatch tables.
|
|
11
|
+
attr_reader :protocols
|
|
12
|
+
# @rbs () -> void
|
|
13
|
+
# Create empty protocol dispatch tables.
|
|
14
|
+
def initialize
|
|
15
|
+
@tables, @protocols, @decode_as = {}, {}, {}
|
|
16
|
+
end
|
|
17
|
+
# @rbs (String key, Integer value, untyped klass) -> void
|
|
18
|
+
# Register a class under a dispatch key and value.
|
|
19
|
+
def register(key, value, klass) = (@tables[key] ||= Hash.new)[value] = klass
|
|
20
|
+
# @rbs (untyped klass) -> void
|
|
21
|
+
# Register a protocol class by its declared identity.
|
|
22
|
+
def register_protocol(klass) = @protocols[klass.protocol_id] = klass
|
|
23
|
+
# @rbs (String key, Integer value) -> untyped
|
|
24
|
+
# Look up a protocol class for a dispatch key and value.
|
|
25
|
+
def lookup(key, value) = (@tables[key] || {})[value]
|
|
26
|
+
# @rbs (String key, Integer src, Integer dst) -> untyped
|
|
27
|
+
# Resolve decode-as rules before registered low/high port dispatch.
|
|
28
|
+
def by_port(key, src, dst)
|
|
29
|
+
ports = [src, dst].sort
|
|
30
|
+
ports.each { |p| return @decode_as[[key, p]] if @decode_as.key?([key, p]) }
|
|
31
|
+
ports.each { |p| return lookup(key, p) if lookup(key, p) }
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
# @rbs (Symbol parent, Context ctx, Cursor cursor) -> untyped
|
|
35
|
+
# Run registered transport heuristics on the bounded payload.
|
|
36
|
+
def heuristic(parent, ctx, cursor)
|
|
37
|
+
return nil unless parent == :tcp || parent == :udp
|
|
38
|
+
(@tables["#{parent}.port"] || {}).each_value do |klass|
|
|
39
|
+
next unless klass.respond_to?(:heuristic?)
|
|
40
|
+
return klass if klass.heuristic?(ctx, cursor)
|
|
41
|
+
end
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
# @rbs (String rule) -> void
|
|
45
|
+
# Apply a transport-port override such as udp.port==8443,dns.
|
|
46
|
+
def decode_as(rule)
|
|
47
|
+
match = /\A(tcp\.port|udp\.port)==(\d+),([a-z0-9_]+)\z/.match(rule)
|
|
48
|
+
raise ConfigurationError, "invalid decode-as rule: #{rule}" unless match
|
|
49
|
+
klass = @protocols[match[3].to_sym]
|
|
50
|
+
port = Integer(match[2])
|
|
51
|
+
raise ConfigurationError, "invalid decode-as protocol or port: #{rule}" unless klass && port.between?(0, 65_535)
|
|
52
|
+
@decode_as[[match[1], port]] = klass
|
|
53
|
+
end
|
|
54
|
+
# @rbs () -> Registry
|
|
55
|
+
# Copy tables and overrides for an independent analysis session.
|
|
56
|
+
def copy
|
|
57
|
+
other = dup
|
|
58
|
+
other.instance_variable_set(:@decode_as, @decode_as.dup)
|
|
59
|
+
other.instance_variable_set(:@tables, @tables.transform_values(&:dup))
|
|
60
|
+
other.instance_variable_set(:@protocols, @protocols.dup)
|
|
61
|
+
other
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# A stateful TCP dissector. Instances belong to one bidirectional flow.
|
|
6
|
+
# Override on_data/on_gap for custom stream protocols; built-in framing is
|
|
7
|
+
# shared with the packet dissectors so validation has one implementation.
|
|
8
|
+
class StreamDissector < Dissector
|
|
9
|
+
# @rbs (?registry: Registry, ?max_bytes: Integer) -> void
|
|
10
|
+
def initialize(registry: Registry.default, max_bytes: 1 << 20)
|
|
11
|
+
@registry, @max_bytes = registry, max_bytes
|
|
12
|
+
@buffers = [''.b, ''.b]
|
|
13
|
+
@origins = [Array.new, Array.new] #: Array[untyped]
|
|
14
|
+
@tls_encrypted = [false, false]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @rbs (untyped stream, Integer direction, String data, Integer frame) -> Array[Layer]
|
|
18
|
+
def on_data(stream, direction, data, frame)
|
|
19
|
+
@buffers[direction] << data
|
|
20
|
+
@origins[direction] << [data.bytesize, frame] unless data.empty?
|
|
21
|
+
if bytesize > @max_bytes
|
|
22
|
+
on_gap(stream, direction, @buffers[direction].bytesize)
|
|
23
|
+
return [Layer.new(self.class.protocol_id, 0, 0, 0).diagnose(:warning, :reassembly_gap, 'stream PDU buffer limit exceeded')]
|
|
24
|
+
end
|
|
25
|
+
layers = [] #: Array[Layer]
|
|
26
|
+
loop do
|
|
27
|
+
buffer = @buffers[direction]
|
|
28
|
+
head_response = head_response?(stream, direction, buffer)
|
|
29
|
+
length = pdu_length(buffer, direction, head_response: head_response)
|
|
30
|
+
break unless length && length.positive?
|
|
31
|
+
layer = parse_pdu(buffer.byteslice(0, length), tls_encrypted: @tls_encrypted[direction], head_response: head_response)
|
|
32
|
+
record_http(stream, direction, layer)
|
|
33
|
+
@tls_encrypted[direction] = true if self.class.protocol_id == :tls && layer.field_values('tls.record.content_type').include?(20)
|
|
34
|
+
layer.add(:reassembled_from, 'tcp.reassembled_from', consume_origins(direction, length))
|
|
35
|
+
layers << layer
|
|
36
|
+
@buffers[direction] = buffer.byteslice(length..) || ''.b
|
|
37
|
+
end
|
|
38
|
+
layers
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @rbs (untyped stream, Integer direction, Integer length) -> Array[Layer]
|
|
42
|
+
def on_gap(stream, direction, length)
|
|
43
|
+
layers = on_close(stream, direction)
|
|
44
|
+
layers.each { |layer| layer.diagnose(:warning, :reassembly_gap, "#{length} TCP bytes missing") }
|
|
45
|
+
stream.http_methods.each(&:clear) if self.class.protocol_id == :http
|
|
46
|
+
layers
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @rbs (untyped stream, Integer direction) -> Array[Layer]
|
|
50
|
+
def on_close(stream, direction)
|
|
51
|
+
buffer = @buffers[direction]
|
|
52
|
+
return [] if buffer.empty?
|
|
53
|
+
layer = parse_pdu(buffer, tls_encrypted: @tls_encrypted[direction], head_response: head_response?(stream, direction, buffer))
|
|
54
|
+
record_http(stream, direction, layer)
|
|
55
|
+
layer.add(:reassembled_from, 'tcp.reassembled_from', consume_origins(direction, buffer.bytesize))
|
|
56
|
+
@buffers[direction] = ''.b
|
|
57
|
+
[layer]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @rbs () -> Integer
|
|
61
|
+
def bytesize = @buffers.sum { |buffer| buffer.empty? ? 0 : buffer.bytesize * 2 + 40 } + @origins.sum { |origins| origins.size * 128 }
|
|
62
|
+
|
|
63
|
+
# @rbs (Integer direction, Integer length) -> Array[Integer]
|
|
64
|
+
def consume_origins(direction, length)
|
|
65
|
+
frames = [] #: Array[Integer]
|
|
66
|
+
while length.positive? && !@origins[direction].empty?
|
|
67
|
+
size, frame = @origins[direction].first
|
|
68
|
+
frames << frame
|
|
69
|
+
if size > length
|
|
70
|
+
@origins[direction][0] = [size - length, frame]
|
|
71
|
+
length = 0
|
|
72
|
+
else
|
|
73
|
+
@origins[direction].shift
|
|
74
|
+
length -= size
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
frames.uniq.sort
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @rbs (String bytes, ?tls_encrypted: bool, ?head_response: bool) -> Layer
|
|
81
|
+
def parse_pdu(bytes, tls_encrypted: false, head_response: false)
|
|
82
|
+
packet = Packet.new(bytes, meta: { tls_encrypted: tls_encrypted, http_head_response: head_response })
|
|
83
|
+
ctx = Context.new(packet, registry: @registry)
|
|
84
|
+
ctx.layers << Layer.new(:tcp, 0, 0, bytes.bytesize)
|
|
85
|
+
Engine.new(registry: @registry).safely(self.class, ctx, ctx.cursor)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @rbs (String buffer, ?Integer direction, ?head_response: bool) -> Integer?
|
|
89
|
+
def pdu_length(buffer, direction = 0, head_response: false)
|
|
90
|
+
case self.class.protocol_id
|
|
91
|
+
when :dns
|
|
92
|
+
return nil if buffer.bytesize < 2
|
|
93
|
+
length = buffer.unpack1('n') #: Integer
|
|
94
|
+
buffer.bytesize >= length + 2 ? length + 2 : nil
|
|
95
|
+
when :http then http_length(buffer, head_response: head_response)
|
|
96
|
+
when :tls then tls_length(buffer, direction)
|
|
97
|
+
else buffer.empty? ? nil : buffer.bytesize
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# @rbs (String buffer, ?head_response: bool) -> Integer?
|
|
102
|
+
def http_length(buffer, head_response: false)
|
|
103
|
+
ending = buffer.index("\r\n\r\n")
|
|
104
|
+
return buffer.bytesize if !ending && buffer.bytesize > 65_536
|
|
105
|
+
return nil unless ending
|
|
106
|
+
head_end = ending + 4
|
|
107
|
+
header = buffer.byteslice(0, ending) #: String
|
|
108
|
+
response = header.start_with?('HTTP/')
|
|
109
|
+
status = response ? header.split(' ', 3)[1].to_i : 0
|
|
110
|
+
return head_end if response && (head_response || status.between?(100, 199) || [204, 304].include?(status))
|
|
111
|
+
lengths = header.scan(/\r\ncontent-length:[ \t]*([^\r\n]*)/in).flatten.flat_map { |value| value.empty? ? [''] : value.split(',', -1).map(&:strip) }
|
|
112
|
+
transfer = header.scan(/\r\ntransfer-encoding:[ \t]*([^\r\n]*)/in).flatten.join(',').downcase.split(',').map(&:strip)
|
|
113
|
+
if (!lengths.empty? && !transfer.empty?) || lengths.uniq.size > 1 || lengths.any? { |value| !value.match?(/\A[0-9]+\z/n) }
|
|
114
|
+
return head_end # The packet parser reports conflicting or invalid framing.
|
|
115
|
+
end
|
|
116
|
+
if transfer.last == 'chunked'
|
|
117
|
+
layer = Layer.new(:http, 0, 0, buffer.bytesize)
|
|
118
|
+
parser = self #: untyped
|
|
119
|
+
_body, pos = parser.chunked(layer, buffer, head_end)
|
|
120
|
+
return nil if layer.diagnostics.any? { |diagnostic| diagnostic.code == :truncated }
|
|
121
|
+
return pos
|
|
122
|
+
end
|
|
123
|
+
return response ? nil : head_end unless transfer.empty?
|
|
124
|
+
unless lengths.empty?
|
|
125
|
+
length = head_end + lengths[0].to_i
|
|
126
|
+
return length <= buffer.bytesize ? length : nil
|
|
127
|
+
end
|
|
128
|
+
response ? nil : head_end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @rbs (untyped stream, Integer direction, String buffer) -> bool
|
|
132
|
+
def head_response?(stream, direction, buffer)
|
|
133
|
+
self.class.protocol_id == :http && buffer.start_with?('HTTP/') && stream.http_methods[1 - direction].first == true
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @rbs (untyped stream, Integer direction, Layer layer) -> void
|
|
137
|
+
def record_http(stream, direction, layer)
|
|
138
|
+
return unless self.class.protocol_id == :http
|
|
139
|
+
method = layer.field_value('http.request.method')
|
|
140
|
+
if method && !layer.error?
|
|
141
|
+
methods = stream.http_methods[direction]
|
|
142
|
+
if methods.size >= [@max_bytes / 16, 1].max
|
|
143
|
+
methods.clear
|
|
144
|
+
layer.diagnose(:warning, :reassembly_gap, 'HTTP request context memory limit exceeded')
|
|
145
|
+
end
|
|
146
|
+
methods << (method == 'HEAD')
|
|
147
|
+
elsif (status = layer.field_value('http.response.code')) && (status >= 200 || status == 101)
|
|
148
|
+
stream.http_methods[1 - direction].shift
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# @rbs (String buffer, Integer direction) -> Integer?
|
|
153
|
+
def tls_length(buffer, direction)
|
|
154
|
+
pos = 0
|
|
155
|
+
while pos + 5 <= buffer.bytesize
|
|
156
|
+
parts = buffer.unpack('Cnn', offset: pos) #: [Integer, Integer, Integer]
|
|
157
|
+
type, version, length = parts
|
|
158
|
+
return pos + 5 unless type.between?(20, 24) && version.between?(0x0300, 0x0303) && length <= 18_432
|
|
159
|
+
break if pos + 5 + length > buffer.bytesize
|
|
160
|
+
pos += 5 + length
|
|
161
|
+
end
|
|
162
|
+
return nil if pos.zero?
|
|
163
|
+
bytes = buffer.byteslice(0, pos) #: String
|
|
164
|
+
candidate = parse_pdu(bytes, tls_encrypted: @tls_encrypted[direction])
|
|
165
|
+
candidate.diagnostics.any? { |diagnostic| diagnostic.code == :truncated } ? nil : pos
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Util
|
|
7
|
+
# @api private
|
|
8
|
+
module Seq
|
|
9
|
+
MOD = 4_294_967_296
|
|
10
|
+
# @rbs (Integer a, Integer b) -> Integer
|
|
11
|
+
def self.diff(a, b) = (a - b) % MOD
|
|
12
|
+
# @rbs (Integer a, Integer b) -> bool
|
|
13
|
+
def self.lt(a, b) = diff(a, b) >= (MOD >> 1)
|
|
14
|
+
# @rbs (Integer a, Integer b) -> Integer
|
|
15
|
+
def self.signed_diff(a, b)
|
|
16
|
+
delta = diff(a, b)
|
|
17
|
+
delta >= (MOD >> 1) ? delta - MOD : delta
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|