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,205 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Protocols
7
+ # @api private
8
+ class Icmp < Dissector
9
+ TYPES = { 0 => 'Echo Reply', 3 => 'Destination Unreachable', 4 => 'Source Quench', 5 => 'Redirect',
10
+ 8 => 'Echo Request', 9 => 'Router Advertisement', 10 => 'Router Solicitation', 11 => 'Time Exceeded',
11
+ 12 => 'Parameter Problem', 13 => 'Timestamp', 14 => 'Timestamp Reply', 17 => 'Address Mask Request', 18 => 'Address Mask Reply' }.freeze
12
+ CODES = {
13
+ 3 => { 0 => 'Network Unreachable', 1 => 'Host Unreachable', 2 => 'Protocol Unreachable', 3 => 'Port Unreachable',
14
+ 4 => 'Fragmentation Needed', 5 => 'Source Route Failed', 6 => 'Network Unknown', 7 => 'Host Unknown',
15
+ 8 => 'Source Host Isolated', 9 => 'Network Administratively Prohibited', 10 => 'Host Administratively Prohibited',
16
+ 11 => 'Network Unreachable for TOS', 12 => 'Host Unreachable for TOS', 13 => 'Administratively Prohibited',
17
+ 14 => 'Host Precedence Violation', 15 => 'Precedence Cutoff' },
18
+ 5 => { 0 => 'Redirect Network', 1 => 'Redirect Host', 2 => 'Redirect TOS Network', 3 => 'Redirect TOS Host' },
19
+ 11 => { 0 => 'TTL Expired', 1 => 'Reassembly Timeout' },
20
+ 12 => { 0 => 'Invalid Pointer', 1 => 'Missing Option', 2 => 'Invalid Length' }
21
+ }.freeze
22
+ protocol :icmp, name: 'Internet Control Message Protocol', short: 'ICMP'
23
+ dissects_on 'ip.proto', 1
24
+ header do
25
+ uint8 :type, 'icmp.type', enum: TYPES
26
+ uint8 :code, 'icmp.code'
27
+ uint16 :checksum, 'icmp.checksum', format: :hex
28
+ end
29
+ # @rbs (Context ctx, Layer layer) -> void
30
+ def dissect(ctx, layer)
31
+ layer.definitions[1] = layer.definitions[1].with(enum: CODES[layer[:type]])
32
+ if [0, 8, 13, 14, 17, 18].include?(layer[:type])
33
+ layer.add(:ident, 'icmp.ident', ctx.cursor.u16(4), offset: 4, length: 2)
34
+ layer.add(:seq, 'icmp.seq', ctx.cursor.u16(6), offset: 6, length: 2)
35
+ else
36
+ ctx.cursor.check(0, 8)
37
+ if layer[:type] == 3 && layer[:code] == 4
38
+ layer.add(:mtu, 'icmp.mtu', ctx.cursor.u16(6), offset: 6, length: 2)
39
+ end
40
+ end
41
+ layer.header_length, layer.payload_offset = 8, layer.offset + 8
42
+ Checksum.verify(ctx, layer, ctx.cursor.bytes(0, ctx.cursor.remaining))
43
+ end
44
+ # @rbs (Context ctx, Layer layer) -> untyped
45
+ def next_dissector(ctx, layer)
46
+ return nil unless [3, 4, 5, 11, 12].include?(layer[:type])
47
+ ctx.embedded = true
48
+ Ipv4
49
+ end
50
+ # @rbs (Layer layer) -> String
51
+ def summary(layer)
52
+ text = "ICMP type #{layer[:type]}, code #{layer[:code]}"
53
+ text += ", id #{layer[:ident]}, seq #{layer[:seq]}" if layer[:ident]
54
+ text += ", mtu #{layer[:mtu]}" if layer[:mtu]
55
+ text
56
+ end
57
+ end
58
+
59
+ # @api private
60
+ class Icmpv6 < Dissector
61
+ TYPES = { 1 => 'Destination Unreachable', 2 => 'Packet Too Big', 3 => 'Time Exceeded', 4 => 'Parameter Problem',
62
+ 128 => 'Echo Request', 129 => 'Echo Reply', 133 => 'Router Solicitation', 134 => 'Router Advertisement',
63
+ 135 => 'Neighbor Solicitation', 136 => 'Neighbor Advertisement', 137 => 'Redirect' }.freeze
64
+ CODES = {
65
+ 1 => { 0 => 'No Route', 1 => 'Administratively Prohibited', 2 => 'Beyond Source Scope', 3 => 'Address Unreachable',
66
+ 4 => 'Port Unreachable', 5 => 'Source Policy Failure', 6 => 'Reject Route', 7 => 'Source Routing Error' },
67
+ 3 => { 0 => 'Hop Limit Exceeded', 1 => 'Reassembly Timeout' },
68
+ 4 => { 0 => 'Invalid Header Field', 1 => 'Unknown Next Header', 2 => 'Unknown Option' }
69
+ }.freeze
70
+ protocol :icmpv6, name: 'ICMPv6 / Neighbor Discovery', short: 'ICMP6'
71
+ dissects_on 'ip.proto', 58
72
+ header do
73
+ uint8 :type, 'icmpv6.type', enum: TYPES
74
+ uint8 :code, 'icmpv6.code'
75
+ uint16 :checksum, 'icmpv6.checksum'
76
+ end
77
+ # @rbs (Context ctx, Layer layer) -> void
78
+ def dissect(ctx, layer)
79
+ cursor, type = ctx.cursor, layer[:type]
80
+ layer.definitions[1] = layer.definitions[1].with(enum: CODES[type])
81
+ length = case type
82
+ when 133 then 8
83
+ when 134 then 16
84
+ when 135, 136 then 24
85
+ when 137 then 40
86
+ else 8
87
+ end
88
+ cursor.check(0, length)
89
+ if [128, 129].include?(type)
90
+ layer.add(:ident, 'icmpv6.echo.identifier', cursor.u16(4), offset: 4, length: 2)
91
+ layer.add(:seq, 'icmpv6.echo.sequence_number', cursor.u16(6), offset: 6, length: 2)
92
+ elsif [135, 136, 137].include?(type)
93
+ family = { 135 => 'ns', 136 => 'na', 137 => 'rd' }.fetch(type)
94
+ layer.add(:target, "icmpv6.nd.#{family}.target_address", cursor.bytes(8, 16), type: :ipv6, offset: 8, length: 16)
95
+ layer.add(:destination, 'icmpv6.rd.na.destination_address', cursor.bytes(24, 16), type: :ipv6, offset: 24, length: 16) if type == 137
96
+ if type == 136
97
+ flags = cursor.u32(4)
98
+ layer.add(:flags, 'icmpv6.nd.na.flag', flags, format: :hex, offset: 4, length: 4)
99
+ { r: 0x80000000, s: 0x40000000, o: 0x20000000 }.each do |name, mask|
100
+ layer.add(name, "icmpv6.nd.na.flag.#{name}", (flags & mask).positive?, type: :boolean, offset: 4, length: 4)
101
+ end
102
+ end
103
+ end
104
+ if type == 134
105
+ layer.add(:hop_limit, 'icmpv6.nd.ra.cur_hop_limit', cursor.u8(4), offset: 4, length: 1)
106
+ layer.add(:lifetime, 'icmpv6.nd.ra.router_lifetime', cursor.u16(6), offset: 6, length: 2)
107
+ flags = cursor.u8(5)
108
+ layer.add(:flags, 'icmpv6.nd.ra.flag', flags, format: :hex, offset: 5, length: 1)
109
+ layer.add(:managed, 'icmpv6.nd.ra.flag.m', (flags & 0x80).positive?, type: :boolean, offset: 5, length: 1)
110
+ layer.add(:other, 'icmpv6.nd.ra.flag.o', (flags & 0x40).positive?, type: :boolean, offset: 5, length: 1)
111
+ layer.add(:reachable, 'icmpv6.nd.ra.reachable_time', cursor.u32(8), offset: 8, length: 4)
112
+ layer.add(:retrans, 'icmpv6.nd.ra.retrans_timer', cursor.u32(12), offset: 12, length: 4)
113
+ end
114
+ if type.between?(133, 137)
115
+ pos = length
116
+ while pos < cursor.remaining
117
+ opt = cursor.u8(pos)
118
+ len = cursor.u8(pos + 1) * 8
119
+ return layer.diagnose(:error, :bad_length, 'NDP option has zero length') if len.zero?
120
+ cursor.check(pos, len)
121
+ layer.add(:"option_type_#{pos}", 'icmpv6.opt.type', opt, offset: pos, length: 1)
122
+ layer.add(:"option_length_#{pos}", 'icmpv6.opt.length', len / 8, offset: pos + 1, length: 1)
123
+ if [1, 2].include?(opt) && len >= 8
124
+ layer.add(:"link_address_#{pos}", 'icmpv6.opt.linkaddr', cursor.bytes(pos + 2, 6), type: :mac, offset: pos + 2, length: 6)
125
+ elsif opt == 3 && len == 32
126
+ layer.add(:"prefix_length_#{pos}", 'icmpv6.opt.prefix.length', cursor.u8(pos + 2), offset: pos + 2, length: 1)
127
+ flags = cursor.u8(pos + 3)
128
+ layer.add(:"prefix_flags_#{pos}", 'icmpv6.opt.prefix.flag', flags, format: :hex, offset: pos + 3, length: 1)
129
+ layer.add(:"prefix_onlink_#{pos}", 'icmpv6.opt.prefix.flag.l', (flags & 0x80).positive?, type: :boolean, offset: pos + 3, length: 1)
130
+ layer.add(:"prefix_autonomous_#{pos}", 'icmpv6.opt.prefix.flag.a', (flags & 0x40).positive?, type: :boolean, offset: pos + 3, length: 1)
131
+ layer.add(:"prefix_valid_#{pos}", 'icmpv6.opt.prefix.valid_lifetime', cursor.u32(pos + 4), offset: pos + 4, length: 4)
132
+ layer.add(:"prefix_preferred_#{pos}", 'icmpv6.opt.prefix.preferred_lifetime', cursor.u32(pos + 8), offset: pos + 8, length: 4)
133
+ layer.add(:"prefix_#{pos}", 'icmpv6.opt.prefix', cursor.bytes(pos + 16, 16), type: :ipv6, offset: pos + 16, length: 16)
134
+ elsif opt == 4 && len >= 8
135
+ layer.add(:"redirected_#{pos}", 'icmpv6.opt.redirected_packet', cursor.bytes(pos + 8, len - 8), type: :bytes, offset: pos + 8, length: len - 8)
136
+ elsif opt == 5 && len == 8
137
+ layer.add(:"mtu_#{pos}", 'icmpv6.opt.mtu', cursor.u32(pos + 4), offset: pos + 4, length: 4)
138
+ else
139
+ layer.add(:"option_#{pos}", 'icmpv6.opt.data', cursor.bytes(pos + 2, len - 2), type: :bytes, offset: pos + 2, length: len - 2)
140
+ end
141
+ pos += len
142
+ end
143
+ length = pos
144
+ end
145
+ layer.header_length, layer.payload_offset = length, layer.offset + length
146
+ Checksum.verify(ctx, layer, cursor.bytes(0, cursor.remaining), 58)
147
+ end
148
+ # @rbs (Context ctx, Layer layer) -> untyped
149
+ def next_dissector(ctx, layer)
150
+ return nil unless layer[:type] < 128
151
+ ctx.embedded = true
152
+ Ipv6
153
+ end
154
+ end
155
+
156
+ # @api private
157
+ class Igmp < Dissector
158
+ protocol :igmp, name: 'Internet Group Management Protocol', short: 'IGMP'
159
+ dissects_on 'ip.proto', 2
160
+ header do
161
+ uint8 :type, 'igmp.type'
162
+ uint8 :response, 'igmp.max_resp'
163
+ uint16 :checksum, 'igmp.checksum'
164
+ end
165
+ # @rbs (Context ctx, Layer layer) -> void
166
+ def dissect(ctx, layer)
167
+ cursor, type = ctx.cursor, layer[:type]
168
+ layer.definitions.reject! { |field| field.key == :response } if [0x12, 0x22].include?(type)
169
+ cursor.check(0, 8)
170
+ version = type == 0x22 || (type == 0x11 && cursor.remaining >= 12) ? 3 : (type == 0x12 || (type == 0x11 && layer[:response].zero?) ? 1 : 2)
171
+ layer.add(:version, 'igmp.version', version)
172
+ pos = 8
173
+ if type == 0x22
174
+ count = cursor.u16(6)
175
+ layer.add(:records, 'igmp.num_grp_recs', count, offset: 6, length: 2)
176
+ count.times do |record|
177
+ cursor.check(pos, 8)
178
+ sources, auxiliary = cursor.u16(pos + 2), cursor.u8(pos + 1) * 4
179
+ cursor.check(pos, 8 + sources * 4 + auxiliary)
180
+ layer.add(:"record_#{record}", 'igmp.record_type', cursor.u8(pos), offset: pos, length: 1)
181
+ layer.add(:"group_#{record}", 'igmp.maddr', cursor.u32(pos + 4), type: :ipv4, offset: pos + 4, length: 4)
182
+ layer.add(:"sources_#{record}", 'igmp.num_src', sources, offset: pos + 2, length: 2)
183
+ sources.times { |i| layer.add(:"source_#{record}_#{i}", 'igmp.saddr', cursor.u32(pos + 8 + i * 4), type: :ipv4, offset: pos + 8 + i * 4, length: 4) }
184
+ layer.add(:"auxiliary_#{record}", 'igmp.aux_data', cursor.bytes(pos + 8 + sources * 4, auxiliary), type: :bytes, offset: pos + 8 + sources * 4, length: auxiliary) if auxiliary.positive?
185
+ pos += 8 + sources * 4 + auxiliary
186
+ end
187
+ else
188
+ layer.add(:group, 'igmp.maddr', cursor.u32(4), type: :ipv4, offset: 4, length: 4)
189
+ if version == 3
190
+ sources = cursor.u16(10)
191
+ cursor.check(8, 4 + sources * 4)
192
+ layer.add(:suppress, 'igmp.s', (cursor.u8(8) & 8).positive?, type: :boolean, offset: 8, length: 1)
193
+ layer.add(:robustness, 'igmp.qrv', cursor.u8(8) & 7, offset: 8, length: 1)
194
+ layer.add(:interval, 'igmp.qqic', cursor.u8(9), offset: 9, length: 1)
195
+ layer.add(:sources, 'igmp.num_src', sources, offset: 10, length: 2)
196
+ sources.times { |i| layer.add(:"source_#{i}", 'igmp.saddr', cursor.u32(12 + i * 4), type: :ipv4, offset: 12 + i * 4, length: 4) }
197
+ pos = 12 + sources * 4
198
+ end
199
+ end
200
+ layer.header_length, layer.payload_offset = pos, layer.offset + pos
201
+ Checksum.verify(ctx, layer, cursor.bytes(0, cursor.remaining))
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,56 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Protocols
7
+ # @api private
8
+ class Ipv4 < Dissector
9
+ protocol :ipv4, name: 'Internet Protocol v4', short: 'IP'
10
+ dissects_on 'ethertype', 0x0800
11
+ dissects_on 'ip.proto', 4
12
+ header do
13
+ bits 8 do
14
+ bit :version, 'ip.version', 4
15
+ bit :hdr_len, 'ip.hdr_len', 4, scale: 4
16
+ end
17
+ bits 8 do
18
+ bit :dscp, 'ip.dsfield.dscp', 6
19
+ bit :ecn, 'ip.dsfield.ecn', 2
20
+ end
21
+ uint16 :len, 'ip.len'
22
+ uint16 :id, 'ip.id', format: :hex
23
+ bits 16 do
24
+ bit :reserved, 'ip.flags.rb', 1
25
+ bit :df, 'ip.flags.df', 1
26
+ bit :mf, 'ip.flags.mf', 1
27
+ bit :frag_offset, 'ip.frag_offset', 13, scale: 8
28
+ end
29
+ uint8 :ttl, 'ip.ttl'
30
+ uint8 :proto, 'ip.proto'
31
+ uint16 :checksum, 'ip.checksum', format: :hex
32
+ ipv4 :src, 'ip.src'
33
+ ipv4 :dst, 'ip.dst'
34
+ end
35
+ # @rbs (Context ctx, Layer layer) -> void
36
+ def dissect(ctx, layer)
37
+ hlen = layer[:hdr_len]
38
+ if layer[:version] != 4 || hlen < 20 || layer[:len] < hlen
39
+ layer.diagnose(:error, :bad_length, 'invalid IPv4 version or length')
40
+ return
41
+ end
42
+ ctx.cursor.check(0, hlen)
43
+ layer.header_length = hlen
44
+ layer.payload_offset = layer.offset + hlen
45
+ layer.payload_end = layer.offset + layer[:len]
46
+ layer.add(:options, 'ip.options', ctx.cursor.bytes(20, hlen - 20), type: :bytes, offset: 20, length: hlen - 20) if hlen > 20
47
+ Checksum.verify(ctx, layer, ctx.cursor.bytes(0, hlen))
48
+ end
49
+ # @rbs (Context ctx, Layer layer) -> untyped
50
+ def next_dissector(ctx, layer)
51
+ return nil unless layer[:frag_offset].zero?
52
+ ctx.registry.lookup('ip.proto', layer[:proto])
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,68 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Protocols
7
+ # @api private
8
+ class Ipv6 < Dissector
9
+ protocol :ipv6, name: 'Internet Protocol v6', short: 'IP6'
10
+ dissects_on 'ethertype', 0x86dd
11
+ dissects_on 'ip.proto', 41
12
+ header do
13
+ bits 32 do
14
+ bit :version, 'ipv6.version', 4
15
+ bit :tclass, 'ipv6.tclass', 8
16
+ bit :flow, 'ipv6.flow', 20, format: :hex
17
+ end
18
+ uint16 :plen, 'ipv6.plen'
19
+ uint8 :nxt, 'ipv6.nxt'
20
+ uint8 :hlim, 'ipv6.hlim'
21
+ ipv6 :src, 'ipv6.src'
22
+ ipv6 :dst, 'ipv6.dst'
23
+ end
24
+ # @rbs (Context ctx, Layer layer) -> void
25
+ def dissect(ctx, layer)
26
+ ctx.extension_count = 0
27
+ layer.diagnose(:error, :malformed, 'IPv6 version is not 6') if layer[:version] != 6
28
+ layer.payload_end = layer.offset + 40 + layer[:plen]
29
+ layer.payload_offset = layer.payload_end if layer[:nxt] == 59
30
+ end
31
+ # @rbs (Context ctx, Layer layer) -> untyped
32
+ def next_dissector(ctx, layer) = ctx.registry.lookup('ip.proto', layer[:nxt])
33
+ end
34
+
35
+ # @api private
36
+ class Ipv6Ext < Dissector
37
+ protocol :ipv6_ext, name: 'IPv6 extension', short: 'IP6 EXT'
38
+ dissects_on 'ip.proto', 0, 43, 44, 60, 51
39
+ # @rbs (Context ctx, Layer layer) -> void
40
+ def dissect(ctx, layer)
41
+ ctx.extension_count += 1
42
+ return layer.diagnose(:error, :malformed, 'more than 16 IPv6 extensions') if ctx.extension_count > 16
43
+ parent = ctx.layers.last
44
+ type = parent.protocol == :ipv6 ? parent[:nxt] : parent.protocol == :ipv4 ? parent[:proto] : parent[:next]
45
+ cursor = ctx.cursor
46
+ nxt = cursor.u8(0)
47
+ layer.add(:next, 'ipv6.nxt', nxt, length: 1)
48
+ layer.add(:type, 'ipv6.extension.type', type)
49
+ length = type == 44 ? 8 : type == 51 ? (cursor.u8(1) + 2) * 4 : (cursor.u8(1) + 1) * 8
50
+ cursor.check(0, length)
51
+ if type == 44
52
+ flags = cursor.u16(2)
53
+ layer.add(:fragment_offset, 'ipv6.fragment.offset', flags & 0xfff8, offset: 2, length: 2)
54
+ layer.add(:more, 'ipv6.fragment.more', flags & 1, offset: 2, length: 2)
55
+ layer.add(:identification, 'ipv6.fragment.id', cursor.u32(4), offset: 4, length: 4)
56
+ end
57
+ layer.header_length = length
58
+ layer.payload_offset += length
59
+ layer.payload_offset = layer.payload_end if nxt == 59
60
+ end
61
+ # @rbs (Context ctx, Layer layer) -> untyped
62
+ def next_dissector(ctx, layer)
63
+ return nil if layer[:fragment_offset] && layer[:fragment_offset] != 0
64
+ ctx.registry.lookup('ip.proto', layer[:next])
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,70 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Protocols
7
+ # @api private
8
+ class Sll < Dissector
9
+ protocol :sll, name: 'Linux cooked capture', short: 'SLL'
10
+ dissects_on 'linktype', 113
11
+ header do
12
+ uint16 :pkttype, 'sll.pkttype'
13
+ uint16 :hatype, 'sll.hatype'
14
+ uint16 :halen, 'sll.halen'
15
+ uint64 :address, 'sll.src'
16
+ uint16 :type, 'sll.etype'
17
+ end
18
+ # @rbs (Context ctx, Layer layer) -> untyped
19
+ def next_dissector(ctx, layer) = ctx.registry.lookup('ethertype', layer[:type])
20
+ end
21
+ # @api private
22
+ class Sll2 < Dissector
23
+ protocol :sll2, name: 'Linux cooked capture v2', short: 'SLL2'
24
+ dissects_on 'linktype', 276
25
+ header do
26
+ uint16 :type, 'sll.etype'
27
+ uint16 :reserved, 'sll.reserved'
28
+ uint32 :ifindex, 'sll.ifindex'
29
+ uint16 :hatype, 'sll.hatype'
30
+ uint8 :pkttype, 'sll.pkttype'
31
+ uint8 :halen, 'sll.halen'
32
+ uint64 :address, 'sll.src'
33
+ end
34
+ # @rbs (Context ctx, Layer layer) -> untyped
35
+ def next_dissector(ctx, layer) = ctx.registry.lookup('ethertype', layer[:type])
36
+ end
37
+ # @api private
38
+ class Null < Dissector
39
+ protocol :null, name: 'BSD loopback', short: 'NULL'
40
+ dissects_on 'linktype', 0, 108
41
+ # @rbs (Context ctx, Layer layer) -> void
42
+ def dissect(ctx, layer)
43
+ bytes = ctx.cursor.bytes(0, 4)
44
+ family = bytes.unpack1(ctx.packet.linktype == 108 ? 'N' : 'L')
45
+ family = bytes.unpack1('N') if family > 255
46
+ layer.add(:family, 'null.family', family, length: 4)
47
+ layer.header_length = 4
48
+ layer.payload_offset += 4
49
+ end
50
+ # @rbs (Context ctx, Layer layer) -> untyped
51
+ def next_dissector(ctx, layer)
52
+ return Ipv4 if layer[:family] == 2
53
+ return Ipv6 if [10, 24, 28, 30].include?(layer[:family])
54
+ nil
55
+ end
56
+ end
57
+ # @api private
58
+ class Raw < Dissector
59
+ protocol :raw, name: 'Raw IP', short: 'RAW'
60
+ dissects_on 'linktype', 101, 228, 229
61
+ # @rbs (Context ctx, Layer layer) -> untyped
62
+ def next_dissector(ctx, layer)
63
+ case ctx.cursor.u8(0) >> 4
64
+ when 4 then Ipv4
65
+ when 6 then Ipv6
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,37 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Protocols
7
+ # @api private
8
+ class Llc < Dissector
9
+ protocol :llc, name: 'Logical Link Control', short: 'LLC'
10
+ header do
11
+ uint8 :dsap, 'llc.dsap'
12
+ uint8 :ssap, 'llc.ssap'
13
+ uint8 :control, 'llc.control'
14
+ end
15
+ # @rbs (Context ctx, Layer layer) -> void
16
+ def dissect(ctx, layer)
17
+ if layer[:control] & 3 != 3
18
+ layer.values[:control] |= ctx.cursor.u8(3) << 8
19
+ layer.definitions[2] = layer.definitions[2].with(type: :uint16, length: 2)
20
+ layer.header_length = 4
21
+ layer.payload_offset = layer.offset + 4
22
+ return
23
+ end
24
+ return unless layer[:dsap] == 0xaa && layer[:ssap] == 0xaa && layer[:control] == 3
25
+ layer.add(:oui, 'llc.oui', ctx.cursor.bytes(3, 3), type: :bytes, offset: 3, length: 3)
26
+ layer.add(:type, 'llc.pid', ctx.cursor.u16(6), offset: 6, length: 2, format: :hex)
27
+ layer.header_length = 8
28
+ layer.payload_offset = layer.offset + 8
29
+ end
30
+ # @rbs (Context ctx, Layer layer) -> untyped
31
+ def next_dissector(ctx, layer)
32
+ return ctx.registry.lookup('ethertype', layer[:type]) if layer[:oui] == "\0\0\0".b
33
+ ctx.registry.lookup('llc.dsap', layer[:dsap])
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,45 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Protocols
7
+ # @api private
8
+ class Ntp < Dissector
9
+ MODES = { 1 => 'symmetric active', 2 => 'symmetric passive', 3 => 'client', 4 => 'server',
10
+ 5 => 'broadcast', 6 => 'control', 7 => 'private' }.freeze
11
+ protocol :ntp, name: 'Network Time Protocol', short: 'NTP'
12
+ dissects_on 'udp.port', 123
13
+ header do
14
+ bits 8 do
15
+ bit :li, 'ntp.flags.li', 2
16
+ bit :vn, 'ntp.flags.vn', 3
17
+ bit :mode, 'ntp.flags.mode', 3, enum: MODES
18
+ end
19
+ uint8 :stratum, 'ntp.stratum'
20
+ int8 :poll, 'ntp.ppoll'
21
+ int8 :precision, 'ntp.precision'
22
+ uint32 :rootdelay, 'ntp.rootdelay'
23
+ uint32 :rootdispersion, 'ntp.rootdispersion'
24
+ uint32 :refid, 'ntp.refid'
25
+ uint64 :reftime, 'ntp.reftime'
26
+ uint64 :org, 'ntp.org'
27
+ uint64 :rec, 'ntp.rec'
28
+ uint64 :xmt, 'ntp.xmt'
29
+ end
30
+
31
+ # @rbs (Context ctx, Layer layer) -> void
32
+ def dissect(ctx, layer)
33
+ layer.diagnose(:error, :malformed, 'invalid NTP version or mode') unless layer[:vn].between?(1, 4) && layer[:mode].between?(1, 7)
34
+ if ctx.cursor.remaining > 48
35
+ layer.add(:extension, 'ntp.extension', ctx.cursor.bytes(48, ctx.cursor.remaining - 48),
36
+ type: :bytes, offset: 48, length: ctx.cursor.remaining - 48)
37
+ end
38
+ layer.payload_offset = layer.payload_end
39
+ end
40
+
41
+ # @rbs (Layer layer) -> String
42
+ def summary(layer) = "NTP v#{layer[:vn]} #{MODES[layer[:mode]]}, stratum #{layer[:stratum]}"
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,88 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Protocols
7
+ # @api private
8
+ class Tcp < Dissector
9
+ FLAGS = { 0x80 => 'C', 0x40 => 'E', 0x20 => 'U', 0x10 => '.', 8 => 'P', 4 => 'R', 2 => 'S', 1 => 'F' }.freeze
10
+ FLAG_FIELDS = %w[fin syn reset push ack urg ecn cwr].each_with_index.map do |name, bit|
11
+ [FieldDefinition.new(:"flag_#{name}", "tcp.flags.#{name}", :boolean, 13, 1, 0, nil, nil, 1, nil, nil), 1 << bit]
12
+ end.freeze
13
+ protocol :tcp, name: 'Transmission Control Protocol', short: 'TCP'
14
+ dissects_on 'ip.proto', 6
15
+ header do
16
+ uint16 :srcport, 'tcp.srcport'
17
+ uint16 :dstport, 'tcp.dstport'
18
+ uint32 :seq, 'tcp.seq'
19
+ uint32 :ack, 'tcp.ack'
20
+ bits 16 do
21
+ bit :hdr_len, 'tcp.hdr_len', 4, scale: 4
22
+ bit :reserved, 'tcp.reserved', 3
23
+ bit :flags, 'tcp.flags', 9, format: :hex
24
+ end
25
+ uint16 :window, 'tcp.window_size_value'
26
+ uint16 :checksum, 'tcp.checksum', format: :hex
27
+ uint16 :urgent, 'tcp.urgent_pointer'
28
+ end
29
+ # @rbs (Context ctx, Layer layer) -> void
30
+ def dissect(ctx, layer)
31
+ hlen = layer[:hdr_len]
32
+ return layer.diagnose(:error, :bad_length, 'TCP data offset is less than 20') if hlen < 20
33
+ ctx.cursor.check(0, hlen)
34
+ layer.header_length, layer.payload_offset = hlen, layer.offset + hlen
35
+ FLAG_FIELDS.each do |definition, mask|
36
+ layer.definitions << definition
37
+ layer.values[definition.key] = (layer[:flags] & mask) != 0
38
+ end
39
+ options(ctx.cursor, layer, hlen)
40
+ layer.add(:length, 'tcp.len', ctx.cursor.remaining - hlen)
41
+ Checksum.verify(ctx, layer, ctx.cursor.bytes(0, ctx.cursor.remaining), 6)
42
+ end
43
+ # @rbs (Cursor cursor, Layer layer, Integer hlen) -> void
44
+ def options(cursor, layer, hlen)
45
+ pos = 20
46
+ while pos < hlen
47
+ kind = cursor.u8(pos)
48
+ break if kind.zero?
49
+ if kind == 1
50
+ pos += 1
51
+ next
52
+ end
53
+ return layer.diagnose(:error, :bad_length, 'truncated TCP option') if pos + 2 > hlen
54
+ len = cursor.u8(pos + 1)
55
+ return layer.diagnose(:error, :bad_length, 'invalid TCP option length') if len < 2 || pos + len > hlen
56
+ valid = case kind
57
+ when 2 then len == 4
58
+ when 3 then len == 3
59
+ when 4 then len == 2
60
+ when 5 then len >= 10 && (len - 2) % 8 == 0
61
+ when 8 then len == 10
62
+ when 34 then len == 2 || len.between?(6, 18)
63
+ else true
64
+ end
65
+ return layer.diagnose(:error, :bad_length, "invalid TCP option #{kind} length") unless valid
66
+ case kind
67
+ when 2 then layer.add(:mss, 'tcp.options.mss_val', cursor.u16(pos + 2), offset: pos + 2, length: 2)
68
+ when 3 then layer.add(:wscale, 'tcp.options.wscale.shift', cursor.u8(pos + 2), offset: pos + 2, length: 1)
69
+ when 4 then layer.add(:sack_permitted, 'tcp.options.sack_perm', true, type: :boolean, offset: pos, length: 2)
70
+ when 5 then layer.add(:sack, 'tcp.options.sack', cursor.bytes(pos + 2, len - 2), type: :bytes, offset: pos + 2, length: len - 2)
71
+ when 8
72
+ layer.add(:tsval, 'tcp.options.timestamp.tsval', cursor.u32(pos + 2), offset: pos + 2, length: 4)
73
+ layer.add(:tsecr, 'tcp.options.timestamp.tsecr', cursor.u32(pos + 6), offset: pos + 6, length: 4)
74
+ when 34 then layer.add(:tfo, 'tcp.options.tfo.cookie', cursor.bytes(pos + 2, len - 2), type: :bytes, offset: pos + 2, length: len - 2)
75
+ end
76
+ pos += len
77
+ end
78
+ end
79
+ # @rbs (Context ctx, Layer layer) -> untyped
80
+ def next_dissector(ctx, layer) = ctx.registry.by_port('tcp.port', layer[:srcport], layer[:dstport])
81
+ # @rbs (Layer layer) -> String
82
+ def summary(layer)
83
+ flags = FLAGS.filter_map { |bit, name| name if layer[:flags] & bit != 0 }.join
84
+ "TCP [#{flags}], seq #{layer[:seq]}, ack #{layer[:ack]}, win #{layer[:window]}, length #{layer[:length]}"
85
+ end
86
+ end
87
+ end
88
+ end