redhound 1.0.0 → 2.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/README.md +97 -31
  4. data/docs/API.md +76 -0
  5. data/docs/FILTERS.md +37 -0
  6. data/docs/MIGRATION.md +23 -0
  7. data/docs/PROTOCOLS.md +45 -0
  8. data/docs/USAGE.md +82 -0
  9. data/docs/VALIDATION.md +59 -0
  10. data/docs/json-schema.json +64 -0
  11. data/exe/redhound +2 -1
  12. data/lib/redhound/analysis/flow_key.rb +53 -0
  13. data/lib/redhound/analysis/flow_table.rb +117 -0
  14. data/lib/redhound/analysis/ip_reassembler.rb +176 -0
  15. data/lib/redhound/analysis/stats.rb +138 -0
  16. data/lib/redhound/analysis/tcp_analysis.rb +43 -0
  17. data/lib/redhound/analysis/tcp_reassembler.rb +124 -0
  18. data/lib/redhound/analysis/tcp_stream.rb +151 -0
  19. data/lib/redhound/analysis.rb +129 -0
  20. data/lib/redhound/capture/bsd/bpf_device.rb +152 -0
  21. data/lib/redhound/capture/bsd/constants.rb +29 -0
  22. data/lib/redhound/capture/file_source.rb +83 -0
  23. data/lib/redhound/capture/interface.rb +66 -0
  24. data/lib/redhound/capture/linktype.rb +30 -0
  25. data/lib/redhound/capture/linux/constants.rb +37 -0
  26. data/lib/redhound/capture/linux/cooked.rb +18 -0
  27. data/lib/redhound/capture/linux/ifreq.rb +42 -0
  28. data/lib/redhound/capture/linux/packet_socket.rb +180 -0
  29. data/lib/redhound/capture/linux/sockaddr_ll.rb +33 -0
  30. data/lib/redhound/capture/linux/tpacket_v3.rb +132 -0
  31. data/lib/redhound/capture/source.rb +79 -0
  32. data/lib/redhound/capture/stats.rb +22 -0
  33. data/lib/redhound/capture.rb +69 -0
  34. data/lib/redhound/cli/command.rb +42 -0
  35. data/lib/redhound/cli/options.rb +133 -0
  36. data/lib/redhound/cli/privileges.rb +29 -0
  37. data/lib/redhound/cli/runner.rb +129 -0
  38. data/lib/redhound/context.rb +20 -0
  39. data/lib/redhound/cursor.rb +69 -0
  40. data/lib/redhound/diagnostic.rb +7 -0
  41. data/lib/redhound/dissector.rb +138 -0
  42. data/lib/redhound/engine.rb +66 -0
  43. data/lib/redhound/errors.rb +38 -0
  44. data/lib/redhound/field.rb +62 -0
  45. data/lib/redhound/file/format.rb +56 -0
  46. data/lib/redhound/file/pcap_reader.rb +43 -0
  47. data/lib/redhound/file/pcap_writer.rb +72 -0
  48. data/lib/redhound/file/pcapng_reader.rb +209 -0
  49. data/lib/redhound/file/pcapng_writer.rb +143 -0
  50. data/lib/redhound/file/rotating_writer.rb +98 -0
  51. data/lib/redhound/filter/analyzer.rb +197 -0
  52. data/lib/redhound/filter/bpf/assembler.rb +121 -0
  53. data/lib/redhound/filter/bpf/disassembler.rb +58 -0
  54. data/lib/redhound/filter/bpf/validator.rb +77 -0
  55. data/lib/redhound/filter/bpf/vm.rb +108 -0
  56. data/lib/redhound/filter/codegen.rb +413 -0
  57. data/lib/redhound/filter/lexer.rb +46 -0
  58. data/lib/redhound/filter/parser.rb +206 -0
  59. data/lib/redhound/filter/program.rb +55 -0
  60. data/lib/redhound/filter.rb +30 -0
  61. data/lib/redhound/layer.rb +75 -0
  62. data/lib/redhound/output/fields.rb +20 -0
  63. data/lib/redhound/output/follow.rb +59 -0
  64. data/lib/redhound/output/hexdump.rb +29 -0
  65. data/lib/redhound/output/json.rb +32 -0
  66. data/lib/redhound/output/summary.rb +59 -0
  67. data/lib/redhound/output/timestamp.rb +34 -0
  68. data/lib/redhound/output/tree.rb +24 -0
  69. data/lib/redhound/packet.rb +63 -0
  70. data/lib/redhound/protocols/arp.rb +39 -0
  71. data/lib/redhound/protocols/checksum.rb +38 -0
  72. data/lib/redhound/protocols/data.rb +21 -0
  73. data/lib/redhound/protocols/dhcp.rb +124 -0
  74. data/lib/redhound/protocols/dns.rb +268 -0
  75. data/lib/redhound/protocols/ethernet.rb +24 -0
  76. data/lib/redhound/protocols/http.rb +183 -0
  77. data/lib/redhound/protocols/icmp.rb +191 -0
  78. data/lib/redhound/protocols/ipv4.rb +56 -0
  79. data/lib/redhound/protocols/ipv6.rb +65 -0
  80. data/lib/redhound/protocols/link.rb +70 -0
  81. data/lib/redhound/protocols/llc.rb +37 -0
  82. data/lib/redhound/protocols/ntp.rb +47 -0
  83. data/lib/redhound/protocols/tcp.rb +80 -0
  84. data/lib/redhound/protocols/tls.rb +218 -0
  85. data/lib/redhound/protocols/tunnel.rb +48 -0
  86. data/lib/redhound/protocols/udp.rb +32 -0
  87. data/lib/redhound/protocols/vlan.rb +23 -0
  88. data/lib/redhound/reader.rb +8 -0
  89. data/lib/redhound/registry.rb +64 -0
  90. data/lib/redhound/stream_dissector.rb +168 -0
  91. data/lib/redhound/util/seq.rb +21 -0
  92. data/lib/redhound/version.rb +2 -1
  93. data/lib/redhound/writer.rb +25 -43
  94. data/lib/redhound.rb +81 -9
  95. data/sig/generated/redhound/analysis/flow_key.rbs +15 -0
  96. data/sig/generated/redhound/analysis/flow_table.rbs +98 -0
  97. data/sig/generated/redhound/analysis/ip_reassembler.rbs +40 -0
  98. data/sig/generated/redhound/analysis/stats.rbs +77 -0
  99. data/sig/generated/redhound/analysis/tcp_analysis.rbs +15 -0
  100. data/sig/generated/redhound/analysis/tcp_reassembler.rbs +27 -0
  101. data/sig/generated/redhound/analysis/tcp_stream.rbs +60 -0
  102. data/sig/generated/redhound/analysis.rbs +38 -0
  103. data/sig/generated/redhound/capture/bsd/bpf_device.rbs +39 -0
  104. data/sig/generated/redhound/capture/bsd/constants.rbs +40 -0
  105. data/sig/generated/redhound/capture/file_source.rbs +34 -0
  106. data/sig/generated/redhound/capture/interface.rbs +53 -0
  107. data/sig/generated/redhound/capture/linktype.rbs +20 -0
  108. data/sig/generated/redhound/capture/linux/constants.rbs +58 -0
  109. data/sig/generated/redhound/capture/linux/cooked.rbs +15 -0
  110. data/sig/generated/redhound/capture/linux/ifreq.rbs +18 -0
  111. data/sig/generated/redhound/capture/linux/packet_socket.rbs +57 -0
  112. data/sig/generated/redhound/capture/linux/sockaddr_ll.rbs +31 -0
  113. data/sig/generated/redhound/capture/linux/tpacket_v3.rbs +35 -0
  114. data/sig/generated/redhound/capture/source.rbs +44 -0
  115. data/sig/generated/redhound/capture/stats.rbs +25 -0
  116. data/sig/generated/redhound/capture.rbs +14 -0
  117. data/sig/generated/redhound/cli/command.rbs +12 -0
  118. data/sig/generated/redhound/cli/options.rbs +25 -0
  119. data/sig/generated/redhound/cli/privileges.rbs +12 -0
  120. data/sig/generated/redhound/cli/runner.rbs +27 -0
  121. data/sig/generated/redhound/context.rbs +27 -0
  122. data/sig/generated/redhound/cursor.rbs +59 -0
  123. data/sig/generated/redhound/diagnostic.rbs +21 -0
  124. data/sig/generated/redhound/dissector.rbs +113 -0
  125. data/sig/generated/redhound/engine.rbs +23 -0
  126. data/sig/generated/redhound/errors.rbs +56 -0
  127. data/sig/generated/redhound/field.rbs +74 -0
  128. data/sig/generated/redhound/file/format.rbs +33 -0
  129. data/sig/generated/redhound/file/pcap_reader.rbs +19 -0
  130. data/sig/generated/redhound/file/pcap_writer.rbs +38 -0
  131. data/sig/generated/redhound/file/pcapng_reader.rbs +54 -0
  132. data/sig/generated/redhound/file/pcapng_writer.rbs +50 -0
  133. data/sig/generated/redhound/file/rotating_writer.rbs +42 -0
  134. data/sig/generated/redhound/filter/analyzer.rbs +42 -0
  135. data/sig/generated/redhound/filter/bpf/assembler.rbs +39 -0
  136. data/sig/generated/redhound/filter/bpf/disassembler.rbs +18 -0
  137. data/sig/generated/redhound/filter/bpf/validator.rbs +30 -0
  138. data/sig/generated/redhound/filter/bpf/vm.rbs +26 -0
  139. data/sig/generated/redhound/filter/codegen.rbs +101 -0
  140. data/sig/generated/redhound/filter/lexer.rbs +15 -0
  141. data/sig/generated/redhound/filter/parser.rbs +65 -0
  142. data/sig/generated/redhound/filter/program.rbs +48 -0
  143. data/sig/generated/redhound/filter.rbs +10 -0
  144. data/sig/generated/redhound/layer.rbs +81 -0
  145. data/sig/generated/redhound/output/fields.rbs +15 -0
  146. data/sig/generated/redhound/output/follow.rbs +21 -0
  147. data/sig/generated/redhound/output/hexdump.rbs +15 -0
  148. data/sig/generated/redhound/output/json.rbs +18 -0
  149. data/sig/generated/redhound/output/summary.rbs +21 -0
  150. data/sig/generated/redhound/output/timestamp.rbs +21 -0
  151. data/sig/generated/redhound/output/tree.rbs +15 -0
  152. data/sig/generated/redhound/packet.rbs +77 -0
  153. data/sig/generated/redhound/protocols/arp.rbs +15 -0
  154. data/sig/generated/redhound/protocols/checksum.rbs +15 -0
  155. data/sig/generated/redhound/protocols/data.rbs +15 -0
  156. data/sig/generated/redhound/protocols/dhcp.rbs +28 -0
  157. data/sig/generated/redhound/protocols/dns.rbs +42 -0
  158. data/sig/generated/redhound/protocols/ethernet.rbs +15 -0
  159. data/sig/generated/redhound/protocols/http.rbs +36 -0
  160. data/sig/generated/redhound/protocols/icmp.rbs +41 -0
  161. data/sig/generated/redhound/protocols/ipv4.rbs +15 -0
  162. data/sig/generated/redhound/protocols/ipv6.rbs +24 -0
  163. data/sig/generated/redhound/protocols/link.rbs +33 -0
  164. data/sig/generated/redhound/protocols/llc.rbs +15 -0
  165. data/sig/generated/redhound/protocols/ntp.rbs +17 -0
  166. data/sig/generated/redhound/protocols/tcp.rbs +25 -0
  167. data/sig/generated/redhound/protocols/tls.rbs +51 -0
  168. data/sig/generated/redhound/protocols/tunnel.rbs +24 -0
  169. data/sig/generated/redhound/protocols/udp.rbs +18 -0
  170. data/sig/generated/redhound/protocols/vlan.rbs +12 -0
  171. data/sig/generated/redhound/reader.rbs +7 -0
  172. data/sig/generated/redhound/registry.rbs +45 -0
  173. data/sig/generated/redhound/stream_dissector.rbs +44 -0
  174. data/sig/generated/redhound/util/seq.rbs +20 -0
  175. data/sig/generated/redhound/version.rbs +1 -0
  176. data/sig/generated/redhound/writer.rbs +6 -19
  177. data/sig/generated/redhound.rbs +18 -0
  178. data/sig/shims/io.rbs +7 -0
  179. data/sig/shims/process.rbs +3 -0
  180. metadata +175 -55
  181. data/CODE_OF_CONDUCT.md +0 -132
  182. data/Dockerfile +0 -7
  183. data/Rakefile +0 -15
  184. data/Steepfile +0 -4
  185. data/docker-compose.yml +0 -9
  186. data/lib/redhound/analyzer.rb +0 -34
  187. data/lib/redhound/builder/packet_mreq.rb +0 -56
  188. data/lib/redhound/builder/socket.rb +0 -35
  189. data/lib/redhound/builder.rb +0 -4
  190. data/lib/redhound/command.rb +0 -67
  191. data/lib/redhound/l2/ether.rb +0 -68
  192. data/lib/redhound/l2/protocol.rb +0 -33
  193. data/lib/redhound/l2.rb +0 -4
  194. data/lib/redhound/l3/arp.rb +0 -114
  195. data/lib/redhound/l3/base.rb +0 -49
  196. data/lib/redhound/l3/ipv4.rb +0 -95
  197. data/lib/redhound/l3/ipv6.rb +0 -69
  198. data/lib/redhound/l3/protocol.rb +0 -173
  199. data/lib/redhound/l3/resolver.rb +0 -30
  200. data/lib/redhound/l3.rb +0 -8
  201. data/lib/redhound/l4/base.rb +0 -31
  202. data/lib/redhound/l4/icmp.rb +0 -71
  203. data/lib/redhound/l4/resolver.rb +0 -28
  204. data/lib/redhound/l4/udp.rb +0 -63
  205. data/lib/redhound/l4.rb +0 -6
  206. data/lib/redhound/receiver.rb +0 -45
  207. data/lib/redhound/resolver.rb +0 -22
  208. data/lib/redhound/source/socket.rb +0 -18
  209. data/lib/redhound/source.rb +0 -3
  210. data/rbs_collection.lock.yaml +0 -20
  211. data/rbs_collection.yaml +0 -17
  212. data/sig/generated/redhound/analyzer.rbs +0 -14
  213. data/sig/generated/redhound/builder/packet_mreq.rbs +0 -39
  214. data/sig/generated/redhound/builder/socket.rbs +0 -24
  215. data/sig/generated/redhound/command.rbs +0 -19
  216. data/sig/generated/redhound/l2/ether.rbs +0 -41
  217. data/sig/generated/redhound/l2/protocol.rbs +0 -15
  218. data/sig/generated/redhound/l3/arp.rbs +0 -57
  219. data/sig/generated/redhound/l3/base.rbs +0 -28
  220. data/sig/generated/redhound/l3/ipv4.rbs +0 -53
  221. data/sig/generated/redhound/l3/ipv6.rbs +0 -38
  222. data/sig/generated/redhound/l3/protocol.rbs +0 -16
  223. data/sig/generated/redhound/l3/resolver.rbs +0 -16
  224. data/sig/generated/redhound/l4/base.rbs +0 -19
  225. data/sig/generated/redhound/l4/icmp.rbs +0 -33
  226. data/sig/generated/redhound/l4/resolver.rbs +0 -16
  227. data/sig/generated/redhound/l4/udp.rbs +0 -36
  228. data/sig/generated/redhound/receiver.rbs +0 -19
  229. data/sig/generated/redhound/resolver.rbs +0 -11
  230. data/sig/generated/redhound/source/socket.rbs +0 -13
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+ require 'ipaddr'
4
+ require 'socket'
5
+
6
+ module Redhound
7
+ # @api private
8
+ module Filter
9
+ # @api private
10
+ class Analyzer
11
+ PROTOCOL_NUMBERS = { 'icmp' => 1, 'igmp' => 2, 'tcp' => 6, 'udp' => 17, 'ipv6' => 41,
12
+ 'ipv6-frag' => 44, 'esp' => 50, 'ah' => 51, 'icmp6' => 58,
13
+ 'ipv6-icmp' => 58, 'ospf' => 89, 'sctp' => 132 }.freeze
14
+ ETHER_TYPES = { 'ip' => 0x0800, 'ip6' => 0x86dd, 'arp' => 0x0806, 'rarp' => 0x8035,
15
+ 'vlan' => 0x8100 }.freeze
16
+
17
+ # @rbs (String expression) -> void
18
+ def initialize(expression)
19
+ @expression = expression
20
+ end
21
+
22
+ # @rbs (untyped node) -> untyped
23
+ def analyze(node)
24
+ case node[0]
25
+ when :and, :or then [node[0], analyze(node[1]), analyze(node[2])]
26
+ when :not then [:not, analyze(node[1])]
27
+ when :primitive then primitive(node)
28
+ when :relation then [:relation, arithmetic(node[1]), node[2], arithmetic(node[3])]
29
+ else node
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ # @rbs (untyped node) -> untyped
36
+ def primitive(node)
37
+ _, proto, direction, type, value, position = node
38
+ error('unsupported qualifier combination', position) if %w[tcp udp sctp].include?(proto) && !%i[port portrange protocol].include?(type)
39
+ case type
40
+ when :port, :portrange
41
+ error('ports require TCP, UDP or SCTP', position) if proto && !%w[tcp udp sctp].include?(proto)
42
+ value = ports(value, proto, type, position)
43
+ when :host, :net
44
+ if proto == 'ether'
45
+ error('Ethernet network qualifiers are unsupported', position) if type == :net
46
+ error('expected a MAC address', position) unless /\A(?:[\da-fA-F]{1,2}:){5}[\da-fA-F]{1,2}\z/.match?(value)
47
+ value = [value.split(':').map { |octet| octet.to_i(16) }.pack('C6')]
48
+ else
49
+ error('host qualifier requires ip, ip6, arp or rarp', position) if proto && !%w[ip ip6 arp rarp].include?(proto)
50
+ value = addresses(value, type, position)
51
+ value.select! { |family, _, _| proto == 'ip6' ? family == 6 : family == 4 } if proto
52
+ error('address family does not match qualifier', position) if value.empty?
53
+ end
54
+ when :proto
55
+ error('protocol qualifier cannot have a direction', position) unless direction == :either
56
+ error('proto requires ip, ip6 or ether', position) if proto && !%w[ip ip6 ether].include?(proto)
57
+ numbers = proto == 'ether' ? ETHER_TYPES : protocol_numbers
58
+ value = integer_or_name(value, numbers, position)
59
+ error('protocol number is out of range', position) unless value.between?(0, proto == 'ether' ? 65535 : 255)
60
+ when :vlan
61
+ error('VLAN ID is out of range', position) if value && !value.between?(0, 4095)
62
+ when :greater, :less
63
+ error('length is out of range', position) unless value.between?(0, 0xffffffff)
64
+ when :broadcast, :multicast
65
+ error('invalid broadcast/multicast protocol', position) if proto && !%w[ether ip ip6].include?(proto)
66
+ error('IPv6 has no broadcast address', position) if type == :broadcast && proto == 'ip6'
67
+ end
68
+ [:primitive, proto, direction, type, value, position]
69
+ end
70
+
71
+ # @rbs (untyped node) -> untyped
72
+ def arithmetic(node)
73
+ case node[0]
74
+ when :number
75
+ error('arithmetic constant is out of range', 0) unless node[1].between?(0, 0xffffffff)
76
+ when :binary
77
+ node = [:binary, node[1], arithmetic(node[2]), arithmetic(node[3])]
78
+ if %w[/ %].include?(node[1]) && node[3] == [:number, 0]
79
+ error('division by zero', 0)
80
+ end
81
+ if %w[<< >>].include?(node[1]) && node[3][0] == :number && node[3][1] > 31
82
+ error('shift count is out of range', 0)
83
+ end
84
+ when :neg then node = [:neg, arithmetic(node[1])]
85
+ when :access then node = [:access, node[1], arithmetic(node[2]), node[3]]
86
+ end
87
+ node
88
+ end
89
+
90
+ # @rbs (String value, String? proto, Symbol type, Integer position) -> untyped
91
+ def ports(value, proto, type, position)
92
+ values = type == :port ? [value, value] : value.split('-', 2)
93
+ error('portrange requires two port values', position) unless values.size == 2
94
+ result = {} #: untyped
95
+ candidates = proto ? [proto] : %w[tcp udp]
96
+ candidates.each do |protocol|
97
+ numbers = values.map do |part|
98
+ if /\A(?:0x[\da-fA-F]+|\d+)\z/.match?(part)
99
+ integer_or_name(part, {}, position)
100
+ else
101
+ error('expected a service name or port number', position) unless /\A[a-zA-Z][a-zA-Z0-9_-]*\z/.match?(part)
102
+ begin
103
+ Socket.getservbyname(part, protocol)
104
+ rescue SocketError
105
+ nil
106
+ end
107
+ end
108
+ end
109
+ next if numbers.any?(&:nil?)
110
+ numbers = numbers.compact
111
+ error('port number is out of range', position) unless numbers.all? { |port| port.between?(0, 65535) }
112
+ result[PROTOCOL_NUMBERS.fetch(protocol)] = numbers.minmax
113
+ end
114
+ error('unknown service name', position) if result.empty?
115
+ if !proto && result.values.uniq.size == 1
116
+ result[132] = result.values.first
117
+ end
118
+ result
119
+ end
120
+
121
+ # @rbs (String value, Symbol type, Integer position) -> untyped
122
+ def addresses(value, type, position)
123
+ address_parts = value.split(' mask ', 2)
124
+ address = address_parts.fetch(0)
125
+ mask = address_parts[1]
126
+ if /\A(?:\d+\.){0,3}\d+(?:\/\d+)?\z/.match?(address) || address.start_with?('0x')
127
+ cidr_parts = address.split('/', 2)
128
+ literal = cidr_parts.fetch(0)
129
+ cidr = cidr_parts[1]
130
+ octets = literal.split('.')
131
+ if octets.size == 1
132
+ number = integer_or_name(literal, {}, position)
133
+ error('IPv4 address is out of range', position) unless number.between?(0, 0xffffffff)
134
+ if type == :net && number <= 255
135
+ number <<= 24
136
+ prefix = 8
137
+ else
138
+ prefix = 32
139
+ end
140
+ ip = IPAddr.new([number].pack('N').unpack('C4').join('.'))
141
+ else
142
+ error('IPv4 octet is out of range', position) unless octets.all? { |part| part.to_i <= 255 }
143
+ prefix = octets.size * 8
144
+ ip = IPAddr.new((octets + Array.new(4 - octets.size, '0')).join('.'))
145
+ end
146
+ prefix = cidr.to_i if cidr
147
+ error('CIDR requires a network qualifier', position) if type == :host && cidr && prefix != 32
148
+ bitmask = mask ? IPAddr.new(mask).to_i : (0xffffffff << (32 - prefix)) & 0xffffffff
149
+ error('invalid IPv4 netmask', position) unless prefix.between?(0, 32) && ((~bitmask & 0xffffffff) & ((~bitmask & 0xffffffff) + 1)).zero?
150
+ error('non-network bits set in network address', position) if type == :net && (ip.to_i & bitmask) != ip.to_i
151
+ return [[4, ip.to_i & bitmask, bitmask]]
152
+ end
153
+ if address.include?(':')
154
+ ip = IPAddr.new(address)
155
+ error('expected IPv6 address', position) unless ip.ipv6?
156
+ prefix = address.include?('/') ? address.split('/').last.to_i : 128
157
+ error('CIDR requires a network qualifier', position) if type == :host && prefix != 128
158
+ return [[6, ip.to_i, ((1 << 128) - 1) ^ ((1 << (128 - prefix)) - 1)]]
159
+ end
160
+ error('network names must be numerical', position) if type == :net
161
+ Addrinfo.getaddrinfo(address, nil, nil, Socket::SOCK_DGRAM).map do |info|
162
+ ip = IPAddr.new(info.ip_address)
163
+ [ip.ipv4? ? 4 : 6, ip.to_i, ip.ipv4? ? 0xffffffff : ((1 << 128) - 1)]
164
+ end.uniq
165
+ rescue IPAddr::Error, SocketError, ArgumentError => e
166
+ error("invalid address #{value.inspect}: #{e.message}", position)
167
+ end
168
+
169
+ # @rbs (String value, Hash[String, Integer] names, Integer position) -> Integer
170
+ def integer_or_name(value, names, position)
171
+ value = value.delete_prefix('\\')
172
+ return names.fetch(value.downcase) if names.key?(value.downcase)
173
+ Integer(value, value.start_with?('0') && value.size > 1 ? 0 : 10)
174
+ rescue ArgumentError
175
+ error("unknown numeric value or name #{value.inspect}", position)
176
+ end
177
+
178
+ # @rbs () -> Hash[String, Integer]
179
+ def protocol_numbers
180
+ names = PROTOCOL_NUMBERS.dup
181
+ if ::File.readable?('/etc/protocols')
182
+ ::File.foreach('/etc/protocols') do |line|
183
+ tokens = line.split('#', 2).first.to_s.split
184
+ next unless tokens[1]&.match?(/\A\d+\z/)
185
+ ([tokens[0]] + tokens.drop(2)).each { |name| names[name] = tokens[1].to_i }
186
+ end
187
+ end
188
+ names
189
+ end
190
+
191
+ # @rbs (String message, Integer position) -> bot
192
+ def error(message, position)
193
+ raise FilterSyntaxError.new(message, expression: @expression, position: position)
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Filter
7
+ # @api private
8
+ module BPF
9
+ # @api private
10
+ class Assembler
11
+ # @rbs () -> void
12
+ def initialize
13
+ @items = [] #: untyped
14
+ @sequence = 0
15
+ end
16
+
17
+ # @rbs () -> Symbol
18
+ def label
19
+ @sequence += 1
20
+ "label_#{@sequence}".to_sym
21
+ end
22
+
23
+ # @rbs (Symbol label) -> void
24
+ def mark(label) = @items << [:label, label]
25
+
26
+ # @rbs (Integer code, ?Integer k) -> void
27
+ def emit(code, k = 0) = @items << [code, 0, 0, k & 0xffffffff]
28
+
29
+ # @rbs (Symbol target) -> void
30
+ def jump(target) = @items << [:jump, target]
31
+
32
+ # @rbs (Integer code, Integer k, Symbol yes, Symbol no) -> void
33
+ def branch(code, k, yes, no) = @items << [:branch, code, k & 0xffffffff, yes, no]
34
+
35
+ # @rbs () -> Array[[Integer, Integer, Integer, Integer]]
36
+ def assemble
37
+ compact!
38
+ widths = @items.map { |item| item[0] == :label ? 0 : 1 }
39
+ labels = {} #: untyped
40
+ loop do
41
+ offset = 0
42
+ @items.each_with_index do |item, index|
43
+ labels[item[1]] = offset if item[0] == :label
44
+ offset += widths[index]
45
+ end
46
+ raise FilterTooLarge, 'BPF programs are limited to 4096 instructions' if offset > 4096
47
+ changed = false
48
+ pc = 0
49
+ @items.each_with_index do |item, index|
50
+ if item[0] == :branch && widths[index] == 1 && [item[3], item[4]].any? { |target| labels.fetch(target) - pc - 1 > 255 }
51
+ widths[index] = 3
52
+ changed = true
53
+ end
54
+ pc += widths[index]
55
+ end
56
+ break unless changed
57
+ end
58
+ result = [] #: Array[[Integer, Integer, Integer, Integer]]
59
+ @items.each_with_index do |item, index|
60
+ pc = result.size
61
+ case item[0]
62
+ when :label then next
63
+ when :jump then result << [5, 0, 0, labels.fetch(item[1]) - pc - 1]
64
+ when :branch
65
+ _, code, k, yes, no = item
66
+ if widths[index] == 1
67
+ result << [code, labels.fetch(yes) - pc - 1, labels.fetch(no) - pc - 1, k]
68
+ else
69
+ result << [code, 0, 1, k]
70
+ result << [5, 0, 0, labels.fetch(yes) - pc - 2]
71
+ result << [5, 0, 0, labels.fetch(no) - pc - 3]
72
+ end
73
+ else result << item
74
+ end
75
+ end
76
+ result
77
+ end
78
+
79
+ private
80
+
81
+ # Remove unreachable blocks and thread jumps through unconditional jumps.
82
+ # @rbs () -> void
83
+ def compact!
84
+ labels = {} #: untyped
85
+ @items.each_with_index { |item, index| labels[item[1]] = index if item[0] == :label }
86
+ target = lambda do |name|
87
+ index = labels.fetch(name) + 1
88
+ index += 1 while @items[index]&.first == :label
89
+ @items[index]&.first == :jump ? target.call(@items[index][1]) : name
90
+ end
91
+ @items.each do |item|
92
+ item[1] = target.call(item[1]) if item[0] == :jump
93
+ if item[0] == :branch
94
+ item[3] = target.call(item[3])
95
+ item[4] = target.call(item[4])
96
+ end
97
+ end
98
+ reachable = {} #: Hash[Integer, bool]
99
+ pending = [0]
100
+ until pending.empty?
101
+ index = pending.pop
102
+ next if !index || index >= @items.size || reachable[index]
103
+ reachable[index] = true
104
+ item = @items[index]
105
+ case item[0]
106
+ when :jump then pending << labels.fetch(item[1])
107
+ when :branch then pending.concat([labels.fetch(item[3]), labels.fetch(item[4])])
108
+ when 6, 0x16 then next
109
+ else pending << index + 1
110
+ end
111
+ end
112
+ @items = @items.each_with_index.filter_map { |item, index| item if reachable[index] || item[0] == :label }
113
+ @items = @items.each_with_index.filter_map do |item, index|
114
+ following = @items[(index + 1)..].take_while { |next_item| next_item[0] == :label }
115
+ item unless item[0] == :jump && following.any? { |label_item| label_item[1] == item[1] }
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Filter
7
+ # @api private
8
+ module BPF
9
+ # @api private
10
+ module Disassembler
11
+ # @rbs (Array[[Integer, Integer, Integer, Integer]] instructions, ?format: Symbol) -> String
12
+ def self.disassemble(instructions, format: :text)
13
+ case format
14
+ when :ruby
15
+ "[\n" + instructions.map { |code, jt, jf, k| format(' [0x%04x, %d, %d, 0x%08x],', code, jt, jf, k) }.join("\n") + "\n]\n"
16
+ when :decimal
17
+ "#{instructions.size}\n" + instructions.map { |i| i.join(' ') }.join("\n") + "\n"
18
+ when :text
19
+ instructions.each_with_index.map do |(code, jt, jf, k), index|
20
+ text = instruction(code, k)
21
+ text += " jt #{index + jt + 1} jf #{index + jf + 1}" if Validator::JUMPS.include?(code)
22
+ text = "ja #{index + k + 1}" if code == 5
23
+ format('(%03d) %s', index, text)
24
+ end.join("\n") + "\n"
25
+ else raise ArgumentError, "unknown disassembly format #{format.inspect}"
26
+ end
27
+ end
28
+
29
+ # @rbs (Integer code, Integer k) -> String
30
+ def self.instruction(code, k)
31
+ source = (code & 8).zero? ? "#0x#{k.to_s(16)}" : 'x'
32
+ case code & 7
33
+ when 0, 1
34
+ mnemonic = (code & 7).zero? ? 'ld' : 'ldx'
35
+ mnemonic += { 0 => '', 8 => 'h', 16 => 'b' }.fetch(code & 0x18)
36
+ operand = case code & 0xe0
37
+ when 0 then "#0x#{k.to_s(16)}"
38
+ when 0x20 then "[#{k}]"
39
+ when 0x40 then "[x + #{k}]"
40
+ when 0x60 then "M[#{k}]"
41
+ when 0x80 then 'len'
42
+ when 0xa0 then "4*([#{k}]&0xf)"
43
+ end
44
+ "#{mnemonic} #{operand}"
45
+ when 2 then "st M[#{k}]"
46
+ when 3 then "stx M[#{k}]"
47
+ when 4
48
+ "#{ { 0 => 'add', 0x10 => 'sub', 0x20 => 'mul', 0x30 => 'div', 0x40 => 'or', 0x50 => 'and', 0x60 => 'lsh', 0x70 => 'rsh', 0x80 => 'neg', 0x90 => 'mod', 0xa0 => 'xor' }.fetch(code & 0xf0)} #{source}"
49
+ when 5 then "#{ { 0 => 'ja', 0x10 => 'jeq', 0x20 => 'jgt', 0x30 => 'jge', 0x40 => 'jset' }.fetch(code & 0xf0)} #{source}"
50
+ when 6 then "ret #{code == 0x16 ? 'a' : "##{k}"}"
51
+ when 7 then code == 7 ? 'tax' : 'txa'
52
+ else raise FilterError, 'invalid BPF opcode'
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Filter
7
+ # @api private
8
+ module BPF
9
+ # Validates the portable cBPF ISA and Linux VLAN ancillary loads.
10
+ # @api private
11
+ module Validator
12
+ LOADS = [0x00, 0x20, 0x28, 0x30, 0x40, 0x48, 0x50, 0x60, 0x80,
13
+ 0x01, 0x61, 0x81, 0xb1].freeze
14
+ ALU = [0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x90, 0xa0].flat_map { |op| [op | 4, op | 12] }.freeze
15
+ JUMPS = [0x15, 0x1d, 0x25, 0x2d, 0x35, 0x3d, 0x45, 0x4d].freeze
16
+ OPCODES = (LOADS + ALU + JUMPS + [0x02, 0x03, 0x05, 0x06, 0x16, 0x07, 0x87, 0x84]).freeze
17
+
18
+ # @rbs (Array[[Integer, Integer, Integer, Integer]] instructions) -> bool
19
+ def self.validate!(instructions)
20
+ raise FilterError, 'BPF instructions must be an Array' unless instructions.is_a?(Array)
21
+ count = instructions.size
22
+ raise FilterTooLarge, 'BPF programs are limited to 4096 instructions' if count > 4096
23
+ raise FilterError, 'BPF program is empty' if count.zero?
24
+
25
+ instructions.each_with_index do |instruction, index|
26
+ unless instruction.is_a?(Array) && instruction.size == 4 && instruction.all? { |field| field.is_a?(Integer) }
27
+ raise FilterError, "invalid BPF instruction #{index}"
28
+ end
29
+ code, jt, jf, k = instruction
30
+ unless OPCODES.include?(code) && jt.between?(0, 255) && jf.between?(0, 255) && k.between?(0, 0xffffffff)
31
+ raise FilterError, "invalid BPF opcode or operand at #{index}"
32
+ end
33
+ raise FilterError, "invalid scratch address at #{index}" if [0x60, 0x61, 2, 3].include?(code) && k > 15
34
+ raise FilterError, "division by zero at #{index}" if [0x34, 0x94].include?(code) && k.zero?
35
+ raise FilterError, "invalid shift at #{index}" if [0x64, 0x74].include?(code) && k > 31
36
+ if [0x20, 0x28, 0x30].include?(code) && k >= 0xfffff000 && (code != 0x20 || ![0xfffff000, 0xfffff02c, 0xfffff030, 0xfffff03c].include?(k))
37
+ raise FilterError, "unsupported ancillary load at #{index}"
38
+ end
39
+ successors(code, jt, jf, k, index).each do |target|
40
+ raise FilterError, "invalid BPF jump at #{index}" unless target > index && target < count
41
+ end
42
+ end
43
+ raise FilterError, 'BPF program must end with RET' unless [6, 0x16].include?(instructions.last[0])
44
+ validate_memory!(instructions)
45
+ true
46
+ end
47
+
48
+ # @rbs (Integer code, Integer jt, Integer jf, Integer k, Integer index) -> Array[Integer]
49
+ def self.successors(code, jt, jf, k, index)
50
+ return [] if [6, 0x16].include?(code)
51
+ return [index + 1 + k] if code == 5
52
+ return [index + 1 + jt, index + 1 + jf] if JUMPS.include?(code)
53
+
54
+ [index + 1]
55
+ end
56
+
57
+ # @rbs (Array[[Integer, Integer, Integer, Integer]] instructions) -> void
58
+ def self.validate_memory!(instructions)
59
+ incoming = {} #: Hash[Integer, Integer]
60
+ incoming[0] = 0
61
+ instructions.each_with_index do |(code, jt, jf, k), index|
62
+ initialized = incoming[index]
63
+ next unless initialized
64
+ if [0x60, 0x61].include?(code) && (initialized & (1 << k)).zero?
65
+ raise FilterError, "uninitialized BPF scratch memory at #{index}"
66
+ end
67
+ initialized |= (1 << k) if [2, 3].include?(code)
68
+ successors(code, jt, jf, k, index).each do |target|
69
+ previous = incoming[target]
70
+ incoming[target] = previous ? previous & initialized : initialized
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Filter
7
+ # @api private
8
+ module BPF
9
+ # @api private
10
+ class VM
11
+ # @rbs (Array[[Integer, Integer, Integer, Integer]] instructions) -> void
12
+ def initialize(instructions)
13
+ Validator.validate!(instructions)
14
+ @instructions = instructions
15
+ end
16
+
17
+ # @rbs (untyped packet) -> Integer
18
+ def evaluate(packet)
19
+ a = 0
20
+ x = 0
21
+ memory = Array.new(16, 0)
22
+ pc = 0
23
+ loop do
24
+ code, jt, jf, k = @instructions.fetch(pc)
25
+ pc += 1
26
+ case code & 7
27
+ when 0, 1
28
+ value = case code & 0xe0
29
+ when 0 then k
30
+ when 0x60 then memory.fetch(k)
31
+ when 0x80 then packet.original_length
32
+ else
33
+ offset = k + ((code & 0xe0) == 0x40 ? x : 0)
34
+ if k >= 0xfffff000 && (code & 0xe0) == 0x20
35
+ ancillary(k, packet.meta)
36
+ else
37
+ size = { 0x00 => 4, 0x08 => 2, 0x10 => 1 }.fetch(code & 0x18)
38
+ return 0 if offset.negative? || offset + size > packet.data.bytesize
39
+ packet.data.unpack1({ 4 => 'N', 2 => 'n', 1 => 'C' }.fetch(size), offset: offset)
40
+ end
41
+ end
42
+ if code == 0xb1
43
+ x = (value & 15) * 4
44
+ elsif (code & 7) == 1
45
+ x = value & 0xffffffff
46
+ else
47
+ a = value & 0xffffffff
48
+ end
49
+ when 2 then memory[k] = a
50
+ when 3 then memory[k] = x
51
+ when 4
52
+ operand = (code & 8).zero? ? k : x
53
+ return 0 if [0x30, 0x90].include?(code & 0xf0) && operand.zero?
54
+ a = arithmetic(code & 0xf0, a, operand) & 0xffffffff
55
+ when 5
56
+ if code == 5
57
+ pc += k
58
+ else
59
+ operand = (code & 8).zero? ? k : x
60
+ result = case code & 0xf0
61
+ when 0x10 then a == operand
62
+ when 0x20 then a > operand
63
+ when 0x30 then a >= operand
64
+ when 0x40 then (a & operand) != 0
65
+ end
66
+ pc += result ? jt : jf
67
+ end
68
+ when 6 then return code == 0x16 ? a : k
69
+ when 7
70
+ code == 7 ? x = a : a = x
71
+ end
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ # @rbs (Integer operation, Integer a, Integer operand) -> Integer
78
+ def arithmetic(operation, a, operand)
79
+ case operation
80
+ when 0x00 then a + operand
81
+ when 0x10 then a - operand
82
+ when 0x20 then a * operand
83
+ when 0x30 then a / operand
84
+ when 0x40 then a | operand
85
+ when 0x50 then a & operand
86
+ when 0x60 then operand >= 32 ? 0 : a << operand
87
+ when 0x70 then operand >= 32 ? 0 : a >> operand
88
+ when 0x80 then -a
89
+ when 0x90 then a % operand
90
+ when 0xa0 then a ^ operand
91
+ else 0
92
+ end
93
+ end
94
+
95
+ # @rbs (Integer offset, untyped meta) -> Integer
96
+ def ancillary(offset, meta)
97
+ case offset
98
+ when 0xfffff000 then meta.fetch(:protocol, 0)
99
+ when 0xfffff030 then meta.key?(:vlan_tci) ? 1 : 0
100
+ when 0xfffff02c then meta.fetch(:vlan_tci, 0)
101
+ when 0xfffff03c then meta.fetch(:vlan_tpid, 0x8100)
102
+ else 0
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end