nl 0.3.0 → 0.4.2
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 +15 -0
- data/Rakefile +7 -0
- data/lib/nl/async/dispatcher.rb +16 -16
- data/lib/nl/async/driver.rb +0 -2
- data/lib/nl/async/mailbox.rb +0 -2
- data/lib/nl/async/operation.rb +0 -2
- data/lib/nl/async.rb +0 -2
- data/lib/nl/attribute_set.rb +173 -0
- data/lib/nl/bitfield32.rb +86 -0
- data/lib/nl/blocking_transport.rb +12 -12
- data/lib/nl/connection.rb +7 -9
- data/lib/nl/datagram.rb +7 -9
- data/lib/nl/datatypes.rb +454 -0
- data/lib/nl/decoder.rb +4 -0
- data/lib/nl/endian.rb +9 -0
- data/lib/nl/exchange.rb +40 -18
- data/lib/nl/family.rb +50 -55
- data/lib/nl/genl/client.rb +105 -0
- data/lib/nl/genl/protocol.rb +59 -0
- data/lib/nl/genl/wire.rb +95 -0
- data/lib/nl/genl.rb +60 -62
- data/lib/nl/notification.rb +3 -6
- data/lib/nl/notification_router.rb +30 -23
- data/lib/nl/raw/client.rb +83 -0
- data/lib/nl/raw/protocol.rb +112 -0
- data/lib/nl/raw/wire.rb +147 -0
- data/lib/nl/raw.rb +104 -0
- data/lib/nl/sequence_allocator.rb +0 -2
- data/lib/nl/socket.rb +21 -5
- 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 +5 -6
- 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 +29 -0
- data/sig/generated/nl/error.rbs +27 -0
- data/sig/generated/nl/exchange.rbs +75 -0
- data/sig/generated/nl/family.rbs +89 -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 +49 -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 +59 -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 +48 -7
- data/lib/nl/core.rb +0 -94
- data/lib/nl/genl/connection.rb +0 -89
- data/lib/nl/protocols/genl.rb +0 -74
- data/lib/nl/protocols/raw.rb +0 -460
|
@@ -1,64 +1,70 @@
|
|
|
1
|
-
require_relative '
|
|
1
|
+
require_relative 'raw/wire'
|
|
2
2
|
require_relative 'notification'
|
|
3
3
|
|
|
4
4
|
module Nl
|
|
5
5
|
# Routes unsolicited frames to per-family notification channels.
|
|
6
6
|
class NotificationRouter
|
|
7
|
-
Entry = Struct.new(:
|
|
7
|
+
Entry = Struct.new(:endpoint, :classes, :channel)
|
|
8
8
|
private_constant :Entry
|
|
9
9
|
|
|
10
|
-
def initialize(
|
|
11
|
-
@
|
|
10
|
+
def initialize(protocol:, capacity:)
|
|
11
|
+
@protocol = protocol
|
|
12
12
|
@capacity = capacity
|
|
13
13
|
@mutex = Mutex.new
|
|
14
14
|
@entries = {}
|
|
15
|
+
@routes = {}
|
|
15
16
|
@closed = false
|
|
16
17
|
end
|
|
17
18
|
|
|
18
|
-
def register(
|
|
19
|
+
def register(endpoint, classes)
|
|
19
20
|
@mutex.synchronize do
|
|
20
21
|
raise ClosedError, 'notification router is closed' if @closed
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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"
|
|
28
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 }
|
|
29
37
|
entry.channel
|
|
30
38
|
end
|
|
31
39
|
end
|
|
32
40
|
|
|
33
|
-
def channel(
|
|
41
|
+
def channel(endpoint)
|
|
34
42
|
@mutex.synchronize do
|
|
35
|
-
@entries.fetch(@
|
|
43
|
+
@entries.fetch(@protocol.notification_channel_key(endpoint)).channel
|
|
36
44
|
end
|
|
37
45
|
end
|
|
38
46
|
|
|
39
47
|
# Returns true if the frame belongs to a registered notification family.
|
|
40
48
|
def route(header, payload)
|
|
41
|
-
if header.type ==
|
|
49
|
+
if header.type == Raw::NLMSG_OVERRUN
|
|
42
50
|
entries = @mutex.synchronize { @entries.values.dup }
|
|
43
51
|
entries.each { it.channel.fail(NotificationLossError.new('kernel reported Netlink overrun')) }
|
|
44
52
|
return true
|
|
45
53
|
end
|
|
46
54
|
|
|
47
55
|
entry = @mutex.synchronize do
|
|
48
|
-
@
|
|
56
|
+
@routes[@protocol.notification_frame_key(header)]
|
|
49
57
|
end
|
|
50
58
|
return false unless entry
|
|
51
|
-
return false unless
|
|
59
|
+
return false unless @protocol.notification_frame?(entry.endpoint, header, payload)
|
|
52
60
|
|
|
53
|
-
message_class =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
UnknownNotification.new(header:, payload: payload.get_string)
|
|
58
|
-
end
|
|
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)
|
|
59
65
|
entry.channel.push(notification)
|
|
60
66
|
true
|
|
61
|
-
rescue
|
|
67
|
+
rescue => error
|
|
62
68
|
entry&.channel&.fail(error)
|
|
63
69
|
true
|
|
64
70
|
end
|
|
@@ -70,6 +76,7 @@ module Nl
|
|
|
70
76
|
@closed = true
|
|
71
77
|
old = @entries.values
|
|
72
78
|
@entries.clear
|
|
79
|
+
@routes.clear
|
|
73
80
|
old
|
|
74
81
|
end
|
|
75
82
|
entries.each { it.channel.close }
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Raw Netlink client handling
|
|
2
|
+
#-
|
|
3
|
+
|
|
4
|
+
require_relative '../connection'
|
|
5
|
+
require_relative 'protocol'
|
|
6
|
+
|
|
7
|
+
module Nl
|
|
8
|
+
module Raw
|
|
9
|
+
# Owns one raw Netlink connection shared by compatible families.
|
|
10
|
+
class Client
|
|
11
|
+
# Opens a client for a raw Netlink protocol.
|
|
12
|
+
#
|
|
13
|
+
# @overload open(protonum:, executor: nil, notification_capacity: Nl::Connection::DEFAULT_NOTIFICATION_CAPACITY)
|
|
14
|
+
# The caller is responsible for closing the client.
|
|
15
|
+
# @param [Integer] protonum the Netlink protocol number
|
|
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 [Client] the opened client
|
|
19
|
+
# @overload open(protonum:, executor: nil, notification_capacity: Nl::Connection::DEFAULT_NOTIFICATION_CAPACITY, &block)
|
|
20
|
+
# The client is automatically closed after the block returns.
|
|
21
|
+
# @param [Integer] protonum the Netlink protocol number
|
|
22
|
+
# @param [:thread, :fiber, nil] executor the asynchronous executor, or `nil` for blocking operation
|
|
23
|
+
# @param [Integer] notification_capacity the maximum number of queued notifications
|
|
24
|
+
# @yieldparam [Client] client the opened client
|
|
25
|
+
# @return [Object] the value returned from the block
|
|
26
|
+
# @rbs (protonum: Integer, ?executor: executor?, ?notification_capacity: Integer?) -> instance
|
|
27
|
+
# | [R] (protonum: Integer, ?executor: executor?, ?notification_capacity: Integer?) { (instance) -> R } -> R
|
|
28
|
+
def self.open(protonum:, executor: nil, notification_capacity: Nl::Connection::DEFAULT_NOTIFICATION_CAPACITY)
|
|
29
|
+
client = new(protonum:, executor:, notification_capacity:)
|
|
30
|
+
return client unless block_given?
|
|
31
|
+
|
|
32
|
+
begin
|
|
33
|
+
yield client
|
|
34
|
+
ensure
|
|
35
|
+
client.close
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param [Integer] protonum the Netlink protocol number
|
|
40
|
+
# @param [:thread, :fiber, nil] executor the asynchronous executor, or `nil` for blocking operation
|
|
41
|
+
# @param [Integer] notification_capacity the maximum number of queued notifications
|
|
42
|
+
# @rbs (protonum: Integer, ?executor: executor?, ?notification_capacity: Integer?) -> void
|
|
43
|
+
def initialize(protonum:, executor: nil, notification_capacity: Nl::Connection::DEFAULT_NOTIFICATION_CAPACITY)
|
|
44
|
+
@protonum = protonum
|
|
45
|
+
@connection = Nl::Connection.new(
|
|
46
|
+
protocol: Protocol.new(protonum),
|
|
47
|
+
executor:,
|
|
48
|
+
notification_capacity:,
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Builds a family backed by this client's connection.
|
|
53
|
+
#
|
|
54
|
+
# @param [Class<Family>] family_class a raw Netlink family class
|
|
55
|
+
# @return [Family] an instance of `family_class`
|
|
56
|
+
# @raise [TypeError] if +family_class+ does not inherit from {Family}
|
|
57
|
+
# @raise [ArgumentError] if the family's protocol number differs from the client's protocol number
|
|
58
|
+
# @rbs [F < Family] (_FamilyClass[F] family_class) -> F
|
|
59
|
+
def family(family_class)
|
|
60
|
+
unless family_class <= Family
|
|
61
|
+
raise TypeError, "family class must inherit from #{Family}"
|
|
62
|
+
end
|
|
63
|
+
unless family_class::PROTONUM == @protonum
|
|
64
|
+
raise ArgumentError,
|
|
65
|
+
"family protonum #{family_class::PROTONUM} does not match client protonum #{@protonum}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
family_class.new(
|
|
69
|
+
@connection,
|
|
70
|
+
endpoint: Endpoint.new(family_class),
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Closes the underlying Netlink connection.
|
|
75
|
+
#
|
|
76
|
+
# @return [void]
|
|
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,147 @@
|
|
|
1
|
+
# Netlink wire definitions
|
|
2
|
+
#-
|
|
3
|
+
|
|
4
|
+
require_relative '../endian'
|
|
5
|
+
|
|
6
|
+
module Nl
|
|
7
|
+
module Raw
|
|
8
|
+
# Constants from <linux/netlink.h>
|
|
9
|
+
module Constants
|
|
10
|
+
NETLINK_ROUTE = 0
|
|
11
|
+
NETLINK_NETFILTER = 12
|
|
12
|
+
NETLINK_GENERIC = 16
|
|
13
|
+
|
|
14
|
+
NLM_F_REQUEST = 1
|
|
15
|
+
NLM_F_MULTI = 2
|
|
16
|
+
NLM_F_ACK = 4
|
|
17
|
+
NLM_F_ECHO = 8
|
|
18
|
+
NLM_F_DUMP_INTR = 16
|
|
19
|
+
NLM_F_DUMP_FILTERED = 32
|
|
20
|
+
NLM_F_ROOT = 0x100
|
|
21
|
+
NLM_F_MATCH = 0x200
|
|
22
|
+
NLM_F_ATOMIC = 0x400
|
|
23
|
+
NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH
|
|
24
|
+
NLM_F_REPLACE = 0x100
|
|
25
|
+
NLM_F_EXCL = 0x200
|
|
26
|
+
NLM_F_CREATE = 0x400
|
|
27
|
+
NLM_F_APPEND = 0x800
|
|
28
|
+
|
|
29
|
+
NLMSG_ALIGNTO = 4
|
|
30
|
+
NLMSG_HDRLEN = 16
|
|
31
|
+
|
|
32
|
+
NLMSG_NOOP = 0x1
|
|
33
|
+
NLMSG_ERROR = 0x2
|
|
34
|
+
NLMSG_DONE = 0x3
|
|
35
|
+
NLMSG_OVERRUN = 0x4
|
|
36
|
+
|
|
37
|
+
NLMSG_MIN_TYPE = 0x10
|
|
38
|
+
|
|
39
|
+
NLA_F_NESTED = 1 << 15
|
|
40
|
+
NLA_F_NET_BYTEORDER = 1 << 14
|
|
41
|
+
NLA_TYPE_MASK = ~(NLA_F_NESTED | NLA_F_NET_BYTEORDER)
|
|
42
|
+
|
|
43
|
+
NLA_ALIGNTO = 4
|
|
44
|
+
NLA_HDRLEN = 4
|
|
45
|
+
end
|
|
46
|
+
include Constants
|
|
47
|
+
|
|
48
|
+
# Fixed-format metadata header prepended to every Netlink message.
|
|
49
|
+
#
|
|
50
|
+
# This corresponds to Linux's +struct nlmsghdr+.
|
|
51
|
+
#
|
|
52
|
+
# @!attribute [rw] len
|
|
53
|
+
# @return [Integer] message length in bytes, including this header
|
|
54
|
+
# @!attribute [rw] type
|
|
55
|
+
# @return [Integer] message content type
|
|
56
|
+
# @!attribute [rw] flags
|
|
57
|
+
# @return [Integer] bitwise combination of +NLM_F_+ flags
|
|
58
|
+
# @!attribute [rw] seq
|
|
59
|
+
# @return [Integer] sequence number used to correlate requests and replies
|
|
60
|
+
# @!attribute [rw] pid
|
|
61
|
+
# @return [Integer] sender's Netlink port ID
|
|
62
|
+
NlMsgHdr = Struct.new(
|
|
63
|
+
:len, #: Integer
|
|
64
|
+
:type, #: Integer
|
|
65
|
+
:flags, #: Integer
|
|
66
|
+
:seq, #: Integer
|
|
67
|
+
:pid, #: Integer
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
class NlMsgHdr
|
|
71
|
+
FORMAT = Ractor.make_shareable([
|
|
72
|
+
Endian::Host::U32,
|
|
73
|
+
Endian::Host::U16,
|
|
74
|
+
Endian::Host::U16,
|
|
75
|
+
Endian::Host::U32,
|
|
76
|
+
Endian::Host::U32,
|
|
77
|
+
])
|
|
78
|
+
private_constant :FORMAT
|
|
79
|
+
|
|
80
|
+
# Decodes a header from the decoder's current position.
|
|
81
|
+
#
|
|
82
|
+
# @param [Decoder] decoder the source decoder
|
|
83
|
+
# @return [NlMsgHdr] the decoded header
|
|
84
|
+
# @rbs (Decoder decoder) -> instance
|
|
85
|
+
def self.decode(decoder)
|
|
86
|
+
obj = new(*decoder.get_values(FORMAT))
|
|
87
|
+
decoder.align_to(Constants::NLMSG_ALIGNTO)
|
|
88
|
+
obj
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Encodes this header at the encoder's current position.
|
|
92
|
+
#
|
|
93
|
+
# @param [Encoder] encoder the destination encoder
|
|
94
|
+
# @return [void]
|
|
95
|
+
# @rbs (Encoder encoder) -> void
|
|
96
|
+
def encode(encoder)
|
|
97
|
+
encoder.reserve(Constants::NLMSG_HDRLEN)
|
|
98
|
+
encoder.put_values(FORMAT, to_a)
|
|
99
|
+
encoder.align_to(Constants::NLMSG_ALIGNTO)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Fixed-format header prepended to every Netlink attribute.
|
|
104
|
+
#
|
|
105
|
+
# This corresponds to Linux's +struct nlattr+.
|
|
106
|
+
#
|
|
107
|
+
# @!attribute [rw] len
|
|
108
|
+
# @return [Integer] attribute length in bytes, including this header but
|
|
109
|
+
# excluding trailing alignment padding
|
|
110
|
+
# @!attribute [rw] type
|
|
111
|
+
# @return [Integer] attribute type combined with optional +NLA_F_+ flags
|
|
112
|
+
NlAttr = Struct.new(
|
|
113
|
+
:len, #: Integer
|
|
114
|
+
:type, #: Integer
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
class NlAttr
|
|
118
|
+
FORMAT = Ractor.make_shareable([
|
|
119
|
+
Endian::Host::U16,
|
|
120
|
+
Endian::Host::U16,
|
|
121
|
+
])
|
|
122
|
+
private_constant :FORMAT
|
|
123
|
+
|
|
124
|
+
# Decodes an attribute header from the decoder's current position.
|
|
125
|
+
#
|
|
126
|
+
# @param [Decoder] decoder the source decoder
|
|
127
|
+
# @return [NlAttr] the decoded header
|
|
128
|
+
# @rbs (Decoder decoder) -> instance
|
|
129
|
+
def self.decode(decoder)
|
|
130
|
+
obj = new(*decoder.get_values(FORMAT))
|
|
131
|
+
decoder.align_to(Constants::NLA_ALIGNTO)
|
|
132
|
+
obj
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Encodes this attribute header at the encoder's current position.
|
|
136
|
+
#
|
|
137
|
+
# @param [Encoder] encoder the destination encoder
|
|
138
|
+
# @return [void]
|
|
139
|
+
# @rbs (Encoder encoder) -> void
|
|
140
|
+
def encode(encoder)
|
|
141
|
+
encoder.reserve(Constants::NLA_HDRLEN)
|
|
142
|
+
encoder.put_values(FORMAT, to_a)
|
|
143
|
+
encoder.align_to(Constants::NLA_ALIGNTO)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
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
|
data/lib/nl/socket.rb
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
# Netlink sockets
|
|
2
|
-
|
|
3
1
|
require 'socket'
|
|
4
2
|
|
|
5
3
|
module Nl
|
|
6
4
|
# Netlink socket
|
|
7
5
|
class Socket < ::Socket
|
|
8
6
|
module Constants
|
|
9
|
-
# From
|
|
7
|
+
# From linux/socket.h
|
|
8
|
+
#-
|
|
10
9
|
PF_NETLINK = AF_NETLINK = 16
|
|
11
10
|
|
|
12
|
-
# From
|
|
11
|
+
# From linux/netlink.h
|
|
12
|
+
#-
|
|
13
13
|
SOL_NETLINK = 270
|
|
14
14
|
NETLINK_ADD_MEMBERSHIP = 1
|
|
15
15
|
NETLINK_DROP_MEMBERSHIP = 2
|
|
@@ -17,17 +17,28 @@ module Nl
|
|
|
17
17
|
include Constants
|
|
18
18
|
|
|
19
19
|
class << self
|
|
20
|
+
# Packs Netlink socket address
|
|
21
|
+
#
|
|
22
|
+
# @rbs (Integer pid, Integer groups) -> String
|
|
20
23
|
def pack_sockaddr_nl(pid, groups) = [Socket::AF_NETLINK, 0, pid, groups].pack('S!S!LL')
|
|
21
24
|
alias sockaddr_nl pack_sockaddr_nl
|
|
22
25
|
|
|
26
|
+
# Unpacks Netlink socket address
|
|
27
|
+
#
|
|
28
|
+
# @rbs (String) -> [Integer, Integer]
|
|
23
29
|
def unpack_sockaddr_nl(sockaddr) = sockaddr.unpack('S!S!LL')[2..3]
|
|
24
30
|
end
|
|
25
31
|
|
|
26
|
-
# @param
|
|
32
|
+
# @param [Integer] protonum Netlink protocol number
|
|
33
|
+
# @rbs (Integer) -> void
|
|
27
34
|
def initialize(protonum)
|
|
28
35
|
super(PF_NETLINK, SOCK_RAW, protonum)
|
|
29
36
|
end
|
|
30
37
|
|
|
38
|
+
# Opens a Netlink socket for the specified protocol number.
|
|
39
|
+
#
|
|
40
|
+
# @rbs (Integer protonum) -> instance
|
|
41
|
+
# | [R] (Integer protonum) { (instance) -> R } -> R
|
|
31
42
|
def self.open(protonum)
|
|
32
43
|
return new(protonum) unless block_given?
|
|
33
44
|
begin
|
|
@@ -39,16 +50,21 @@ module Nl
|
|
|
39
50
|
end
|
|
40
51
|
|
|
41
52
|
# @return [Integer] Local Netlink port ID assigned to this socket
|
|
53
|
+
# @rbs () -> Integer
|
|
42
54
|
def local_port_id
|
|
43
55
|
Socket.unpack_sockaddr_nl(local_address.to_sockaddr).first
|
|
44
56
|
end
|
|
45
57
|
|
|
46
58
|
# Adds this socket to a Netlink multicast group.
|
|
59
|
+
#
|
|
60
|
+
# @rbs (Integer group_id) -> void
|
|
47
61
|
def add_membership(group_id)
|
|
48
62
|
setsockopt(SOL_NETLINK, NETLINK_ADD_MEMBERSHIP, group_id)
|
|
49
63
|
end
|
|
50
64
|
|
|
51
65
|
# Removes this socket from a Netlink multicast group.
|
|
66
|
+
#
|
|
67
|
+
# @rbs (Integer group_id) -> void
|
|
52
68
|
def drop_membership(group_id)
|
|
53
69
|
setsockopt(SOL_NETLINK, NETLINK_DROP_MEMBERSHIP, group_id)
|
|
54
70
|
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
|