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.
Files changed (230) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/README.md +97 -31
  4. data/docs/API.md +76 -0
  5. data/docs/FILTERS.md +37 -0
  6. data/docs/MIGRATION.md +23 -0
  7. data/docs/PROTOCOLS.md +45 -0
  8. data/docs/USAGE.md +82 -0
  9. data/docs/VALIDATION.md +59 -0
  10. data/docs/json-schema.json +64 -0
  11. data/exe/redhound +2 -1
  12. data/lib/redhound/analysis/flow_key.rb +53 -0
  13. data/lib/redhound/analysis/flow_table.rb +117 -0
  14. data/lib/redhound/analysis/ip_reassembler.rb +176 -0
  15. data/lib/redhound/analysis/stats.rb +138 -0
  16. data/lib/redhound/analysis/tcp_analysis.rb +43 -0
  17. data/lib/redhound/analysis/tcp_reassembler.rb +124 -0
  18. data/lib/redhound/analysis/tcp_stream.rb +151 -0
  19. data/lib/redhound/analysis.rb +129 -0
  20. data/lib/redhound/capture/bsd/bpf_device.rb +152 -0
  21. data/lib/redhound/capture/bsd/constants.rb +29 -0
  22. data/lib/redhound/capture/file_source.rb +83 -0
  23. data/lib/redhound/capture/interface.rb +66 -0
  24. data/lib/redhound/capture/linktype.rb +30 -0
  25. data/lib/redhound/capture/linux/constants.rb +37 -0
  26. data/lib/redhound/capture/linux/cooked.rb +18 -0
  27. data/lib/redhound/capture/linux/ifreq.rb +42 -0
  28. data/lib/redhound/capture/linux/packet_socket.rb +180 -0
  29. data/lib/redhound/capture/linux/sockaddr_ll.rb +33 -0
  30. data/lib/redhound/capture/linux/tpacket_v3.rb +132 -0
  31. data/lib/redhound/capture/source.rb +79 -0
  32. data/lib/redhound/capture/stats.rb +22 -0
  33. data/lib/redhound/capture.rb +69 -0
  34. data/lib/redhound/cli/command.rb +42 -0
  35. data/lib/redhound/cli/options.rb +133 -0
  36. data/lib/redhound/cli/privileges.rb +29 -0
  37. data/lib/redhound/cli/runner.rb +129 -0
  38. data/lib/redhound/context.rb +20 -0
  39. data/lib/redhound/cursor.rb +69 -0
  40. data/lib/redhound/diagnostic.rb +7 -0
  41. data/lib/redhound/dissector.rb +138 -0
  42. data/lib/redhound/engine.rb +66 -0
  43. data/lib/redhound/errors.rb +38 -0
  44. data/lib/redhound/field.rb +62 -0
  45. data/lib/redhound/file/format.rb +56 -0
  46. data/lib/redhound/file/pcap_reader.rb +43 -0
  47. data/lib/redhound/file/pcap_writer.rb +72 -0
  48. data/lib/redhound/file/pcapng_reader.rb +209 -0
  49. data/lib/redhound/file/pcapng_writer.rb +143 -0
  50. data/lib/redhound/file/rotating_writer.rb +98 -0
  51. data/lib/redhound/filter/analyzer.rb +197 -0
  52. data/lib/redhound/filter/bpf/assembler.rb +121 -0
  53. data/lib/redhound/filter/bpf/disassembler.rb +58 -0
  54. data/lib/redhound/filter/bpf/validator.rb +77 -0
  55. data/lib/redhound/filter/bpf/vm.rb +108 -0
  56. data/lib/redhound/filter/codegen.rb +413 -0
  57. data/lib/redhound/filter/lexer.rb +46 -0
  58. data/lib/redhound/filter/parser.rb +206 -0
  59. data/lib/redhound/filter/program.rb +55 -0
  60. data/lib/redhound/filter.rb +30 -0
  61. data/lib/redhound/layer.rb +75 -0
  62. data/lib/redhound/output/fields.rb +20 -0
  63. data/lib/redhound/output/follow.rb +59 -0
  64. data/lib/redhound/output/hexdump.rb +29 -0
  65. data/lib/redhound/output/json.rb +32 -0
  66. data/lib/redhound/output/summary.rb +59 -0
  67. data/lib/redhound/output/timestamp.rb +34 -0
  68. data/lib/redhound/output/tree.rb +24 -0
  69. data/lib/redhound/packet.rb +63 -0
  70. data/lib/redhound/protocols/arp.rb +39 -0
  71. data/lib/redhound/protocols/checksum.rb +38 -0
  72. data/lib/redhound/protocols/data.rb +21 -0
  73. data/lib/redhound/protocols/dhcp.rb +124 -0
  74. data/lib/redhound/protocols/dns.rb +268 -0
  75. data/lib/redhound/protocols/ethernet.rb +24 -0
  76. data/lib/redhound/protocols/http.rb +183 -0
  77. data/lib/redhound/protocols/icmp.rb +191 -0
  78. data/lib/redhound/protocols/ipv4.rb +56 -0
  79. data/lib/redhound/protocols/ipv6.rb +65 -0
  80. data/lib/redhound/protocols/link.rb +70 -0
  81. data/lib/redhound/protocols/llc.rb +37 -0
  82. data/lib/redhound/protocols/ntp.rb +47 -0
  83. data/lib/redhound/protocols/tcp.rb +80 -0
  84. data/lib/redhound/protocols/tls.rb +218 -0
  85. data/lib/redhound/protocols/tunnel.rb +48 -0
  86. data/lib/redhound/protocols/udp.rb +32 -0
  87. data/lib/redhound/protocols/vlan.rb +23 -0
  88. data/lib/redhound/reader.rb +8 -0
  89. data/lib/redhound/registry.rb +64 -0
  90. data/lib/redhound/stream_dissector.rb +168 -0
  91. data/lib/redhound/util/seq.rb +21 -0
  92. data/lib/redhound/version.rb +2 -1
  93. data/lib/redhound/writer.rb +25 -43
  94. data/lib/redhound.rb +81 -9
  95. data/sig/generated/redhound/analysis/flow_key.rbs +15 -0
  96. data/sig/generated/redhound/analysis/flow_table.rbs +98 -0
  97. data/sig/generated/redhound/analysis/ip_reassembler.rbs +40 -0
  98. data/sig/generated/redhound/analysis/stats.rbs +77 -0
  99. data/sig/generated/redhound/analysis/tcp_analysis.rbs +15 -0
  100. data/sig/generated/redhound/analysis/tcp_reassembler.rbs +27 -0
  101. data/sig/generated/redhound/analysis/tcp_stream.rbs +60 -0
  102. data/sig/generated/redhound/analysis.rbs +38 -0
  103. data/sig/generated/redhound/capture/bsd/bpf_device.rbs +39 -0
  104. data/sig/generated/redhound/capture/bsd/constants.rbs +40 -0
  105. data/sig/generated/redhound/capture/file_source.rbs +34 -0
  106. data/sig/generated/redhound/capture/interface.rbs +53 -0
  107. data/sig/generated/redhound/capture/linktype.rbs +20 -0
  108. data/sig/generated/redhound/capture/linux/constants.rbs +58 -0
  109. data/sig/generated/redhound/capture/linux/cooked.rbs +15 -0
  110. data/sig/generated/redhound/capture/linux/ifreq.rbs +18 -0
  111. data/sig/generated/redhound/capture/linux/packet_socket.rbs +57 -0
  112. data/sig/generated/redhound/capture/linux/sockaddr_ll.rbs +31 -0
  113. data/sig/generated/redhound/capture/linux/tpacket_v3.rbs +35 -0
  114. data/sig/generated/redhound/capture/source.rbs +44 -0
  115. data/sig/generated/redhound/capture/stats.rbs +25 -0
  116. data/sig/generated/redhound/capture.rbs +14 -0
  117. data/sig/generated/redhound/cli/command.rbs +12 -0
  118. data/sig/generated/redhound/cli/options.rbs +25 -0
  119. data/sig/generated/redhound/cli/privileges.rbs +12 -0
  120. data/sig/generated/redhound/cli/runner.rbs +27 -0
  121. data/sig/generated/redhound/context.rbs +27 -0
  122. data/sig/generated/redhound/cursor.rbs +59 -0
  123. data/sig/generated/redhound/diagnostic.rbs +21 -0
  124. data/sig/generated/redhound/dissector.rbs +113 -0
  125. data/sig/generated/redhound/engine.rbs +23 -0
  126. data/sig/generated/redhound/errors.rbs +56 -0
  127. data/sig/generated/redhound/field.rbs +74 -0
  128. data/sig/generated/redhound/file/format.rbs +33 -0
  129. data/sig/generated/redhound/file/pcap_reader.rbs +19 -0
  130. data/sig/generated/redhound/file/pcap_writer.rbs +38 -0
  131. data/sig/generated/redhound/file/pcapng_reader.rbs +54 -0
  132. data/sig/generated/redhound/file/pcapng_writer.rbs +50 -0
  133. data/sig/generated/redhound/file/rotating_writer.rbs +42 -0
  134. data/sig/generated/redhound/filter/analyzer.rbs +42 -0
  135. data/sig/generated/redhound/filter/bpf/assembler.rbs +39 -0
  136. data/sig/generated/redhound/filter/bpf/disassembler.rbs +18 -0
  137. data/sig/generated/redhound/filter/bpf/validator.rbs +30 -0
  138. data/sig/generated/redhound/filter/bpf/vm.rbs +26 -0
  139. data/sig/generated/redhound/filter/codegen.rbs +101 -0
  140. data/sig/generated/redhound/filter/lexer.rbs +15 -0
  141. data/sig/generated/redhound/filter/parser.rbs +65 -0
  142. data/sig/generated/redhound/filter/program.rbs +48 -0
  143. data/sig/generated/redhound/filter.rbs +10 -0
  144. data/sig/generated/redhound/layer.rbs +81 -0
  145. data/sig/generated/redhound/output/fields.rbs +15 -0
  146. data/sig/generated/redhound/output/follow.rbs +21 -0
  147. data/sig/generated/redhound/output/hexdump.rbs +15 -0
  148. data/sig/generated/redhound/output/json.rbs +18 -0
  149. data/sig/generated/redhound/output/summary.rbs +21 -0
  150. data/sig/generated/redhound/output/timestamp.rbs +21 -0
  151. data/sig/generated/redhound/output/tree.rbs +15 -0
  152. data/sig/generated/redhound/packet.rbs +77 -0
  153. data/sig/generated/redhound/protocols/arp.rbs +15 -0
  154. data/sig/generated/redhound/protocols/checksum.rbs +15 -0
  155. data/sig/generated/redhound/protocols/data.rbs +15 -0
  156. data/sig/generated/redhound/protocols/dhcp.rbs +28 -0
  157. data/sig/generated/redhound/protocols/dns.rbs +42 -0
  158. data/sig/generated/redhound/protocols/ethernet.rbs +15 -0
  159. data/sig/generated/redhound/protocols/http.rbs +36 -0
  160. data/sig/generated/redhound/protocols/icmp.rbs +41 -0
  161. data/sig/generated/redhound/protocols/ipv4.rbs +15 -0
  162. data/sig/generated/redhound/protocols/ipv6.rbs +24 -0
  163. data/sig/generated/redhound/protocols/link.rbs +33 -0
  164. data/sig/generated/redhound/protocols/llc.rbs +15 -0
  165. data/sig/generated/redhound/protocols/ntp.rbs +17 -0
  166. data/sig/generated/redhound/protocols/tcp.rbs +25 -0
  167. data/sig/generated/redhound/protocols/tls.rbs +51 -0
  168. data/sig/generated/redhound/protocols/tunnel.rbs +24 -0
  169. data/sig/generated/redhound/protocols/udp.rbs +18 -0
  170. data/sig/generated/redhound/protocols/vlan.rbs +12 -0
  171. data/sig/generated/redhound/reader.rbs +7 -0
  172. data/sig/generated/redhound/registry.rbs +45 -0
  173. data/sig/generated/redhound/stream_dissector.rbs +44 -0
  174. data/sig/generated/redhound/util/seq.rbs +20 -0
  175. data/sig/generated/redhound/version.rbs +1 -0
  176. data/sig/generated/redhound/writer.rbs +6 -19
  177. data/sig/generated/redhound.rbs +18 -0
  178. data/sig/shims/io.rbs +7 -0
  179. data/sig/shims/process.rbs +3 -0
  180. metadata +175 -55
  181. data/CODE_OF_CONDUCT.md +0 -132
  182. data/Dockerfile +0 -7
  183. data/Rakefile +0 -15
  184. data/Steepfile +0 -4
  185. data/docker-compose.yml +0 -9
  186. data/lib/redhound/analyzer.rb +0 -34
  187. data/lib/redhound/builder/packet_mreq.rb +0 -56
  188. data/lib/redhound/builder/socket.rb +0 -35
  189. data/lib/redhound/builder.rb +0 -4
  190. data/lib/redhound/command.rb +0 -67
  191. data/lib/redhound/l2/ether.rb +0 -68
  192. data/lib/redhound/l2/protocol.rb +0 -33
  193. data/lib/redhound/l2.rb +0 -4
  194. data/lib/redhound/l3/arp.rb +0 -114
  195. data/lib/redhound/l3/base.rb +0 -49
  196. data/lib/redhound/l3/ipv4.rb +0 -95
  197. data/lib/redhound/l3/ipv6.rb +0 -69
  198. data/lib/redhound/l3/protocol.rb +0 -173
  199. data/lib/redhound/l3/resolver.rb +0 -30
  200. data/lib/redhound/l3.rb +0 -8
  201. data/lib/redhound/l4/base.rb +0 -31
  202. data/lib/redhound/l4/icmp.rb +0 -71
  203. data/lib/redhound/l4/resolver.rb +0 -28
  204. data/lib/redhound/l4/udp.rb +0 -63
  205. data/lib/redhound/l4.rb +0 -6
  206. data/lib/redhound/receiver.rb +0 -45
  207. data/lib/redhound/resolver.rb +0 -22
  208. data/lib/redhound/source/socket.rb +0 -18
  209. data/lib/redhound/source.rb +0 -3
  210. data/rbs_collection.lock.yaml +0 -20
  211. data/rbs_collection.yaml +0 -17
  212. data/sig/generated/redhound/analyzer.rbs +0 -14
  213. data/sig/generated/redhound/builder/packet_mreq.rbs +0 -39
  214. data/sig/generated/redhound/builder/socket.rbs +0 -24
  215. data/sig/generated/redhound/command.rbs +0 -19
  216. data/sig/generated/redhound/l2/ether.rbs +0 -41
  217. data/sig/generated/redhound/l2/protocol.rbs +0 -15
  218. data/sig/generated/redhound/l3/arp.rbs +0 -57
  219. data/sig/generated/redhound/l3/base.rbs +0 -28
  220. data/sig/generated/redhound/l3/ipv4.rbs +0 -53
  221. data/sig/generated/redhound/l3/ipv6.rbs +0 -38
  222. data/sig/generated/redhound/l3/protocol.rbs +0 -16
  223. data/sig/generated/redhound/l3/resolver.rbs +0 -16
  224. data/sig/generated/redhound/l4/base.rbs +0 -19
  225. data/sig/generated/redhound/l4/icmp.rbs +0 -33
  226. data/sig/generated/redhound/l4/resolver.rbs +0 -16
  227. data/sig/generated/redhound/l4/udp.rbs +0 -36
  228. data/sig/generated/redhound/receiver.rbs +0 -19
  229. data/sig/generated/redhound/resolver.rbs +0 -11
  230. 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