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