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,18 @@
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
+ module Cooked
11
+ # @rbs (SockaddrLL address) -> String
12
+ def self.header(address)
13
+ [address.protocol, 0, address.index, address.hatype, address.pkttype, address.address.bytesize, address.address].pack('n n N n C C a8')
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,42 @@
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
+ module Ifreq
11
+ # @rbs (String name, Integer command) -> String
12
+ def self.query(name, command)
13
+ raise ArgumentError, 'invalid interface name' if name.bytesize >= 16 || name.include?("\0")
14
+
15
+ socket = ::Socket.new(::Socket::AF_INET, ::Socket::SOCK_DGRAM, 0)
16
+ bytes = [name].pack('a16') + "\0" * 24
17
+ begin
18
+ socket.ioctl(command, bytes)
19
+ bytes
20
+ ensure
21
+ socket.close
22
+ end
23
+ end
24
+
25
+ # @rbs (String name, ?index: Integer) -> Interface
26
+ def self.interface(name, index: 0)
27
+ hardware = query(name, Constants::SIOCGIFHWADDR)
28
+ hatype = hardware.unpack1('S', offset: 16) #: Integer
29
+ mac_bytes = hardware.byteslice(18, 6) #: String
30
+ mac_values = mac_bytes.unpack('C6') #: Array[Integer]
31
+ mac = mac_values.map { |value| format('%02x', value) }.join(':')
32
+ flags = query(name, Constants::SIOCGIFFLAGS).unpack1('S', offset: 16) #: Integer
33
+ mtu = query(name, Constants::SIOCGIFMTU).unpack1('i', offset: 16) #: Integer
34
+ Interface.new(name:, index:, linktype: Linktype.for_hardware(hatype), flags:, mtu:, mac:,
35
+ meta: { hardware_type: hatype })
36
+ rescue Errno::ENODEV, Errno::ENXIO
37
+ raise InterfaceNotFound, "interface #{name.inspect} not found"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,180 @@
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
+ if direction == :in
36
+ begin
37
+ @socket.setsockopt(Constants::SOL_PACKET, Constants::PACKET_IGNORE_OUTGOING, [1].pack('i'))
38
+ rescue Errno::ENOPROTOOPT, Errno::EINVAL
39
+ # Older kernels are filtered by packet direction below.
40
+ end
41
+ end
42
+ rescue Errno::EPERM, Errno::EACCES => error
43
+ close_resources
44
+ raise PermissionDenied, "capture requires root or CAP_NET_RAW: #{error.message}"
45
+ rescue StandardError
46
+ close_resources
47
+ raise
48
+ end
49
+
50
+ # @rbs (?timeout: Numeric?) -> Packet?
51
+ def next_packet(timeout: nil)
52
+ deadline = deadline_for(timeout)
53
+ until @stopped || @closed
54
+ return nil unless wait_readable(@socket, deadline)
55
+
56
+ result = @socket.recvfrom_nonblock(Constants::RECEIVE_SIZE, exception: false)
57
+ next if result == :wait_readable
58
+
59
+ data, address_info = result
60
+ address = SockaddrLL.parse(address_info.to_sockaddr)
61
+ next unless accept_direction?(address)
62
+
63
+ original_length = data.bytesize
64
+ data = Cooked.header(address) + data if @cooked
65
+ original_length += 20 if @cooked
66
+ timestamp_ns = timestamp
67
+ meta = { pkttype: address.pkttype, csum_not_ready: address.hatype == 772 || address.pkttype == Constants::PACKET_OUTGOING }
68
+ meta[:possible_truncation] = true if original_length >= Constants::RECEIVE_SIZE + (@cooked ? 20 : 0)
69
+ @number += 1
70
+ @capture_stats.captured += 1
71
+ return Packet.new(data.byteslice(0, @snaplen), timestamp_ns:, original_length:, linktype: @linktype,
72
+ interface: packet_interface(address), direction: address.pkttype == Constants::PACKET_OUTGOING ? :out : :in, number: @number, meta:)
73
+ end
74
+ nil
75
+ end
76
+
77
+ # @rbs (untyped program) -> void
78
+ def attach_filter(program)
79
+ instructions = program ? program.instructions : [[0x06, 0, 0, receive_snaplen]] #: Array[[Integer, Integer, Integer, Integer]]
80
+ Filter::BPF::Validator.validate!(instructions)
81
+ instructions = instructions.map do |instruction|
82
+ code, jt, jf, k = instruction #: [Integer, Integer, Integer, Integer]
83
+ [code, jt, jf, code == 0x06 && k.positive? ? receive_snaplen : k]
84
+ end
85
+ packed = instructions.map { |instruction| instruction.pack('S C C L') }.join
86
+ @socket.setsockopt(::Socket::SOL_SOCKET, Constants::SO_ATTACH_FILTER, [instructions.size, packed].pack('S x6 P'))
87
+ end
88
+
89
+ # @rbs () -> Stats
90
+ def stats
91
+ unless @closed
92
+ counters = @socket.getsockopt(Constants::SOL_PACKET, Constants::PACKET_STATISTICS).data.unpack('L*')
93
+ @capture_stats.received += counters.fetch(0, 0)
94
+ @capture_stats.dropped += counters.fetch(1, 0)
95
+ @capture_stats.freeze_count += counters.fetch(2, 0)
96
+ @capture_stats.if_dropped = [interface_dropped - @interface_drops, 0].max
97
+ end
98
+ @capture_stats
99
+ end
100
+
101
+ # @rbs () -> void
102
+ def close
103
+ return if @closed
104
+
105
+ begin
106
+ stats
107
+ ensure
108
+ super
109
+ close_resources
110
+ end
111
+ end
112
+
113
+ protected
114
+
115
+ # @rbs () -> void
116
+ def close_resources
117
+ @socket.close if @socket && !@socket.closed?
118
+ end
119
+
120
+ # @rbs () -> void
121
+ def prime_timestamp
122
+ @socket.ioctl(Constants::SIOCGSTAMPNS, "\0" * 16)
123
+ rescue Errno::ENOENT, Errno::ENOTTY, Errno::EINVAL
124
+ nil
125
+ end
126
+
127
+ # @rbs () -> Integer
128
+ def receive_snaplen = Constants::RECEIVE_SIZE
129
+
130
+ # @rbs (Integer? buffer_size) -> void
131
+ def setup_receive_buffer(buffer_size)
132
+ return unless buffer_size
133
+ raise ArgumentError, 'buffer size must be positive' unless buffer_size.positive?
134
+
135
+ @socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVBUF, [buffer_size].pack('i'))
136
+ begin
137
+ @socket.setsockopt(::Socket::SOL_SOCKET, Constants::SO_RCVBUFFORCE, [buffer_size].pack('i'))
138
+ rescue Errno::EPERM, Errno::EACCES, Errno::ENOPROTOOPT
139
+ # SO_RCVBUF already applied without CAP_NET_ADMIN.
140
+ end
141
+ end
142
+
143
+ # @rbs (SockaddrLL address) -> bool
144
+ def accept_direction?(address)
145
+ outgoing = address.pkttype == Constants::PACKET_OUTGOING
146
+ return false if outgoing && address.hatype == 772 && @direction != :out
147
+
148
+ @direction == :inout || (@direction == :out) == outgoing
149
+ end
150
+
151
+ # @rbs (SockaddrLL address) -> Interface
152
+ def packet_interface(address)
153
+ return @interface unless @interface.name == 'any'
154
+
155
+ @any_interfaces ||= Interface.all.to_h { |interface| [interface.index, interface] }
156
+ @any_interfaces.fetch(address.index) { Interface.new(name: "if#{address.index}", index: address.index, linktype: 276) }
157
+ end
158
+
159
+ # @rbs () -> Integer
160
+ def interface_dropped
161
+ return 0 if @interface.name == 'any'
162
+
163
+ ::File.read("/sys/class/net/#{@interface.name}/statistics/rx_dropped").to_i
164
+ rescue Errno::ENOENT, Errno::EACCES
165
+ 0
166
+ end
167
+
168
+ # @rbs () -> Integer
169
+ def timestamp
170
+ buffer = "\0" * 16
171
+ @socket.ioctl(Constants::SIOCGSTAMPNS, buffer)
172
+ seconds, nanos = buffer.unpack('q q') #: [Integer, Integer]
173
+ seconds * 1_000_000_000 + nanos
174
+ rescue Errno::ENOENT, Errno::ENOTTY, Errno::EINVAL
175
+ Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
176
+ end
177
+ end
178
+ end
179
+ end
180
+ 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,132 @@
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
+ until @stopped || @closed
15
+ unless @pending_count.positive?
16
+ base = @current_block * @block_size
17
+ unless (u32(base + 8) & Constants::TP_STATUS_USER).positive?
18
+ return nil unless wait_readable(@socket, deadline)
19
+ next
20
+ end
21
+ @block_end = base + u32(base + 20)
22
+ @pending_count = u32(base + 12)
23
+ @packet_offset = base + u32(base + 16)
24
+ if @pending_count.zero?
25
+ release_block
26
+ next
27
+ end
28
+ end
29
+
30
+ offset = @packet_offset
31
+ check_ring_bounds(offset, 68)
32
+ next_offset, seconds, nanos, caplen, original_length, status = @ring.get_string(offset, 24).unpack('L6')
33
+ mac_offset = @ring.get_value(:u16, offset + 24)
34
+ check_ring_bounds(offset + mac_offset, caplen)
35
+ data = @ring.get_string(offset + mac_offset, caplen)
36
+ address = SockaddrLL.parse(@ring.get_string(offset + 48, 20))
37
+ meta = { pkttype: address.pkttype, csum_not_ready: (status & Constants::TP_STATUS_CSUMNOTREADY).positive?,
38
+ csum_valid: (status & Constants::TP_STATUS_CSUM_VALID).positive? }
39
+ if (status & Constants::TP_STATUS_VLAN_VALID).positive?
40
+ tci = u32(offset + 32) & 0xffff
41
+ tpid = (status & Constants::TP_STATUS_VLAN_TPID_VALID).positive? ? @ring.get_value(:u16, offset + 36) : 0x8100
42
+ meta[:vlan_tci], meta[:vlan_tpid] = tci, tpid
43
+ if !@cooked && @linktype == 1
44
+ original_length += 4
45
+ data = data.byteslice(0, 12) + [tpid, tci].pack('n2') + data.byteslice(12..) if data.bytesize >= 12
46
+ elsif @cooked
47
+ data = [tci, address.protocol].pack('n2') + data
48
+ original_length += 4
49
+ address = SockaddrLL.new(protocol: tpid, index: address.index, hatype: address.hatype,
50
+ pkttype: address.pkttype, address: address.address)
51
+ end
52
+ end
53
+ if @cooked
54
+ data = Cooked.header(address) + data
55
+ original_length += 20
56
+ end
57
+ @pending_count -= 1
58
+ if @pending_count.zero?
59
+ release_block
60
+ else
61
+ raise CaptureError, 'invalid ring packet offset' if next_offset < 68
62
+ @packet_offset += next_offset
63
+ end
64
+ next unless accept_direction?(address)
65
+
66
+ @number += 1
67
+ @capture_stats.captured += 1
68
+ return Packet.new(data.byteslice(0, @snaplen), timestamp_ns: seconds * 1_000_000_000 + nanos,
69
+ original_length:, linktype: @linktype, interface: packet_interface(address),
70
+ direction: address.pkttype == Constants::PACKET_OUTGOING ? :out : :in, number: @number, meta:)
71
+ end
72
+ nil
73
+ end
74
+
75
+ protected
76
+
77
+ # @rbs () -> void
78
+ def close_resources
79
+ begin
80
+ @ring.free if @ring
81
+ ensure
82
+ @ring = nil
83
+ super
84
+ end
85
+ end
86
+
87
+ # @rbs () -> Integer
88
+ def receive_snaplen = @cooked ? [@snaplen - 20, 1].max : @snaplen
89
+
90
+ # @rbs (Integer? buffer_size) -> void
91
+ def setup_receive_buffer(buffer_size)
92
+ raise ArgumentError, 'buffer size must be positive' if buffer_size && !buffer_size.positive?
93
+
94
+ @block_size = 1 << 20
95
+ @block_size *= 2 while @block_size < @snaplen + 128
96
+ @block_count = buffer_size ? [(buffer_size.to_f / @block_size).ceil, 1].max : 8
97
+ frame_size = 2048
98
+ request = [@block_size, @block_count, frame_size, @block_size * @block_count / frame_size, 50, 0, 0].pack('L7')
99
+ @socket.setsockopt(Constants::SOL_PACKET, Constants::PACKET_VERSION, [Constants::TPACKET_V3].pack('i'))
100
+ @socket.setsockopt(Constants::SOL_PACKET, Constants::PACKET_RX_RING, request)
101
+ experimental = Warning[:experimental]
102
+ begin
103
+ Warning[:experimental] = false
104
+ @ring = IO::Buffer.map(@socket, @block_size * @block_count, 0)
105
+ ensure
106
+ Warning[:experimental] = experimental
107
+ end
108
+ @current_block, @pending_count, @packet_offset, @block_end = 0, 0, 0, 0
109
+ end
110
+
111
+ private
112
+
113
+ # @rbs (Integer offset) -> Integer
114
+ def u32(offset) = @ring.get_value(:u32, offset)
115
+
116
+ # @rbs (Integer offset, Integer length) -> void
117
+ def check_ring_bounds(offset, length)
118
+ unless offset >= @current_block * @block_size && length >= 0 && offset + length <= @block_end && @block_end <= (@current_block + 1) * @block_size
119
+ raise CaptureError, 'invalid ring packet bounds'
120
+ end
121
+ end
122
+
123
+ # @rbs () -> void
124
+ def release_block
125
+ @ring.set_value(:u32, @current_block * @block_size + 8, 0)
126
+ @current_block = (@current_block + 1) % @block_count
127
+ @pending_count = 0
128
+ end
129
+ end
130
+ end
131
+ end
132
+ 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,69 @@
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/bpf_device'
16
+ require_relative 'file/format'
17
+ require_relative 'file/pcap_reader'
18
+ require_relative 'file/pcapng_reader'
19
+ require_relative 'file/pcap_writer'
20
+ require_relative 'file/pcapng_writer'
21
+ require_relative 'file/rotating_writer'
22
+ require_relative 'capture/file_source'
23
+ require_relative 'reader'
24
+
25
+ module Redhound
26
+ # @api private
27
+ module Capture
28
+ # @rbs (interface: String | Integer | Interface, ?backend: Symbol | String, ?snaplen: Integer, ?promiscuous: bool, ?buffer_size: Integer?, ?direction: Symbol, ?filter: String?) -> Source
29
+ # @rbs [T] (interface: String | Integer | Interface, ?backend: Symbol | String, ?snaplen: Integer, ?promiscuous: bool, ?buffer_size: Integer?, ?direction: Symbol, ?filter: String?) { (Source) -> T } -> T
30
+ def self.open(interface:, backend: :auto, snaplen: 262_144, promiscuous: true, buffer_size: nil, direction: :inout, filter: nil)
31
+ options = { snaplen:, promiscuous:, buffer_size:, direction:, filter: } #: Hash[Symbol, untyped]
32
+ backend = backend.to_sym
33
+ source = if RUBY_PLATFORM.include?('linux')
34
+ case backend
35
+ when :socket then Linux::PacketSocket.new(interface:, **options)
36
+ when :ring then Linux::TPacketV3.new(interface:, **options)
37
+ when :auto
38
+ if RUBY_PLATFORM.include?('x86_64')
39
+ begin
40
+ Linux::TPacketV3.new(interface:, **options)
41
+ rescue SystemCallError, IO::Buffer::AccessError, IO::Buffer::AllocationError, NotImplementedError => error
42
+ warn "redhound: ring backend unavailable (#{error.message}); using socket"
43
+ Linux::PacketSocket.new(interface:, **options)
44
+ end
45
+ else
46
+ Linux::PacketSocket.new(interface:, **options)
47
+ end
48
+ else raise ArgumentError, "invalid Linux capture backend: #{backend}"
49
+ end
50
+ elsif RUBY_PLATFORM.include?('darwin')
51
+ raise ArgumentError, "invalid macOS capture backend: #{backend}" unless %i[auto bpf].include?(backend)
52
+
53
+ Bsd::BpfDevice.new(interface:, **options)
54
+ else
55
+ raise UnsupportedPlatform, "unsupported capture platform: #{RUBY_PLATFORM}"
56
+ end
57
+ return source unless block_given?
58
+
59
+ begin
60
+ yield source
61
+ ensure
62
+ source.close
63
+ end
64
+ end
65
+
66
+ # @rbs () -> Array[Interface]
67
+ def self.interfaces = Interface.all
68
+ end
69
+ 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.index}.#{i.name} [#{i.flags & 1 != 0 ? 'Up' : 'Down'}]#{i.mac ? " #{i.mac}" : ''}#{i.mtu ? " mtu #{i.mtu}" : ''}") }
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