nl 0.2.4 → 0.4.1
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 +21 -0
- data/Rakefile +7 -0
- data/lib/nl/async/dispatcher.rb +199 -0
- data/lib/nl/async/driver.rb +37 -0
- data/lib/nl/async/mailbox.rb +81 -0
- data/lib/nl/async/operation.rb +277 -0
- data/lib/nl/async.rb +13 -0
- data/lib/nl/attribute_set.rb +173 -0
- data/lib/nl/bitfield32.rb +86 -0
- data/lib/nl/blocking_transport.rb +131 -0
- data/lib/nl/connection.rb +61 -0
- data/lib/nl/datagram.rb +22 -0
- data/lib/nl/datatypes.rb +454 -0
- data/lib/nl/decoder.rb +13 -1
- data/lib/nl/endian.rb +10 -0
- data/lib/nl/error.rb +10 -0
- data/lib/nl/exchange.rb +124 -0
- data/lib/nl/family.rb +117 -23
- data/lib/nl/genl/client.rb +104 -0
- data/lib/nl/genl/protocol.rb +59 -0
- data/lib/nl/genl/wire.rb +94 -0
- data/lib/nl/genl.rb +60 -73
- data/lib/nl/notification.rb +112 -0
- data/lib/nl/notification_router.rb +92 -0
- data/lib/nl/raw/client.rb +83 -0
- data/lib/nl/raw/protocol.rb +112 -0
- data/lib/nl/raw/wire.rb +146 -0
- data/lib/nl/raw.rb +104 -0
- data/lib/nl/sequence_allocator.rb +34 -0
- data/lib/nl/socket.rb +15 -12
- data/lib/nl/structured_payload.rb +61 -0
- data/lib/nl/sub_message.rb +94 -0
- data/lib/nl/version.rb +1 -1
- data/lib/nl.rb +10 -12
- data/sig/generated/nl/async/dispatcher.rbs +53 -0
- data/sig/generated/nl/async/driver.rbs +21 -0
- data/sig/generated/nl/async/mailbox.rbs +32 -0
- data/sig/generated/nl/async/operation.rbs +123 -0
- data/sig/generated/nl/async.rbs +9 -0
- data/sig/generated/nl/attribute_set.rbs +50 -0
- data/sig/generated/nl/bitfield32.rbs +41 -0
- data/sig/generated/nl/blocking_transport.rbs +33 -0
- data/sig/generated/nl/connection.rbs +26 -0
- data/sig/generated/nl/datagram.rbs +11 -0
- data/sig/generated/nl/datatypes.rbs +181 -0
- data/sig/generated/nl/decoder.rbs +38 -0
- data/sig/generated/nl/encoder.rbs +29 -0
- data/sig/generated/nl/endian.rbs +28 -0
- data/sig/generated/nl/error.rbs +27 -0
- data/sig/generated/nl/exchange.rbs +75 -0
- data/sig/generated/nl/family.rbs +71 -0
- data/sig/generated/nl/genl/client.rbs +74 -0
- data/sig/generated/nl/genl/protocol.rbs +37 -0
- data/sig/generated/nl/genl/wire.rbs +113 -0
- data/sig/generated/nl/genl.rbs +50 -0
- data/sig/generated/nl/notification.rbs +53 -0
- data/sig/generated/nl/notification_router.rbs +30 -0
- data/sig/generated/nl/raw/client.rbs +50 -0
- data/sig/generated/nl/raw/protocol.rbs +122 -0
- data/sig/generated/nl/raw/wire.rbs +151 -0
- data/sig/generated/nl/raw.rbs +70 -0
- data/sig/generated/nl/sequence_allocator.rbs +15 -0
- data/sig/generated/nl/socket.rbs +40 -0
- data/sig/generated/nl/structured_payload.rbs +25 -0
- data/sig/generated/nl/sub_message.rbs +74 -0
- data/sig/generated/nl/version.rbs +5 -0
- data/sig/generated/nl.rbs +2 -0
- metadata +65 -9
- data/lib/nl/core.rb +0 -94
- data/lib/nl/protocols/genl.rb +0 -55
- data/lib/nl/protocols/raw.rb +0 -395
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
require_relative 'raw/wire'
|
|
2
|
+
require_relative 'notification'
|
|
3
|
+
|
|
4
|
+
module Nl
|
|
5
|
+
# Routes unsolicited frames to per-family notification channels.
|
|
6
|
+
class NotificationRouter
|
|
7
|
+
Entry = Struct.new(:endpoint, :classes, :channel)
|
|
8
|
+
private_constant :Entry
|
|
9
|
+
|
|
10
|
+
def initialize(protocol:, capacity:)
|
|
11
|
+
@protocol = protocol
|
|
12
|
+
@capacity = capacity
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
@entries = {}
|
|
15
|
+
@routes = {}
|
|
16
|
+
@closed = false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def register(endpoint, classes)
|
|
20
|
+
@mutex.synchronize do
|
|
21
|
+
raise ClosedError, 'notification router is closed' if @closed
|
|
22
|
+
|
|
23
|
+
channel_key = @protocol.notification_channel_key(endpoint)
|
|
24
|
+
entry = @entries[channel_key]
|
|
25
|
+
merged_classes = entry ? entry.classes.merge(classes) : classes.dup
|
|
26
|
+
route_keys = @protocol.notification_route_keys(endpoint, merged_classes)
|
|
27
|
+
if route_key = route_keys.find { @routes[it] && !@routes[it].equal?(entry) }
|
|
28
|
+
raise ArgumentError, "notification route #{route_key.inspect} is already registered"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
unless entry
|
|
32
|
+
entry = Entry.new(endpoint, merged_classes, NotificationChannel.new(capacity: @capacity))
|
|
33
|
+
@entries[channel_key] = entry
|
|
34
|
+
end
|
|
35
|
+
entry.classes.replace(merged_classes)
|
|
36
|
+
route_keys.each { @routes[it] = entry }
|
|
37
|
+
entry.channel
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def channel(endpoint)
|
|
42
|
+
@mutex.synchronize do
|
|
43
|
+
@entries.fetch(@protocol.notification_channel_key(endpoint)).channel
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns true if the frame belongs to a registered notification family.
|
|
48
|
+
def route(header, payload)
|
|
49
|
+
if header.type == Raw::NLMSG_OVERRUN
|
|
50
|
+
entries = @mutex.synchronize { @entries.values.dup }
|
|
51
|
+
entries.each { it.channel.fail(NotificationLossError.new('kernel reported Netlink overrun')) }
|
|
52
|
+
return true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
entry = @mutex.synchronize do
|
|
56
|
+
@routes[@protocol.notification_frame_key(header)]
|
|
57
|
+
end
|
|
58
|
+
return false unless entry
|
|
59
|
+
return false unless @protocol.notification_frame?(entry.endpoint, header, payload)
|
|
60
|
+
|
|
61
|
+
message_class = @protocol.notification_class(entry.endpoint, header, payload, entry.classes)
|
|
62
|
+
return true unless message_class
|
|
63
|
+
|
|
64
|
+
notification = @protocol.decode_notification(entry.endpoint, header, payload, message_class)
|
|
65
|
+
entry.channel.push(notification)
|
|
66
|
+
true
|
|
67
|
+
rescue => error
|
|
68
|
+
entry&.channel&.fail(error)
|
|
69
|
+
true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def close
|
|
73
|
+
entries = @mutex.synchronize do
|
|
74
|
+
return nil if @closed
|
|
75
|
+
|
|
76
|
+
@closed = true
|
|
77
|
+
old = @entries.values
|
|
78
|
+
@entries.clear
|
|
79
|
+
@routes.clear
|
|
80
|
+
old
|
|
81
|
+
end
|
|
82
|
+
entries.each { it.channel.close }
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def lose_all(error)
|
|
87
|
+
entries = @mutex.synchronize { @entries.values.dup }
|
|
88
|
+
entries.each { it.channel.fail(error) }
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Raw Netlink client handling
|
|
2
|
+
|
|
3
|
+
require_relative '../connection'
|
|
4
|
+
require_relative 'protocol'
|
|
5
|
+
|
|
6
|
+
module Nl
|
|
7
|
+
module Raw
|
|
8
|
+
# Owns one raw Netlink connection shared by compatible families.
|
|
9
|
+
class Client
|
|
10
|
+
# Opens a client for a raw Netlink protocol.
|
|
11
|
+
#
|
|
12
|
+
# @overload open(protonum:, executor: nil, notification_capacity: Nl::Connection::DEFAULT_NOTIFICATION_CAPACITY)
|
|
13
|
+
# The caller is responsible for closing the client.
|
|
14
|
+
# @param [Integer] protonum the Netlink protocol number
|
|
15
|
+
# @param [:thread, :fiber, nil] executor the asynchronous executor, or `nil` for blocking operation
|
|
16
|
+
# @param [Integer] notification_capacity the maximum number of queued notifications
|
|
17
|
+
# @return [Client] the opened client
|
|
18
|
+
# @overload open(protonum:, executor: nil, notification_capacity: Nl::Connection::DEFAULT_NOTIFICATION_CAPACITY, &block)
|
|
19
|
+
# The client is automatically closed after the block returns.
|
|
20
|
+
# @param [Integer] protonum the Netlink protocol number
|
|
21
|
+
# @param [:thread, :fiber, nil] executor the asynchronous executor, or `nil` for blocking operation
|
|
22
|
+
# @param [Integer] notification_capacity the maximum number of queued notifications
|
|
23
|
+
# @yieldparam [Client] client the opened client
|
|
24
|
+
# @return [Object] the value returned from the block
|
|
25
|
+
# @rbs (protonum: Integer, ?executor: executor?, ?notification_capacity: Integer?) -> instance
|
|
26
|
+
# | [R] (protonum: Integer, ?executor: executor?, ?notification_capacity: Integer?) { (instance) -> R } -> R
|
|
27
|
+
def self.open(protonum:, executor: nil, notification_capacity: Nl::Connection::DEFAULT_NOTIFICATION_CAPACITY)
|
|
28
|
+
client = new(protonum:, executor:, notification_capacity:)
|
|
29
|
+
return client unless block_given?
|
|
30
|
+
|
|
31
|
+
begin
|
|
32
|
+
yield client
|
|
33
|
+
ensure
|
|
34
|
+
client.close
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param [Integer] protonum the Netlink protocol number
|
|
39
|
+
# @param [:thread, :fiber, nil] executor the asynchronous executor, or `nil` for blocking operation
|
|
40
|
+
# @param [Integer] notification_capacity the maximum number of queued notifications
|
|
41
|
+
# @rbs (protonum: Integer, ?executor: executor?, ?notification_capacity: Integer?) -> void
|
|
42
|
+
def initialize(protonum:, executor: nil, notification_capacity: Nl::Connection::DEFAULT_NOTIFICATION_CAPACITY)
|
|
43
|
+
@protonum = protonum
|
|
44
|
+
@connection = Nl::Connection.new(
|
|
45
|
+
protocol: Protocol.new(protonum),
|
|
46
|
+
executor:,
|
|
47
|
+
notification_capacity:,
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Builds a family backed by this client's connection.
|
|
52
|
+
#
|
|
53
|
+
# @param [Class<Family>] family_class a raw Netlink family class
|
|
54
|
+
# @return [Family] an instance of `family_class`
|
|
55
|
+
# @raise [TypeError] if +family_class+ does not inherit from {Family}
|
|
56
|
+
# @raise [ArgumentError] if the family's protocol number differs from the client's protocol number
|
|
57
|
+
# @rbs [F < Family] (_FamilyClass[F] family_class) -> F
|
|
58
|
+
def family(family_class)
|
|
59
|
+
unless family_class <= Family
|
|
60
|
+
raise TypeError, "family class must inherit from #{Family}"
|
|
61
|
+
end
|
|
62
|
+
unless family_class::PROTONUM == @protonum
|
|
63
|
+
raise ArgumentError,
|
|
64
|
+
"family protonum #{family_class::PROTONUM} does not match client protonum #{@protonum}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
family_class.new(
|
|
68
|
+
@connection,
|
|
69
|
+
endpoint: Endpoint.new(family_class),
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Closes the underlying Netlink connection.
|
|
74
|
+
#
|
|
75
|
+
# @return [void]
|
|
76
|
+
#--
|
|
77
|
+
# @rbs () -> void
|
|
78
|
+
def close
|
|
79
|
+
@connection.close
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
require_relative 'wire'
|
|
2
|
+
require_relative '../decoder'
|
|
3
|
+
require_relative '../encoder'
|
|
4
|
+
require_relative '../error'
|
|
5
|
+
require_relative '../socket'
|
|
6
|
+
|
|
7
|
+
module Nl
|
|
8
|
+
module Raw
|
|
9
|
+
AckFrame = Data.define(:header)
|
|
10
|
+
ErrorFrame = Data.define(:header, :errno)
|
|
11
|
+
DoneFrame = Data.define(:header, :errno)
|
|
12
|
+
UnknownFrame = Data.define(:header)
|
|
13
|
+
DataFrame = Data.define(:header, :message)
|
|
14
|
+
Request = Data.define(:type, :flags, :message)
|
|
15
|
+
|
|
16
|
+
# A generated raw Netlink family bound to its fixed wire identity.
|
|
17
|
+
class Endpoint
|
|
18
|
+
attr_reader :definition
|
|
19
|
+
|
|
20
|
+
def initialize(definition)
|
|
21
|
+
@definition = definition
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def name = @definition::NAME
|
|
25
|
+
def frame_type(message_class) = message_class::TYPE
|
|
26
|
+
|
|
27
|
+
def multicast_group_id(name, value)
|
|
28
|
+
value or raise UnresolvedMulticastGroupError,
|
|
29
|
+
"multicast group #{name.inspect} has no fixed ID"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Socket-wide wire behavior for a classic (netlink-raw) protocol.
|
|
34
|
+
class Protocol
|
|
35
|
+
attr_reader :protonum
|
|
36
|
+
|
|
37
|
+
def initialize(protonum)
|
|
38
|
+
@protonum = protonum
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def encode_message(encoder, _endpoint, request, seq:, pid:)
|
|
42
|
+
header = Raw::NlMsgHdr.new(0, request.type, request.flags, seq, pid)
|
|
43
|
+
encoder.measure(Endian::Host::U16) do
|
|
44
|
+
header.encode(encoder)
|
|
45
|
+
request.message.encode(encoder)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Decodes one frame using the reply class associated with its sequence.
|
|
50
|
+
def decode_frame(_endpoint, header, payload, message_class)
|
|
51
|
+
decoder = Decoder.new(payload)
|
|
52
|
+
if header.type < Raw::NLMSG_MIN_TYPE
|
|
53
|
+
case header.type
|
|
54
|
+
when Raw::NLMSG_ERROR
|
|
55
|
+
errno = decoder.get_value(Endian::Host::SINT)
|
|
56
|
+
if errno.positive?
|
|
57
|
+
raise ProtocolViolation, "expected zero or negative NLMSG_ERROR errno, got #{errno}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
errno.zero? ? AckFrame.new(header:) : ErrorFrame.new(header:, errno: -errno)
|
|
61
|
+
when Raw::NLMSG_DONE
|
|
62
|
+
return DoneFrame.new(header:, errno: nil) unless decoder.available?
|
|
63
|
+
|
|
64
|
+
errno = decoder.get_value(Endian::Host::SINT)
|
|
65
|
+
if errno.positive?
|
|
66
|
+
raise ProtocolViolation, "expected zero or negative NLMSG_DONE errno, got #{errno}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
DoneFrame.new(header:, errno: errno.negative? ? -errno : nil)
|
|
70
|
+
else
|
|
71
|
+
UnknownFrame.new(header:)
|
|
72
|
+
end
|
|
73
|
+
else
|
|
74
|
+
raise ArgumentError, 'reply class is required for a data message' unless message_class
|
|
75
|
+
|
|
76
|
+
DataFrame.new(header:, message: message_class.decode(decoder, type: header.type))
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def send_message(socket, endpoint, request, seq:, pid:)
|
|
81
|
+
encoder = Encoder.new
|
|
82
|
+
encode_message(encoder, endpoint, request, seq:, pid:)
|
|
83
|
+
socket.sendmsg(encoder.buffer.get_string, 0, Socket.sockaddr_nl(0, 0))
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def build_request(endpoint, kind, request_class, args)
|
|
88
|
+
flags = Raw::NLM_F_REQUEST
|
|
89
|
+
flags |= kind == :dump ? Raw::NLM_F_DUMP : Raw::NLM_F_ACK
|
|
90
|
+
|
|
91
|
+
message = request_class.from_params(args)
|
|
92
|
+
Request.new(type: endpoint.frame_type(request_class), flags:, message:)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def notification_channel_key(endpoint) = endpoint.definition
|
|
96
|
+
def notification_route_keys(_endpoint, classes) = classes.keys
|
|
97
|
+
def notification_frame_key(header) = header.type
|
|
98
|
+
|
|
99
|
+
def notification_frame?(_endpoint, header, _payload)
|
|
100
|
+
header.type >= Raw::NLMSG_MIN_TYPE
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def notification_class(_endpoint, header, _payload, classes)
|
|
104
|
+
classes[header.type]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def decode_notification(endpoint, header, payload, message_class)
|
|
108
|
+
decode_frame(endpoint, header, payload, message_class).message
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
data/lib/nl/raw/wire.rb
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Netlink wire definitions
|
|
2
|
+
|
|
3
|
+
require_relative '../endian'
|
|
4
|
+
|
|
5
|
+
module Nl
|
|
6
|
+
module Raw
|
|
7
|
+
# Constants from <linux/netlink.h>
|
|
8
|
+
module Constants
|
|
9
|
+
NETLINK_ROUTE = 0
|
|
10
|
+
NETLINK_NETFILTER = 12
|
|
11
|
+
NETLINK_GENERIC = 16
|
|
12
|
+
|
|
13
|
+
NLM_F_REQUEST = 1
|
|
14
|
+
NLM_F_MULTI = 2
|
|
15
|
+
NLM_F_ACK = 4
|
|
16
|
+
NLM_F_ECHO = 8
|
|
17
|
+
NLM_F_DUMP_INTR = 16
|
|
18
|
+
NLM_F_DUMP_FILTERED = 32
|
|
19
|
+
NLM_F_ROOT = 0x100
|
|
20
|
+
NLM_F_MATCH = 0x200
|
|
21
|
+
NLM_F_ATOMIC = 0x400
|
|
22
|
+
NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH
|
|
23
|
+
NLM_F_REPLACE = 0x100
|
|
24
|
+
NLM_F_EXCL = 0x200
|
|
25
|
+
NLM_F_CREATE = 0x400
|
|
26
|
+
NLM_F_APPEND = 0x800
|
|
27
|
+
|
|
28
|
+
NLMSG_ALIGNTO = 4
|
|
29
|
+
NLMSG_HDRLEN = 16
|
|
30
|
+
|
|
31
|
+
NLMSG_NOOP = 0x1
|
|
32
|
+
NLMSG_ERROR = 0x2
|
|
33
|
+
NLMSG_DONE = 0x3
|
|
34
|
+
NLMSG_OVERRUN = 0x4
|
|
35
|
+
|
|
36
|
+
NLMSG_MIN_TYPE = 0x10
|
|
37
|
+
|
|
38
|
+
NLA_F_NESTED = 1 << 15
|
|
39
|
+
NLA_F_NET_BYTEORDER = 1 << 14
|
|
40
|
+
NLA_TYPE_MASK = ~(NLA_F_NESTED | NLA_F_NET_BYTEORDER)
|
|
41
|
+
|
|
42
|
+
NLA_ALIGNTO = 4
|
|
43
|
+
NLA_HDRLEN = 4
|
|
44
|
+
end
|
|
45
|
+
include Constants
|
|
46
|
+
|
|
47
|
+
# Fixed-format metadata header prepended to every Netlink message.
|
|
48
|
+
#
|
|
49
|
+
# This corresponds to Linux's +struct nlmsghdr+.
|
|
50
|
+
#
|
|
51
|
+
# @!attribute [rw] len
|
|
52
|
+
# @return [Integer] message length in bytes, including this header
|
|
53
|
+
# @!attribute [rw] type
|
|
54
|
+
# @return [Integer] message content type
|
|
55
|
+
# @!attribute [rw] flags
|
|
56
|
+
# @return [Integer] bitwise combination of +NLM_F_+ flags
|
|
57
|
+
# @!attribute [rw] seq
|
|
58
|
+
# @return [Integer] sequence number used to correlate requests and replies
|
|
59
|
+
# @!attribute [rw] pid
|
|
60
|
+
# @return [Integer] sender's Netlink port ID
|
|
61
|
+
NlMsgHdr = Struct.new(
|
|
62
|
+
:len, #: Integer
|
|
63
|
+
:type, #: Integer
|
|
64
|
+
:flags, #: Integer
|
|
65
|
+
:seq, #: Integer
|
|
66
|
+
:pid, #: Integer
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
class NlMsgHdr
|
|
70
|
+
FORMAT = Ractor.make_shareable([
|
|
71
|
+
Endian::Host::U32,
|
|
72
|
+
Endian::Host::U16,
|
|
73
|
+
Endian::Host::U16,
|
|
74
|
+
Endian::Host::U32,
|
|
75
|
+
Endian::Host::U32,
|
|
76
|
+
])
|
|
77
|
+
private_constant :FORMAT
|
|
78
|
+
|
|
79
|
+
# Decodes a header from the decoder's current position.
|
|
80
|
+
#
|
|
81
|
+
# @param [Decoder] decoder the source decoder
|
|
82
|
+
# @return [NlMsgHdr] the decoded header
|
|
83
|
+
# @rbs (Decoder decoder) -> instance
|
|
84
|
+
def self.decode(decoder)
|
|
85
|
+
obj = new(*decoder.get_values(FORMAT))
|
|
86
|
+
decoder.align_to(Constants::NLMSG_ALIGNTO)
|
|
87
|
+
obj
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Encodes this header at the encoder's current position.
|
|
91
|
+
#
|
|
92
|
+
# @param [Encoder] encoder the destination encoder
|
|
93
|
+
# @return [void]
|
|
94
|
+
# @rbs (Encoder encoder) -> void
|
|
95
|
+
def encode(encoder)
|
|
96
|
+
encoder.reserve(Constants::NLMSG_HDRLEN)
|
|
97
|
+
encoder.put_values(FORMAT, to_a)
|
|
98
|
+
encoder.align_to(Constants::NLMSG_ALIGNTO)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Fixed-format header prepended to every Netlink attribute.
|
|
103
|
+
#
|
|
104
|
+
# This corresponds to Linux's +struct nlattr+.
|
|
105
|
+
#
|
|
106
|
+
# @!attribute [rw] len
|
|
107
|
+
# @return [Integer] attribute length in bytes, including this header but
|
|
108
|
+
# excluding trailing alignment padding
|
|
109
|
+
# @!attribute [rw] type
|
|
110
|
+
# @return [Integer] attribute type combined with optional +NLA_F_+ flags
|
|
111
|
+
NlAttr = Struct.new(
|
|
112
|
+
:len, #: Integer
|
|
113
|
+
:type, #: Integer
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
class NlAttr
|
|
117
|
+
FORMAT = Ractor.make_shareable([
|
|
118
|
+
Endian::Host::U16,
|
|
119
|
+
Endian::Host::U16,
|
|
120
|
+
])
|
|
121
|
+
private_constant :FORMAT
|
|
122
|
+
|
|
123
|
+
# Decodes an attribute header from the decoder's current position.
|
|
124
|
+
#
|
|
125
|
+
# @param [Decoder] decoder the source decoder
|
|
126
|
+
# @return [NlAttr] the decoded header
|
|
127
|
+
# @rbs (Decoder decoder) -> instance
|
|
128
|
+
def self.decode(decoder)
|
|
129
|
+
obj = new(*decoder.get_values(FORMAT))
|
|
130
|
+
decoder.align_to(Constants::NLA_ALIGNTO)
|
|
131
|
+
obj
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Encodes this attribute header at the encoder's current position.
|
|
135
|
+
#
|
|
136
|
+
# @param [Encoder] encoder the destination encoder
|
|
137
|
+
# @return [void]
|
|
138
|
+
# @rbs (Encoder encoder) -> void
|
|
139
|
+
def encode(encoder)
|
|
140
|
+
encoder.reserve(Constants::NLA_HDRLEN)
|
|
141
|
+
encoder.put_values(FORMAT, to_a)
|
|
142
|
+
encoder.align_to(Constants::NLA_ALIGNTO)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
data/lib/nl/raw.rb
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
require_relative 'family'
|
|
2
|
+
require_relative 'raw/protocol'
|
|
3
|
+
require_relative 'raw/client'
|
|
4
|
+
require_relative 'attribute_set'
|
|
5
|
+
require_relative 'structured_payload'
|
|
6
|
+
|
|
7
|
+
module Nl
|
|
8
|
+
# Classic (Raw) Netlink families.
|
|
9
|
+
module Raw
|
|
10
|
+
# Base class for Raw Netlink families.
|
|
11
|
+
class Family < Nl::Family
|
|
12
|
+
# Opens a session for this raw Netlink family.
|
|
13
|
+
#
|
|
14
|
+
# @overload open(executor: nil, notification_capacity: DEFAULT_NOTIFICATION_CAPACITY)
|
|
15
|
+
# The caller is responsible for closing the session.
|
|
16
|
+
# @param [:thread, :fiber, nil] executor the asynchronous executor, or `nil` for blocking operation
|
|
17
|
+
# @param [Integer] notification_capacity the maximum number of queued notifications
|
|
18
|
+
# @return [Family] the opened family session
|
|
19
|
+
# @overload open(executor: nil, notification_capacity: DEFAULT_NOTIFICATION_CAPACITY, &block)
|
|
20
|
+
# The session is automatically closed after the block returns.
|
|
21
|
+
# @param [:thread, :fiber, nil] executor the asynchronous executor, or `nil` for blocking operation
|
|
22
|
+
# @param [Integer] notification_capacity the maximum number of queued notifications
|
|
23
|
+
# @yieldparam [Family] session the opened family session
|
|
24
|
+
# @return [Object] the value returned from the block
|
|
25
|
+
# @rbs (?executor: executor?, ?notification_capacity: Integer?) -> (Nl::Family::Session & instance)
|
|
26
|
+
# | [R] (?executor: executor?, ?notification_capacity: Integer?) { (instance) -> R } -> R
|
|
27
|
+
def self.open(executor: nil, notification_capacity: DEFAULT_NOTIFICATION_CAPACITY)
|
|
28
|
+
session = build_session(executor:, notification_capacity:)
|
|
29
|
+
return session unless block_given?
|
|
30
|
+
|
|
31
|
+
begin
|
|
32
|
+
yield session
|
|
33
|
+
ensure
|
|
34
|
+
session.close
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
# Builds a session that owns its underlying client.
|
|
40
|
+
#
|
|
41
|
+
# @param [:thread, :fiber, nil] executor the asynchronous executor, or `nil` for blocking operation
|
|
42
|
+
# @param [Integer] notification_capacity the maximum number of queued notifications
|
|
43
|
+
# @return [Family] the opened family session
|
|
44
|
+
# @rbs (executor: executor?, notification_capacity: Integer) -> (Nl::Family::Session & instance)
|
|
45
|
+
private def build_session(executor:, notification_capacity:)
|
|
46
|
+
owner = Client.new(protonum: self::PROTONUM, executor:, notification_capacity:)
|
|
47
|
+
owner.family(self).extend(Nl::Family::Session)
|
|
48
|
+
rescue Exception
|
|
49
|
+
owner&.close
|
|
50
|
+
raise
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Base class for raw Netlink messages.
|
|
56
|
+
#
|
|
57
|
+
# A message may contain a fixed header followed by a set of Netlink
|
|
58
|
+
# attributes.
|
|
59
|
+
class Message
|
|
60
|
+
include Nl::StructuredPayload
|
|
61
|
+
|
|
62
|
+
# @param [Object, nil] fixed_header the family-specific fixed header
|
|
63
|
+
# @param [AttributeSet, nil] attributes the message's attributes
|
|
64
|
+
# @rbs (?untyped fixed_header, ?AttributeSet? attributes) -> void
|
|
65
|
+
def initialize(fixed_header = nil, attributes = self.class::ATTRIBUTE_SET.new)
|
|
66
|
+
super
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Appends an attribute to the message.
|
|
70
|
+
#
|
|
71
|
+
# @param [AttributeSet::Attribute] attribute the attribute to append
|
|
72
|
+
# @return [void]
|
|
73
|
+
# @rbs (AttributeSet::Attribute attribute) -> void
|
|
74
|
+
def append_attribute(attribute)
|
|
75
|
+
@attributes << attribute
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Decodes a raw Netlink message payload.
|
|
79
|
+
#
|
|
80
|
+
# @param [Decoder] decoder the source decoder
|
|
81
|
+
# @param [Integer] type the message type from its Netlink header
|
|
82
|
+
# @return [Message] the decoded message
|
|
83
|
+
# @raise [RuntimeError] if +type+ does not match the message class's type
|
|
84
|
+
# @rbs (Decoder decoder, type: Integer) -> instance
|
|
85
|
+
def self.decode(decoder, type:)
|
|
86
|
+
unless self::TYPE == type
|
|
87
|
+
raise "Expected message type #{self::TYPE}, got #{type}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
super(decoder)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
class << self
|
|
94
|
+
# Returns the attribute names accepted by this message.
|
|
95
|
+
#
|
|
96
|
+
# @return [Array<Symbol>] the attribute names
|
|
97
|
+
# @rbs () -> Array[Symbol]
|
|
98
|
+
private def attribute_names
|
|
99
|
+
self::ATTRIBUTES
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require_relative 'error'
|
|
2
|
+
|
|
3
|
+
module Nl
|
|
4
|
+
# Allocates nonzero 32-bit Netlink sequence numbers.
|
|
5
|
+
class SequenceAllocator
|
|
6
|
+
MAX = 0xFFFFFFFF
|
|
7
|
+
private_constant :MAX
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@last = 0
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Returns the next sequence number not rejected by the optional block.
|
|
14
|
+
def next
|
|
15
|
+
first = advance
|
|
16
|
+
candidate = first
|
|
17
|
+
|
|
18
|
+
loop do
|
|
19
|
+
return candidate unless block_given? && yield(candidate)
|
|
20
|
+
|
|
21
|
+
candidate = advance
|
|
22
|
+
if candidate == first
|
|
23
|
+
raise ExhaustedSequenceNumber, 'all Netlink sequence numbers are in use'
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private def advance
|
|
29
|
+
@last = @last == MAX ? 1 : @last + 1
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private_constant :SequenceAllocator
|
|
34
|
+
end
|
data/lib/nl/socket.rb
CHANGED
|
@@ -8,6 +8,11 @@ module Nl
|
|
|
8
8
|
module Constants
|
|
9
9
|
# From include/linux/socket.h
|
|
10
10
|
PF_NETLINK = AF_NETLINK = 16
|
|
11
|
+
|
|
12
|
+
# From include/uapi/linux/netlink.h
|
|
13
|
+
SOL_NETLINK = 270
|
|
14
|
+
NETLINK_ADD_MEMBERSHIP = 1
|
|
15
|
+
NETLINK_DROP_MEMBERSHIP = 2
|
|
11
16
|
end
|
|
12
17
|
include Constants
|
|
13
18
|
|
|
@@ -21,7 +26,6 @@ module Nl
|
|
|
21
26
|
# @param protonum [Integer] Netlink protocol number
|
|
22
27
|
def initialize(protonum)
|
|
23
28
|
super(PF_NETLINK, SOCK_RAW, protonum)
|
|
24
|
-
@seq = 0 # last-used sequence number
|
|
25
29
|
end
|
|
26
30
|
|
|
27
31
|
def self.open(protonum)
|
|
@@ -34,20 +38,19 @@ module Nl
|
|
|
34
38
|
end
|
|
35
39
|
end
|
|
36
40
|
|
|
37
|
-
#
|
|
41
|
+
# @return [Integer] Local Netlink port ID assigned to this socket
|
|
42
|
+
def local_port_id
|
|
43
|
+
Socket.unpack_sockaddr_nl(local_address.to_sockaddr).first
|
|
44
|
+
end
|
|
38
45
|
|
|
39
|
-
#
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
nseq = 1 if nseq == 0 # seq=0 is for notification
|
|
43
|
-
@seq = nseq
|
|
46
|
+
# Adds this socket to a Netlink multicast group.
|
|
47
|
+
def add_membership(group_id)
|
|
48
|
+
setsockopt(SOL_NETLINK, NETLINK_ADD_MEMBERSHIP, group_id)
|
|
44
49
|
end
|
|
45
50
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
hdr.pid ||= Socket.unpack_sockaddr_nl(local_address.to_sockaddr).first,
|
|
50
|
-
]
|
|
51
|
+
# Removes this socket from a Netlink multicast group.
|
|
52
|
+
def drop_membership(group_id)
|
|
53
|
+
setsockopt(SOL_NETLINK, NETLINK_DROP_MEMBERSHIP, group_id)
|
|
51
54
|
end
|
|
52
55
|
end
|
|
53
56
|
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Nl
|
|
2
|
+
# Shared handling for a payload containing an optional fixed header followed
|
|
3
|
+
# by an optional attribute set.
|
|
4
|
+
module StructuredPayload
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.extend(ClassMethods)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
attr_accessor :fixed_header, :attributes
|
|
10
|
+
|
|
11
|
+
def initialize(fixed_header = nil, attributes = nil)
|
|
12
|
+
@fixed_header = fixed_header
|
|
13
|
+
@attributes = attributes
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module ClassMethods
|
|
17
|
+
def from_params(params = nil, external_selectors: [], **keywords)
|
|
18
|
+
params = (params || {}).merge(keywords).transform_keys(&:to_sym)
|
|
19
|
+
|
|
20
|
+
if self::FIXED_HEADER
|
|
21
|
+
header_params = params.slice(*self::FIXED_HEADER.members)
|
|
22
|
+
fixed_header = self::FIXED_HEADER.new(**header_params)
|
|
23
|
+
else
|
|
24
|
+
header_params = {}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if self::ATTRIBUTE_SET
|
|
28
|
+
attribute_params = params.slice(*attribute_names)
|
|
29
|
+
attributes = self::ATTRIBUTE_SET.build_attributes(
|
|
30
|
+
attribute_params,
|
|
31
|
+
external_selectors:,
|
|
32
|
+
)
|
|
33
|
+
else
|
|
34
|
+
attribute_params = {}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
unknown = params.keys - header_params.keys - attribute_params.keys
|
|
38
|
+
unless unknown.empty?
|
|
39
|
+
raise ArgumentError, "unknown parameters: #{unknown.join(', ')}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
new(fixed_header, attributes)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def decode(decoder, external_selectors: [])
|
|
46
|
+
fixed_header = self::FIXED_HEADER&.decode(decoder)
|
|
47
|
+
attributes = self::ATTRIBUTE_SET&.decode(decoder, external_selectors:)
|
|
48
|
+
new(fixed_header, attributes)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private def attribute_names
|
|
52
|
+
self::ATTRIBUTE_SET::BY_NAME.keys
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def encode(encoder, external_selectors: [])
|
|
57
|
+
fixed_header&.encode(encoder)
|
|
58
|
+
attributes&.encode(encoder, external_selectors:)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|