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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +51 -0
- data/README.md +97 -31
- data/docs/API.md +104 -0
- data/docs/FILTERS.md +37 -0
- data/docs/MIGRATION.md +23 -0
- data/docs/PROTOCOLS.md +54 -0
- data/docs/USAGE.md +86 -0
- data/docs/VALIDATION.md +175 -0
- data/docs/json-schema.json +64 -0
- data/exe/redhound +2 -1
- data/lib/redhound/analysis/flow_key.rb +59 -0
- data/lib/redhound/analysis/flow_table.rb +124 -0
- data/lib/redhound/analysis/ip_reassembler.rb +209 -0
- data/lib/redhound/analysis/stats.rb +150 -0
- data/lib/redhound/analysis/tcp_analysis.rb +43 -0
- data/lib/redhound/analysis/tcp_reassembler.rb +176 -0
- data/lib/redhound/analysis/tcp_stream.rb +156 -0
- data/lib/redhound/analysis.rb +157 -0
- data/lib/redhound/capture/bsd/bpf_device.rb +155 -0
- data/lib/redhound/capture/bsd/constants.rb +29 -0
- data/lib/redhound/capture/bsd/ifreq.rb +41 -0
- data/lib/redhound/capture/file_source.rb +214 -0
- data/lib/redhound/capture/interface.rb +70 -0
- data/lib/redhound/capture/linktype.rb +30 -0
- data/lib/redhound/capture/linux/constants.rb +37 -0
- data/lib/redhound/capture/linux/cooked.rb +18 -0
- data/lib/redhound/capture/linux/ifreq.rb +42 -0
- data/lib/redhound/capture/linux/packet_socket.rb +224 -0
- data/lib/redhound/capture/linux/sockaddr_ll.rb +33 -0
- data/lib/redhound/capture/linux/tpacket_v3.rb +137 -0
- data/lib/redhound/capture/source.rb +79 -0
- data/lib/redhound/capture/stats.rb +22 -0
- data/lib/redhound/capture.rb +70 -0
- data/lib/redhound/cli/command.rb +42 -0
- data/lib/redhound/cli/options.rb +133 -0
- data/lib/redhound/cli/privileges.rb +29 -0
- data/lib/redhound/cli/runner.rb +129 -0
- data/lib/redhound/context.rb +20 -0
- data/lib/redhound/cursor.rb +69 -0
- data/lib/redhound/diagnostic.rb +7 -0
- data/lib/redhound/dissector.rb +152 -0
- data/lib/redhound/engine.rb +69 -0
- data/lib/redhound/errors.rb +38 -0
- data/lib/redhound/field.rb +62 -0
- data/lib/redhound/file/format.rb +63 -0
- data/lib/redhound/file/pcap_reader.rb +43 -0
- data/lib/redhound/file/pcap_writer.rb +79 -0
- data/lib/redhound/file/pcapng_reader.rb +221 -0
- data/lib/redhound/file/pcapng_writer.rb +151 -0
- data/lib/redhound/file/rotating_writer.rb +98 -0
- data/lib/redhound/filter/analyzer.rb +197 -0
- data/lib/redhound/filter/bpf/assembler.rb +121 -0
- data/lib/redhound/filter/bpf/disassembler.rb +58 -0
- data/lib/redhound/filter/bpf/validator.rb +77 -0
- data/lib/redhound/filter/bpf/vm.rb +109 -0
- data/lib/redhound/filter/codegen.rb +413 -0
- data/lib/redhound/filter/lexer.rb +46 -0
- data/lib/redhound/filter/parser.rb +206 -0
- data/lib/redhound/filter/program.rb +55 -0
- data/lib/redhound/filter.rb +30 -0
- data/lib/redhound/layer.rb +75 -0
- data/lib/redhound/output/fields.rb +20 -0
- data/lib/redhound/output/follow.rb +64 -0
- data/lib/redhound/output/hexdump.rb +29 -0
- data/lib/redhound/output/json.rb +32 -0
- data/lib/redhound/output/summary.rb +84 -0
- data/lib/redhound/output/timestamp.rb +34 -0
- data/lib/redhound/output/tree.rb +25 -0
- data/lib/redhound/packet.rb +77 -0
- data/lib/redhound/protocols/arp.rb +39 -0
- data/lib/redhound/protocols/checksum.rb +38 -0
- data/lib/redhound/protocols/data.rb +23 -0
- data/lib/redhound/protocols/dhcp.rb +140 -0
- data/lib/redhound/protocols/dns.rb +268 -0
- data/lib/redhound/protocols/ethernet.rb +24 -0
- data/lib/redhound/protocols/http.rb +185 -0
- data/lib/redhound/protocols/icmp.rb +205 -0
- data/lib/redhound/protocols/ipv4.rb +56 -0
- data/lib/redhound/protocols/ipv6.rb +68 -0
- data/lib/redhound/protocols/link.rb +70 -0
- data/lib/redhound/protocols/llc.rb +37 -0
- data/lib/redhound/protocols/ntp.rb +45 -0
- data/lib/redhound/protocols/tcp.rb +88 -0
- data/lib/redhound/protocols/tls.rb +218 -0
- data/lib/redhound/protocols/tunnel.rb +48 -0
- data/lib/redhound/protocols/udp.rb +32 -0
- data/lib/redhound/protocols/vlan.rb +23 -0
- data/lib/redhound/reader.rb +8 -0
- data/lib/redhound/registry.rb +64 -0
- data/lib/redhound/stream_dissector.rb +168 -0
- data/lib/redhound/util/seq.rb +21 -0
- data/lib/redhound/version.rb +2 -1
- data/lib/redhound/writer.rb +25 -43
- data/lib/redhound.rb +81 -9
- data/sig/generated/redhound/analysis/flow_key.rbs +15 -0
- data/sig/generated/redhound/analysis/flow_table.rbs +102 -0
- data/sig/generated/redhound/analysis/ip_reassembler.rbs +52 -0
- data/sig/generated/redhound/analysis/stats.rbs +80 -0
- data/sig/generated/redhound/analysis/tcp_analysis.rbs +15 -0
- data/sig/generated/redhound/analysis/tcp_reassembler.rbs +39 -0
- data/sig/generated/redhound/analysis/tcp_stream.rbs +60 -0
- data/sig/generated/redhound/analysis.rbs +38 -0
- data/sig/generated/redhound/capture/bsd/bpf_device.rbs +39 -0
- data/sig/generated/redhound/capture/bsd/constants.rbs +40 -0
- data/sig/generated/redhound/capture/bsd/ifreq.rbs +15 -0
- data/sig/generated/redhound/capture/file_source.rbs +67 -0
- data/sig/generated/redhound/capture/interface.rbs +53 -0
- data/sig/generated/redhound/capture/linktype.rbs +20 -0
- data/sig/generated/redhound/capture/linux/constants.rbs +58 -0
- data/sig/generated/redhound/capture/linux/cooked.rbs +15 -0
- data/sig/generated/redhound/capture/linux/ifreq.rbs +18 -0
- data/sig/generated/redhound/capture/linux/packet_socket.rbs +64 -0
- data/sig/generated/redhound/capture/linux/sockaddr_ll.rbs +31 -0
- data/sig/generated/redhound/capture/linux/tpacket_v3.rbs +35 -0
- data/sig/generated/redhound/capture/source.rbs +44 -0
- data/sig/generated/redhound/capture/stats.rbs +25 -0
- data/sig/generated/redhound/capture.rbs +14 -0
- data/sig/generated/redhound/cli/command.rbs +12 -0
- data/sig/generated/redhound/cli/options.rbs +25 -0
- data/sig/generated/redhound/cli/privileges.rbs +12 -0
- data/sig/generated/redhound/cli/runner.rbs +27 -0
- data/sig/generated/redhound/context.rbs +27 -0
- data/sig/generated/redhound/cursor.rbs +59 -0
- data/sig/generated/redhound/diagnostic.rbs +21 -0
- data/sig/generated/redhound/dissector.rbs +117 -0
- data/sig/generated/redhound/engine.rbs +23 -0
- data/sig/generated/redhound/errors.rbs +56 -0
- data/sig/generated/redhound/field.rbs +74 -0
- data/sig/generated/redhound/file/format.rbs +39 -0
- data/sig/generated/redhound/file/pcap_reader.rbs +19 -0
- data/sig/generated/redhound/file/pcap_writer.rbs +38 -0
- data/sig/generated/redhound/file/pcapng_reader.rbs +54 -0
- data/sig/generated/redhound/file/pcapng_writer.rbs +50 -0
- data/sig/generated/redhound/file/rotating_writer.rbs +42 -0
- data/sig/generated/redhound/filter/analyzer.rbs +42 -0
- data/sig/generated/redhound/filter/bpf/assembler.rbs +39 -0
- data/sig/generated/redhound/filter/bpf/disassembler.rbs +18 -0
- data/sig/generated/redhound/filter/bpf/validator.rbs +30 -0
- data/sig/generated/redhound/filter/bpf/vm.rbs +28 -0
- data/sig/generated/redhound/filter/codegen.rbs +101 -0
- data/sig/generated/redhound/filter/lexer.rbs +15 -0
- data/sig/generated/redhound/filter/parser.rbs +65 -0
- data/sig/generated/redhound/filter/program.rbs +48 -0
- data/sig/generated/redhound/filter.rbs +10 -0
- data/sig/generated/redhound/layer.rbs +81 -0
- data/sig/generated/redhound/output/fields.rbs +15 -0
- data/sig/generated/redhound/output/follow.rbs +21 -0
- data/sig/generated/redhound/output/hexdump.rbs +15 -0
- data/sig/generated/redhound/output/json.rbs +18 -0
- data/sig/generated/redhound/output/summary.rbs +21 -0
- data/sig/generated/redhound/output/timestamp.rbs +21 -0
- data/sig/generated/redhound/output/tree.rbs +15 -0
- data/sig/generated/redhound/packet.rbs +82 -0
- data/sig/generated/redhound/protocols/arp.rbs +15 -0
- data/sig/generated/redhound/protocols/checksum.rbs +15 -0
- data/sig/generated/redhound/protocols/data.rbs +17 -0
- data/sig/generated/redhound/protocols/dhcp.rbs +31 -0
- data/sig/generated/redhound/protocols/dns.rbs +42 -0
- data/sig/generated/redhound/protocols/ethernet.rbs +15 -0
- data/sig/generated/redhound/protocols/http.rbs +36 -0
- data/sig/generated/redhound/protocols/icmp.rbs +41 -0
- data/sig/generated/redhound/protocols/ipv4.rbs +15 -0
- data/sig/generated/redhound/protocols/ipv6.rbs +24 -0
- data/sig/generated/redhound/protocols/link.rbs +33 -0
- data/sig/generated/redhound/protocols/llc.rbs +15 -0
- data/sig/generated/redhound/protocols/ntp.rbs +17 -0
- data/sig/generated/redhound/protocols/tcp.rbs +25 -0
- data/sig/generated/redhound/protocols/tls.rbs +51 -0
- data/sig/generated/redhound/protocols/tunnel.rbs +24 -0
- data/sig/generated/redhound/protocols/udp.rbs +18 -0
- data/sig/generated/redhound/protocols/vlan.rbs +12 -0
- data/sig/generated/redhound/reader.rbs +7 -0
- data/sig/generated/redhound/registry.rbs +45 -0
- data/sig/generated/redhound/stream_dissector.rbs +44 -0
- data/sig/generated/redhound/util/seq.rbs +20 -0
- data/sig/generated/redhound/version.rbs +1 -0
- data/sig/generated/redhound/writer.rbs +6 -19
- data/sig/generated/redhound.rbs +18 -0
- data/sig/shims/io.rbs +7 -0
- data/sig/shims/process.rbs +3 -0
- metadata +177 -55
- data/CODE_OF_CONDUCT.md +0 -132
- data/Dockerfile +0 -7
- data/Rakefile +0 -15
- data/Steepfile +0 -4
- data/docker-compose.yml +0 -9
- data/lib/redhound/analyzer.rb +0 -34
- data/lib/redhound/builder/packet_mreq.rb +0 -56
- data/lib/redhound/builder/socket.rb +0 -35
- data/lib/redhound/builder.rb +0 -4
- data/lib/redhound/command.rb +0 -67
- data/lib/redhound/l2/ether.rb +0 -68
- data/lib/redhound/l2/protocol.rb +0 -33
- data/lib/redhound/l2.rb +0 -4
- data/lib/redhound/l3/arp.rb +0 -114
- data/lib/redhound/l3/base.rb +0 -49
- data/lib/redhound/l3/ipv4.rb +0 -95
- data/lib/redhound/l3/ipv6.rb +0 -69
- data/lib/redhound/l3/protocol.rb +0 -173
- data/lib/redhound/l3/resolver.rb +0 -30
- data/lib/redhound/l3.rb +0 -9
- data/lib/redhound/l4/base.rb +0 -31
- data/lib/redhound/l4/icmp.rb +0 -71
- data/lib/redhound/l4/resolver.rb +0 -28
- data/lib/redhound/l4/udp.rb +0 -63
- data/lib/redhound/l4.rb +0 -6
- data/lib/redhound/receiver.rb +0 -45
- data/lib/redhound/resolver.rb +0 -22
- data/lib/redhound/source/socket.rb +0 -18
- data/lib/redhound/source.rb +0 -3
- data/rbs_collection.lock.yaml +0 -20
- data/rbs_collection.yaml +0 -17
- data/sig/generated/redhound/analyzer.rbs +0 -14
- data/sig/generated/redhound/builder/packet_mreq.rbs +0 -39
- data/sig/generated/redhound/builder/socket.rbs +0 -24
- data/sig/generated/redhound/command.rbs +0 -19
- data/sig/generated/redhound/l2/ether.rbs +0 -41
- data/sig/generated/redhound/l2/protocol.rbs +0 -15
- data/sig/generated/redhound/l3/arp.rbs +0 -57
- data/sig/generated/redhound/l3/base.rbs +0 -28
- data/sig/generated/redhound/l3/ipv4.rbs +0 -53
- data/sig/generated/redhound/l3/ipv6.rbs +0 -38
- data/sig/generated/redhound/l3/protocol.rbs +0 -16
- data/sig/generated/redhound/l3/resolver.rbs +0 -16
- data/sig/generated/redhound/l4/base.rbs +0 -19
- data/sig/generated/redhound/l4/icmp.rbs +0 -33
- data/sig/generated/redhound/l4/resolver.rbs +0 -16
- data/sig/generated/redhound/l4/udp.rbs +0 -36
- data/sig/generated/redhound/receiver.rbs +0 -19
- data/sig/generated/redhound/resolver.rbs +0 -11
- data/sig/generated/redhound/source/socket.rbs +0 -13
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'ipaddr'
|
|
5
|
+
|
|
6
|
+
module Redhound
|
|
7
|
+
# Immutable description of a field location, type and display metadata.
|
|
8
|
+
FieldDefinition = Data.define(:key, :name, :type, :offset, :length, :index, :shift, :mask, :scale, :format, :enum)
|
|
9
|
+
|
|
10
|
+
# A decoded field with its definition, raw value and absolute location.
|
|
11
|
+
class Field
|
|
12
|
+
# Definition, raw decoded value and capture location.
|
|
13
|
+
attr_reader :definition, :raw_value, :offset, :length
|
|
14
|
+
|
|
15
|
+
# @rbs (untyped definition, untyped value, Integer base) -> void
|
|
16
|
+
# Bind a field definition to its value and layer base offset.
|
|
17
|
+
def initialize(definition, value, base)
|
|
18
|
+
@definition, @raw_value = definition, value
|
|
19
|
+
@offset, @length = base + definition.offset, definition.length
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @rbs () -> String
|
|
23
|
+
# Stable dotted field name.
|
|
24
|
+
def name = @definition.name
|
|
25
|
+
# @rbs () -> Symbol
|
|
26
|
+
# Declared field representation.
|
|
27
|
+
def type = @definition.type
|
|
28
|
+
|
|
29
|
+
# @rbs () -> untyped
|
|
30
|
+
# Decoded value; addresses are returned in human-readable text.
|
|
31
|
+
def value
|
|
32
|
+
case type
|
|
33
|
+
when :ipv4 then "#{(@raw_value >> 24) & 255}.#{(@raw_value >> 16) & 255}.#{(@raw_value >> 8) & 255}.#{@raw_value & 255}"
|
|
34
|
+
when :ipv6 then IPAddr.new_ntoh(@raw_value).to_s
|
|
35
|
+
when :mac then @raw_value.unpack('H2' * 6).join(':')
|
|
36
|
+
else @raw_value
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @rbs () -> untyped
|
|
41
|
+
# JSON-safe value; opaque bytes and invalid text are encoded in hexadecimal.
|
|
42
|
+
def json_value
|
|
43
|
+
return @raw_value.unpack1('H*') if type == :bytes
|
|
44
|
+
val = value
|
|
45
|
+
return val unless val.is_a?(String)
|
|
46
|
+
utf8 = val.dup.force_encoding(Encoding::UTF_8)
|
|
47
|
+
utf8.valid_encoding? ? utf8 : val.unpack1('H*')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @rbs () -> String
|
|
51
|
+
# Terminal-safe field text with escaped control and non-ASCII bytes.
|
|
52
|
+
def display
|
|
53
|
+
val = value
|
|
54
|
+
return val if type == :ipv4 || type == :ipv6 || type == :mac
|
|
55
|
+
return "#{@definition.enum[val]} (#{val})" if @definition.enum&.key?(val)
|
|
56
|
+
return format('0x%x', val) if @definition.format == :hex
|
|
57
|
+
return @raw_value.unpack1('H*') if type == :bytes
|
|
58
|
+
return val.b.gsub(/[^\x20-\x7e]/n) { |c| format('\\x%02x', c.getbyte(0)) } if val.is_a?(String)
|
|
59
|
+
val.to_s
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module File
|
|
7
|
+
# @api private
|
|
8
|
+
module Format
|
|
9
|
+
MAX_RECORD = 16 * 1024 * 1024
|
|
10
|
+
MAX_INTERFACES = 4096
|
|
11
|
+
MAX_INTERFACE_METADATA = 16 * 1024 * 1024
|
|
12
|
+
MAX_OPTIONS = 4096
|
|
13
|
+
PCAP_MAGICS = {
|
|
14
|
+
"\xd4\xc3\xb2\xa1".b => [:little, 1_000], "\xa1\xb2\xc3\xd4".b => [:big, 1_000],
|
|
15
|
+
"\x4d\x3c\xb2\xa1".b => [:little, 1], "\xa1\xb2\x3c\x4d".b => [:big, 1]
|
|
16
|
+
}.freeze
|
|
17
|
+
PCAPNG_MAGIC = "\x0a\x0d\x0d\x0a".b.freeze
|
|
18
|
+
|
|
19
|
+
# @rbs (untyped io, Integer length, ?eof: false) -> String
|
|
20
|
+
# @rbs (untyped io, Integer length, eof: true) -> String?
|
|
21
|
+
def self.read_exact(io, length, eof: false)
|
|
22
|
+
raise FileFormatError, "invalid record length #{length}" unless length.between?(0, MAX_RECORD)
|
|
23
|
+
return ''.b if length.zero?
|
|
24
|
+
|
|
25
|
+
result = io.read(length)
|
|
26
|
+
return result if result && result.bytesize == length
|
|
27
|
+
return nil if eof && (!result || result.empty?)
|
|
28
|
+
|
|
29
|
+
result ||= ''.b
|
|
30
|
+
while result.bytesize < length
|
|
31
|
+
part = io.read(length - result.bytesize)
|
|
32
|
+
if !part || part.empty?
|
|
33
|
+
return nil if eof && result.empty?
|
|
34
|
+
|
|
35
|
+
raise FileFormatError, "truncated capture file (wanted #{length}, read #{result.bytesize})"
|
|
36
|
+
end
|
|
37
|
+
result << part
|
|
38
|
+
end
|
|
39
|
+
result
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @rbs (Integer length) -> Integer
|
|
43
|
+
def self.padded(length) = (length + 3) & ~3
|
|
44
|
+
|
|
45
|
+
# @rbs (String data) -> String
|
|
46
|
+
def self.pad(data) = data + "\0" * (padded(data.bytesize) - data.bytesize)
|
|
47
|
+
|
|
48
|
+
# NULL carries a host-order address family; libpcap also normalizes it on read.
|
|
49
|
+
# @rbs (String data, Integer linktype, String integer_format) -> String
|
|
50
|
+
def self.normalize_null(data, linktype, integer_format)
|
|
51
|
+
data[0, 4] = [data.unpack1(integer_format)].pack('L') if linktype.zero? && data.bytesize >= 4
|
|
52
|
+
data
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @rbs (Integer caplen, Integer original_length, Integer snaplen) -> void
|
|
56
|
+
def self.check_lengths(caplen, original_length, snaplen)
|
|
57
|
+
unless caplen.between?(0, MAX_RECORD) && (snaplen.zero? || caplen <= snaplen) && original_length >= caplen
|
|
58
|
+
raise FileFormatError, "invalid packet lengths (captured #{caplen}, original #{original_length}, snaplen #{snaplen})"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module File
|
|
7
|
+
# @api private
|
|
8
|
+
class PcapReader
|
|
9
|
+
attr_reader :interfaces, :linktype
|
|
10
|
+
|
|
11
|
+
# @rbs (untyped io, String magic) -> void
|
|
12
|
+
def initialize(io, magic)
|
|
13
|
+
@io = io
|
|
14
|
+
endian, @multiplier = Format::PCAP_MAGICS.fetch(magic) #: [Symbol, Integer]
|
|
15
|
+
@integer_format = endian == :little ? 'V' : 'N'
|
|
16
|
+
header = Format.read_exact(io, 20)
|
|
17
|
+
major, minor, _zone, _accuracy, @snaplen, network = header.unpack(endian == :little ? 'vvV4' : 'nnN4') #: [Integer, Integer, Integer, Integer, Integer, Integer]
|
|
18
|
+
raise FileFormatError, "unsupported pcap version #{major}.#{minor}" unless major == 2 && minor == 4
|
|
19
|
+
raise FileFormatError, 'invalid pcap snaplen' unless @snaplen.between?(1, Format::MAX_RECORD)
|
|
20
|
+
|
|
21
|
+
@linktype = network & 0xffff
|
|
22
|
+
@interfaces = [Capture::Interface.new(name: 'pcap', linktype: @linktype, snaplen: @snaplen)]
|
|
23
|
+
@number = 0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @rbs () -> Packet?
|
|
27
|
+
def next_packet
|
|
28
|
+
record = Format.read_exact(@io, 16, eof: true)
|
|
29
|
+
return nil unless record
|
|
30
|
+
|
|
31
|
+
seconds, fraction, caplen, original_length = record.unpack("#{@integer_format}4") #: [Integer, Integer, Integer, Integer]
|
|
32
|
+
Format.check_lengths(caplen, original_length, @snaplen)
|
|
33
|
+
raise FileFormatError, 'invalid pcap timestamp fraction' unless fraction < 1_000_000_000 / @multiplier
|
|
34
|
+
|
|
35
|
+
data = Format.normalize_null(Format.read_exact(@io, caplen), @linktype, @integer_format)
|
|
36
|
+
@number += 1
|
|
37
|
+
timestamp_ns = seconds * 1_000_000_000 + fraction * @multiplier #: Integer
|
|
38
|
+
Packet.new(data, timestamp_ns:,
|
|
39
|
+
original_length:, linktype: @linktype, interface: @interfaces.first, number: @number)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module File
|
|
7
|
+
# @api private
|
|
8
|
+
class PcapWriter
|
|
9
|
+
attr_reader :bytes_written, :path
|
|
10
|
+
|
|
11
|
+
# @rbs (untyped output, ?linktype: Integer | Symbol | String, ?snaplen: Integer, ?precision: Symbol, ?packet_buffered: bool, **untyped) -> void
|
|
12
|
+
def initialize(output, linktype: :ethernet, snaplen: 262_144, precision: :nano, packet_buffered: false, **_options)
|
|
13
|
+
raise ArgumentError, 'snaplen must be between 1 and 16 MiB' unless snaplen.between?(1, Format::MAX_RECORD)
|
|
14
|
+
raise ArgumentError, 'precision must be micro or nano' unless %i[micro nano].include?(precision)
|
|
15
|
+
|
|
16
|
+
@linktype = Capture::Linktype.resolve(linktype)
|
|
17
|
+
@snaplen, @precision, @packet_buffered = snaplen, precision, packet_buffered
|
|
18
|
+
@owned = output.is_a?(String) && output != '-'
|
|
19
|
+
@path = output.is_a?(String) ? output : nil
|
|
20
|
+
@io = output.is_a?(String) ? (output == '-' ? $stdout : ::File.open(output, ::File::WRONLY | ::File::CREAT | ::File::TRUNC, 0o600)) : output
|
|
21
|
+
@io.chmod(0o600) if @owned
|
|
22
|
+
@io.binmode if @io.respond_to?(:binmode)
|
|
23
|
+
@bytes_written = 0
|
|
24
|
+
@closed = false
|
|
25
|
+
magic = precision == :nano ? 0xa1b23c4d : 0xa1b2c3d4
|
|
26
|
+
write_bytes([magic, 2, 4, 0, 0, snaplen, @linktype].pack('VvvV4'))
|
|
27
|
+
rescue StandardError, Interrupt
|
|
28
|
+
@io.close if @owned && @io && !@io.closed?
|
|
29
|
+
raise
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @rbs (Packet packet) -> self
|
|
33
|
+
def write(packet)
|
|
34
|
+
raise IOError, 'writer is closed' if @closed
|
|
35
|
+
raise FileFormatError, 'pcap cannot store multiple link types; use pcapng' unless packet.linktype == @linktype
|
|
36
|
+
raise FileFormatError, 'packet original length exceeds unsigned 32-bit range' unless packet.original_length <= 0xffff_ffff
|
|
37
|
+
|
|
38
|
+
seconds, nanos = packet.timestamp_ns.divmod(1_000_000_000)
|
|
39
|
+
raise FileFormatError, 'pcap timestamp exceeds unsigned 32-bit seconds' unless seconds.between?(0, 0xffff_ffff)
|
|
40
|
+
|
|
41
|
+
data = packet.data.byteslice(0, @snaplen)
|
|
42
|
+
fraction = @precision == :nano ? nanos : nanos / 1_000
|
|
43
|
+
write_bytes([seconds, fraction, data.bytesize, packet.original_length].pack('V4') + data)
|
|
44
|
+
@io.flush if @packet_buffered
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
alias << write
|
|
49
|
+
|
|
50
|
+
# @rbs (Packet packet) -> Integer
|
|
51
|
+
def record_size(packet) = 16 + [packet.caplen, @snaplen].min
|
|
52
|
+
|
|
53
|
+
# @rbs (Capture::Stats stats, ?interface: Capture::Interface?) -> void
|
|
54
|
+
def write_stats(stats, interface: nil); end
|
|
55
|
+
|
|
56
|
+
# @rbs () -> void
|
|
57
|
+
def flush = @io.flush
|
|
58
|
+
|
|
59
|
+
# @rbs () -> void
|
|
60
|
+
def close
|
|
61
|
+
return if @closed
|
|
62
|
+
|
|
63
|
+
begin
|
|
64
|
+
@io.flush
|
|
65
|
+
ensure
|
|
66
|
+
@closed = true
|
|
67
|
+
@io.close if @owned && !@io.closed?
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# @rbs (String bytes) -> void
|
|
74
|
+
def write_bytes(bytes)
|
|
75
|
+
@bytes_written += @io.write(bytes)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module File
|
|
7
|
+
# @api private
|
|
8
|
+
class PcapngReader
|
|
9
|
+
attr_reader :interfaces, :stats
|
|
10
|
+
|
|
11
|
+
# @rbs (untyped io, String magic) -> void
|
|
12
|
+
def initialize(io, magic)
|
|
13
|
+
@io = io
|
|
14
|
+
@interfaces = [] #: Array[Capture::Interface]
|
|
15
|
+
@section_interfaces = [] #: Array[Capture::Interface]
|
|
16
|
+
@statistics = {} #: Hash[Integer, Hash[Symbol, Integer]]
|
|
17
|
+
@packet_counts = {} #: Hash[Integer, Integer]
|
|
18
|
+
@stats = Capture::Stats.new
|
|
19
|
+
@number = 0
|
|
20
|
+
@interface_metadata = 0
|
|
21
|
+
@magic = magic
|
|
22
|
+
@integer_format = 'V'
|
|
23
|
+
read_block
|
|
24
|
+
while (block = read_block)
|
|
25
|
+
type, body = block
|
|
26
|
+
if type == 1
|
|
27
|
+
read_interface(body)
|
|
28
|
+
break
|
|
29
|
+
end
|
|
30
|
+
raise FileFormatError, 'pcapng packet precedes interface description' if [3, 5, 6].include?(type)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @rbs () -> Integer
|
|
35
|
+
def linktype = @interfaces.first&.linktype || 1
|
|
36
|
+
|
|
37
|
+
# @rbs () -> bool
|
|
38
|
+
def statistics? = !@statistics.empty?
|
|
39
|
+
|
|
40
|
+
# @rbs () -> Packet?
|
|
41
|
+
def next_packet
|
|
42
|
+
while (block = read_block)
|
|
43
|
+
type, body = block
|
|
44
|
+
case type
|
|
45
|
+
when 1 then read_interface(body)
|
|
46
|
+
when 6 then return read_enhanced(body)
|
|
47
|
+
when 3 then return read_simple(body)
|
|
48
|
+
when 5 then read_statistics(body)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# @rbs () -> [Integer, String]?
|
|
57
|
+
def read_block
|
|
58
|
+
header = @magic ? @magic + Format.read_exact(@io, 4) : Format.read_exact(@io, 8, eof: true)
|
|
59
|
+
@magic = nil
|
|
60
|
+
return nil unless header
|
|
61
|
+
|
|
62
|
+
section = header.byteslice(0, 4) == Format::PCAPNG_MAGIC
|
|
63
|
+
prefix = section ? Format.read_exact(@io, 4) : ''.b
|
|
64
|
+
if section
|
|
65
|
+
@integer_format = case prefix
|
|
66
|
+
when "\x4d\x3c\x2b\x1a".b then 'V'
|
|
67
|
+
when "\x1a\x2b\x3c\x4d".b then 'N'
|
|
68
|
+
else raise FileFormatError, 'invalid pcapng byte order magic'
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
type, length = header.unpack("#{@integer_format}2") #: [Integer, Integer]
|
|
72
|
+
minimum = section ? 28 : 12
|
|
73
|
+
unless length.between?(minimum, Format::MAX_RECORD) && (length & 3).zero?
|
|
74
|
+
raise FileFormatError, "invalid pcapng block length #{length}"
|
|
75
|
+
end
|
|
76
|
+
contents = prefix + Format.read_exact(@io, length - 8 - prefix.bytesize)
|
|
77
|
+
raise FileFormatError, 'pcapng block lengths do not match' unless contents.unpack1(@integer_format, offset: contents.bytesize - 4) == length
|
|
78
|
+
|
|
79
|
+
body = contents.byteslice(0, contents.bytesize - 4) #: String
|
|
80
|
+
if section
|
|
81
|
+
version = body.unpack(@integer_format == 'V' ? 'vv' : 'nn', offset: 4)
|
|
82
|
+
raise FileFormatError, 'unsupported pcapng section version' unless version == [1, 0]
|
|
83
|
+
|
|
84
|
+
@section_interfaces = [] #: Array[Capture::Interface]
|
|
85
|
+
end
|
|
86
|
+
[type, body]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @rbs (String body) -> void
|
|
90
|
+
def read_interface(body)
|
|
91
|
+
require_size(body, 8)
|
|
92
|
+
raise FileFormatError, 'pcapng interface limit exceeded' if @interfaces.size >= Format::MAX_INTERFACES
|
|
93
|
+
linktype, _reserved, snaplen = body.unpack(@integer_format == 'V' ? 'vvV' : 'nnN') #: [Integer, Integer, Integer]
|
|
94
|
+
raise FileFormatError, 'invalid pcapng snaplen' if snaplen > Format::MAX_RECORD
|
|
95
|
+
|
|
96
|
+
opts = options(body, 8, [2, 3, 9, 11, 14])
|
|
97
|
+
resolution = opts.fetch(9, ["\x06".b]).fetch(0)
|
|
98
|
+
raise FileFormatError, 'invalid pcapng timestamp resolution' unless resolution.bytesize == 1
|
|
99
|
+
|
|
100
|
+
exponent = resolution.getbyte(0) #: Integer
|
|
101
|
+
units = exponent < 128 ? 10**exponent : 2**(exponent & 127)
|
|
102
|
+
offset = opts.fetch(14, ["\0" * 8]).fetch(0)
|
|
103
|
+
raise FileFormatError, 'invalid pcapng timestamp offset' unless offset.bytesize == 8
|
|
104
|
+
|
|
105
|
+
index = @section_interfaces.size
|
|
106
|
+
filter_option = opts[11]&.first
|
|
107
|
+
interface = Capture::Interface.new(name: opts.fetch(2, ["interface#{index}"]).fetch(0), index:, linktype:, snaplen:,
|
|
108
|
+
description: opts[3]&.first, filter: filter_option && filter_option.getbyte(0) == 0 ? filter_option.byteslice(1..) : nil,
|
|
109
|
+
meta: { timestamp_units: units, timestamp_offset: offset.unpack1(@integer_format == 'V' ? 'q<' : 'q>') })
|
|
110
|
+
metadata_size = 512 + interface.name.bytesize + (interface.description&.bytesize || 0) + (interface.filter&.bytesize || 0)
|
|
111
|
+
if @interface_metadata + metadata_size > Format::MAX_INTERFACE_METADATA
|
|
112
|
+
raise FileFormatError, 'pcapng interface metadata limit exceeded'
|
|
113
|
+
end
|
|
114
|
+
@interface_metadata += metadata_size
|
|
115
|
+
@section_interfaces << interface
|
|
116
|
+
@interfaces << interface
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @rbs (String body) -> Packet
|
|
120
|
+
def read_enhanced(body)
|
|
121
|
+
require_size(body, 20)
|
|
122
|
+
index, high, low, caplen, original_length = body.unpack("#{@integer_format}5") #: [Integer, Integer, Integer, Integer, Integer]
|
|
123
|
+
interface = interface_at(index)
|
|
124
|
+
Format.check_lengths(caplen, original_length, interface.snaplen)
|
|
125
|
+
require_size(body, 20 + Format.padded(caplen))
|
|
126
|
+
opts = options(body, 20 + Format.padded(caplen), [1, 2])
|
|
127
|
+
flags_option = opts[2]&.first
|
|
128
|
+
raise FileFormatError, 'invalid pcapng packet flags' if flags_option && flags_option.bytesize != 4
|
|
129
|
+
|
|
130
|
+
flags = flags_option ? flags_option.unpack1(@integer_format) : 0 #: Integer
|
|
131
|
+
direction = { 1 => :in, 2 => :out }[flags & 3]
|
|
132
|
+
meta = { epb_flags: flags } #: Hash[Symbol, untyped]
|
|
133
|
+
meta[:comment] = opts[1].join("\n") if opts[1]
|
|
134
|
+
pkttype = direction == :out ? 4 : { 1 => 0, 2 => 2, 3 => 1, 4 => 3 }[(flags >> 2) & 7]
|
|
135
|
+
meta[:pkttype] = pkttype if pkttype
|
|
136
|
+
timestamp_ns = ((high << 32) | low) * 1_000_000_000 / interface.meta.fetch(:timestamp_units) +
|
|
137
|
+
interface.meta.fetch(:timestamp_offset) * 1_000_000_000
|
|
138
|
+
data = body.byteslice(20, caplen) #: String
|
|
139
|
+
make_packet(data, interface, original_length, timestamp_ns, direction, meta)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# @rbs (String body) -> Packet
|
|
143
|
+
def read_simple(body)
|
|
144
|
+
require_size(body, 4)
|
|
145
|
+
interface = interface_at(0)
|
|
146
|
+
original_length = body.unpack1(@integer_format) #: Integer
|
|
147
|
+
caplen = interface.snaplen.zero? ? original_length : [original_length, interface.snaplen].min
|
|
148
|
+
Format.check_lengths(caplen, original_length, interface.snaplen)
|
|
149
|
+
raise FileFormatError, 'invalid pcapng simple packet length' unless body.bytesize == 4 + Format.padded(caplen)
|
|
150
|
+
|
|
151
|
+
data = body.byteslice(4, caplen) #: String
|
|
152
|
+
make_packet(data, interface, original_length, 0, nil, {})
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @rbs (String body) -> void
|
|
156
|
+
def read_statistics(body)
|
|
157
|
+
require_size(body, 12)
|
|
158
|
+
index = body.unpack1(@integer_format) #: Integer
|
|
159
|
+
interface = interface_at(index)
|
|
160
|
+
opts = options(body, 12, [4, 5, 7, 8])
|
|
161
|
+
values = {} #: Hash[Symbol, Integer]
|
|
162
|
+
{ received: 4, dropped: 5, if_dropped: 7, captured: 8 }.each do |name, code|
|
|
163
|
+
option = opts[code]&.first
|
|
164
|
+
next unless option
|
|
165
|
+
raise FileFormatError, 'invalid pcapng statistics counter' unless option.bytesize == 8
|
|
166
|
+
|
|
167
|
+
count = option.unpack1(@integer_format == 'V' ? 'Q<' : 'Q>') #: Integer
|
|
168
|
+
values[name] = count
|
|
169
|
+
end
|
|
170
|
+
@statistics[interface.object_id] = values
|
|
171
|
+
totals = %i[received dropped if_dropped captured].to_h { |name| [name, @statistics.values.sum { |entry| entry.fetch(name, 0) }] }
|
|
172
|
+
%i[received captured].each do |name|
|
|
173
|
+
totals[name] = @packet_counts.values.sum unless @statistics.values.any? { |entry| entry.key?(name) }
|
|
174
|
+
end
|
|
175
|
+
@stats = Capture::Stats.new(**totals)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# @rbs (String data, Capture::Interface interface, Integer original_length, Integer timestamp_ns, Symbol? direction, Hash[Symbol, untyped] meta) -> Packet
|
|
179
|
+
def make_packet(data, interface, original_length, timestamp_ns, direction, meta)
|
|
180
|
+
data = Format.normalize_null(data, interface.linktype, @integer_format)
|
|
181
|
+
@number += 1
|
|
182
|
+
@packet_counts[interface.object_id] = @packet_counts.fetch(interface.object_id, 0) + 1
|
|
183
|
+
Packet.new(data, timestamp_ns:, original_length:, linktype: interface.linktype, interface:, direction:, number: @number, meta:)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# @rbs (Integer index) -> Capture::Interface
|
|
187
|
+
def interface_at(index)
|
|
188
|
+
@section_interfaces.fetch(index) { raise FileFormatError, "pcapng references unknown interface #{index}" }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# @rbs (String body, Integer offset, Array[Integer] allowed) -> Hash[Integer, Array[String]]
|
|
192
|
+
def options(body, offset, allowed)
|
|
193
|
+
result = {} #: Hash[Integer, Array[String]]
|
|
194
|
+
count = 0
|
|
195
|
+
while offset < body.bytesize
|
|
196
|
+
require_size(body, offset + 4)
|
|
197
|
+
code, length = body.unpack(@integer_format == 'V' ? 'vv' : 'nn', offset:) #: [Integer, Integer]
|
|
198
|
+
offset += 4
|
|
199
|
+
if code.zero?
|
|
200
|
+
raise FileFormatError, 'invalid pcapng end of options' unless length.zero?
|
|
201
|
+
break
|
|
202
|
+
end
|
|
203
|
+
count += 1
|
|
204
|
+
raise FileFormatError, 'pcapng option count limit exceeded' if count > Format::MAX_OPTIONS
|
|
205
|
+
require_size(body, offset + Format.padded(length))
|
|
206
|
+
if allowed.include?(code)
|
|
207
|
+
value = body.byteslice(offset, length) #: String
|
|
208
|
+
(result[code] ||= []) << value
|
|
209
|
+
end
|
|
210
|
+
offset += Format.padded(length)
|
|
211
|
+
end
|
|
212
|
+
result
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# @rbs (String body, Integer size) -> void
|
|
216
|
+
def require_size(body, size)
|
|
217
|
+
raise FileFormatError, 'truncated pcapng block' if body.bytesize < size
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module File
|
|
7
|
+
# @api private
|
|
8
|
+
class PcapngWriter
|
|
9
|
+
attr_reader :bytes_written, :path
|
|
10
|
+
|
|
11
|
+
# @rbs (untyped output, ?linktype: Integer | Symbol | String, ?snaplen: Integer, ?precision: Symbol, ?packet_buffered: bool, ?filter: String?, ?comment: String?, **untyped) -> void
|
|
12
|
+
def initialize(output, linktype: :ethernet, snaplen: 262_144, precision: :nano, packet_buffered: false,
|
|
13
|
+
filter: nil, comment: nil, **_options)
|
|
14
|
+
raise ArgumentError, 'snaplen must be between 1 and 16 MiB' unless snaplen.between?(1, Format::MAX_RECORD)
|
|
15
|
+
|
|
16
|
+
@linktype, @snaplen, @packet_buffered = Capture::Linktype.resolve(linktype), snaplen, packet_buffered
|
|
17
|
+
@filter, @comment = filter, comment
|
|
18
|
+
@owned = output.is_a?(String) && output != '-'
|
|
19
|
+
@path = output.is_a?(String) ? output : nil
|
|
20
|
+
@io = output.is_a?(String) ? (output == '-' ? $stdout : ::File.open(output, ::File::WRONLY | ::File::CREAT | ::File::TRUNC, 0o600)) : output
|
|
21
|
+
@io.chmod(0o600) if @owned
|
|
22
|
+
@io.binmode if @io.respond_to?(:binmode)
|
|
23
|
+
@bytes_written = 0
|
|
24
|
+
@interfaces, @counts, @start_times, @end_times, @statistics = {}, {}, {}, {}, {}
|
|
25
|
+
@closed = false
|
|
26
|
+
opts = { 3 => RUBY_PLATFORM, 4 => "Redhound #{VERSION}" }
|
|
27
|
+
opts[1] = comment if comment
|
|
28
|
+
write_block(0x0a0d0d0a, [0x1a2b3c4d, 1, 0, 0xffff_ffff_ffff_ffff].pack('VvvQ<') + options(opts))
|
|
29
|
+
rescue StandardError, Interrupt
|
|
30
|
+
@io.close if @owned && @io && !@io.closed?
|
|
31
|
+
raise
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @rbs (Packet packet) -> self
|
|
35
|
+
def write(packet)
|
|
36
|
+
raise IOError, 'writer is closed' if @closed
|
|
37
|
+
raise FileFormatError, 'pcapng timestamp must be nonnegative' if packet.timestamp_ns.negative?
|
|
38
|
+
raise FileFormatError, 'packet original length exceeds unsigned 32-bit range' unless packet.original_length <= 0xffff_ffff
|
|
39
|
+
|
|
40
|
+
index = ensure_interface(packet)
|
|
41
|
+
data = packet.data.byteslice(0, @snaplen)
|
|
42
|
+
flags = packet_flags(packet)
|
|
43
|
+
opts = { 2 => [flags].pack('V') }
|
|
44
|
+
opts[1] = packet.meta[:comment].to_s if packet.meta[:comment]
|
|
45
|
+
high, low = packet.timestamp_ns >> 32, packet.timestamp_ns & 0xffff_ffff
|
|
46
|
+
raise FileFormatError, 'pcapng timestamp exceeds unsigned 64-bit range' if high > 0xffff_ffff
|
|
47
|
+
|
|
48
|
+
body = [index, high, low, data.bytesize, packet.original_length].pack('V5') + Format.pad(data) + options(opts)
|
|
49
|
+
write_block(6, body)
|
|
50
|
+
@counts[index] = @counts.fetch(index, 0) + 1
|
|
51
|
+
@start_times[index] ||= packet.timestamp_ns
|
|
52
|
+
@end_times[index] = packet.timestamp_ns
|
|
53
|
+
@io.flush if @packet_buffered
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
alias << write
|
|
58
|
+
|
|
59
|
+
# @rbs (Packet packet) -> Integer
|
|
60
|
+
def record_size(packet)
|
|
61
|
+
44 + Format.padded([packet.caplen, @snaplen].min) + (packet.meta[:comment] ? 4 + Format.padded(packet.meta[:comment].to_s.bytesize) : 0)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @rbs (Capture::Stats stats, ?interface: Capture::Interface?) -> void
|
|
65
|
+
def write_stats(stats, interface: nil)
|
|
66
|
+
if interface
|
|
67
|
+
@interfaces.each { |key, index| @statistics[index] = stats if key[0] == interface.object_id }
|
|
68
|
+
elsif @interfaces.size == 1
|
|
69
|
+
@statistics[0] = stats
|
|
70
|
+
elsif @interfaces.size > 1
|
|
71
|
+
# Source statistics describe the entire capture; record them once to avoid double counting.
|
|
72
|
+
@statistics[0] = stats
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @rbs () -> void
|
|
77
|
+
def flush = @io.flush
|
|
78
|
+
|
|
79
|
+
# @rbs () -> void
|
|
80
|
+
def close
|
|
81
|
+
return if @closed
|
|
82
|
+
|
|
83
|
+
begin
|
|
84
|
+
@interfaces.each_value do |index|
|
|
85
|
+
stats = @statistics[index]
|
|
86
|
+
start_time, end_time = @start_times.fetch(index, 0), @end_times.fetch(index, 0)
|
|
87
|
+
opts = { 2 => timestamp_words(start_time), 3 => timestamp_words(end_time),
|
|
88
|
+
8 => [@counts.fetch(index, 0)].pack('Q<') }
|
|
89
|
+
if stats
|
|
90
|
+
opts[4], opts[5], opts[7] = [stats.received].pack('Q<'), [stats.dropped].pack('Q<'), [stats.if_dropped].pack('Q<')
|
|
91
|
+
opts[6] = [stats.captured].pack('Q<')
|
|
92
|
+
end
|
|
93
|
+
write_block(5, [index].pack('V') + timestamp_words(end_time) + options(opts))
|
|
94
|
+
end
|
|
95
|
+
@io.flush
|
|
96
|
+
ensure
|
|
97
|
+
@closed = true
|
|
98
|
+
@io.close if @owned && !@io.closed?
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
# @rbs (Packet packet) -> Integer
|
|
105
|
+
def ensure_interface(packet)
|
|
106
|
+
interface = packet.interface
|
|
107
|
+
key = [interface&.object_id || 0, packet.linktype]
|
|
108
|
+
return @interfaces[key] if @interfaces.key?(key)
|
|
109
|
+
raise FileFormatError, 'pcapng interface limit exceeded' if @interfaces.size >= Format::MAX_INTERFACES
|
|
110
|
+
|
|
111
|
+
index = @interfaces.size
|
|
112
|
+
opts = { 2 => interface&.name || "interface#{index}", 9 => "\x09".b, 12 => RUBY_PLATFORM }
|
|
113
|
+
opts[3] = interface.description if interface&.description
|
|
114
|
+
expression = @filter || interface&.filter
|
|
115
|
+
opts[11] = "\0" + expression if expression
|
|
116
|
+
write_block(1, [packet.linktype, 0, @snaplen].pack('vvV') + options(opts))
|
|
117
|
+
@interfaces[key] = index
|
|
118
|
+
index
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @rbs (Packet packet) -> Integer
|
|
122
|
+
def packet_flags(packet)
|
|
123
|
+
direction = { in: 1, out: 2 }.fetch(packet.direction, 0)
|
|
124
|
+
pkttype = packet.meta[:pkttype]
|
|
125
|
+
direction = pkttype == 4 ? 2 : 1 if direction.zero? && pkttype
|
|
126
|
+
reception = { 0 => 1, 1 => 3, 2 => 2, 3 => 4 }.fetch(pkttype, 0)
|
|
127
|
+
direction | (reception << 2)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @rbs (Integer timestamp) -> String
|
|
131
|
+
def timestamp_words(timestamp) = [timestamp >> 32, timestamp & 0xffff_ffff].pack('V2')
|
|
132
|
+
|
|
133
|
+
# @rbs (Hash[Integer, String] values) -> String
|
|
134
|
+
def options(values)
|
|
135
|
+
values.map do |code, value|
|
|
136
|
+
raise FileFormatError, 'pcapng option exceeds 65535 bytes' if value.bytesize > 65_535
|
|
137
|
+
|
|
138
|
+
[code, value.bytesize].pack('vv') + Format.pad(value.b)
|
|
139
|
+
end.join.b + "\0" * 4
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# @rbs (Integer type, String body) -> void
|
|
143
|
+
def write_block(type, body)
|
|
144
|
+
length = body.bytesize + 12
|
|
145
|
+
raise FileFormatError, 'pcapng block exceeds 16 MiB' if length > Format::MAX_RECORD
|
|
146
|
+
|
|
147
|
+
@bytes_written += @io.write([type, length].pack('V2') + body + [length].pack('V'))
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|