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,224 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Capture
7
+ # @api private
8
+ module Linux
9
+ # @api private
10
+ class PacketSocket < Source
11
+ attr_reader :socket, :linktype, :interfaces
12
+
13
+ # @rbs (interface: String | Integer | Interface, ?snaplen: Integer, ?promiscuous: bool, ?buffer_size: Integer?, ?direction: Symbol, ?filter: String?) -> void
14
+ def initialize(interface:, snaplen: 262_144, promiscuous: true, buffer_size: nil, direction: :inout, filter: nil)
15
+ super()
16
+ raise ArgumentError, 'snaplen must be between 1 and 16 MiB' unless snaplen.between?(1, File::Format::MAX_RECORD)
17
+ raise ArgumentError, 'direction must be in, out or inout' unless %i[in out inout].include?(direction)
18
+
19
+ @interface = interface.is_a?(Interface) ? interface : Interface.find(interface)
20
+ @interfaces = [@interface]
21
+ @snaplen, @direction = snaplen, direction
22
+ @linktype = @interface.linktype
23
+ @cooked = @linktype == 276
24
+ @socket = ::Socket.new(Constants::AF_PACKET, @cooked ? ::Socket::SOCK_DGRAM : ::Socket::SOCK_RAW, 0)
25
+ @interface_drops = interface_dropped
26
+ setup_receive_buffer(buffer_size)
27
+ attach_filter(filter ? Filter.compile(filter, linktype: @linktype, snaplen:, live: true) : nil)
28
+ if promiscuous && @interface.name == 'any'
29
+ warn 'redhound: promiscuous mode is unavailable on any'
30
+ elsif promiscuous
31
+ @socket.setsockopt(Constants::SOL_PACKET, Constants::PACKET_ADD_MEMBERSHIP, [@interface.index, Constants::PACKET_MR_PROMISC, 0, ''.b].pack('i S S a8'))
32
+ end
33
+ @socket.bind(SockaddrLL.new(index: @interface.index).to_sockaddr)
34
+ prime_timestamp
35
+ warn_vlan_offload
36
+ if direction == :in
37
+ begin
38
+ @socket.setsockopt(Constants::SOL_PACKET, Constants::PACKET_IGNORE_OUTGOING, [1].pack('i'))
39
+ rescue Errno::ENOPROTOOPT, Errno::EINVAL
40
+ # Older kernels are filtered by packet direction below.
41
+ end
42
+ end
43
+ rescue Errno::EPERM, Errno::EACCES => error
44
+ close_resources
45
+ raise PermissionDenied, "capture requires root or CAP_NET_RAW: #{error.message}"
46
+ rescue StandardError
47
+ close_resources
48
+ raise
49
+ end
50
+
51
+ # @rbs (?timeout: Numeric?) -> Packet?
52
+ def next_packet(timeout: nil)
53
+ deadline = deadline_for(timeout)
54
+ polled = false
55
+ until @stopped || @closed
56
+ return nil if polled && deadline && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
57
+ polled = true
58
+ result = @socket.recvfrom_nonblock(Constants::RECEIVE_SIZE, exception: false)
59
+ if result == :wait_readable
60
+ return nil unless wait_readable(@socket, deadline)
61
+
62
+ next
63
+ end
64
+
65
+ data, address_info = result
66
+ address = SockaddrLL.parse(address_info.to_sockaddr)
67
+ next unless accept_direction?(address)
68
+
69
+ original_length = data.bytesize
70
+ data = Cooked.header(address) + data if @cooked
71
+ original_length += 20 if @cooked
72
+ timestamp_ns = timestamp
73
+ meta = { pkttype: address.pkttype, csum_not_ready: address.hatype == 772 || address.pkttype == Constants::PACKET_OUTGOING }
74
+ meta[:possible_truncation] = true if original_length >= Constants::RECEIVE_SIZE + (@cooked ? 20 : 0)
75
+ @number += 1
76
+ @capture_stats.captured += 1
77
+ return Packet.new(data.byteslice(0, @snaplen), timestamp_ns:, original_length:, linktype: @linktype,
78
+ interface: packet_interface(address), direction: address.pkttype == Constants::PACKET_OUTGOING ? :out : :in, number: @number, meta:)
79
+ end
80
+ nil
81
+ end
82
+
83
+ # @rbs (untyped program) -> void
84
+ def attach_filter(program)
85
+ instructions = program ? program.instructions : [[0x06, 0, 0, receive_snaplen]] #: Array[[Integer, Integer, Integer, Integer]]
86
+ Filter::BPF::Validator.validate!(instructions)
87
+ instructions = receive_filter(instructions)
88
+ packed = instructions.map { |instruction| instruction.pack('S C C L') }.join
89
+ @socket.setsockopt(::Socket::SOL_SOCKET, Constants::SO_ATTACH_FILTER, [instructions.size, packed].pack('S x6 P'))
90
+ end
91
+
92
+ # @rbs () -> Stats
93
+ def stats
94
+ unless @closed
95
+ counters = @socket.getsockopt(Constants::SOL_PACKET, Constants::PACKET_STATISTICS).data.unpack('L*')
96
+ @capture_stats.received += counters.fetch(0, 0)
97
+ @capture_stats.dropped += counters.fetch(1, 0)
98
+ @capture_stats.freeze_count += counters.fetch(2, 0)
99
+ @capture_stats.if_dropped = [interface_dropped - @interface_drops, 0].max
100
+ end
101
+ @capture_stats
102
+ end
103
+
104
+ # @rbs () -> void
105
+ def close
106
+ return if @closed
107
+
108
+ begin
109
+ stats
110
+ ensure
111
+ super
112
+ close_resources
113
+ end
114
+ end
115
+
116
+ protected
117
+
118
+ # Positive filter returns select packets; the source applies its configured snaplen.
119
+ # @rbs (Array[[Integer, Integer, Integer, Integer]] instructions) -> Array[[Integer, Integer, Integer, Integer]]
120
+ def receive_filter(instructions)
121
+ unless instructions.any? { |instruction| instruction[0] == 0x16 }
122
+ return instructions.map do |code, jt, jf, k|
123
+ [code, jt, jf, code == 0x06 && k.positive? ? receive_snaplen : k]
124
+ end
125
+ end
126
+
127
+ assembler = Filter::BPF::Assembler.new
128
+ labels = instructions.map { assembler.label }
129
+ accepted, rejected = assembler.label, assembler.label
130
+ instructions.each_with_index do |(code, jt, jf, k), index|
131
+ assembler.mark(labels.fetch(index))
132
+ if code == 0x16
133
+ assembler.branch(0x15, 0, rejected, accepted)
134
+ elsif code == 5
135
+ assembler.jump(labels.fetch(index + 1 + k))
136
+ elsif Filter::BPF::Validator::JUMPS.include?(code)
137
+ assembler.branch(code, k, labels.fetch(index + 1 + jt), labels.fetch(index + 1 + jf))
138
+ else
139
+ assembler.emit(code, code == 6 && k.positive? ? receive_snaplen : k)
140
+ end
141
+ end
142
+ assembler.mark(accepted)
143
+ assembler.emit(6, receive_snaplen)
144
+ assembler.mark(rejected)
145
+ assembler.emit(6, 0)
146
+ result = assembler.assemble
147
+ Filter::BPF::Validator.validate!(result)
148
+ result
149
+ end
150
+
151
+ # @rbs () -> void
152
+ def warn_vlan_offload
153
+ return unless instance_of?(PacketSocket) && [1, 276].include?(@linktype) && !@interface.loopback?
154
+
155
+ name = @interface.name == 'any' ? '<interface>' : @interface.name
156
+ warn "redhound: socket capture cannot restore stripped VLAN tags; disable receive VLAN offload with ethtool -K #{name} rxvlan off on the receiving interface"
157
+ end
158
+
159
+ # @rbs () -> void
160
+ def close_resources
161
+ @socket.close if @socket && !@socket.closed?
162
+ end
163
+
164
+ # @rbs () -> void
165
+ def prime_timestamp
166
+ @socket.ioctl(Constants::SIOCGSTAMPNS, "\0" * 16)
167
+ rescue Errno::ENOENT, Errno::ENOTTY, Errno::EINVAL
168
+ nil
169
+ end
170
+
171
+ # @rbs () -> Integer
172
+ def receive_snaplen = Constants::RECEIVE_SIZE
173
+
174
+ # @rbs (Integer? buffer_size) -> void
175
+ def setup_receive_buffer(buffer_size)
176
+ return unless buffer_size
177
+ raise ArgumentError, 'buffer size must be positive' unless buffer_size.positive?
178
+
179
+ @socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVBUF, [buffer_size].pack('i'))
180
+ begin
181
+ @socket.setsockopt(::Socket::SOL_SOCKET, Constants::SO_RCVBUFFORCE, [buffer_size].pack('i'))
182
+ rescue Errno::EPERM, Errno::EACCES, Errno::ENOPROTOOPT => error
183
+ warn "redhound: SO_RCVBUFFORCE unavailable (#{error.message}); using SO_RCVBUF with the system limit"
184
+ end
185
+ end
186
+
187
+ # @rbs (SockaddrLL address) -> bool
188
+ def accept_direction?(address)
189
+ outgoing = address.pkttype == Constants::PACKET_OUTGOING
190
+ return false if outgoing && address.hatype == 772 && @direction != :out
191
+
192
+ @direction == :inout || (@direction == :out) == outgoing
193
+ end
194
+
195
+ # @rbs (SockaddrLL address) -> Interface
196
+ def packet_interface(address)
197
+ return @interface unless @interface.name == 'any'
198
+
199
+ @any_interfaces ||= Interface.all.to_h { |interface| [interface.index, interface] }
200
+ @any_interfaces.fetch(address.index) { Interface.new(name: "if#{address.index}", index: address.index, linktype: 276) }
201
+ end
202
+
203
+ # @rbs () -> Integer
204
+ def interface_dropped
205
+ return 0 if @interface.name == 'any'
206
+
207
+ ::File.read("/sys/class/net/#{@interface.name}/statistics/rx_dropped").to_i
208
+ rescue Errno::ENOENT, Errno::EACCES
209
+ 0
210
+ end
211
+
212
+ # @rbs () -> Integer
213
+ def timestamp
214
+ buffer = "\0" * 16
215
+ @socket.ioctl(Constants::SIOCGSTAMPNS, buffer)
216
+ seconds, nanos = buffer.unpack('q q') #: [Integer, Integer]
217
+ seconds * 1_000_000_000 + nanos
218
+ rescue Errno::ENOENT, Errno::ENOTTY, Errno::EINVAL
219
+ Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,33 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Capture
7
+ # @api private
8
+ module Linux
9
+ # @api private
10
+ class SockaddrLL
11
+ attr_reader :protocol, :index, :hatype, :pkttype, :address
12
+
13
+ # @rbs (?protocol: Integer, ?index: Integer, ?hatype: Integer, ?pkttype: Integer, ?address: String) -> void
14
+ def initialize(protocol: Constants::ETH_P_ALL, index: 0, hatype: 0, pkttype: 0, address: ''.b)
15
+ @protocol, @index, @hatype, @pkttype, @address = protocol, index, hatype, pkttype, address.byteslice(0, 8)
16
+ end
17
+
18
+ # @rbs () -> String
19
+ def to_sockaddr
20
+ [Constants::AF_PACKET, @protocol, @index, @hatype, @pkttype, @address.bytesize, @address].pack('S n l S C C a8')
21
+ end
22
+
23
+ # @rbs (String bytes) -> SockaddrLL
24
+ def self.parse(bytes)
25
+ raise CaptureError, 'truncated sockaddr_ll' if bytes.bytesize < 20
26
+
27
+ _family, protocol, index, hatype, pkttype, length, address = bytes.unpack('S n l S C C a8') #: [Integer, Integer, Integer, Integer, Integer, Integer, String]
28
+ new(protocol:, index:, hatype:, pkttype:, address: address.byteslice(0, [length, 8].min) || ''.b)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,137 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Capture
7
+ # @api private
8
+ module Linux
9
+ # @api private
10
+ class TPacketV3 < PacketSocket
11
+ # @rbs (?timeout: Numeric?) -> Packet?
12
+ def next_packet(timeout: nil)
13
+ deadline = deadline_for(timeout)
14
+ polled = false
15
+ until @stopped || @closed
16
+ return nil if polled && deadline && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
17
+ polled = true
18
+ unless @pending_count.positive?
19
+ base = @current_block * @block_size
20
+ unless (u32(base + 8) & Constants::TP_STATUS_USER).positive?
21
+ return nil unless wait_readable(@socket, deadline)
22
+ next
23
+ end
24
+ @block_end = base + u32(base + 20)
25
+ @pending_count = u32(base + 12)
26
+ @packet_offset = base + u32(base + 16)
27
+ if @pending_count.zero?
28
+ release_block
29
+ next
30
+ end
31
+ end
32
+
33
+ offset = @packet_offset
34
+ check_ring_bounds(offset, 68)
35
+ next_offset, seconds, nanos, caplen, original_length, status = @ring.get_string(offset, 24).unpack('L6')
36
+ mac_offset = @ring.get_value(:u16, offset + 24)
37
+ check_ring_bounds(offset + mac_offset, caplen)
38
+ data = @ring.get_string(offset + mac_offset, caplen)
39
+ address = SockaddrLL.parse(@ring.get_string(offset + 48, 20))
40
+ meta = { pkttype: address.pkttype, csum_not_ready: (status & Constants::TP_STATUS_CSUMNOTREADY).positive?,
41
+ csum_valid: (status & Constants::TP_STATUS_CSUM_VALID).positive? }
42
+ if (status & Constants::TP_STATUS_VLAN_VALID).positive?
43
+ tci = u32(offset + 32) & 0xffff
44
+ tpid = (status & Constants::TP_STATUS_VLAN_TPID_VALID).positive? ? @ring.get_value(:u16, offset + 36) : 0x8100
45
+ meta[:vlan_tci], meta[:vlan_tpid] = tci, tpid
46
+ if !@cooked && @linktype == 1
47
+ original_length += 4
48
+ data = data.byteslice(0, 12) + [tpid, tci].pack('n2') + data.byteslice(12..) if data.bytesize >= 12
49
+ elsif @cooked
50
+ data = [tci, address.protocol].pack('n2') + data
51
+ original_length += 4
52
+ address = SockaddrLL.new(protocol: tpid, index: address.index, hatype: address.hatype,
53
+ pkttype: address.pkttype, address: address.address)
54
+ end
55
+ end
56
+ if @cooked
57
+ data = Cooked.header(address) + data
58
+ original_length += 20
59
+ end
60
+ @pending_count -= 1
61
+ if @pending_count.zero?
62
+ release_block
63
+ else
64
+ raise CaptureError, 'invalid ring packet offset' if next_offset < 68
65
+ @packet_offset += next_offset
66
+ end
67
+ next unless accept_direction?(address)
68
+
69
+ @number += 1
70
+ @capture_stats.captured += 1
71
+ return Packet.new(data.byteslice(0, @snaplen), timestamp_ns: seconds * 1_000_000_000 + nanos,
72
+ original_length:, linktype: @linktype, interface: packet_interface(address),
73
+ direction: address.pkttype == Constants::PACKET_OUTGOING ? :out : :in, number: @number, meta:)
74
+ end
75
+ nil
76
+ end
77
+
78
+ protected
79
+
80
+ # @rbs () -> void
81
+ def close_resources
82
+ begin
83
+ @ring.free if @ring
84
+ ensure
85
+ @ring = nil
86
+ super
87
+ end
88
+ end
89
+
90
+ # @rbs () -> Integer
91
+ def receive_snaplen = @cooked ? [@snaplen - 20, 1].max : @snaplen
92
+
93
+ # @rbs (Integer? buffer_size) -> void
94
+ def setup_receive_buffer(buffer_size)
95
+ raise ArgumentError, 'buffer size must be positive' if buffer_size && !buffer_size.positive?
96
+
97
+ @block_size = 1 << 20
98
+ @block_size *= 2 while @block_size < @snaplen + 128
99
+ @block_count = buffer_size ? [(buffer_size.to_f / @block_size).ceil, 1].max : 8
100
+ frame_size = 2048
101
+ request = [@block_size, @block_count, frame_size, @block_size * @block_count / frame_size, 50, 0, 0].pack('L7')
102
+ @socket.setsockopt(Constants::SOL_PACKET, Constants::PACKET_VERSION, [Constants::TPACKET_V3].pack('i'))
103
+ @socket.setsockopt(Constants::SOL_PACKET, Constants::PACKET_RX_RING, request)
104
+ experimental = Warning[:experimental]
105
+ begin
106
+ Warning[:experimental] = false
107
+ @ring = IO::Buffer.map(@socket, @block_size * @block_count, 0)
108
+ rescue ArgumentError => error
109
+ raise UnsupportedPlatform, "Ruby #{RUBY_VERSION} IO::Buffer.map cannot map a packet socket: #{error.message}"
110
+ ensure
111
+ Warning[:experimental] = experimental
112
+ end
113
+ @current_block, @pending_count, @packet_offset, @block_end = 0, 0, 0, 0
114
+ end
115
+
116
+ private
117
+
118
+ # @rbs (Integer offset) -> Integer
119
+ def u32(offset) = @ring.get_value(:u32, offset)
120
+
121
+ # @rbs (Integer offset, Integer length) -> void
122
+ def check_ring_bounds(offset, length)
123
+ unless offset >= @current_block * @block_size && length >= 0 && offset + length <= @block_end && @block_end <= (@current_block + 1) * @block_size
124
+ raise CaptureError, 'invalid ring packet bounds'
125
+ end
126
+ end
127
+
128
+ # @rbs () -> void
129
+ def release_block
130
+ @ring.set_value(:u32, @current_block * @block_size + 8, 0)
131
+ @current_block = (@current_block + 1) % @block_count
132
+ @pending_count = 0
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,79 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Capture
7
+ # @api private
8
+ class Source
9
+ include Enumerable #[Packet]
10
+
11
+ # @rbs () -> void
12
+ def initialize
13
+ @stopped = false
14
+ @closed = false
15
+ @number = 0
16
+ @capture_stats = Stats.new
17
+ end
18
+
19
+ # @rbs () { (Packet) -> void } -> self
20
+ # @rbs () -> Enumerator[Packet, self]
21
+ def each_packet
22
+ return enum_for(:each_packet) unless block_given?
23
+
24
+ until @stopped || @closed
25
+ packet = next_packet(timeout: 0.1)
26
+ yield packet if packet
27
+ end
28
+ self
29
+ end
30
+
31
+ alias each each_packet
32
+
33
+ # @rbs (?timeout: Numeric?) -> Packet?
34
+ def next_packet(timeout: nil)
35
+ raise NotImplementedError
36
+ end
37
+
38
+ # @rbs () -> Stats
39
+ def stats = @capture_stats
40
+
41
+ # @rbs () -> bool
42
+ def stopped? = @stopped
43
+
44
+ # @rbs () -> void
45
+ def stop
46
+ @stopped = true
47
+ end
48
+
49
+ # @rbs () -> void
50
+ def close
51
+ stop
52
+ @closed = true
53
+ end
54
+
55
+ private
56
+
57
+ # @rbs (IO io, Numeric? deadline) -> bool
58
+ def wait_readable(io, deadline)
59
+ until @stopped || @closed
60
+ remaining = deadline && deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
61
+ return !!IO.select([io], nil, nil, 0) if remaining && remaining <= 0
62
+ return true if IO.select([io], nil, nil, remaining ? [remaining, 0.1].min : 0.1)
63
+ end
64
+ false
65
+ rescue IOError, Errno::EBADF
66
+ return false if @closed || @stopped
67
+
68
+ raise
69
+ end
70
+
71
+ # @rbs (Numeric? timeout) -> Numeric?
72
+ def deadline_for(timeout)
73
+ raise ArgumentError, 'timeout must be nonnegative' if timeout && timeout.negative?
74
+
75
+ timeout && Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,22 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module Capture
7
+ # @api private
8
+ class Stats
9
+ attr_accessor :received, :dropped, :if_dropped, :freeze_count, :captured
10
+
11
+ # @rbs (?received: Integer, ?dropped: Integer, ?if_dropped: Integer, ?freeze_count: Integer, ?captured: Integer) -> void
12
+ def initialize(received: 0, dropped: 0, if_dropped: 0, freeze_count: 0, captured: 0)
13
+ @received, @dropped, @if_dropped, @freeze_count, @captured = received, dropped, if_dropped, freeze_count, captured
14
+ end
15
+
16
+ # @rbs () -> Hash[Symbol, Integer]
17
+ def to_h
18
+ { received: @received, dropped: @dropped, if_dropped: @if_dropped, freeze_count: @freeze_count, captured: @captured }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,70 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'capture/linktype'
5
+ require_relative 'capture/interface'
6
+ require_relative 'capture/stats'
7
+ require_relative 'capture/source'
8
+ require_relative 'capture/linux/constants'
9
+ require_relative 'capture/linux/sockaddr_ll'
10
+ require_relative 'capture/linux/ifreq'
11
+ require_relative 'capture/linux/cooked'
12
+ require_relative 'capture/linux/packet_socket'
13
+ require_relative 'capture/linux/tpacket_v3'
14
+ require_relative 'capture/bsd/constants'
15
+ require_relative 'capture/bsd/ifreq'
16
+ require_relative 'capture/bsd/bpf_device'
17
+ require_relative 'file/format'
18
+ require_relative 'file/pcap_reader'
19
+ require_relative 'file/pcapng_reader'
20
+ require_relative 'file/pcap_writer'
21
+ require_relative 'file/pcapng_writer'
22
+ require_relative 'file/rotating_writer'
23
+ require_relative 'capture/file_source'
24
+ require_relative 'reader'
25
+
26
+ module Redhound
27
+ # @api private
28
+ module Capture
29
+ # @rbs (interface: String | Integer | Interface, ?backend: Symbol | String, ?snaplen: Integer, ?promiscuous: bool, ?buffer_size: Integer?, ?direction: Symbol, ?filter: String?) -> Source
30
+ # @rbs [T] (interface: String | Integer | Interface, ?backend: Symbol | String, ?snaplen: Integer, ?promiscuous: bool, ?buffer_size: Integer?, ?direction: Symbol, ?filter: String?) { (Source) -> T } -> T
31
+ def self.open(interface:, backend: :auto, snaplen: 262_144, promiscuous: true, buffer_size: nil, direction: :inout, filter: nil)
32
+ options = { snaplen:, promiscuous:, buffer_size:, direction:, filter: } #: Hash[Symbol, untyped]
33
+ backend = backend.to_sym
34
+ source = if RUBY_PLATFORM.include?('linux')
35
+ case backend
36
+ when :socket then Linux::PacketSocket.new(interface:, **options)
37
+ when :ring then Linux::TPacketV3.new(interface:, **options)
38
+ when :auto
39
+ if RUBY_PLATFORM.start_with?('x86_64-', 'aarch64-', 'arm64-')
40
+ begin
41
+ Linux::TPacketV3.new(interface:, **options)
42
+ rescue SystemCallError, IO::Buffer::AccessError, IO::Buffer::AllocationError, NotImplementedError, UnsupportedPlatform => error
43
+ warn "redhound: ring backend unavailable (#{error.message}); using socket"
44
+ Linux::PacketSocket.new(interface:, **options)
45
+ end
46
+ else
47
+ Linux::PacketSocket.new(interface:, **options)
48
+ end
49
+ else raise ArgumentError, "invalid Linux capture backend: #{backend}"
50
+ end
51
+ elsif RUBY_PLATFORM.include?('darwin')
52
+ raise ArgumentError, "invalid macOS capture backend: #{backend}" unless %i[auto bpf].include?(backend)
53
+
54
+ Bsd::BpfDevice.new(interface:, **options)
55
+ else
56
+ raise UnsupportedPlatform, "unsupported capture platform: #{RUBY_PLATFORM}"
57
+ end
58
+ return source unless block_given?
59
+
60
+ begin
61
+ yield source
62
+ ensure
63
+ source.close
64
+ end
65
+ end
66
+
67
+ # @rbs () -> Array[Interface]
68
+ def self.interfaces = Interface.all
69
+ end
70
+ end
@@ -0,0 +1,42 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Redhound
5
+ # @api private
6
+ module CLI
7
+ # @api private
8
+ class Command
9
+ # @rbs (Array[String] argv, ?out: untyped, ?err: untyped) -> Integer
10
+ def run(argv, out: $stdout, err: $stderr)
11
+ options = Options.new(argv)
12
+ values = options.values
13
+ values[:plugins].each { |path| require ::File.expand_path(path) }
14
+ if values[:help]
15
+ out.puts(options.parser)
16
+ elsif values[:version]
17
+ out.puts("Redhound #{VERSION}")
18
+ elsif values[:list_interfaces]
19
+ Capture.interfaces.each { |i| out.puts(i.to_s) }
20
+ elsif values[:list_protocols]
21
+ Registry.default.protocols.each_value do |klass|
22
+ fields = klass.compiled_header&.definitions&.map(&:name) || []
23
+ out.puts("#{klass.protocol_id}: #{klass.protocol_name} #{fields.join(' ')}")
24
+ end
25
+ else
26
+ Runner.new(values, out: out, err: err).run
27
+ end
28
+ 0
29
+ rescue ConfigurationError, FilterError, ArgumentError => e
30
+ err.puts("redhound: #{e.message}")
31
+ if e.is_a?(FilterSyntaxError) && e.expression && e.position
32
+ err.puts(" #{e.expression}", " #{' ' * e.position}^")
33
+ end
34
+ 2
35
+ rescue Error, SystemCallError, IOError, SocketError, LoadError => e
36
+ err.puts("redhound: #{e.message}")
37
+ err.puts(e.backtrace) if values && (values[:debug] || ENV['REDHOUND_DEBUG'] == '1')
38
+ 1
39
+ end
40
+ end
41
+ end
42
+ end