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,155 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Capture
|
|
7
|
+
# @api private
|
|
8
|
+
module Bsd
|
|
9
|
+
# @api private
|
|
10
|
+
class BpfDevice < Source
|
|
11
|
+
attr_reader :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
|
+
raise UnsupportedPlatform, 'any capture is only available on Linux' if @interface.name == 'any'
|
|
21
|
+
|
|
22
|
+
@snaplen, @direction = snaplen, direction
|
|
23
|
+
@device = open_device
|
|
24
|
+
if buffer_size
|
|
25
|
+
raise ArgumentError, 'buffer size must be positive' unless buffer_size.positive?
|
|
26
|
+
@device.ioctl(Constants::BIOCSBLEN, [buffer_size].pack('I'))
|
|
27
|
+
end
|
|
28
|
+
@device.ioctl(Constants::BIOCSETIF, [@interface.name].pack('a16') + "\0" * 16)
|
|
29
|
+
buffer = "\0" * 4
|
|
30
|
+
@device.ioctl(Constants::BIOCGBLEN, buffer)
|
|
31
|
+
@buffer_size = buffer.unpack1('I')
|
|
32
|
+
@device.ioctl(Constants::BIOCGDLT, buffer)
|
|
33
|
+
dlt = buffer.unpack1('I')
|
|
34
|
+
@linktype = dlt == 12 ? 101 : dlt
|
|
35
|
+
@interfaces = [Interface.new(name: @interface.name, index: @interface.index, flags: @interface.flags,
|
|
36
|
+
linktype: @linktype, snaplen:)]
|
|
37
|
+
attach_filter(filter ? Filter.compile(filter, linktype: @linktype, snaplen:) : nil)
|
|
38
|
+
@device.ioctl(Constants::BIOCPROMISC, 0) if promiscuous && !@interface.loopback?
|
|
39
|
+
@device.ioctl(Constants::BIOCIMMEDIATE, [1].pack('I'))
|
|
40
|
+
configure_direction
|
|
41
|
+
@device.ioctl(Constants::BIOCFLUSH, 0)
|
|
42
|
+
@records, @offset = ''.b, 0
|
|
43
|
+
rescue Errno::EPERM, Errno::EACCES => error
|
|
44
|
+
@device&.close
|
|
45
|
+
raise PermissionDenied, "capture requires access to /dev/bpf*: #{error.message}"
|
|
46
|
+
rescue StandardError
|
|
47
|
+
@device&.close
|
|
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
|
+
if @offset >= @records.bytesize
|
|
59
|
+
return nil unless wait_readable(@device, deadline)
|
|
60
|
+
result = @device.read_nonblock(@buffer_size, exception: false)
|
|
61
|
+
next if result == :wait_readable
|
|
62
|
+
return nil unless result
|
|
63
|
+
|
|
64
|
+
@records, @offset = result, 0
|
|
65
|
+
next if @records.empty?
|
|
66
|
+
end
|
|
67
|
+
raise CaptureError, 'truncated BPF record header' if @offset + 18 > @records.bytesize
|
|
68
|
+
|
|
69
|
+
seconds, micros, caplen, original_length, header_length = @records.unpack('I4S', offset: @offset)
|
|
70
|
+
unless header_length >= 18 && caplen <= original_length && @offset + header_length + caplen <= @records.bytesize
|
|
71
|
+
raise CaptureError, 'invalid BPF record length'
|
|
72
|
+
end
|
|
73
|
+
direction = if @extended_header && header_length >= 20
|
|
74
|
+
(@records.getbyte(@offset + 19) & 1).positive? ? :out : :in
|
|
75
|
+
elsif @direction != :inout
|
|
76
|
+
@direction
|
|
77
|
+
end
|
|
78
|
+
data = @records.byteslice(@offset + header_length, [caplen, @snaplen].min)
|
|
79
|
+
@offset += (header_length + caplen + 3) & ~3
|
|
80
|
+
@number += 1
|
|
81
|
+
@capture_stats.captured += 1
|
|
82
|
+
return Packet.new(data, timestamp_ns: seconds * 1_000_000_000 + micros * 1_000,
|
|
83
|
+
original_length:, linktype: @linktype, interface: @interfaces.first, direction:, number: @number,
|
|
84
|
+
meta: { csum_not_ready: direction == :out })
|
|
85
|
+
end
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @rbs (untyped program) -> void
|
|
90
|
+
def attach_filter(program)
|
|
91
|
+
instructions = program ? program.instructions : [[0x06, 0, 0, @snaplen]] #: Array[[Integer, Integer, Integer, Integer]]
|
|
92
|
+
Filter::BPF::Validator.validate!(instructions)
|
|
93
|
+
raise FilterTooLarge, 'macOS BPF programs are limited to 512 instructions' if instructions.size > 512
|
|
94
|
+
packed = instructions.map { |instruction| instruction.pack('S C C L') }.join
|
|
95
|
+
@device.ioctl(Constants::BIOCSETF, [instructions.size, packed].pack('L x4 P'))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# @rbs () -> Stats
|
|
99
|
+
def stats
|
|
100
|
+
unless @closed
|
|
101
|
+
buffer = "\0" * 8
|
|
102
|
+
@device.ioctl(Constants::BIOCGSTATS, buffer)
|
|
103
|
+
@capture_stats.received, @capture_stats.dropped = buffer.unpack('I2')
|
|
104
|
+
end
|
|
105
|
+
@capture_stats
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @rbs () -> void
|
|
109
|
+
def close
|
|
110
|
+
return if @closed
|
|
111
|
+
|
|
112
|
+
begin
|
|
113
|
+
stats
|
|
114
|
+
ensure
|
|
115
|
+
super
|
|
116
|
+
@device.close
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
# @rbs () -> ::File
|
|
123
|
+
def open_device
|
|
124
|
+
256.times do |index|
|
|
125
|
+
begin
|
|
126
|
+
return ::File.open("/dev/bpf#{index}", ::File::RDWR)
|
|
127
|
+
rescue Errno::EBUSY
|
|
128
|
+
next
|
|
129
|
+
rescue Errno::ENOENT
|
|
130
|
+
break
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
raise CaptureError, 'no available BPF device'
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @rbs () -> void
|
|
137
|
+
def configure_direction
|
|
138
|
+
begin
|
|
139
|
+
@device.ioctl(Constants::BIOCSDIRECTION, [{ in: 1, out: 2, inout: 3 }.fetch(@direction)].pack('I'))
|
|
140
|
+
rescue Errno::EINVAL, Errno::ENOTTY
|
|
141
|
+
raise UnsupportedPlatform, 'outgoing-only capture is unavailable on this macOS version' if @direction == :out
|
|
142
|
+
|
|
143
|
+
@device.ioctl(Constants::BIOCSSEESENT, [@direction == :in ? 0 : 1].pack('I'))
|
|
144
|
+
end
|
|
145
|
+
begin
|
|
146
|
+
@device.ioctl(Constants::BIOCSEXTHDR, [1].pack('I'))
|
|
147
|
+
@extended_header = true
|
|
148
|
+
rescue Errno::EINVAL, Errno::ENOTTY
|
|
149
|
+
@extended_header = false
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Capture
|
|
7
|
+
# @api private
|
|
8
|
+
module Bsd
|
|
9
|
+
# @api private
|
|
10
|
+
module Constants
|
|
11
|
+
# Verified against the macOS SDK's net/bpf.h on arm64 and 64-bit ioctl layout.
|
|
12
|
+
BIOCSBLEN = 0xc0044266
|
|
13
|
+
BIOCGBLEN = 0x40044266
|
|
14
|
+
BIOCSETF = 0x80104267
|
|
15
|
+
BIOCFLUSH = 0x20004268
|
|
16
|
+
BIOCPROMISC = 0x20004269
|
|
17
|
+
BIOCGDLT = 0x4004426a
|
|
18
|
+
BIOCSETIF = 0x8020426c
|
|
19
|
+
BIOCGSTATS = 0x4008426f
|
|
20
|
+
BIOCIMMEDIATE = 0x80044270
|
|
21
|
+
BIOCSSEESENT = 0x80044277
|
|
22
|
+
# Apple XNU bsd/net/bpf_private.h: extended headers expose direction at byte 19.
|
|
23
|
+
BIOCSEXTHDR = 0x8004427c
|
|
24
|
+
BIOCSDIRECTION = 0x8004428b
|
|
25
|
+
BPF_ALIGNMENT = 4
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
module Capture
|
|
6
|
+
module Bsd
|
|
7
|
+
# macOS interface metadata can be queried without opening a BPF device.
|
|
8
|
+
module Ifreq
|
|
9
|
+
SIOCGIFMTU = 0xc0206933 # Verified against sys/sockio.h; sizeof(ifreq) is 32.
|
|
10
|
+
|
|
11
|
+
# @rbs (String name, index: Integer, flags: Integer, link_address: String?) -> Interface
|
|
12
|
+
def self.interface(name, index:, flags:, link_address:)
|
|
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" * 16
|
|
17
|
+
begin
|
|
18
|
+
socket.ioctl(SIOCGIFMTU, bytes)
|
|
19
|
+
ensure
|
|
20
|
+
socket.close
|
|
21
|
+
end
|
|
22
|
+
mtu = bytes.unpack1('i', offset: 16) #: Integer
|
|
23
|
+
mac = nil # @rbs String?
|
|
24
|
+
if link_address && link_address.bytesize >= 8
|
|
25
|
+
name_length = link_address.getbyte(5) #: Integer
|
|
26
|
+
address_length = link_address.getbyte(6) #: Integer
|
|
27
|
+
if address_length == 6 && link_address.bytesize >= 8 + name_length + address_length
|
|
28
|
+
mac_bytes = link_address.byteslice(8 + name_length, address_length) #: String
|
|
29
|
+
mac_values = mac_bytes.unpack('C6') #: Array[Integer]
|
|
30
|
+
mac = mac_values.map { |value| format('%02x', value) }.join(':')
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
linktype = (flags & 8).positive? || name.start_with?('utun') ? 0 : 1
|
|
34
|
+
Interface.new(name:, index:, flags:, mtu:, mac:, linktype:)
|
|
35
|
+
rescue Errno::ENXIO, Errno::ENODEV
|
|
36
|
+
raise InterfaceNotFound, "interface #{name.inspect} not found"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Capture
|
|
7
|
+
# @api private
|
|
8
|
+
class FileSource < Source
|
|
9
|
+
class ReadStopped < StandardError; end
|
|
10
|
+
|
|
11
|
+
# @api private
|
|
12
|
+
class FileInput
|
|
13
|
+
# @rbs (::File io) -> void
|
|
14
|
+
def initialize(io)
|
|
15
|
+
@io, @buffer, @offset = io, ''.b, 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @rbs (Integer length) -> String?
|
|
19
|
+
def read(length)
|
|
20
|
+
ending = @offset + length
|
|
21
|
+
if ending <= @buffer.bytesize
|
|
22
|
+
result = @buffer.byteslice(@offset, length)
|
|
23
|
+
@offset = ending
|
|
24
|
+
return result
|
|
25
|
+
end
|
|
26
|
+
result = @buffer.byteslice(@offset..) || ''.b
|
|
27
|
+
while result.bytesize < length
|
|
28
|
+
begin
|
|
29
|
+
@buffer = @io.readpartial(65_536)
|
|
30
|
+
rescue EOFError
|
|
31
|
+
@buffer, @offset = ''.b, 0
|
|
32
|
+
return result.empty? ? nil : result
|
|
33
|
+
end
|
|
34
|
+
amount = [length - result.bytesize, @buffer.bytesize].min #: Integer
|
|
35
|
+
result << @buffer.byteslice(0, amount)
|
|
36
|
+
@offset = amount
|
|
37
|
+
end
|
|
38
|
+
result
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
private_constant :FileInput
|
|
42
|
+
|
|
43
|
+
# Nonregular inputs need bounded polling so Source#stop can interrupt an idle pipe.
|
|
44
|
+
class StreamInput
|
|
45
|
+
# @rbs (IO io, Source source) -> void
|
|
46
|
+
def initialize(io, source)
|
|
47
|
+
@io, @source = io, source
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @rbs (Integer length) -> String?
|
|
51
|
+
def read(length)
|
|
52
|
+
loop do
|
|
53
|
+
raise ReadStopped if @source.stopped?
|
|
54
|
+
next unless IO.select([@io], nil, nil, 0.1)
|
|
55
|
+
|
|
56
|
+
part = @io.read_nonblock(length, exception: false)
|
|
57
|
+
next if part == :wait_readable
|
|
58
|
+
|
|
59
|
+
return part
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
attr_reader :reader
|
|
65
|
+
|
|
66
|
+
# @rbs (untyped input, ?filter: String?) -> void
|
|
67
|
+
def initialize(input, filter: nil)
|
|
68
|
+
super()
|
|
69
|
+
@owned = input.is_a?(String) && input != '-'
|
|
70
|
+
@io = input.is_a?(String) ? (input == '-' ? $stdin : ::File.open(input, 'rb')) : input
|
|
71
|
+
@io.binmode if @io.respond_to?(:binmode)
|
|
72
|
+
regular = @io.is_a?(IO) && @io.stat.file?
|
|
73
|
+
reader_io = if @owned && regular
|
|
74
|
+
FileInput.new(@io)
|
|
75
|
+
elsif @io.is_a?(IO) && !regular
|
|
76
|
+
StreamInput.new(@io, self)
|
|
77
|
+
else
|
|
78
|
+
@io
|
|
79
|
+
end
|
|
80
|
+
@stream = reader_io.is_a?(StreamInput)
|
|
81
|
+
@packets = nil # @rbs Thread::SizedQueue[Packet | StandardError]?
|
|
82
|
+
@reader_thread = nil # @rbs Thread?
|
|
83
|
+
magic = File::Format.read_exact(reader_io, 4)
|
|
84
|
+
@reader = if magic == File::Format::PCAPNG_MAGIC
|
|
85
|
+
File::PcapngReader.new(reader_io, magic)
|
|
86
|
+
elsif File::Format::PCAP_MAGICS.key?(magic)
|
|
87
|
+
File::PcapReader.new(reader_io, magic)
|
|
88
|
+
else
|
|
89
|
+
raise FileFormatError, 'unrecognized capture file magic'
|
|
90
|
+
end
|
|
91
|
+
@filter_expression = filter
|
|
92
|
+
@filter_program = nil # @rbs Filter::Program?
|
|
93
|
+
@filters = {} #: Hash[Integer, untyped]
|
|
94
|
+
@filters[@reader.linktype] = Filter.compile(filter, linktype: @reader.linktype) if filter
|
|
95
|
+
rescue StandardError, Interrupt
|
|
96
|
+
@io.close if @owned && @io && !@io.closed?
|
|
97
|
+
raise
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @rbs (untyped input, ?filter: String?) -> FileSource
|
|
101
|
+
# @rbs [T] (untyped input, ?filter: String?) { (FileSource) -> T } -> T
|
|
102
|
+
def self.open(input, filter: nil)
|
|
103
|
+
source = new(input, filter:)
|
|
104
|
+
return source unless block_given?
|
|
105
|
+
|
|
106
|
+
begin
|
|
107
|
+
yield source
|
|
108
|
+
ensure
|
|
109
|
+
source.close
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# @rbs (?timeout: Numeric?) -> Packet?
|
|
114
|
+
def next_packet(timeout: nil)
|
|
115
|
+
return nil if @stopped || @closed
|
|
116
|
+
|
|
117
|
+
deadline = deadline_for(timeout)
|
|
118
|
+
polled = false
|
|
119
|
+
until @stopped || @closed
|
|
120
|
+
return nil if polled && deadline && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
121
|
+
polled = true
|
|
122
|
+
remaining = deadline && [deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
|
|
123
|
+
packet = read_packet(remaining)
|
|
124
|
+
return nil if packet == :wait_readable
|
|
125
|
+
break unless packet
|
|
126
|
+
|
|
127
|
+
@capture_stats.received += 1
|
|
128
|
+
next if @filter_program && !@filter_program.match?(packet)
|
|
129
|
+
if @filter_expression
|
|
130
|
+
program = (@filters[packet.linktype] ||= Filter.compile(@filter_expression, linktype: packet.linktype))
|
|
131
|
+
next unless program.match?(packet)
|
|
132
|
+
end
|
|
133
|
+
@capture_stats.captured += 1
|
|
134
|
+
return packet
|
|
135
|
+
end
|
|
136
|
+
stop
|
|
137
|
+
nil
|
|
138
|
+
rescue ReadStopped
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# @rbs (Filter::Program? program) -> void
|
|
143
|
+
def attach_filter(program)
|
|
144
|
+
@filter_expression = nil
|
|
145
|
+
@filters.clear
|
|
146
|
+
@filter_program = program
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# @rbs () -> Integer
|
|
150
|
+
def linktype = @reader.linktype
|
|
151
|
+
|
|
152
|
+
# @rbs () -> Array[Interface]
|
|
153
|
+
def interfaces = @reader.interfaces
|
|
154
|
+
|
|
155
|
+
# @rbs () -> Stats
|
|
156
|
+
def stats
|
|
157
|
+
@reader.respond_to?(:statistics?) && @reader.statistics? ? @reader.stats : super
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# @rbs () -> void
|
|
161
|
+
def stop
|
|
162
|
+
super
|
|
163
|
+
@packets&.close
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# @rbs () -> void
|
|
167
|
+
def close
|
|
168
|
+
return if @closed
|
|
169
|
+
|
|
170
|
+
super
|
|
171
|
+
@reader_thread&.join(0.2)
|
|
172
|
+
@io.close if @owned
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
private
|
|
176
|
+
|
|
177
|
+
# A single bounded worker preserves partial records across caller timeouts.
|
|
178
|
+
# @rbs (Numeric? timeout) -> (Packet | nil | :wait_readable)
|
|
179
|
+
def read_packet(timeout)
|
|
180
|
+
return @reader.next_packet unless @stream
|
|
181
|
+
|
|
182
|
+
unless @packets
|
|
183
|
+
queue = Thread::SizedQueue.new(1) #: Thread::SizedQueue[Packet | StandardError]
|
|
184
|
+
@packets = queue
|
|
185
|
+
@reader_thread = Thread.new do
|
|
186
|
+
begin
|
|
187
|
+
while !stopped? && (packet = @reader.next_packet)
|
|
188
|
+
queue << packet
|
|
189
|
+
end
|
|
190
|
+
rescue ReadStopped, ClosedQueueError
|
|
191
|
+
nil
|
|
192
|
+
rescue StandardError => error
|
|
193
|
+
begin
|
|
194
|
+
queue << error
|
|
195
|
+
rescue ClosedQueueError
|
|
196
|
+
nil
|
|
197
|
+
end
|
|
198
|
+
ensure
|
|
199
|
+
queue.close
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
queue = @packets #: Thread::SizedQueue[Packet | StandardError]
|
|
204
|
+
result = queue.pop(timeout: timeout && Float(timeout))
|
|
205
|
+
raise result if result.is_a?(StandardError)
|
|
206
|
+
|
|
207
|
+
return result if result
|
|
208
|
+
return nil if queue.closed?
|
|
209
|
+
|
|
210
|
+
:wait_readable
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'socket'
|
|
5
|
+
|
|
6
|
+
module Redhound
|
|
7
|
+
# @api private
|
|
8
|
+
module Capture
|
|
9
|
+
# @api private
|
|
10
|
+
class Interface
|
|
11
|
+
attr_reader :name, :index, :linktype, :snaplen, :flags, :mtu, :mac, :description, :filter, :meta
|
|
12
|
+
|
|
13
|
+
# @rbs (name: String, ?index: Integer, ?linktype: Integer, ?snaplen: Integer, ?flags: Integer, ?mtu: Integer?, ?mac: String?, ?description: String?, ?filter: String?, ?meta: Hash[Symbol, untyped]) -> void
|
|
14
|
+
def initialize(name:, index: 0, linktype: 1, snaplen: 262_144, flags: 0, mtu: nil, mac: nil,
|
|
15
|
+
description: nil, filter: nil, meta: {})
|
|
16
|
+
@name, @index, @linktype, @snaplen, @flags = name, index, linktype, snaplen, flags
|
|
17
|
+
@mtu, @mac, @description, @filter, @meta = mtu, mac, description, filter, meta
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @rbs () -> Integer
|
|
21
|
+
def ifindex = @index
|
|
22
|
+
|
|
23
|
+
# @rbs () -> bool
|
|
24
|
+
def up? = (@flags & 1).positive?
|
|
25
|
+
|
|
26
|
+
# @rbs () -> bool
|
|
27
|
+
def loopback? = (@flags & 8).positive?
|
|
28
|
+
|
|
29
|
+
# @rbs () -> bool
|
|
30
|
+
def running? = (@flags & 0x40).positive?
|
|
31
|
+
|
|
32
|
+
# @rbs () -> String
|
|
33
|
+
def to_s
|
|
34
|
+
state = [up? ? 'Up' : 'Down', running? ? 'Running' : nil, loopback? ? 'Loopback' : nil].compact.join(', ')
|
|
35
|
+
"#{@index}.#{@name} [#{state}]#{@mac ? " #{@mac}" : ''}#{@mtu ? " mtu #{@mtu}" : ''}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @rbs () -> Array[Interface]
|
|
39
|
+
def self.all
|
|
40
|
+
addresses = Socket.getifaddrs
|
|
41
|
+
addresses.uniq(&:name).map do |address|
|
|
42
|
+
index = address.ifindex || 0
|
|
43
|
+
interface = new(name: address.name, index:, flags: address.flags)
|
|
44
|
+
if RUBY_PLATFORM.include?('linux')
|
|
45
|
+
Linux::Ifreq.interface(address.name, index:)
|
|
46
|
+
elsif RUBY_PLATFORM.include?('darwin')
|
|
47
|
+
link = addresses.find { |entry| entry.name == address.name && entry.addr&.afamily == 18 }
|
|
48
|
+
Bsd::Ifreq.interface(address.name, index:, flags: address.flags, link_address: link&.addr&.to_sockaddr)
|
|
49
|
+
else
|
|
50
|
+
interface
|
|
51
|
+
end
|
|
52
|
+
end.sort_by(&:index)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @rbs (String | Integer value) -> Interface
|
|
56
|
+
def self.find(value)
|
|
57
|
+
return new(name: 'any', linktype: 276) if value.to_s == 'any'
|
|
58
|
+
|
|
59
|
+
found = all.find { |item| item.name == value.to_s || item.index.to_s == value.to_s }
|
|
60
|
+
return found if found
|
|
61
|
+
|
|
62
|
+
names = all.map(&:name)
|
|
63
|
+
suggestions = names.select { |name| name.include?(value.to_s[0, 2]) }
|
|
64
|
+
message = "interface #{value.inspect} not found"
|
|
65
|
+
message += "; available: #{(suggestions.empty? ? names : suggestions).join(', ')}"
|
|
66
|
+
raise InterfaceNotFound, message
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Redhound
|
|
5
|
+
# @api private
|
|
6
|
+
module Capture
|
|
7
|
+
# @api private
|
|
8
|
+
module Linktype
|
|
9
|
+
VALUES = { null: 0, ethernet: 1, ether: 1, raw: 101, loop: 108, sll: 113,
|
|
10
|
+
linux_sll: 113, radiotap: 127, ipv4: 228, ipv6: 229, sll2: 276, linux_sll2: 276 }.freeze
|
|
11
|
+
|
|
12
|
+
# @rbs (Integer | Symbol | String value) -> Integer
|
|
13
|
+
def self.resolve(value)
|
|
14
|
+
return value if value.is_a?(Integer) && value.between?(0, 65_535)
|
|
15
|
+
|
|
16
|
+
VALUES.fetch(value.to_s.downcase.to_sym) { raise ArgumentError, "unknown link type: #{value}" }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @rbs (Integer value) -> String
|
|
20
|
+
def self.name(value)
|
|
21
|
+
VALUES.key(value)&.to_s || "LINKTYPE_#{value}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @rbs (Integer hardware_type) -> Integer
|
|
25
|
+
def self.for_hardware(hardware_type)
|
|
26
|
+
{ 1 => 1, 772 => 1, 65_534 => 101, 803 => 127 }.fetch(hardware_type, 276)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
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 Constants
|
|
11
|
+
AF_PACKET = 17
|
|
12
|
+
SOL_PACKET = 263
|
|
13
|
+
ETH_P_ALL = 3
|
|
14
|
+
PACKET_ADD_MEMBERSHIP = 1
|
|
15
|
+
PACKET_RX_RING = 5
|
|
16
|
+
PACKET_STATISTICS = 6
|
|
17
|
+
PACKET_VERSION = 10
|
|
18
|
+
PACKET_IGNORE_OUTGOING = 23
|
|
19
|
+
PACKET_MR_PROMISC = 1
|
|
20
|
+
PACKET_OUTGOING = 4
|
|
21
|
+
SO_ATTACH_FILTER = 26
|
|
22
|
+
SO_RCVBUFFORCE = 33
|
|
23
|
+
TPACKET_V3 = 2
|
|
24
|
+
SIOCGSTAMPNS = 0x8907
|
|
25
|
+
SIOCGIFHWADDR = 0x8927
|
|
26
|
+
SIOCGIFMTU = 0x8921
|
|
27
|
+
SIOCGIFFLAGS = 0x8913
|
|
28
|
+
TP_STATUS_USER = 0x01
|
|
29
|
+
TP_STATUS_CSUMNOTREADY = 0x08
|
|
30
|
+
TP_STATUS_VLAN_VALID = 0x10
|
|
31
|
+
TP_STATUS_VLAN_TPID_VALID = 0x40
|
|
32
|
+
TP_STATUS_CSUM_VALID = 0x80
|
|
33
|
+
RECEIVE_SIZE = 262_144
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -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
|