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,413 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Filter
7
+ # @api private
8
+ class CodeGen
9
+ LINKTYPES = { ethernet: 1, raw: 101, null: 0, loop: 108, linux_sll: 113, sll: 113,
10
+ linux_sll2: 276, sll2: 276, ipv4: 228, ipv6: 229 }.freeze
11
+ LAYOUTS = { 1 => [14, 12], 113 => [16, 14], 276 => [20, 0], 0 => [4, 0],
12
+ 108 => [4, 0], 101 => [0, nil], 12 => [0, nil], 228 => [0, nil], 229 => [0, nil] }.freeze
13
+ ALU = { '+' => 0x00, '-' => 0x10, '*' => 0x20, '/' => 0x30, '|' => 0x40,
14
+ '&' => 0x50, '<<' => 0x60, '>>' => 0x70, '%' => 0x90, '^' => 0xa0 }.freeze
15
+
16
+ # @rbs (Integer linktype, Integer snaplen, bool live, String expression) -> void
17
+ def initialize(linktype, snaplen, live, expression)
18
+ @linktype = linktype
19
+ @snaplen = snaplen
20
+ @live = live
21
+ @expression = expression
22
+ @assembler = BPF::Assembler.new
23
+ @shift = 0
24
+ @dynamic_vlan = false
25
+ @vlan_count = 0
26
+ @network, @ethertype = LAYOUTS.fetch(linktype) { raise FilterError, "unsupported filter linktype #{linktype}" }
27
+ @cooked_live = live && linktype == 276
28
+ @network = 0 if @cooked_live
29
+ end
30
+
31
+ # @rbs (untyped ast) -> Program
32
+ def compile(ast)
33
+ if @live && @linktype == 1 && includes_vlan?(ast)
34
+ @assembler.emit(0x00, 0)
35
+ @assembler.emit(0x02, 15)
36
+ end
37
+ accepted = @assembler.label
38
+ rejected = @assembler.label
39
+ emit(ast, accepted, rejected)
40
+ @assembler.mark(accepted)
41
+ @assembler.emit(6, @snaplen)
42
+ @assembler.mark(rejected)
43
+ @assembler.emit(6, 0)
44
+ Program.new(@assembler.assemble, linktype: @linktype, cooked_live: @cooked_live)
45
+ end
46
+
47
+ private
48
+
49
+ # @rbs (untyped ast) -> bool
50
+ def includes_vlan?(ast)
51
+ return ast[3] == :vlan if ast[0] == :primitive
52
+ return includes_vlan?(ast[1]) if ast[0] == :not
53
+ return includes_vlan?(ast[1]) || includes_vlan?(ast[2]) if %i[and or].include?(ast[0])
54
+ false
55
+ end
56
+
57
+ # @rbs (untyped node, Symbol yes, Symbol no) -> void
58
+ def emit(node, yes, no)
59
+ case node[0]
60
+ when :true then @assembler.jump(yes)
61
+ when :false then @assembler.jump(no)
62
+ when :not then emit(node[1], no, yes)
63
+ when :and, :or
64
+ middle = @assembler.label
65
+ emit(node[1], node[0] == :and ? middle : yes, node[0] == :and ? no : middle)
66
+ @assembler.mark(middle)
67
+ emit(node[2], yes, no)
68
+ when :primitive
69
+ if node[3] == :vlan
70
+ vlan(node[4], yes, no)
71
+ else
72
+ emit(primitive(node), yes, no)
73
+ end
74
+ when :test
75
+ _, offset, size, operation, value, mode = node
76
+ load(offset, size, mode)
77
+ compare(operation, value, yes, no)
78
+ when :relation
79
+ guards = access_protocols(node[1]) + access_protocols(node[3])
80
+ guards.uniq.each do |protocol|
81
+ next if protocol == 'ether'
82
+ following = @assembler.label
83
+ guard = if %w[ip ip6 arp rarp].include?(protocol)
84
+ family(protocol)
85
+ elsif protocol == 'icmp6'
86
+ conjunction(family('ip6'), next_protocol(6, 58))
87
+ else
88
+ conjunction(family('ip'), next_protocol(4, Analyzer::PROTOCOL_NUMBERS.fetch(protocol)), fragment_guard)
89
+ end
90
+ emit(guard, following, no)
91
+ @assembler.mark(following)
92
+ end
93
+ arithmetic(node[1], 0)
94
+ if node[3][0] == :number
95
+ compare(node[2], node[3][1], yes, no)
96
+ else
97
+ @assembler.emit(2, 0)
98
+ arithmetic(node[3], 1)
99
+ @assembler.emit(7)
100
+ @assembler.emit(0x60, 0)
101
+ compare(node[2], 0, yes, no, true)
102
+ end
103
+ else raise FilterError, "invalid filter AST #{node[0]}"
104
+ end
105
+ end
106
+
107
+ # @rbs (untyped node) -> untyped
108
+ def primitive(node)
109
+ _, proto, direction, type, value, position = node
110
+ case type
111
+ when :protocol
112
+ return @linktype == 1 ? [:true] : [:false] if proto == 'ether'
113
+ return family(proto) if %w[ip ip6 arp rarp].include?(proto)
114
+ number = Analyzer::PROTOCOL_NUMBERS.fetch(proto)
115
+ return protocol(number, proto == 'icmp' || proto == 'igmp' ? [4] : proto == 'icmp6' ? [6] : [4, 6])
116
+ when :proto
117
+ return test(0xfffff000, 4, :eq, value, :absolute) if proto == 'ether' && @cooked_live
118
+ return test(@ethertype.to_i + @shift, 2, :eq, value) if proto == 'ether' && @ethertype && ![0, 108].include?(@linktype)
119
+ error('ether proto is unavailable for this linktype', position) if proto == 'ether'
120
+ versions = proto == 'ip' ? [4] : proto == 'ip6' ? [6] : [4, 6]
121
+ return protocol(value, versions)
122
+ when :host, :net
123
+ return ethernet_host(value.first, direction, position) if proto == 'ether'
124
+ return addresses(value, proto, direction)
125
+ when :port, :portrange then ports(value, direction)
126
+ when :greater, :less then [:relation, [:len], type == :greater ? '>=' : '<=', [:number, value]]
127
+ when :broadcast, :multicast then broadcast(proto, type, position)
128
+ else raise FilterError, "unsupported filter primitive #{type}"
129
+ end
130
+ end
131
+
132
+ # @rbs (String name) -> untyped
133
+ def family(name)
134
+ value = Analyzer::ETHER_TYPES.fetch(name)
135
+ return test(0xfffff000, 4, :eq, value, :absolute) if @cooked_live
136
+ if @ethertype && ![0, 108].include?(@linktype)
137
+ test(@ethertype + @shift, 2, :eq, value)
138
+ elsif [0, 108].include?(@linktype)
139
+ return [:false] unless %w[ip ip6].include?(name)
140
+ values = name == 'ip' ? [2] : [24, 28, 30]
141
+ if @linktype.zero?
142
+ values.map! do |v|
143
+ word = [v].pack('L').unpack1('N') #: Integer
144
+ word
145
+ end
146
+ end
147
+ disjunction(*values.map { |v| test(0, 4, :eq, v, :absolute) })
148
+ elsif name == 'ip' || name == 'ip6'
149
+ version = name == 'ip' ? 4 : 6
150
+ return [:false] if (@linktype == 228 && version != 4) || (@linktype == 229 && version != 6)
151
+ [:relation, [:binary, '&', [:read, 0, 1, :absolute], [:number, 0xf0]], '=', [:number, version << 4]]
152
+ else
153
+ [:false]
154
+ end
155
+ end
156
+
157
+ # @rbs (Integer number, Array[Integer] versions) -> untyped
158
+ def protocol(number, versions)
159
+ disjunction(*versions.map do |version|
160
+ guard = next_protocol(version, number)
161
+ if version == 6
162
+ fragment = conjunction(next_protocol(6, 44), test(network + 40, 1, :eq, number))
163
+ guard = disjunction(guard, fragment)
164
+ end
165
+ conjunction(family(version == 4 ? 'ip' : 'ip6'), guard)
166
+ end)
167
+ end
168
+
169
+ # @rbs (Integer version, Integer number) -> untyped
170
+ def next_protocol(version, number) = test(network + (version == 4 ? 9 : 6), 1, :eq, number)
171
+
172
+ # @rbs () -> untyped
173
+ def fragment_guard = test(network + 6, 2, :unset, 0x1fff)
174
+
175
+ # @rbs () -> Integer
176
+ def network = @network + @shift
177
+
178
+ # @rbs (Integer offset, Integer size, Symbol operation, Integer value, ?Symbol? mode) -> untyped
179
+ def test(offset, size, operation, value, mode = nil) = [:test, offset, size, operation, value, mode]
180
+
181
+ # @rbs (*untyped nodes) -> untyped
182
+ def conjunction(*nodes) = nodes.reduce { |left, right| [:and, left, right] } || [:true]
183
+
184
+ # @rbs (*untyped nodes) -> untyped
185
+ def disjunction(*nodes) = nodes.reduce { |left, right| [:or, left, right] } || [:false]
186
+
187
+ # @rbs (Symbol direction, untyped src, untyped dst) -> untyped
188
+ def directional(direction, src, dst)
189
+ return src if direction == :src
190
+ return dst if direction == :dst
191
+ direction == :both ? conjunction(src, dst) : disjunction(src, dst)
192
+ end
193
+
194
+ # @rbs (untyped values, String? proto, Symbol direction) -> untyped
195
+ def addresses(values, proto, direction)
196
+ disjunction(*values.map do |version, address, mask|
197
+ families = proto ? [proto] : version == 4 ? %w[ip arp rarp] : ['ip6']
198
+ disjunction(*families.map do |name|
199
+ src = network + (version == 6 ? 8 : %w[arp rarp].include?(name) ? 14 : 12)
200
+ dst = network + (version == 6 ? 24 : %w[arp rarp].include?(name) ? 24 : 16)
201
+ conjunction(family(name), directional(direction, address_test(src, version, address, mask), address_test(dst, version, address, mask)))
202
+ end)
203
+ end)
204
+ end
205
+
206
+ # @rbs (Integer offset, Integer version, Integer address, Integer mask) -> untyped
207
+ def address_test(offset, version, address, mask)
208
+ count = version == 6 ? 4 : 1
209
+ conjunction(*(0...count).filter_map do |index|
210
+ shift = (count - index - 1) * 32
211
+ word_mask = (mask >> shift) & 0xffffffff
212
+ next if word_mask.zero?
213
+ expected = (address >> shift) & word_mask
214
+ read = [:read, offset + index * 4, 4, nil]
215
+ read = [:binary, '&', read, [:number, word_mask]] unless word_mask == 0xffffffff
216
+ [:relation, read, '=', [:number, expected]]
217
+ end)
218
+ end
219
+
220
+ # @rbs (String mac, Symbol direction, Integer position) -> untyped
221
+ def ethernet_host(mac, direction, position)
222
+ error('Ethernet addresses are unavailable for this linktype', position) unless @linktype == 1
223
+ parts = mac.unpack('Nn') #: [Integer, Integer]
224
+ src = conjunction(test(6, 4, :eq, parts[0], :absolute), test(10, 2, :eq, parts[1], :absolute))
225
+ dst = conjunction(test(0, 4, :eq, parts[0], :absolute), test(4, 2, :eq, parts[1], :absolute))
226
+ directional(direction, src, dst)
227
+ end
228
+
229
+ # @rbs (untyped ranges, Symbol direction) -> untyped
230
+ def ports(ranges, direction)
231
+ disjunction(*(ranges.flat_map do |number, limits|
232
+ [4, 6].map do |version|
233
+ guard = conjunction(family(version == 4 ? 'ip' : 'ip6'), next_protocol(version, number))
234
+ guard = conjunction(guard, fragment_guard) if version == 4
235
+ src = port_test(version, 0, limits)
236
+ dst = port_test(version, 2, limits)
237
+ conjunction(guard, directional(direction, src, dst))
238
+ end
239
+ end))
240
+ end
241
+
242
+ # @rbs (Integer version, Integer offset, [Integer, Integer] limits) -> untyped
243
+ def port_test(version, offset, limits)
244
+ mode = version == 4 ? :transport : nil
245
+ location = network + (version == 4 ? offset : 40 + offset)
246
+ low, high = limits
247
+ return test(location, 2, :eq, low, mode) if low == high
248
+ conjunction(test(location, 2, :ge, low, mode), test(location, 2, :le, high, mode))
249
+ end
250
+
251
+ # @rbs (String? proto, Symbol type, Integer position) -> untyped
252
+ def broadcast(proto, type, position)
253
+ if !proto || proto == 'ether'
254
+ error('link broadcast/multicast requires Ethernet', position) unless @linktype == 1
255
+ return type == :broadcast ? conjunction(test(0, 4, :eq, 0xffffffff, :absolute), test(4, 2, :eq, 0xffff, :absolute)) : test(0, 1, :set, 1, :absolute)
256
+ end
257
+ if proto == 'ip6'
258
+ return conjunction(family('ip6'), test(network + 24, 1, :eq, 0xff))
259
+ end
260
+ condition = type == :broadcast ? disjunction(test(network + 16, 4, :eq, 0), test(network + 16, 4, :eq, 0xffffffff)) : test(network + 16, 1, :ge, 0xe0)
261
+ conjunction(family('ip'), condition)
262
+ end
263
+
264
+ # @rbs (Integer? id, Symbol yes, Symbol no) -> void
265
+ def vlan(id, yes, no)
266
+ error('VLAN filtering requires Ethernet', 0) unless @linktype == 1
267
+ tag_offset = 12 + @shift
268
+ condition = disjunction(*[0x8100, 0x88a8, 0x9100].map { |type| test(tag_offset, 2, :eq, type) })
269
+ condition = conjunction(condition, [:relation, [:binary, '&', [:read, tag_offset + 2, 2, nil], [:number, 0xfff]], '=', [:number, id]]) if id
270
+ if @live && @vlan_count.zero?
271
+ inline = @assembler.label
272
+ metadata = @assembler.label
273
+ @assembler.emit(0x20, 0xfffff030)
274
+ @assembler.branch(0x15, 1, metadata, inline)
275
+ @assembler.mark(metadata)
276
+ if id
277
+ @assembler.emit(0x20, 0xfffff02c)
278
+ @assembler.emit(0x54, 0xfff)
279
+ @assembler.branch(0x15, id, yes, no)
280
+ else
281
+ @assembler.jump(yes)
282
+ end
283
+ @assembler.mark(inline)
284
+ @assembler.emit(0x00, 4)
285
+ @assembler.emit(0x02, 15)
286
+ emit(condition, yes, no)
287
+ @dynamic_vlan = true
288
+ elsif @dynamic_vlan
289
+ matched = @assembler.label
290
+ emit(condition, matched, no)
291
+ @assembler.mark(matched)
292
+ @assembler.emit(0x60, 15)
293
+ @assembler.emit(0x04, 4)
294
+ @assembler.emit(0x02, 15)
295
+ @assembler.jump(yes)
296
+ else
297
+ emit(condition, yes, no)
298
+ @shift += 4
299
+ end
300
+ @vlan_count += 1
301
+ end
302
+
303
+ # @rbs (Integer offset, Integer size, Symbol? mode) -> void
304
+ def load(offset, size, mode)
305
+ opcode = { 4 => 0x20, 2 => 0x28, 1 => 0x30 }.fetch(size)
306
+ if mode == :transport
307
+ transport_index
308
+ @assembler.emit(opcode + 0x20, offset)
309
+ elsif @dynamic_vlan && mode != :absolute
310
+ @assembler.emit(0x61, 15)
311
+ @assembler.emit(opcode + 0x20, offset)
312
+ else
313
+ @assembler.emit(opcode, offset)
314
+ end
315
+ end
316
+
317
+ # @rbs () -> void
318
+ def transport_index
319
+ if @dynamic_vlan
320
+ @assembler.emit(0x61, 15)
321
+ @assembler.emit(0x50, network)
322
+ @assembler.emit(0x54, 15)
323
+ @assembler.emit(0x64, 2)
324
+ @assembler.emit(0x0c)
325
+ @assembler.emit(7)
326
+ else
327
+ @assembler.emit(0xb1, network)
328
+ end
329
+ end
330
+
331
+ # @rbs (untyped operation, Integer value, Symbol yes, Symbol no, ?bool from_x) -> void
332
+ def compare(operation, value, yes, no, from_x = false)
333
+ code = case operation.to_s
334
+ when 'eq', '=', '==' then 0x15
335
+ when 'ne', '!=' then yes, no = no, yes; 0x15
336
+ when 'gt', '>' then 0x25
337
+ when 'ge', '>=' then 0x35
338
+ when 'lt', '<' then yes, no = no, yes; 0x35
339
+ when 'le', '<=' then yes, no = no, yes; 0x25
340
+ when 'set' then 0x45
341
+ when 'unset' then yes, no = no, yes; 0x45
342
+ else raise FilterError, "unknown comparison #{operation}"
343
+ end
344
+ @assembler.branch(code | (from_x ? 8 : 0), value, yes, no)
345
+ end
346
+
347
+ # @rbs (untyped node) -> Array[String]
348
+ def access_protocols(node)
349
+ case node[0]
350
+ when :access then [node[1]] + access_protocols(node[2])
351
+ when :binary then access_protocols(node[2]) + access_protocols(node[3])
352
+ when :neg then access_protocols(node[1])
353
+ else []
354
+ end
355
+ end
356
+
357
+ # @rbs (untyped node, Integer depth) -> void
358
+ def arithmetic(node, depth)
359
+ error('arithmetic expression requires more than 14 scratch registers', 0) if depth >= 14
360
+ case node[0]
361
+ when :number then @assembler.emit(0x00, node[1])
362
+ when :len
363
+ @assembler.emit(0x80)
364
+ @assembler.emit(0x04, 20) if @cooked_live
365
+ when :read then load(node[1], node[2], node[3])
366
+ when :neg then arithmetic(node[1], depth); @assembler.emit(0x84)
367
+ when :binary
368
+ arithmetic(node[2], depth)
369
+ if node[3][0] == :number
370
+ @assembler.emit(ALU.fetch(node[1]) | 4, node[3][1])
371
+ else
372
+ @assembler.emit(2, depth)
373
+ arithmetic(node[3], depth + 1)
374
+ @assembler.emit(7)
375
+ @assembler.emit(0x60, depth)
376
+ @assembler.emit(ALU.fetch(node[1]) | 12)
377
+ end
378
+ when :access then accessor(node, depth)
379
+ else raise FilterError, "invalid arithmetic AST #{node[0]}"
380
+ end
381
+ end
382
+
383
+ # @rbs (untyped node, Integer depth) -> void
384
+ def accessor(node, depth)
385
+ _, protocol, offset, size = node
386
+ transport = !%w[ether ip ip6 arp rarp icmp6].include?(protocol)
387
+ base = protocol == 'ether' ? 0 : network + (protocol == 'icmp6' ? 40 : 0)
388
+ if offset[0] == :number
389
+ load(base + offset[1], size, transport ? :transport : protocol == 'ether' ? :absolute : nil)
390
+ else
391
+ arithmetic(offset, depth)
392
+ @assembler.emit(2, depth)
393
+ if transport
394
+ transport_index
395
+ elsif @dynamic_vlan && protocol != 'ether'
396
+ @assembler.emit(0x61, 15)
397
+ else
398
+ @assembler.emit(0x01, 0)
399
+ end
400
+ @assembler.emit(0x60, depth)
401
+ @assembler.emit(0x0c)
402
+ @assembler.emit(7)
403
+ @assembler.emit({ 4 => 0x40, 2 => 0x48, 1 => 0x50 }.fetch(size), base)
404
+ end
405
+ end
406
+
407
+ # @rbs (String message, Integer position) -> bot
408
+ def error(message, position)
409
+ raise FilterSyntaxError.new(message, expression: @expression, position: position)
410
+ end
411
+ end
412
+ end
413
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Filter
7
+ # @api private
8
+ class Lexer
9
+ # @rbs (String expression) -> void
10
+ def initialize(expression)
11
+ @expression = expression
12
+ end
13
+
14
+ # @rbs () -> Array[[String, Integer]]
15
+ def tokens
16
+ result = [] #: Array[[String, Integer]]
17
+ position = 0
18
+ brackets = 0
19
+ while position < @expression.length
20
+ tail = @expression[position..]
21
+ if (space = /\A\s+/.match(tail))
22
+ position += space[0].length
23
+ next
24
+ end
25
+ patterns = [ /\A(?:&&|\|\||!=|==|>=|<=|<<|>>)/ ]
26
+ if brackets.zero?
27
+ patterns += [ /\A(?:[a-fA-F0-9]{1,2}:){5}[a-fA-F0-9]{1,2}/,
28
+ /\A(?=[a-fA-F0-9:.]*:)[a-fA-F0-9:.]+(?:\/\d+)?/,
29
+ /\A\d+(?:\.\d+){1,3}(?:\/\d+)?/ ]
30
+ end
31
+ patterns += [ /\A(?:0[xX][\da-fA-F]+|\d+)/,
32
+ /\A\\?[a-zA-Z_][a-zA-Z0-9_.-]*/, /\A[()\[\]:+*\/%&|^!<>=-]/ ]
33
+ token = patterns.filter_map { |pattern| pattern.match(tail)&.[](0) }.first
34
+ unless token
35
+ raise FilterSyntaxError.new("unexpected character #{tail[0].inspect}", expression: @expression, position: position)
36
+ end
37
+ result << [token, position]
38
+ brackets += 1 if token == '['
39
+ brackets -= 1 if token == ']'
40
+ position += token.length
41
+ end
42
+ result << ['', @expression.length]
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Filter
7
+ # ASTs stay private; primitive operands are resolved before code generation.
8
+ # @api private
9
+ class Parser
10
+ PROTOCOLS = %w[ether ip ip6 arp rarp tcp udp sctp icmp icmp6 igmp].freeze
11
+ TYPES = %w[host net port portrange proto].freeze
12
+ COMPARISONS = %w[= == != > >= < <=].freeze
13
+ PRECEDENCE = { '|' => 1, '^' => 2, '&' => 3, '<<' => 4, '>>' => 4, '+' => 5, '-' => 5, '*' => 6, '/' => 6, '%' => 6 }.freeze
14
+ CONSTANTS = { 'tcpflags' => 13, 'tcp-fin' => 1, 'tcp-syn' => 2, 'tcp-rst' => 4,
15
+ 'tcp-push' => 8, 'tcp-ack' => 16, 'tcp-urg' => 32, 'tcp-ece' => 64, 'tcp-cwr' => 128,
16
+ 'icmptype' => 0, 'icmpcode' => 1, 'icmp-echoreply' => 0, 'icmp-echo' => 8,
17
+ 'icmp-unreach' => 3, 'icmp-timxceed' => 11 }.freeze
18
+
19
+ # @rbs (String expression) -> void
20
+ def initialize(expression)
21
+ @expression = expression
22
+ @tokens = Lexer.new(expression).tokens
23
+ @index = 0
24
+ @qualifier = nil #: untyped
25
+ end
26
+
27
+ # @rbs () -> untyped
28
+ def parse
29
+ return [:true] if peek.empty?
30
+ ast = expression
31
+ error("unexpected token #{peek.inspect}") unless peek.empty?
32
+ ast
33
+ end
34
+
35
+ private
36
+
37
+ # libpcap gives AND and OR the same precedence, left associative.
38
+ # @rbs () -> untyped
39
+ def expression
40
+ node = unary
41
+ while %w[and && or ||].include?(peek)
42
+ operation = %w[and &&].include?(take) ? :and : :or
43
+ node = [operation, node, unary]
44
+ end
45
+ node
46
+ end
47
+
48
+ # @rbs () -> untyped
49
+ def unary
50
+ if %w[not !].include?(peek)
51
+ take
52
+ return [:not, unary]
53
+ end
54
+ if peek == '(' || peek == '-' || numeric?(peek) || CONSTANTS.key?(peek)
55
+ saved_index = @index
56
+ begin
57
+ return relation
58
+ rescue FilterSyntaxError
59
+ @index = saved_index
60
+ end
61
+ if peek == '('
62
+ take
63
+ node = expression
64
+ expect(')')
65
+ return node
66
+ end
67
+ end
68
+ return relation if peek == 'len' || @tokens[@index + 1]&.first == '['
69
+
70
+ primitive
71
+ end
72
+
73
+ # @rbs () -> untyped
74
+ def primitive
75
+ position = @tokens[@index][1]
76
+ if %w[vlan greater less broadcast multicast].include?(peek)
77
+ kind = take.to_sym
78
+ value = %i[broadcast multicast].include?(kind) || (kind == :vlan && !numeric?(peek)) ? nil : number
79
+ @qualifier = nil
80
+ return [:primitive, nil, :either, kind, value, position]
81
+ end
82
+ proto = PROTOCOLS.include?(peek) ? take : nil
83
+ if proto && %w[broadcast multicast].include?(peek)
84
+ @qualifier = nil
85
+ return [:primitive, proto, :either, take.to_sym, nil, position]
86
+ end
87
+ direction = nil
88
+ if %w[src dst].include?(peek)
89
+ direction = take.to_sym
90
+ if %w[and or].include?(peek) && %w[src dst].include?(@tokens[@index + 1]&.first)
91
+ operation = take
92
+ other = take
93
+ error('direction must combine src and dst') if other == direction.to_s
94
+ direction = operation == 'and' ? :both : :either
95
+ end
96
+ end
97
+ type = TYPES.include?(peek) ? take.to_sym : nil
98
+ if proto && !direction && !type
99
+ @qualifier = nil
100
+ return [:primitive, proto, :either, :protocol, nil, position]
101
+ end
102
+ explicit = proto || direction || type
103
+ qualifier = explicit ? [proto, direction || :either, type || :host] : @qualifier
104
+ qualifier ||= [nil, :either, :host]
105
+ @qualifier = qualifier
106
+ if peek == '('
107
+ take
108
+ node = expression
109
+ expect(')')
110
+ return node
111
+ end
112
+ value = take
113
+ error('expected a filter operand', position) if value.empty? || %w[and or && || )].include?(value)
114
+ if qualifier[2] == :portrange && peek == '-'
115
+ take
116
+ value += '-' + take
117
+ elsif qualifier[2] == :net && peek == 'mask'
118
+ take
119
+ value += ' mask ' + take
120
+ end
121
+ [:primitive, *qualifier, value, position]
122
+ end
123
+
124
+ # @rbs () -> untyped
125
+ def relation
126
+ left = arithmetic
127
+ error('expected an arithmetic comparison') unless COMPARISONS.include?(peek)
128
+ operator = take
129
+ right = arithmetic
130
+ @qualifier = nil
131
+ [:relation, left, operator, right]
132
+ end
133
+
134
+ # @rbs (?Integer minimum) -> untyped
135
+ def arithmetic(minimum = 0)
136
+ position = @tokens[@index][1]
137
+ value = take
138
+ node = if value == '('
139
+ expression = arithmetic
140
+ expect(')')
141
+ expression
142
+ elsif value == '-'
143
+ [:neg, arithmetic(7)]
144
+ elsif value == 'len'
145
+ [:len]
146
+ elsif CONSTANTS.key?(value)
147
+ [:number, CONSTANTS.fetch(value)]
148
+ elsif numeric?(value)
149
+ [:number, integer(value)]
150
+ elsif PROTOCOLS.include?(value) && peek == '['
151
+ take
152
+ offset = arithmetic
153
+ size = peek == ':' ? (take; number) : 1
154
+ expect(']')
155
+ error('byte accessor width must be 1, 2 or 4', position) unless [1, 2, 4].include?(size)
156
+ [:access, value, offset, size]
157
+ else
158
+ error("invalid arithmetic operand #{value.inspect}", position)
159
+ end
160
+ while PRECEDENCE.fetch(peek, -1) >= minimum
161
+ operation = take
162
+ node = [:binary, operation, node, arithmetic(PRECEDENCE.fetch(operation) + 1)]
163
+ end
164
+ node
165
+ end
166
+
167
+ # @rbs (String value) -> bool
168
+ def numeric?(value) = /\A(?:0[xX][\da-fA-F]+|\d+)\z/.match?(value)
169
+
170
+ # @rbs (String value) -> Integer
171
+ def integer(value)
172
+ Integer(value, value.start_with?('0') && value.length > 1 ? 0 : 10)
173
+ rescue ArgumentError
174
+ error("invalid integer #{value.inspect}")
175
+ end
176
+
177
+ # @rbs () -> Integer
178
+ def number
179
+ value = take
180
+ error("expected a number, got #{value.inspect}") unless numeric?(value)
181
+ integer(value)
182
+ end
183
+
184
+ # @rbs () -> String
185
+ def peek = @tokens[@index][0]
186
+
187
+ # @rbs () -> String
188
+ def take
189
+ token = @tokens[@index][0]
190
+ @index += 1 unless token.empty?
191
+ token
192
+ end
193
+
194
+ # @rbs (String value) -> void
195
+ def expect(value)
196
+ error("expected #{value.inspect}, got #{peek.inspect}") unless peek == value
197
+ take
198
+ end
199
+
200
+ # @rbs (String message, ?Integer? position) -> bot
201
+ def error(message, position = nil)
202
+ raise FilterSyntaxError.new(message, expression: @expression, position: position || @tokens[@index][1])
203
+ end
204
+ end
205
+ end
206
+ end