redhound 1.0.0 → 2.0.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +97 -31
- data/docs/API.md +76 -0
- data/docs/FILTERS.md +37 -0
- data/docs/MIGRATION.md +23 -0
- data/docs/PROTOCOLS.md +45 -0
- data/docs/USAGE.md +82 -0
- data/docs/VALIDATION.md +59 -0
- data/docs/json-schema.json +64 -0
- data/exe/redhound +2 -1
- data/lib/redhound/analysis/flow_key.rb +53 -0
- data/lib/redhound/analysis/flow_table.rb +117 -0
- data/lib/redhound/analysis/ip_reassembler.rb +176 -0
- data/lib/redhound/analysis/stats.rb +138 -0
- data/lib/redhound/analysis/tcp_analysis.rb +43 -0
- data/lib/redhound/analysis/tcp_reassembler.rb +124 -0
- data/lib/redhound/analysis/tcp_stream.rb +151 -0
- data/lib/redhound/analysis.rb +129 -0
- data/lib/redhound/capture/bsd/bpf_device.rb +152 -0
- data/lib/redhound/capture/bsd/constants.rb +29 -0
- data/lib/redhound/capture/file_source.rb +83 -0
- data/lib/redhound/capture/interface.rb +66 -0
- data/lib/redhound/capture/linktype.rb +30 -0
- data/lib/redhound/capture/linux/constants.rb +37 -0
- data/lib/redhound/capture/linux/cooked.rb +18 -0
- data/lib/redhound/capture/linux/ifreq.rb +42 -0
- data/lib/redhound/capture/linux/packet_socket.rb +180 -0
- data/lib/redhound/capture/linux/sockaddr_ll.rb +33 -0
- data/lib/redhound/capture/linux/tpacket_v3.rb +132 -0
- data/lib/redhound/capture/source.rb +79 -0
- data/lib/redhound/capture/stats.rb +22 -0
- data/lib/redhound/capture.rb +69 -0
- data/lib/redhound/cli/command.rb +42 -0
- data/lib/redhound/cli/options.rb +133 -0
- data/lib/redhound/cli/privileges.rb +29 -0
- data/lib/redhound/cli/runner.rb +129 -0
- data/lib/redhound/context.rb +20 -0
- data/lib/redhound/cursor.rb +69 -0
- data/lib/redhound/diagnostic.rb +7 -0
- data/lib/redhound/dissector.rb +138 -0
- data/lib/redhound/engine.rb +66 -0
- data/lib/redhound/errors.rb +38 -0
- data/lib/redhound/field.rb +62 -0
- data/lib/redhound/file/format.rb +56 -0
- data/lib/redhound/file/pcap_reader.rb +43 -0
- data/lib/redhound/file/pcap_writer.rb +72 -0
- data/lib/redhound/file/pcapng_reader.rb +209 -0
- data/lib/redhound/file/pcapng_writer.rb +143 -0
- data/lib/redhound/file/rotating_writer.rb +98 -0
- data/lib/redhound/filter/analyzer.rb +197 -0
- data/lib/redhound/filter/bpf/assembler.rb +121 -0
- data/lib/redhound/filter/bpf/disassembler.rb +58 -0
- data/lib/redhound/filter/bpf/validator.rb +77 -0
- data/lib/redhound/filter/bpf/vm.rb +108 -0
- data/lib/redhound/filter/codegen.rb +413 -0
- data/lib/redhound/filter/lexer.rb +46 -0
- data/lib/redhound/filter/parser.rb +206 -0
- data/lib/redhound/filter/program.rb +55 -0
- data/lib/redhound/filter.rb +30 -0
- data/lib/redhound/layer.rb +75 -0
- data/lib/redhound/output/fields.rb +20 -0
- data/lib/redhound/output/follow.rb +59 -0
- data/lib/redhound/output/hexdump.rb +29 -0
- data/lib/redhound/output/json.rb +32 -0
- data/lib/redhound/output/summary.rb +59 -0
- data/lib/redhound/output/timestamp.rb +34 -0
- data/lib/redhound/output/tree.rb +24 -0
- data/lib/redhound/packet.rb +63 -0
- data/lib/redhound/protocols/arp.rb +39 -0
- data/lib/redhound/protocols/checksum.rb +38 -0
- data/lib/redhound/protocols/data.rb +21 -0
- data/lib/redhound/protocols/dhcp.rb +124 -0
- data/lib/redhound/protocols/dns.rb +268 -0
- data/lib/redhound/protocols/ethernet.rb +24 -0
- data/lib/redhound/protocols/http.rb +183 -0
- data/lib/redhound/protocols/icmp.rb +191 -0
- data/lib/redhound/protocols/ipv4.rb +56 -0
- data/lib/redhound/protocols/ipv6.rb +65 -0
- data/lib/redhound/protocols/link.rb +70 -0
- data/lib/redhound/protocols/llc.rb +37 -0
- data/lib/redhound/protocols/ntp.rb +47 -0
- data/lib/redhound/protocols/tcp.rb +80 -0
- data/lib/redhound/protocols/tls.rb +218 -0
- data/lib/redhound/protocols/tunnel.rb +48 -0
- data/lib/redhound/protocols/udp.rb +32 -0
- data/lib/redhound/protocols/vlan.rb +23 -0
- data/lib/redhound/reader.rb +8 -0
- data/lib/redhound/registry.rb +64 -0
- data/lib/redhound/stream_dissector.rb +168 -0
- data/lib/redhound/util/seq.rb +21 -0
- data/lib/redhound/version.rb +2 -1
- data/lib/redhound/writer.rb +25 -43
- data/lib/redhound.rb +81 -9
- data/sig/generated/redhound/analysis/flow_key.rbs +15 -0
- data/sig/generated/redhound/analysis/flow_table.rbs +98 -0
- data/sig/generated/redhound/analysis/ip_reassembler.rbs +40 -0
- data/sig/generated/redhound/analysis/stats.rbs +77 -0
- data/sig/generated/redhound/analysis/tcp_analysis.rbs +15 -0
- data/sig/generated/redhound/analysis/tcp_reassembler.rbs +27 -0
- data/sig/generated/redhound/analysis/tcp_stream.rbs +60 -0
- data/sig/generated/redhound/analysis.rbs +38 -0
- data/sig/generated/redhound/capture/bsd/bpf_device.rbs +39 -0
- data/sig/generated/redhound/capture/bsd/constants.rbs +40 -0
- data/sig/generated/redhound/capture/file_source.rbs +34 -0
- data/sig/generated/redhound/capture/interface.rbs +53 -0
- data/sig/generated/redhound/capture/linktype.rbs +20 -0
- data/sig/generated/redhound/capture/linux/constants.rbs +58 -0
- data/sig/generated/redhound/capture/linux/cooked.rbs +15 -0
- data/sig/generated/redhound/capture/linux/ifreq.rbs +18 -0
- data/sig/generated/redhound/capture/linux/packet_socket.rbs +57 -0
- data/sig/generated/redhound/capture/linux/sockaddr_ll.rbs +31 -0
- data/sig/generated/redhound/capture/linux/tpacket_v3.rbs +35 -0
- data/sig/generated/redhound/capture/source.rbs +44 -0
- data/sig/generated/redhound/capture/stats.rbs +25 -0
- data/sig/generated/redhound/capture.rbs +14 -0
- data/sig/generated/redhound/cli/command.rbs +12 -0
- data/sig/generated/redhound/cli/options.rbs +25 -0
- data/sig/generated/redhound/cli/privileges.rbs +12 -0
- data/sig/generated/redhound/cli/runner.rbs +27 -0
- data/sig/generated/redhound/context.rbs +27 -0
- data/sig/generated/redhound/cursor.rbs +59 -0
- data/sig/generated/redhound/diagnostic.rbs +21 -0
- data/sig/generated/redhound/dissector.rbs +113 -0
- data/sig/generated/redhound/engine.rbs +23 -0
- data/sig/generated/redhound/errors.rbs +56 -0
- data/sig/generated/redhound/field.rbs +74 -0
- data/sig/generated/redhound/file/format.rbs +33 -0
- data/sig/generated/redhound/file/pcap_reader.rbs +19 -0
- data/sig/generated/redhound/file/pcap_writer.rbs +38 -0
- data/sig/generated/redhound/file/pcapng_reader.rbs +54 -0
- data/sig/generated/redhound/file/pcapng_writer.rbs +50 -0
- data/sig/generated/redhound/file/rotating_writer.rbs +42 -0
- data/sig/generated/redhound/filter/analyzer.rbs +42 -0
- data/sig/generated/redhound/filter/bpf/assembler.rbs +39 -0
- data/sig/generated/redhound/filter/bpf/disassembler.rbs +18 -0
- data/sig/generated/redhound/filter/bpf/validator.rbs +30 -0
- data/sig/generated/redhound/filter/bpf/vm.rbs +26 -0
- data/sig/generated/redhound/filter/codegen.rbs +101 -0
- data/sig/generated/redhound/filter/lexer.rbs +15 -0
- data/sig/generated/redhound/filter/parser.rbs +65 -0
- data/sig/generated/redhound/filter/program.rbs +48 -0
- data/sig/generated/redhound/filter.rbs +10 -0
- data/sig/generated/redhound/layer.rbs +81 -0
- data/sig/generated/redhound/output/fields.rbs +15 -0
- data/sig/generated/redhound/output/follow.rbs +21 -0
- data/sig/generated/redhound/output/hexdump.rbs +15 -0
- data/sig/generated/redhound/output/json.rbs +18 -0
- data/sig/generated/redhound/output/summary.rbs +21 -0
- data/sig/generated/redhound/output/timestamp.rbs +21 -0
- data/sig/generated/redhound/output/tree.rbs +15 -0
- data/sig/generated/redhound/packet.rbs +77 -0
- data/sig/generated/redhound/protocols/arp.rbs +15 -0
- data/sig/generated/redhound/protocols/checksum.rbs +15 -0
- data/sig/generated/redhound/protocols/data.rbs +15 -0
- data/sig/generated/redhound/protocols/dhcp.rbs +28 -0
- data/sig/generated/redhound/protocols/dns.rbs +42 -0
- data/sig/generated/redhound/protocols/ethernet.rbs +15 -0
- data/sig/generated/redhound/protocols/http.rbs +36 -0
- data/sig/generated/redhound/protocols/icmp.rbs +41 -0
- data/sig/generated/redhound/protocols/ipv4.rbs +15 -0
- data/sig/generated/redhound/protocols/ipv6.rbs +24 -0
- data/sig/generated/redhound/protocols/link.rbs +33 -0
- data/sig/generated/redhound/protocols/llc.rbs +15 -0
- data/sig/generated/redhound/protocols/ntp.rbs +17 -0
- data/sig/generated/redhound/protocols/tcp.rbs +25 -0
- data/sig/generated/redhound/protocols/tls.rbs +51 -0
- data/sig/generated/redhound/protocols/tunnel.rbs +24 -0
- data/sig/generated/redhound/protocols/udp.rbs +18 -0
- data/sig/generated/redhound/protocols/vlan.rbs +12 -0
- data/sig/generated/redhound/reader.rbs +7 -0
- data/sig/generated/redhound/registry.rbs +45 -0
- data/sig/generated/redhound/stream_dissector.rbs +44 -0
- data/sig/generated/redhound/util/seq.rbs +20 -0
- data/sig/generated/redhound/version.rbs +1 -0
- data/sig/generated/redhound/writer.rbs +6 -19
- data/sig/generated/redhound.rbs +18 -0
- data/sig/shims/io.rbs +7 -0
- data/sig/shims/process.rbs +3 -0
- metadata +175 -55
- data/CODE_OF_CONDUCT.md +0 -132
- data/Dockerfile +0 -7
- data/Rakefile +0 -15
- data/Steepfile +0 -4
- data/docker-compose.yml +0 -9
- data/lib/redhound/analyzer.rb +0 -34
- data/lib/redhound/builder/packet_mreq.rb +0 -56
- data/lib/redhound/builder/socket.rb +0 -35
- data/lib/redhound/builder.rb +0 -4
- data/lib/redhound/command.rb +0 -67
- data/lib/redhound/l2/ether.rb +0 -68
- data/lib/redhound/l2/protocol.rb +0 -33
- data/lib/redhound/l2.rb +0 -4
- data/lib/redhound/l3/arp.rb +0 -114
- data/lib/redhound/l3/base.rb +0 -49
- data/lib/redhound/l3/ipv4.rb +0 -95
- data/lib/redhound/l3/ipv6.rb +0 -69
- data/lib/redhound/l3/protocol.rb +0 -173
- data/lib/redhound/l3/resolver.rb +0 -30
- data/lib/redhound/l3.rb +0 -8
- data/lib/redhound/l4/base.rb +0 -31
- data/lib/redhound/l4/icmp.rb +0 -71
- data/lib/redhound/l4/resolver.rb +0 -28
- data/lib/redhound/l4/udp.rb +0 -63
- data/lib/redhound/l4.rb +0 -6
- data/lib/redhound/receiver.rb +0 -45
- data/lib/redhound/resolver.rb +0 -22
- data/lib/redhound/source/socket.rb +0 -18
- data/lib/redhound/source.rb +0 -3
- data/rbs_collection.lock.yaml +0 -20
- data/rbs_collection.yaml +0 -17
- data/sig/generated/redhound/analyzer.rbs +0 -14
- data/sig/generated/redhound/builder/packet_mreq.rbs +0 -39
- data/sig/generated/redhound/builder/socket.rbs +0 -24
- data/sig/generated/redhound/command.rbs +0 -19
- data/sig/generated/redhound/l2/ether.rbs +0 -41
- data/sig/generated/redhound/l2/protocol.rbs +0 -15
- data/sig/generated/redhound/l3/arp.rbs +0 -57
- data/sig/generated/redhound/l3/base.rbs +0 -28
- data/sig/generated/redhound/l3/ipv4.rbs +0 -53
- data/sig/generated/redhound/l3/ipv6.rbs +0 -38
- data/sig/generated/redhound/l3/protocol.rbs +0 -16
- data/sig/generated/redhound/l3/resolver.rbs +0 -16
- data/sig/generated/redhound/l4/base.rbs +0 -19
- data/sig/generated/redhound/l4/icmp.rbs +0 -33
- data/sig/generated/redhound/l4/resolver.rbs +0 -16
- data/sig/generated/redhound/l4/udp.rbs +0 -36
- data/sig/generated/redhound/receiver.rbs +0 -19
- data/sig/generated/redhound/resolver.rbs +0 -11
- data/sig/generated/redhound/source/socket.rbs +0 -13
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Analysis
|
|
7
|
+
# @api private
|
|
8
|
+
module FlowKey
|
|
9
|
+
# @rbs (Packet packet, ?Symbol? type) -> untyped
|
|
10
|
+
def self.from(packet, type = nil)
|
|
11
|
+
layers = packet.layers.reject(&:embedded)
|
|
12
|
+
if type == :eth
|
|
13
|
+
layer = layers.find { |item| item.protocol == :eth }
|
|
14
|
+
return nil unless layer && layer[:src] && layer[:dst]
|
|
15
|
+
return normalize(:eth, [layer.display(:src), 0], [layer.display(:dst), 0])
|
|
16
|
+
end
|
|
17
|
+
transport = layers.reverse.find { |item| %i[tcp udp icmp icmpv6].include?(item.protocol) }
|
|
18
|
+
if type && %i[tcp udp].include?(type)
|
|
19
|
+
return nil unless transport && transport.protocol == type
|
|
20
|
+
end
|
|
21
|
+
network = if type == :ip || type == :ipv6
|
|
22
|
+
layers.find { |item| item.protocol == (type == :ip ? :ipv4 : :ipv6) }
|
|
23
|
+
elsif transport
|
|
24
|
+
position = layers.index(transport) #: Integer
|
|
25
|
+
layers.take(position).reverse.find { |item| %i[ipv4 ipv6].include?(item.protocol) }
|
|
26
|
+
else
|
|
27
|
+
layers.reverse.find { |item| %i[ipv4 ipv6].include?(item.protocol) }
|
|
28
|
+
end
|
|
29
|
+
return nil unless network && network[:src] && network[:dst]
|
|
30
|
+
proto = type || (transport ? transport.protocol : network.protocol)
|
|
31
|
+
if %i[tcp udp].include?(proto)
|
|
32
|
+
return nil unless transport && transport[:srcport] && transport[:dstport]
|
|
33
|
+
ports = [transport[:srcport], transport[:dstport]]
|
|
34
|
+
elsif %i[icmp icmpv6].include?(proto)
|
|
35
|
+
return nil unless transport
|
|
36
|
+
# Echo request/reply share a type series and identifier in both directions.
|
|
37
|
+
series = { 8 => 0, 128 => 129 }.fetch(transport[:type], transport[:type])
|
|
38
|
+
ports = [[series, transport[:ident] || 0], [series, transport[:ident] || 0]]
|
|
39
|
+
else
|
|
40
|
+
ports = [0, 0]
|
|
41
|
+
end
|
|
42
|
+
normalize(proto, [network.display(:src), ports[0]], [network.display(:dst), ports[1]])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @rbs (Symbol proto, untyped src, untyped dst) -> untyped
|
|
46
|
+
def self.normalize(proto, src, dst)
|
|
47
|
+
reverse = (src <=> dst).positive?
|
|
48
|
+
a, b = reverse ? [dst, src] : [src, dst]
|
|
49
|
+
[[proto, *a, *b].freeze, reverse ? 1 : 0]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Analysis
|
|
7
|
+
# @api private
|
|
8
|
+
class Flow
|
|
9
|
+
BASE_BYTES = 4096
|
|
10
|
+
attr_reader :key, :id, :first_ns, :packets, :bytes, :streams, :applications, :acks, :keep_alives, :http_methods
|
|
11
|
+
attr_accessor :last_ns, :closed_ns, :syn_ns, :syn_direction, :synack_ns, :synack_seq, :initial_rtt, :accounted_bytes
|
|
12
|
+
# @rbs (untyped key, Integer id, Integer timestamp_ns) -> void
|
|
13
|
+
def initialize(key, id, timestamp_ns)
|
|
14
|
+
@key, @id, @first_ns, @last_ns = key, id, timestamp_ns, timestamp_ns
|
|
15
|
+
@packets, @bytes, @streams, @applications, @acks = [0, 0], [0, 0], [nil, nil], [nil, nil], [nil, nil]
|
|
16
|
+
@keep_alives = [false, false]
|
|
17
|
+
@http_methods = [Array.new, Array.new] #: Array[untyped]
|
|
18
|
+
@closed_ns = @syn_ns = @syn_direction = @synack_ns = @synack_seq = @initial_rtt = nil # @rbs untyped
|
|
19
|
+
@accounted_bytes = BASE_BYTES
|
|
20
|
+
end
|
|
21
|
+
# @rbs () -> bool
|
|
22
|
+
def closed? = !@closed_ns.nil?
|
|
23
|
+
# @rbs () -> Integer
|
|
24
|
+
def bytesize = BASE_BYTES + @streams.compact.sum(&:bytesize) + @applications.compact.sum(&:bytesize) + @http_methods.sum { |methods| methods.size * 16 }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @api private
|
|
28
|
+
class FlowTable
|
|
29
|
+
attr_reader :evicted, :expired
|
|
30
|
+
attr_accessor :on_remove
|
|
31
|
+
# @rbs (?max_flows: Integer) -> void
|
|
32
|
+
def initialize(max_flows: 100_000)
|
|
33
|
+
raise ArgumentError, 'max_flows must be positive' unless max_flows.positive?
|
|
34
|
+
@max_flows, @next_id, @evicted, @expired, @clock = max_flows, 0, 0, 0, 0
|
|
35
|
+
@next_sweep, @bytes, @tcp_bytes = 0, 0, 0
|
|
36
|
+
@on_remove = nil # @rbs untyped
|
|
37
|
+
@entries = {} #: Hash[untyped, Flow]
|
|
38
|
+
end
|
|
39
|
+
# @rbs (Packet packet) -> untyped
|
|
40
|
+
def update(packet)
|
|
41
|
+
advance(packet.timestamp_ns)
|
|
42
|
+
result = FlowKey.from(packet)
|
|
43
|
+
return nil unless result
|
|
44
|
+
key, direction = result
|
|
45
|
+
flow = @entries.delete(key)
|
|
46
|
+
transport = packet.layers.reverse.find { |layer| layer.protocol == :tcp && !layer.embedded }
|
|
47
|
+
if flow && transport && transport[:flags] && transport[:flags] & 0x12 == 2
|
|
48
|
+
previous = flow.streams[direction]
|
|
49
|
+
if flow.closed? || (previous && previous.base_seq && previous.base_seq != transport[:seq])
|
|
50
|
+
discard(flow)
|
|
51
|
+
flow = nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
unless flow
|
|
55
|
+
evict if @entries.size >= @max_flows
|
|
56
|
+
flow = Flow.new(key, key[0] == :tcp ? @next_id : -1, @clock)
|
|
57
|
+
@next_id += 1 if key[0] == :tcp
|
|
58
|
+
@bytes += flow.accounted_bytes
|
|
59
|
+
end
|
|
60
|
+
flow.last_ns = @clock
|
|
61
|
+
flow.packets[direction] += 1
|
|
62
|
+
flow.bytes[direction] += packet.original_length
|
|
63
|
+
flow.closed_ns = @clock if transport && transport[:flags] && transport[:flags] & 4 != 0
|
|
64
|
+
@entries[key] = flow
|
|
65
|
+
[flow, direction]
|
|
66
|
+
end
|
|
67
|
+
# @rbs (Integer now_ns) -> void
|
|
68
|
+
def advance(now_ns)
|
|
69
|
+
@clock = [@clock, now_ns].max
|
|
70
|
+
expire(@clock) if @clock >= @next_sweep
|
|
71
|
+
end
|
|
72
|
+
# @rbs (Integer now_ns) -> void
|
|
73
|
+
def expire(now_ns)
|
|
74
|
+
@next_sweep = now_ns + 1_000_000_000
|
|
75
|
+
@entries.delete_if do |_key, flow|
|
|
76
|
+
timeout = flow.closed? ? 10 : flow.key[0] == :tcp ? 120 : 30
|
|
77
|
+
stale = now_ns - flow.last_ns >= timeout * 1_000_000_000
|
|
78
|
+
if stale
|
|
79
|
+
discard(flow)
|
|
80
|
+
@expired += 1
|
|
81
|
+
end
|
|
82
|
+
stale
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
# @rbs () -> Flow?
|
|
86
|
+
def evict
|
|
87
|
+
pair = @entries.shift
|
|
88
|
+
return nil unless pair
|
|
89
|
+
@evicted += 1
|
|
90
|
+
discard(pair[1])
|
|
91
|
+
pair[1]
|
|
92
|
+
end
|
|
93
|
+
# @rbs (Flow flow) -> void
|
|
94
|
+
def discard(flow)
|
|
95
|
+
@bytes -= flow.accounted_bytes
|
|
96
|
+
@tcp_bytes -= flow.accounted_bytes - Flow::BASE_BYTES if flow.key[0] == :tcp
|
|
97
|
+
@on_remove&.call(flow)
|
|
98
|
+
end
|
|
99
|
+
# @rbs (Flow flow) -> void
|
|
100
|
+
def account(flow)
|
|
101
|
+
size = flow.bytesize
|
|
102
|
+
delta = size - flow.accounted_bytes
|
|
103
|
+
@bytes += delta
|
|
104
|
+
@tcp_bytes += delta if flow.key[0] == :tcp
|
|
105
|
+
flow.accounted_bytes = size
|
|
106
|
+
end
|
|
107
|
+
# @rbs () -> Integer
|
|
108
|
+
def size = @entries.size
|
|
109
|
+
# @rbs () -> Array[Flow]
|
|
110
|
+
def values = @entries.values
|
|
111
|
+
# @rbs () -> Integer
|
|
112
|
+
def bytesize = @bytes
|
|
113
|
+
# @rbs () -> Integer
|
|
114
|
+
def tcp_bytesize = @tcp_bytes
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
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 IpReassembler
|
|
9
|
+
attr_reader :evicted, :expired
|
|
10
|
+
# @rbs (?max_bytes: Integer, ?timeout_ns: Integer) -> void
|
|
11
|
+
def initialize(max_bytes: 16 << 20, timeout_ns: 30_000_000_000)
|
|
12
|
+
raise ArgumentError, 'fragment limits must be positive' unless max_bytes.positive? && timeout_ns.positive?
|
|
13
|
+
@max_bytes, @timeout_ns, @clock, @evicted, @expired = max_bytes, timeout_ns, 0, 0, 0
|
|
14
|
+
@next_sweep = 0
|
|
15
|
+
@entries, @discarded = {}, {} # @rbs untyped
|
|
16
|
+
end
|
|
17
|
+
# @rbs (Packet packet) -> untyped
|
|
18
|
+
def descriptor(packet)
|
|
19
|
+
layers = packet.layers.reject(&:embedded)
|
|
20
|
+
layers.each_with_index do |layer, index|
|
|
21
|
+
if layer.protocol == :ipv4 && layer[:src] && layer[:dst] && (layer[:mf] == 1 || layer[:frag_offset].to_i.positive?)
|
|
22
|
+
return [layer, layer, [4, layer[:src], layer[:dst], layer[:proto], layer[:id]], layer[:frag_offset], layer[:mf] == 1]
|
|
23
|
+
elsif layer.protocol == :ipv6_ext && layer[:type] == 44 && layer[:fragment_offset] && layer[:identification]
|
|
24
|
+
network = layers.take(index).reverse.find { |item| item.protocol == :ipv6 }
|
|
25
|
+
return [network, layer, [6, network[:src], network[:dst], layer[:identification]], layer[:fragment_offset], layer[:more] == 1] if network && network[:src] && network[:dst]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
# @rbs (Packet packet) -> Packet?
|
|
31
|
+
def update(packet)
|
|
32
|
+
advance(packet.timestamp_ns)
|
|
33
|
+
info = descriptor(packet)
|
|
34
|
+
return nil unless info
|
|
35
|
+
network, fragment, key, offset, more = info
|
|
36
|
+
return nil if @discarded[key]
|
|
37
|
+
return nil if network.error? || fragment.error?
|
|
38
|
+
length = network.payload_end - fragment.payload_offset
|
|
39
|
+
if offset + length > 65_535 || length.negative? || (more && (length.zero? || length % 8 != 0))
|
|
40
|
+
fragment.diagnose(:error, :malformed, 'invalid IP fragment size or offset')
|
|
41
|
+
@entries.delete(key)
|
|
42
|
+
return nil
|
|
43
|
+
end
|
|
44
|
+
if network.diagnostics.any? { |diagnostic| diagnostic.code == :truncated }
|
|
45
|
+
fragment.diagnose(:note, :reassembly_gap, 'truncated IP fragment cannot be reassembled')
|
|
46
|
+
return nil
|
|
47
|
+
end
|
|
48
|
+
data = packet.data.byteslice(fragment.payload_offset, length) #: String
|
|
49
|
+
state = @entries.delete(key) || { parts: Array.new, frames: Array.new, header: nil, next_offset: 6, next: fragment[:next], end: nil } #: untyped
|
|
50
|
+
if key[0] == 6 && state[:next] != fragment[:next]
|
|
51
|
+
fragment.diagnose(:error, :malformed, 'IPv6 fragments have inconsistent next headers')
|
|
52
|
+
return nil
|
|
53
|
+
end
|
|
54
|
+
state[:time] = @clock
|
|
55
|
+
old_parts = state[:parts]
|
|
56
|
+
overlap = old_parts.any? { |pos, bytes| offset < pos + bytes.bytesize && offset + length > pos }
|
|
57
|
+
if overlap
|
|
58
|
+
fragment.diagnose(:warning, :fragment_overlap, 'overlapping IP fragments')
|
|
59
|
+
if key[0] == 6
|
|
60
|
+
@discarded[key] = @clock
|
|
61
|
+
while @discarded.size > [4096, @max_bytes / 128].min
|
|
62
|
+
@discarded.shift
|
|
63
|
+
@evicted += 1
|
|
64
|
+
end
|
|
65
|
+
while bytesize > @max_bytes && @entries.shift
|
|
66
|
+
@evicted += 1
|
|
67
|
+
end
|
|
68
|
+
return nil
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
pieces = [[offset, data]]
|
|
72
|
+
old_parts.each do |pos, bytes|
|
|
73
|
+
replacement = [] #: Array[untyped]
|
|
74
|
+
pieces.each do |start, payload|
|
|
75
|
+
ending, old_end = start + payload.bytesize, pos + bytes.bytesize
|
|
76
|
+
if start < old_end && ending > pos
|
|
77
|
+
from, to = [start, pos].max, [ending, old_end].min
|
|
78
|
+
fragment.diagnose(:warning, :overlap_mismatch) if payload.byteslice(from - start, to - from) != bytes.byteslice(from - pos, to - from)
|
|
79
|
+
replacement << [start, payload.byteslice(0, pos - start)] if start < pos
|
|
80
|
+
replacement << [old_end, payload.byteslice(old_end - start..)] if ending > old_end
|
|
81
|
+
else
|
|
82
|
+
replacement << [start, payload]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
pieces = replacement
|
|
86
|
+
end
|
|
87
|
+
first_final = !more && !state[:end]
|
|
88
|
+
unless more
|
|
89
|
+
if state[:end] && state[:end] != offset + length
|
|
90
|
+
fragment.diagnose(:error, :malformed, 'inconsistent final IP fragment length')
|
|
91
|
+
return nil
|
|
92
|
+
end
|
|
93
|
+
state[:end] = offset + length
|
|
94
|
+
end
|
|
95
|
+
state[:frames] << packet.number if !pieces.empty? || first_final
|
|
96
|
+
state[:parts].concat(pieces).sort_by!(&:first)
|
|
97
|
+
if offset.zero? && !state[:header]
|
|
98
|
+
state[:header] = packet.data.byteslice(network.offset, fragment.payload_offset - network.offset - (key[0] == 6 ? 8 : 0))
|
|
99
|
+
if key[0] == 6
|
|
100
|
+
position = packet.layers.index(fragment) #: Integer
|
|
101
|
+
previous = packet.layers.take(position).last #: Layer
|
|
102
|
+
state[:next_offset] = previous == network ? 6 : previous.offset - network.offset
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
@entries[key] = state
|
|
106
|
+
if complete?(state)
|
|
107
|
+
@entries.delete(key)
|
|
108
|
+
return virtual_packet(packet, state, key[0])
|
|
109
|
+
end
|
|
110
|
+
while bytesize > @max_bytes
|
|
111
|
+
removed = @entries.shift
|
|
112
|
+
break unless removed
|
|
113
|
+
@evicted += 1
|
|
114
|
+
fragment.diagnose(:warning, :reassembly_gap, 'IP reassembly memory limit exceeded')
|
|
115
|
+
end
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
# @rbs (untyped state) -> bool
|
|
119
|
+
def complete?(state)
|
|
120
|
+
return false unless state[:header] && state[:end]
|
|
121
|
+
expected = 0
|
|
122
|
+
state[:parts].each do |offset, bytes|
|
|
123
|
+
return false unless offset == expected
|
|
124
|
+
expected += bytes.bytesize
|
|
125
|
+
end
|
|
126
|
+
expected == state[:end]
|
|
127
|
+
end
|
|
128
|
+
# @rbs (Packet packet, untyped state, Integer version) -> Packet?
|
|
129
|
+
def virtual_packet(packet, state, version)
|
|
130
|
+
payload = state[:parts].map(&:last).join.b
|
|
131
|
+
header = state[:header].dup
|
|
132
|
+
if header.bytesize + payload.bytesize > 65_535
|
|
133
|
+
packet.layers.last.diagnose(:error, :bad_length, 'reassembled IP datagram exceeds 65535 bytes')
|
|
134
|
+
return nil
|
|
135
|
+
end
|
|
136
|
+
if version == 4
|
|
137
|
+
header[2, 2] = [header.bytesize + payload.bytesize].pack('n')
|
|
138
|
+
header[6, 2] = [header.unpack1('n', offset: 6) & 0x4000].pack('n')
|
|
139
|
+
header[10, 2] = "\0\0"
|
|
140
|
+
header[10, 2] = [Protocols::Checksum.value(header)].pack('n')
|
|
141
|
+
else
|
|
142
|
+
header.setbyte(state[:next_offset], state[:next])
|
|
143
|
+
header[4, 2] = [header.bytesize - 40 + payload.bytesize].pack('n')
|
|
144
|
+
end
|
|
145
|
+
result = Packet.new(header + payload, linktype: 101, timestamp_ns: packet.timestamp_ns, number: packet.number,
|
|
146
|
+
interface: packet.interface, direction: packet.direction,
|
|
147
|
+
meta: packet.meta.merge(reassembled_from: state[:frames].uniq.sort))
|
|
148
|
+
result.engine = packet.engine
|
|
149
|
+
result
|
|
150
|
+
end
|
|
151
|
+
# @rbs (Integer now_ns) -> void
|
|
152
|
+
def advance(now_ns)
|
|
153
|
+
@clock = [@clock, now_ns].max
|
|
154
|
+
expire(@clock) if @clock >= @next_sweep
|
|
155
|
+
end
|
|
156
|
+
# @rbs (Integer now_ns) -> void
|
|
157
|
+
def expire(now_ns)
|
|
158
|
+
@next_sweep = now_ns + 1_000_000_000
|
|
159
|
+
@entries.delete_if do |_key, state|
|
|
160
|
+
stale = now_ns - state[:time] >= @timeout_ns
|
|
161
|
+
@expired += 1 if stale
|
|
162
|
+
stale
|
|
163
|
+
end
|
|
164
|
+
@discarded.delete_if { |_key, time| now_ns - time >= @timeout_ns }
|
|
165
|
+
end
|
|
166
|
+
# @rbs () -> Integer
|
|
167
|
+
def bytesize
|
|
168
|
+
@discarded.size * 128 + @entries.values.sum do |state|
|
|
169
|
+
512 + (state[:header]&.bytesize || 0) + state[:frames].size * 32 + state[:parts].sum { |_pos, bytes| bytes.bytesize + 128 }
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
# @rbs () -> Integer
|
|
173
|
+
def size = @entries.size
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
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("#{@evicted} statistics rows evicted") if @evicted.positive?
|
|
42
|
+
end
|
|
43
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
44
|
+
def to_h = { kind: :stats, rows: @rows.values }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @api private
|
|
48
|
+
class Io < Table
|
|
49
|
+
# @rbs ((Integer | Float) seconds, ?max_rows: Integer) -> void
|
|
50
|
+
def initialize(seconds, max_rows: 100_000)
|
|
51
|
+
super(max_rows: max_rows)
|
|
52
|
+
@interval_ns = (seconds * 1_000_000_000).to_i
|
|
53
|
+
raise ArgumentError, 'statistics interval must be positive' unless @interval_ns.positive?
|
|
54
|
+
@start_ns = nil # @rbs Integer?
|
|
55
|
+
end
|
|
56
|
+
# @rbs (Packet packet) -> void
|
|
57
|
+
def update(packet)
|
|
58
|
+
@start_ns ||= packet.timestamp_ns
|
|
59
|
+
start = @start_ns #: Integer
|
|
60
|
+
interval = [(packet.timestamp_ns - start) / @interval_ns, 0].max
|
|
61
|
+
item = row(interval) { { interval: interval, start_ns: start + interval * @interval_ns, end_ns: start + (interval + 1) * @interval_ns, packets: 0, bytes: 0 } }
|
|
62
|
+
item[:packets] += 1
|
|
63
|
+
item[:bytes] += packet.original_length
|
|
64
|
+
end
|
|
65
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
66
|
+
def to_h = { kind: :io, interval_ns: @interval_ns, rows: @rows.values.sort_by { |item| item[:interval] } }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @api private
|
|
70
|
+
class Conversations < Table
|
|
71
|
+
# @rbs (Symbol type, ?max_rows: Integer) -> void
|
|
72
|
+
def initialize(type, max_rows: 100_000)
|
|
73
|
+
super(max_rows: max_rows)
|
|
74
|
+
@type = type
|
|
75
|
+
end
|
|
76
|
+
# @rbs (Packet packet) -> void
|
|
77
|
+
def update(packet)
|
|
78
|
+
result = FlowKey.from(packet, @type)
|
|
79
|
+
return unless result
|
|
80
|
+
key, direction = result
|
|
81
|
+
item = row(key) do
|
|
82
|
+
{ addr_a: key[1], port_a: key[2], addr_b: key[3], port_b: key[4],
|
|
83
|
+
packets_ab: 0, packets_ba: 0, bytes_ab: 0, bytes_ba: 0,
|
|
84
|
+
start_ns: packet.timestamp_ns, end_ns: packet.timestamp_ns, duration_ns: 0 }
|
|
85
|
+
end
|
|
86
|
+
suffix = direction.zero? ? 'ab' : 'ba'
|
|
87
|
+
item["packets_#{suffix}".to_sym] += 1
|
|
88
|
+
item["bytes_#{suffix}".to_sym] += packet.original_length
|
|
89
|
+
item[:start_ns] = [item[:start_ns], packet.timestamp_ns].min
|
|
90
|
+
item[:end_ns] = [item[:end_ns], packet.timestamp_ns].max
|
|
91
|
+
item[:duration_ns] = item[:end_ns] - item[:start_ns]
|
|
92
|
+
end
|
|
93
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
94
|
+
def to_h = { kind: :conv, type: @type, rows: @rows.values }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @api private
|
|
98
|
+
class Endpoints < Table
|
|
99
|
+
# @rbs (Symbol type, ?max_rows: Integer) -> void
|
|
100
|
+
def initialize(type, max_rows: 100_000)
|
|
101
|
+
super(max_rows: max_rows)
|
|
102
|
+
@type = type
|
|
103
|
+
end
|
|
104
|
+
# @rbs (Packet packet) -> void
|
|
105
|
+
def update(packet)
|
|
106
|
+
result = FlowKey.from(packet, @type)
|
|
107
|
+
return unless result
|
|
108
|
+
key, direction = result
|
|
109
|
+
endpoints = [[key[1], key[2]], [key[3], key[4]]]
|
|
110
|
+
endpoints.each_with_index do |endpoint, index|
|
|
111
|
+
item = row(endpoint) { { address: endpoint[0], port: endpoint[1], packets_tx: 0, packets_rx: 0, bytes_tx: 0, bytes_rx: 0 } }
|
|
112
|
+
suffix = index == direction ? 'tx' : 'rx'
|
|
113
|
+
item["packets_#{suffix}".to_sym] += 1
|
|
114
|
+
item["bytes_#{suffix}".to_sym] += packet.original_length
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
118
|
+
def to_h = { kind: :endpoints, type: @type, rows: @rows.values }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @api private
|
|
122
|
+
class Hierarchy < Table
|
|
123
|
+
# @rbs (Packet packet) -> void
|
|
124
|
+
def update(packet)
|
|
125
|
+
path = [] #: Array[Symbol]
|
|
126
|
+
packet.layers.reject(&:embedded).each do |layer|
|
|
127
|
+
path << layer.protocol
|
|
128
|
+
item = row(path.dup) { { path: path.dup, packets: 0, bytes: 0 } }
|
|
129
|
+
item[:packets] += 1
|
|
130
|
+
item[:bytes] += packet.original_length
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
134
|
+
def to_h = { kind: :phs, rows: @rows.values }
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
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,124 @@
|
|
|
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] = klass.new(registry: @registry, max_bytes: @stream_bytes) 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, application.on_close(flow, direction)) 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
|
+
layers = application ? application.on_gap(flow, direction, origin) : Array.new #: Array[Layer]
|
|
63
|
+
completed.concat(layers)
|
|
64
|
+
attach(packet, layers) if packet
|
|
65
|
+
next
|
|
66
|
+
end
|
|
67
|
+
@follow&.write(flow, direction, data)
|
|
68
|
+
layers = if application
|
|
69
|
+
begin
|
|
70
|
+
application.on_data(flow, direction, data, origin)
|
|
71
|
+
rescue StandardError => error
|
|
72
|
+
raise if ENV['REDHOUND_STRICT'] == '1'
|
|
73
|
+
application.on_gap(flow, direction, data.bytesize)
|
|
74
|
+
[Layer.new(application.class.protocol_id, 0, 0, 0).diagnose(:error, :dissector_bug, "#{error.class}: #{error.message}")]
|
|
75
|
+
end
|
|
76
|
+
else
|
|
77
|
+
Array.new
|
|
78
|
+
end #: Array[Layer]
|
|
79
|
+
attach(packet, layers) if packet
|
|
80
|
+
completed.concat(layers)
|
|
81
|
+
end
|
|
82
|
+
completed
|
|
83
|
+
ensure
|
|
84
|
+
stream.clear_deliveries
|
|
85
|
+
end
|
|
86
|
+
# @rbs (Packet packet, Array[Layer] layers) -> void
|
|
87
|
+
def attach(packet, layers)
|
|
88
|
+
layers.each do |layer|
|
|
89
|
+
layer.add(:reassembled_in, 'tcp.reassembled_in', packet.number)
|
|
90
|
+
packet.layers << layer
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
# @rbs (Flow flow, ?Packet? packet) -> Array[Layer]
|
|
94
|
+
def finish_flow(flow, packet = nil)
|
|
95
|
+
completed = [] #: Array[Layer]
|
|
96
|
+
flow.streams.each_with_index do |stream, direction|
|
|
97
|
+
next unless stream
|
|
98
|
+
application = flow.applications[direction]
|
|
99
|
+
while stream.pending?
|
|
100
|
+
stream.flush_gap
|
|
101
|
+
completed.concat(deliver(flow, direction, stream, application, packet))
|
|
102
|
+
end
|
|
103
|
+
if application
|
|
104
|
+
layers = application.on_close(flow, direction)
|
|
105
|
+
attach(packet, layers) if packet
|
|
106
|
+
completed.concat(layers)
|
|
107
|
+
end
|
|
108
|
+
stream.release
|
|
109
|
+
end
|
|
110
|
+
flow.http_methods.each(&:clear)
|
|
111
|
+
flow.closed_ns ||= packet ? packet.timestamp_ns : flow.last_ns
|
|
112
|
+
completed
|
|
113
|
+
end
|
|
114
|
+
# @rbs (FlowTable flows, Packet packet) -> void
|
|
115
|
+
def enforce_limit(flows, packet)
|
|
116
|
+
while flows.tcp_bytesize > @max_bytes
|
|
117
|
+
flow = flows.evict
|
|
118
|
+
break unless flow
|
|
119
|
+
packet.layers.last.diagnose(:warning, :reassembly_gap, 'TCP reassembly memory limit exceeded')
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|