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,150 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Analysis
|
|
7
|
+
# @api private
|
|
8
|
+
module Stats
|
|
9
|
+
# @api private
|
|
10
|
+
class Table
|
|
11
|
+
attr_reader :evicted
|
|
12
|
+
# @rbs (?max_rows: Integer) -> void
|
|
13
|
+
def initialize(max_rows: 100_000)
|
|
14
|
+
@max_rows, @evicted = max_rows, 0
|
|
15
|
+
@rows = {} #: Hash[untyped, untyped]
|
|
16
|
+
end
|
|
17
|
+
# @rbs (untyped key) { () -> untyped } -> untyped
|
|
18
|
+
def row(key)
|
|
19
|
+
item = @rows.delete(key)
|
|
20
|
+
unless item
|
|
21
|
+
if @rows.size >= @max_rows
|
|
22
|
+
@rows.shift
|
|
23
|
+
@evicted += 1
|
|
24
|
+
end
|
|
25
|
+
item = yield
|
|
26
|
+
end
|
|
27
|
+
@rows[key] = item
|
|
28
|
+
item
|
|
29
|
+
end
|
|
30
|
+
# @rbs () -> Integer
|
|
31
|
+
def bytesize = @rows.size * 1024
|
|
32
|
+
# @rbs () -> void
|
|
33
|
+
def evict
|
|
34
|
+
@evicted += 1 if @rows.shift
|
|
35
|
+
end
|
|
36
|
+
# @rbs (untyped io) -> void
|
|
37
|
+
def format(io)
|
|
38
|
+
result = to_h
|
|
39
|
+
io.puts("#{result[:kind]}#{result[:type] ? ",#{result[:type]}" : ''}")
|
|
40
|
+
result[:rows].each { |item| io.puts(item.map { |name, value| "#{name}=#{value.is_a?(Array) ? value.join(':') : value}" }.join(' ')) }
|
|
41
|
+
io.puts("#{result[:omitted_intervals]} earlier IO intervals omitted") if result[:omitted_intervals] && result[:omitted_intervals].positive?
|
|
42
|
+
io.puts("#{@evicted} statistics rows evicted") if @evicted.positive?
|
|
43
|
+
end
|
|
44
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
45
|
+
def to_h = { kind: :stats, rows: @rows.values }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @api private
|
|
49
|
+
class Io < Table
|
|
50
|
+
# @rbs ((Integer | Float) seconds, ?max_rows: Integer) -> void
|
|
51
|
+
def initialize(seconds, max_rows: 100_000)
|
|
52
|
+
super(max_rows: max_rows)
|
|
53
|
+
@interval_ns = (seconds * 1_000_000_000).to_i
|
|
54
|
+
raise ArgumentError, 'statistics interval must be positive' unless @interval_ns.positive?
|
|
55
|
+
@start_ns = nil # @rbs Integer?
|
|
56
|
+
end
|
|
57
|
+
# @rbs (Packet packet) -> void
|
|
58
|
+
def update(packet)
|
|
59
|
+
@start_ns ||= packet.timestamp_ns
|
|
60
|
+
start = @start_ns #: Integer
|
|
61
|
+
interval = [(packet.timestamp_ns - start) / @interval_ns, 0].max
|
|
62
|
+
item = row(interval) { interval_row(interval) }
|
|
63
|
+
item[:packets] += 1
|
|
64
|
+
item[:bytes] += packet.original_length
|
|
65
|
+
end
|
|
66
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
67
|
+
def to_h
|
|
68
|
+
return { kind: :io, interval_ns: @interval_ns, rows: [] } if @rows.empty?
|
|
69
|
+
last = @rows.keys.max #: Integer
|
|
70
|
+
first = [@rows.keys.min, last - @max_rows + 1].max #: Integer
|
|
71
|
+
rows = (first..last).map { |interval| @rows[interval] || interval_row(interval) }
|
|
72
|
+
{ kind: :io, interval_ns: @interval_ns, omitted_intervals: first, rows: rows }
|
|
73
|
+
end
|
|
74
|
+
# @rbs (Integer interval) -> Hash[Symbol, Integer]
|
|
75
|
+
def interval_row(interval)
|
|
76
|
+
start = @start_ns || 0
|
|
77
|
+
{ interval: interval, start_ns: start + interval * @interval_ns, end_ns: start + (interval + 1) * @interval_ns, packets: 0, bytes: 0 }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @api private
|
|
82
|
+
class Conversations < Table
|
|
83
|
+
# @rbs (Symbol type, ?max_rows: Integer) -> void
|
|
84
|
+
def initialize(type, max_rows: 100_000)
|
|
85
|
+
super(max_rows: max_rows)
|
|
86
|
+
@type = type
|
|
87
|
+
end
|
|
88
|
+
# @rbs (Packet packet) -> void
|
|
89
|
+
def update(packet)
|
|
90
|
+
result = FlowKey.from(packet, @type)
|
|
91
|
+
return unless result
|
|
92
|
+
key, direction = result
|
|
93
|
+
item = row(key) do
|
|
94
|
+
{ addr_a: key[1], port_a: key[2], addr_b: key[3], port_b: key[4],
|
|
95
|
+
packets_ab: 0, packets_ba: 0, bytes_ab: 0, bytes_ba: 0,
|
|
96
|
+
start_ns: packet.timestamp_ns, end_ns: packet.timestamp_ns, duration_ns: 0 }
|
|
97
|
+
end
|
|
98
|
+
suffix = direction.zero? ? 'ab' : 'ba'
|
|
99
|
+
item["packets_#{suffix}".to_sym] += 1
|
|
100
|
+
item["bytes_#{suffix}".to_sym] += packet.original_length
|
|
101
|
+
item[:start_ns] = [item[:start_ns], packet.timestamp_ns].min
|
|
102
|
+
item[:end_ns] = [item[:end_ns], packet.timestamp_ns].max
|
|
103
|
+
item[:duration_ns] = item[:end_ns] - item[:start_ns]
|
|
104
|
+
end
|
|
105
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
106
|
+
def to_h = { kind: :conv, type: @type, rows: @rows.values }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @api private
|
|
110
|
+
class Endpoints < Table
|
|
111
|
+
# @rbs (Symbol type, ?max_rows: Integer) -> void
|
|
112
|
+
def initialize(type, max_rows: 100_000)
|
|
113
|
+
super(max_rows: max_rows)
|
|
114
|
+
@type = type
|
|
115
|
+
end
|
|
116
|
+
# @rbs (Packet packet) -> void
|
|
117
|
+
def update(packet)
|
|
118
|
+
result = FlowKey.from(packet, @type)
|
|
119
|
+
return unless result
|
|
120
|
+
key, direction = result
|
|
121
|
+
endpoints = [[key[1], key[2]], [key[3], key[4]]]
|
|
122
|
+
endpoints.each_with_index do |endpoint, index|
|
|
123
|
+
item = row(endpoint) { { address: endpoint[0], port: endpoint[1], packets_tx: 0, packets_rx: 0, bytes_tx: 0, bytes_rx: 0 } }
|
|
124
|
+
suffix = index == direction ? 'tx' : 'rx'
|
|
125
|
+
item["packets_#{suffix}".to_sym] += 1
|
|
126
|
+
item["bytes_#{suffix}".to_sym] += packet.original_length
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
130
|
+
def to_h = { kind: :endpoints, type: @type, rows: @rows.values }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# @api private
|
|
134
|
+
class Hierarchy < Table
|
|
135
|
+
# @rbs (Packet packet) -> void
|
|
136
|
+
def update(packet)
|
|
137
|
+
path = [] #: Array[Symbol]
|
|
138
|
+
packet.layers.reject(&:embedded).each do |layer|
|
|
139
|
+
path << layer.protocol
|
|
140
|
+
item = row(path.dup) { { path: path.dup, packets: 0, bytes: 0 } }
|
|
141
|
+
item[:packets] += 1
|
|
142
|
+
item[:bytes] += packet.original_length
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
146
|
+
def to_h = { kind: :phs, rows: @rows.values }
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Analysis
|
|
7
|
+
# @api private
|
|
8
|
+
module TcpAnalysis
|
|
9
|
+
# @rbs (Flow flow, Integer direction, TcpStream stream, Layer layer, Integer timestamp_ns) -> void
|
|
10
|
+
def self.update(flow, direction, stream, layer, timestamp_ns)
|
|
11
|
+
flags, seq, ack, length = layer[:flags], layer[:seq], layer[:ack], layer[:length]
|
|
12
|
+
layer.add(:stream, 'tcp.stream', flow.id)
|
|
13
|
+
layer.add(:seq_relative, 'tcp.seq_relative', Util::Seq.diff(seq, stream.base_seq || seq))
|
|
14
|
+
peer = flow.streams[1 - direction]
|
|
15
|
+
layer.add(:ack_relative, 'tcp.ack_relative', Util::Seq.diff(ack, peer.base_seq)) if peer && peer.base_seq && flags & 0x10 != 0
|
|
16
|
+
keep_alive = stream.base_seq && flags & 7 == 0 && length <= 1 && seq == (stream.next_sequence - 1) % Util::Seq::MOD
|
|
17
|
+
keep_alive_ack = flow.keep_alives[1 - direction] && peer && flags & 0x17 == 0x10 && length.zero? && ack == peer.next_sequence
|
|
18
|
+
if flags & 0x10 != 0
|
|
19
|
+
flag(layer, :duplicate_ack) if flags & 7 == 0 && length.zero? && layer[:window].positive? && flow.acks[direction] == [ack, layer[:window], 0] && !keep_alive_ack
|
|
20
|
+
flow.acks[direction] = [ack, layer[:window], length]
|
|
21
|
+
flow.keep_alives[1 - direction] = false
|
|
22
|
+
end
|
|
23
|
+
flag(layer, :zero_window) if layer[:window].zero? && flags & 6 == 0
|
|
24
|
+
if keep_alive
|
|
25
|
+
flag(layer, :keep_alive)
|
|
26
|
+
flow.keep_alives[direction] = true
|
|
27
|
+
end
|
|
28
|
+
if flags & 2 != 0 && flags & 0x10 == 0
|
|
29
|
+
flow.syn_ns ||= timestamp_ns
|
|
30
|
+
flow.syn_direction ||= direction
|
|
31
|
+
elsif flags & 0x12 == 0x12 && flow.syn_ns && flow.syn_direction != direction
|
|
32
|
+
flow.synack_ns ||= timestamp_ns
|
|
33
|
+
flow.synack_seq ||= seq
|
|
34
|
+
elsif flags & 0x10 != 0 && flow.synack_ns && flow.syn_direction == direction && !flow.initial_rtt && ack == (flow.synack_seq + 1) % Util::Seq::MOD
|
|
35
|
+
flow.initial_rtt = [timestamp_ns - flow.syn_ns, 0].max.fdiv(1_000_000_000)
|
|
36
|
+
end
|
|
37
|
+
layer.add(:initial_rtt, 'tcp.analysis.initial_rtt', flow.initial_rtt) if flow.initial_rtt && flags & 0x17 == 0x10
|
|
38
|
+
end
|
|
39
|
+
# @rbs (Layer layer, Symbol name) -> void
|
|
40
|
+
def self.flag(layer, name) = layer.add("analysis_#{name}".to_sym, "tcp.analysis.#{name}", true, type: :boolean)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Analysis
|
|
7
|
+
# @api private
|
|
8
|
+
class TcpReassembler
|
|
9
|
+
# @rbs (?registry: Registry, ?max_bytes: Integer, ?stream_bytes: Integer, ?protocol_streams: bool, ?follow: untyped) -> void
|
|
10
|
+
def initialize(registry: Registry.default, max_bytes: 64 << 20, stream_bytes: 1 << 20, protocol_streams: true, follow: nil)
|
|
11
|
+
@registry, @max_bytes, @stream_bytes, @protocol_streams, @follow = registry, max_bytes, stream_bytes, protocol_streams, follow
|
|
12
|
+
end
|
|
13
|
+
# @rbs (Packet packet, Flow flow, Integer direction) -> void
|
|
14
|
+
def update(packet, flow, direction)
|
|
15
|
+
layer = packet.layers.reverse.find { |item| item.protocol == :tcp && !item.embedded }
|
|
16
|
+
return unless layer && !layer.error? && layer[:seq] && layer[:flags]
|
|
17
|
+
stream = flow.streams[direction] ||= TcpStream.new(max_bytes: @stream_bytes)
|
|
18
|
+
application = flow.applications[direction]
|
|
19
|
+
if !application && @protocol_streams
|
|
20
|
+
klass = @registry.by_port('tcp.port', layer[:srcport], layer[:dstport])
|
|
21
|
+
unless klass
|
|
22
|
+
recognized = packet.layers.find { |item| (candidate = @registry.protocols[item.protocol]) && candidate <= StreamDissector }
|
|
23
|
+
klass = recognized ? @registry.protocols[recognized.protocol] : flow.applications[1 - direction]&.class
|
|
24
|
+
end
|
|
25
|
+
flow.applications[direction] = create_application(klass, flow, direction, layer) if klass && klass <= StreamDissector
|
|
26
|
+
application = flow.applications[direction]
|
|
27
|
+
end
|
|
28
|
+
TcpAnalysis.update(flow, direction, stream, layer, packet.timestamp_ns)
|
|
29
|
+
flags = layer[:flags]
|
|
30
|
+
data = packet.data.byteslice(layer.payload_offset, layer.payload_end - layer.payload_offset) #: String
|
|
31
|
+
stream.push(layer[:seq], data, syn: flags & 2 != 0, fin: flags & 1 != 0, frame: packet.number)
|
|
32
|
+
# Packet-local application parsing cannot frame a multi-segment PDU.
|
|
33
|
+
if application
|
|
34
|
+
index = packet.layers.index(layer) #: Integer
|
|
35
|
+
packet.layers.slice!(index + 1, packet.layers.length)
|
|
36
|
+
end
|
|
37
|
+
stream.diagnostics.uniq.each do |code|
|
|
38
|
+
if %i[retransmission out_of_order lost_segment].include?(code)
|
|
39
|
+
TcpAnalysis.flag(layer, code) unless layer.field_value('tcp.analysis.keep_alive')
|
|
40
|
+
else
|
|
41
|
+
layer.diagnose(:warning, code)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
deliver(flow, direction, stream, application, packet)
|
|
45
|
+
if flags & 4 != 0
|
|
46
|
+
finish_flow(flow, packet)
|
|
47
|
+
elsif flow.streams.compact.size == 2 && flow.streams.compact.all?(&:fin_seen?)
|
|
48
|
+
finish_flow(flow, packet)
|
|
49
|
+
elsif stream.fin
|
|
50
|
+
attach(packet, invoke(flow, direction, application, :on_close)) if application
|
|
51
|
+
flow.closed_ns ||= packet.timestamp_ns if flow.streams.compact.size == 2 && flow.streams.compact.all?(&:fin)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
# @rbs (Flow flow, Integer direction, TcpStream stream, untyped application, Packet? packet) -> Array[Layer]
|
|
55
|
+
def deliver(flow, direction, stream, application, packet)
|
|
56
|
+
completed = [] #: Array[Layer]
|
|
57
|
+
stream.deliveries.each do |data, origin|
|
|
58
|
+
unless data
|
|
59
|
+
gap = Layer.new(:tcp, 0, 0, 0).diagnose(:warning, :reassembly_gap, "#{origin} TCP bytes missing")
|
|
60
|
+
packet&.[](:tcp)&.diagnose(:warning, :reassembly_gap, gap.diagnostics.first.message)
|
|
61
|
+
completed << gap unless packet
|
|
62
|
+
flow.probes[direction].clear
|
|
63
|
+
flow.probe_disabled[direction] = false
|
|
64
|
+
application = flow.applications[direction]
|
|
65
|
+
layers = application ? invoke(flow, direction, application, :on_gap, origin) : Array.new #: Array[Layer]
|
|
66
|
+
completed.concat(layers)
|
|
67
|
+
attach(packet, layers) if packet
|
|
68
|
+
next
|
|
69
|
+
end
|
|
70
|
+
@follow&.write(flow, direction, data)
|
|
71
|
+
layers = application_data(flow, direction, data, origin, packet)
|
|
72
|
+
attach(packet, layers) if packet
|
|
73
|
+
completed.concat(layers)
|
|
74
|
+
end
|
|
75
|
+
completed
|
|
76
|
+
ensure
|
|
77
|
+
stream.clear_deliveries
|
|
78
|
+
end
|
|
79
|
+
# @rbs (Flow flow, Integer direction, String data, Integer origin, Packet? packet) -> Array[Layer]
|
|
80
|
+
def application_data(flow, direction, data, origin, packet)
|
|
81
|
+
application = flow.applications[direction]
|
|
82
|
+
probe = flow.probes[direction]
|
|
83
|
+
unless application
|
|
84
|
+
return [] unless @protocol_streams && !flow.probe_disabled[direction]
|
|
85
|
+
sample = (probe.map(&:first).join + data.byteslice(0, 128)).byteslice(0, 128) #: String
|
|
86
|
+
ctx = Context.new(Packet.new(sample), registry: @registry)
|
|
87
|
+
ctx.layers << Layer.new(:tcp, 0, 0, sample.bytesize)
|
|
88
|
+
klass = @registry.heuristic(:tcp, ctx, ctx.cursor)
|
|
89
|
+
if klass && klass <= StreamDissector
|
|
90
|
+
diagnostic_layer = packet&.[](:tcp) || Layer.new(:tcp, 0, 0, 0)
|
|
91
|
+
application = flow.applications[direction] = create_application(klass, flow, direction, diagnostic_layer)
|
|
92
|
+
return packet ? [] : [diagnostic_layer] unless application
|
|
93
|
+
if packet
|
|
94
|
+
position = packet.layers.index { |layer| layer.protocol == :tcp && !layer.embedded }
|
|
95
|
+
packet.layers.slice!(position + 1, packet.layers.size) if position
|
|
96
|
+
end
|
|
97
|
+
else
|
|
98
|
+
if sample.bytesize < 128 && probe.sum { |bytes, _frame| bytes.bytesize * 2 + 128 } + data.bytesize * 2 + 128 <= @stream_bytes
|
|
99
|
+
probe << [data.dup, origin]
|
|
100
|
+
else
|
|
101
|
+
probe.clear
|
|
102
|
+
flow.probe_disabled[direction] = true
|
|
103
|
+
end
|
|
104
|
+
return []
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
chunks = probe + [[data, origin]]
|
|
108
|
+
probe.clear
|
|
109
|
+
chunks.flat_map { |bytes, frame| invoke(flow, direction, application, :on_data, bytes, frame) }
|
|
110
|
+
rescue StandardError => error
|
|
111
|
+
[application_error(flow, direction, :tcp, error)]
|
|
112
|
+
end
|
|
113
|
+
# @rbs (untyped klass, Flow flow, Integer direction, Layer layer) -> untyped
|
|
114
|
+
def create_application(klass, flow, direction, layer)
|
|
115
|
+
klass.new(registry: @registry, max_bytes: @stream_bytes)
|
|
116
|
+
rescue StandardError => error
|
|
117
|
+
layer.diagnostics.concat(application_error(flow, direction, klass.protocol_id, error).diagnostics)
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
# @rbs (Flow flow, Integer direction, untyped application, Symbol method, *untyped arguments) -> Array[Layer]
|
|
121
|
+
def invoke(flow, direction, application, method, *arguments)
|
|
122
|
+
layers = application.public_send(method, flow, direction, *arguments)
|
|
123
|
+
raise TypeError, 'stream callback must return an array of Layers' unless layers.is_a?(Array) && layers.all? { |layer| layer.is_a?(Layer) }
|
|
124
|
+
layers
|
|
125
|
+
rescue StandardError => error
|
|
126
|
+
[application_error(flow, direction, application.class.protocol_id, error)]
|
|
127
|
+
end
|
|
128
|
+
# @rbs (Flow flow, Integer direction, Symbol protocol, StandardError error) -> Layer
|
|
129
|
+
def application_error(flow, direction, protocol, error)
|
|
130
|
+
raise error if ENV['REDHOUND_STRICT'] == '1'
|
|
131
|
+
flow.applications[direction] = nil
|
|
132
|
+
flow.probes[direction].clear
|
|
133
|
+
flow.probe_disabled[direction] = true
|
|
134
|
+
Layer.new(protocol, 0, 0, 0).diagnose(:error, :dissector_bug, "#{error.class}: #{error.message}")
|
|
135
|
+
end
|
|
136
|
+
# @rbs (Packet packet, Array[Layer] layers) -> void
|
|
137
|
+
def attach(packet, layers)
|
|
138
|
+
layers.each do |layer|
|
|
139
|
+
layer.add(:reassembled_in, 'tcp.reassembled_in', packet.number)
|
|
140
|
+
packet.layers << layer
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
# @rbs (Flow flow, ?Packet? packet) -> Array[Layer]
|
|
144
|
+
def finish_flow(flow, packet = nil)
|
|
145
|
+
completed = [] #: Array[Layer]
|
|
146
|
+
flow.streams.each_with_index do |stream, direction|
|
|
147
|
+
next unless stream
|
|
148
|
+
application = flow.applications[direction]
|
|
149
|
+
while stream.pending?
|
|
150
|
+
stream.flush_gap
|
|
151
|
+
completed.concat(deliver(flow, direction, stream, application, packet))
|
|
152
|
+
end
|
|
153
|
+
application = flow.applications[direction]
|
|
154
|
+
if application
|
|
155
|
+
layers = invoke(flow, direction, application, :on_close)
|
|
156
|
+
attach(packet, layers) if packet
|
|
157
|
+
completed.concat(layers)
|
|
158
|
+
end
|
|
159
|
+
stream.release
|
|
160
|
+
end
|
|
161
|
+
flow.http_methods.each(&:clear)
|
|
162
|
+
flow.probes.each(&:clear)
|
|
163
|
+
flow.closed_ns ||= packet ? packet.timestamp_ns : flow.last_ns
|
|
164
|
+
completed
|
|
165
|
+
end
|
|
166
|
+
# @rbs (FlowTable flows, Packet packet) -> void
|
|
167
|
+
def enforce_limit(flows, packet)
|
|
168
|
+
while flows.tcp_bytesize > @max_bytes
|
|
169
|
+
flow = flows.evict(:tcp)
|
|
170
|
+
break unless flow
|
|
171
|
+
packet.layers.last.diagnose(:warning, :reassembly_gap, 'TCP reassembly memory limit exceeded')
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Analysis
|
|
7
|
+
# @api private
|
|
8
|
+
class TcpStream
|
|
9
|
+
attr_reader :base_seq, :expected, :diagnostics, :deliveries, :gap_lengths, :fin
|
|
10
|
+
# @rbs (?max_bytes: Integer) -> void
|
|
11
|
+
def initialize(max_bytes: 1 << 20)
|
|
12
|
+
raise ArgumentError, 'max_bytes must be positive' unless max_bytes.positive?
|
|
13
|
+
@max_bytes, @expected, @history_start = max_bytes, 0, 0
|
|
14
|
+
@highest = 0
|
|
15
|
+
@base_seq = nil # @rbs Integer?
|
|
16
|
+
@history = ''.b
|
|
17
|
+
@queue = [] #: Array[untyped]
|
|
18
|
+
@diagnostics, @deliveries, @gap_lengths = [], [], [] # @rbs untyped
|
|
19
|
+
@fin, @fin_offset = false, nil # @rbs untyped
|
|
20
|
+
end
|
|
21
|
+
# @rbs (Integer sequence, String data, ?syn: bool, ?fin: bool, ?frame: Integer) -> Array[String]
|
|
22
|
+
def push(sequence, data, syn: false, fin: false, frame: 0)
|
|
23
|
+
@diagnostics, @deliveries, @gap_lengths = [], [], []
|
|
24
|
+
if @base_seq && ((syn && sequence == @base_seq) || (fin && Util::Seq.lt((sequence + data.bytesize) % Util::Seq::MOD, next_sequence)))
|
|
25
|
+
@diagnostics << :retransmission
|
|
26
|
+
end
|
|
27
|
+
unless @base_seq
|
|
28
|
+
@base_seq = sequence
|
|
29
|
+
@expected = syn ? 1 : 0
|
|
30
|
+
@history_start = @expected
|
|
31
|
+
@highest = @expected
|
|
32
|
+
end
|
|
33
|
+
base = @base_seq #: Integer
|
|
34
|
+
start = @expected + Util::Seq.signed_diff(sequence, (base + @expected) % Util::Seq::MOD) + (syn ? 1 : 0)
|
|
35
|
+
@fin_offset = start + data.bytesize if fin
|
|
36
|
+
@diagnostics << :out_of_order if !data.empty? && start >= @expected && start < @highest
|
|
37
|
+
@diagnostics << :lost_segment if (!data.empty? || fin) && start > @highest
|
|
38
|
+
@diagnostics << :retransmission if !data.empty? && start < @expected
|
|
39
|
+
@highest = [@highest, start + data.bytesize + (fin ? 1 : 0)].max
|
|
40
|
+
pieces = [[start, data, frame]]
|
|
41
|
+
if start < @expected
|
|
42
|
+
length = [data.bytesize, @expected - start].min
|
|
43
|
+
compare_overlap(start, data, @history_start, @history)
|
|
44
|
+
pieces = length >= data.bytesize ? Array.new : [[start + length, data.byteslice(length..), frame]] #: Array[untyped]
|
|
45
|
+
end
|
|
46
|
+
@queue.each do |old_start, old_data, _old_frame|
|
|
47
|
+
replacement = [] #: Array[untyped]
|
|
48
|
+
pieces.each do |pos, bytes, origin|
|
|
49
|
+
compare_overlap(pos, bytes, old_start, old_data)
|
|
50
|
+
ending, old_end = pos + bytes.bytesize, old_start + old_data.bytesize
|
|
51
|
+
if pos < old_end && ending > old_start
|
|
52
|
+
replacement << [pos, bytes.byteslice(0, old_start - pos), origin] if pos < old_start
|
|
53
|
+
replacement << [old_end, bytes.byteslice(old_end - pos..), origin] if ending > old_end
|
|
54
|
+
else
|
|
55
|
+
replacement << [pos, bytes, origin]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
pieces = replacement
|
|
59
|
+
end
|
|
60
|
+
if !data.empty? && pieces.empty?
|
|
61
|
+
@diagnostics.delete(:out_of_order)
|
|
62
|
+
@diagnostics.delete(:lost_segment)
|
|
63
|
+
@diagnostics << :retransmission unless @diagnostics.include?(:retransmission)
|
|
64
|
+
end
|
|
65
|
+
pieces.each do |piece|
|
|
66
|
+
next if piece[1].empty?
|
|
67
|
+
index = @queue.bsearch_index { |entry| entry[0] >= piece[0] } || @queue.length
|
|
68
|
+
@queue.insert(index, piece)
|
|
69
|
+
end
|
|
70
|
+
drain
|
|
71
|
+
while bytesize > @max_bytes || (@queue.first && @queue.first[0] - @expected > @max_bytes)
|
|
72
|
+
break if @queue.empty?
|
|
73
|
+
flush_gap(false)
|
|
74
|
+
end
|
|
75
|
+
trim_history
|
|
76
|
+
consume_fin
|
|
77
|
+
@deliveries.filter_map(&:first)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @rbs (Integer start, String data, Integer old_start, String old_data) -> void
|
|
81
|
+
def compare_overlap(start, data, old_start, old_data)
|
|
82
|
+
from = [start, old_start].max
|
|
83
|
+
to = [start + data.bytesize, old_start + old_data.bytesize].min
|
|
84
|
+
return unless from < to
|
|
85
|
+
if data.byteslice(from - start, to - from) != old_data.byteslice(from - old_start, to - from) && !@diagnostics.include?(:overlap_mismatch)
|
|
86
|
+
@diagnostics << :overlap_mismatch
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
# @rbs () -> void
|
|
90
|
+
def drain
|
|
91
|
+
while @queue.first && @queue.first[0] == @expected
|
|
92
|
+
pos, data, frame = @queue.shift
|
|
93
|
+
@deliveries << [data, frame]
|
|
94
|
+
@history_start = pos if @history.empty?
|
|
95
|
+
@history << data
|
|
96
|
+
@expected += data.bytesize
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
# @rbs (?bool clear) -> Array[String]
|
|
100
|
+
def flush_gap(clear = true)
|
|
101
|
+
@deliveries, @gap_lengths = [], [] if clear
|
|
102
|
+
target = @queue.empty? ? @fin_offset : @queue.first[0]
|
|
103
|
+
return [] unless target
|
|
104
|
+
gap = target - @expected
|
|
105
|
+
if gap.positive?
|
|
106
|
+
@diagnostics << :reassembly_gap
|
|
107
|
+
@gap_lengths << gap
|
|
108
|
+
@deliveries << [nil, gap]
|
|
109
|
+
@expected += gap
|
|
110
|
+
@history.clear
|
|
111
|
+
@history_start = @expected
|
|
112
|
+
end
|
|
113
|
+
drain
|
|
114
|
+
consume_fin
|
|
115
|
+
trim_history
|
|
116
|
+
@deliveries.filter_map(&:first)
|
|
117
|
+
end
|
|
118
|
+
# @rbs () -> void
|
|
119
|
+
def trim_history
|
|
120
|
+
# Previously delivered bytes share the stream's bounded budget with holes.
|
|
121
|
+
# Ruby grows mutable String buffers geometrically. Reserve twice their
|
|
122
|
+
# length plus object storage so capacity also stays inside the budget.
|
|
123
|
+
limit = [(@max_bytes - @queue.sum { |item| item[1].bytesize * 2 + 168 } - 40) / 2, 0].max
|
|
124
|
+
excess = @history.bytesize - limit
|
|
125
|
+
if excess.positive?
|
|
126
|
+
@history = @history.byteslice(excess..) #: String
|
|
127
|
+
@history_start += excess
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
# @rbs () -> Integer
|
|
131
|
+
def bytesize = (@history.empty? ? 0 : @history.bytesize * 2 + 40) + @queue.sum { |item| item[1].bytesize * 2 + 168 }
|
|
132
|
+
# @rbs () -> bool
|
|
133
|
+
def pending? = !@queue.empty? || (!@fin && !@fin_offset.nil?)
|
|
134
|
+
# @rbs () -> bool
|
|
135
|
+
def fin_seen? = !@fin_offset.nil?
|
|
136
|
+
# @rbs () -> void
|
|
137
|
+
def consume_fin
|
|
138
|
+
if !@fin && @fin_offset && @expected >= @fin_offset
|
|
139
|
+
@fin = true
|
|
140
|
+
@expected += 1
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
# @rbs () -> Integer
|
|
144
|
+
def next_sequence = ((@base_seq || 0) + @expected) % Util::Seq::MOD
|
|
145
|
+
# @rbs () -> void
|
|
146
|
+
def clear_deliveries = @deliveries.clear
|
|
147
|
+
# @rbs () -> void
|
|
148
|
+
def release
|
|
149
|
+
@history.clear
|
|
150
|
+
@queue.clear
|
|
151
|
+
@deliveries.clear
|
|
152
|
+
@gap_lengths.clear
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative 'util/seq'
|
|
5
|
+
require_relative 'stream_dissector'
|
|
6
|
+
require_relative 'analysis/flow_key'
|
|
7
|
+
require_relative 'analysis/tcp_stream'
|
|
8
|
+
require_relative 'analysis/flow_table'
|
|
9
|
+
require_relative 'analysis/ip_reassembler'
|
|
10
|
+
require_relative 'analysis/tcp_analysis'
|
|
11
|
+
require_relative 'analysis/tcp_reassembler'
|
|
12
|
+
require_relative 'analysis/stats'
|
|
13
|
+
require_relative 'output/follow'
|
|
14
|
+
|
|
15
|
+
module Redhound
|
|
16
|
+
# @api private
|
|
17
|
+
module Analysis
|
|
18
|
+
# @api private
|
|
19
|
+
class Session
|
|
20
|
+
attr_reader :flows, :ip_reassembler, :tcp_reassembler, :statistics
|
|
21
|
+
# @rbs (?stats: Array[String], ?follow: String?, ?registry: Registry, ?max_state_bytes: Integer, ?max_flows: Integer, ?ip_bytes: Integer, ?tcp_bytes: Integer, ?stream_bytes: Integer, ?protocol_streams: bool) -> void
|
|
22
|
+
def initialize(stats: [], follow: nil, registry: Registry.default, max_state_bytes: 256 << 20, max_flows: 100_000,
|
|
23
|
+
ip_bytes: 16 << 20, tcp_bytes: 64 << 20, stream_bytes: 1 << 20, protocol_streams: true)
|
|
24
|
+
raise ArgumentError, 'state limit must be positive' unless max_state_bytes.positive?
|
|
25
|
+
@max_state_bytes, @finished = max_state_bytes, false
|
|
26
|
+
@flows = FlowTable.new(max_flows: max_flows)
|
|
27
|
+
@ip_reassembler = IpReassembler.new(max_bytes: [ip_bytes, max_state_bytes].min)
|
|
28
|
+
@follow = follow ? Output::Follow.new(follow) : nil
|
|
29
|
+
@tcp_reassembler = TcpReassembler.new(registry: registry, max_bytes: [tcp_bytes, max_state_bytes].min,
|
|
30
|
+
stream_bytes: stream_bytes, protocol_streams: protocol_streams, follow: @follow)
|
|
31
|
+
@removed_gaps, @removed_incomplete, @removed_bugs = 0, 0, 0
|
|
32
|
+
@flows.on_remove = lambda do |flow|
|
|
33
|
+
@tcp_reassembler.finish_flow(flow).each do |layer|
|
|
34
|
+
layer.diagnostics.each do |diagnostic|
|
|
35
|
+
@removed_gaps += 1 if diagnostic.code == :reassembly_gap
|
|
36
|
+
@removed_incomplete += 1 if diagnostic.code == :truncated
|
|
37
|
+
@removed_bugs += 1 if diagnostic.code == :dissector_bug
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
specs = stats.uniq
|
|
42
|
+
raise ConfigurationError, 'too many statistics specifications (maximum 64)' if specs.size > 64
|
|
43
|
+
row_limit = [100_000, [((128 << 20) / [specs.size, 1].max) / 1024, 1].max].min
|
|
44
|
+
@statistics = specs.map { |spec| statistic(spec, row_limit) }
|
|
45
|
+
end
|
|
46
|
+
# @rbs (String specification, Integer max_rows) -> untyped
|
|
47
|
+
def statistic(specification, max_rows)
|
|
48
|
+
kind, argument = specification.split(',', 2)
|
|
49
|
+
case kind
|
|
50
|
+
when 'io'
|
|
51
|
+
raise ConfigurationError, 'io statistics requires an interval of at least one nanosecond' unless argument && argument.match?(/\A(?:\d+(?:\.\d*)?|\.\d+)\z/) && argument.to_f.finite? && argument.to_f >= 0.000_000_001
|
|
52
|
+
Stats::Io.new(argument.to_f, max_rows: max_rows)
|
|
53
|
+
when 'conv', 'endpoints'
|
|
54
|
+
raise ConfigurationError, 'statistics type must be eth, ip, ipv6, tcp or udp' unless argument && %w[eth ip ipv6 tcp udp].include?(argument)
|
|
55
|
+
klass = kind == 'conv' ? Stats::Conversations : Stats::Endpoints
|
|
56
|
+
klass.new(argument.to_sym, max_rows: max_rows)
|
|
57
|
+
when 'phs'
|
|
58
|
+
raise ConfigurationError, 'phs does not accept an argument' if argument
|
|
59
|
+
Stats::Hierarchy.new(max_rows: max_rows)
|
|
60
|
+
else raise ConfigurationError, "unknown statistics: #{specification}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
# @rbs (Packet packet) -> void
|
|
64
|
+
def update(packet)
|
|
65
|
+
return if @finished
|
|
66
|
+
@flows.advance(packet.timestamp_ns)
|
|
67
|
+
@ip_reassembler.advance(packet.timestamp_ns)
|
|
68
|
+
fragment = @ip_reassembler.descriptor(packet)
|
|
69
|
+
analyzed = packet
|
|
70
|
+
if fragment
|
|
71
|
+
boundary = packet.layers.index(fragment[1]) #: Integer
|
|
72
|
+
packet.layers.slice!(boundary + 1, packet.layers.length)
|
|
73
|
+
virtual = @ip_reassembler.update(packet)
|
|
74
|
+
if virtual
|
|
75
|
+
packet.meta[:reassembled_from] = virtual.meta[:reassembled_from]
|
|
76
|
+
packet.meta[:reassembled_packet] = virtual
|
|
77
|
+
analyzed = virtual
|
|
78
|
+
else
|
|
79
|
+
@statistics.each { |stat| stat.update(packet) }
|
|
80
|
+
enforce_limit(packet)
|
|
81
|
+
return
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
result = @flows.update(analyzed)
|
|
85
|
+
if result
|
|
86
|
+
flow, direction = result
|
|
87
|
+
@tcp_reassembler.update(analyzed, flow, direction) if flow.key[0] == :tcp
|
|
88
|
+
@flows.account(flow)
|
|
89
|
+
end
|
|
90
|
+
if analyzed != packet
|
|
91
|
+
packet.layers.concat(analyzed.layers.reject { |layer| %i[ipv4 ipv6 ipv6_ext].include?(layer.protocol) })
|
|
92
|
+
packet.layers.last.add(:ip_reassembled_from, 'ip.reassembled_from', packet.meta[:reassembled_from])
|
|
93
|
+
end
|
|
94
|
+
@statistics.each { |stat| stat.update(packet) }
|
|
95
|
+
@tcp_reassembler.enforce_limit(@flows, packet)
|
|
96
|
+
enforce_limit(packet)
|
|
97
|
+
end
|
|
98
|
+
# @rbs (Packet packet) -> void
|
|
99
|
+
def enforce_limit(packet)
|
|
100
|
+
while bytesize > @max_state_bytes
|
|
101
|
+
flow = @flows.evict
|
|
102
|
+
unless flow
|
|
103
|
+
statistic = @statistics.max_by(&:bytesize)
|
|
104
|
+
break unless statistic && statistic.bytesize.positive?
|
|
105
|
+
statistic.evict
|
|
106
|
+
end
|
|
107
|
+
packet.layers.last.diagnose(:warning, :state_evicted, 'analysis state memory limit exceeded')
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
# @rbs () -> Integer
|
|
111
|
+
def bytesize = @flows.bytesize + @ip_reassembler.bytesize + @statistics.sum(&:bytesize)
|
|
112
|
+
# @rbs (untyped err) -> void
|
|
113
|
+
def snapshot(err)
|
|
114
|
+
@statistics.each { |stat| stat.format(err) }
|
|
115
|
+
err.puts("#{@removed_gaps} TCP reassembly_gap events during flow removal") if @removed_gaps.positive?
|
|
116
|
+
err.puts("#{@removed_incomplete} truncated application PDUs during flow removal") if @removed_incomplete.positive?
|
|
117
|
+
err.puts("#{@removed_bugs} dissector_bug events during flow removal") if @removed_bugs.positive?
|
|
118
|
+
err.puts("#{@flows.evicted} flows evicted; #{@flows.expired} flows expired") if @flows.evicted.positive? || @flows.expired.positive?
|
|
119
|
+
err.puts("#{@ip_reassembler.evicted} fragmented datagrams evicted; #{@ip_reassembler.expired} expired") if @ip_reassembler.evicted.positive? || @ip_reassembler.expired.positive?
|
|
120
|
+
end
|
|
121
|
+
# @rbs (untyped out, untyped err) -> void
|
|
122
|
+
def finish(out, err)
|
|
123
|
+
return if @finished
|
|
124
|
+
@flows.values.each do |flow|
|
|
125
|
+
@tcp_reassembler.finish_flow(flow).each do |layer|
|
|
126
|
+
layer.diagnostics.each do |diagnostic|
|
|
127
|
+
message = diagnostic.message.b.gsub(/[^\x20-\x7e]/n) { |byte| format('\\x%02x', byte.getbyte(0)) }
|
|
128
|
+
err.puts("stream #{flow.id} #{layer.protocol}: #{diagnostic.code}: #{message}")
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
@flows.account(flow)
|
|
132
|
+
end
|
|
133
|
+
incomplete = @ip_reassembler.finish
|
|
134
|
+
err.puts("#{incomplete} incomplete IP datagrams: reassembly_gap at EOF") if incomplete.positive?
|
|
135
|
+
@follow&.format(out)
|
|
136
|
+
snapshot(err)
|
|
137
|
+
ensure
|
|
138
|
+
original_error = $!
|
|
139
|
+
@finished = true
|
|
140
|
+
@flows.values.each do |flow|
|
|
141
|
+
flow.streams.compact.each(&:release)
|
|
142
|
+
flow.applications.fill(nil)
|
|
143
|
+
flow.http_methods.each(&:clear)
|
|
144
|
+
flow.probes.each(&:clear)
|
|
145
|
+
flow.closed_ns ||= flow.last_ns
|
|
146
|
+
@flows.account(flow)
|
|
147
|
+
end
|
|
148
|
+
@ip_reassembler.finish
|
|
149
|
+
begin
|
|
150
|
+
@follow&.close
|
|
151
|
+
rescue StandardError
|
|
152
|
+
raise unless original_error
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|