nl 0.2.4 → 0.3.0

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.
@@ -0,0 +1,115 @@
1
+ # rbs_inline: enabled
2
+
3
+ require_relative 'error'
4
+
5
+ module Nl
6
+ # A multicast group. `name` is the kernel-facing name.
7
+ # `id` is the fixed ID when the specification provides one.
8
+ McastGroup = Data.define(:name, :id)
9
+
10
+ # An unsolicited message whose wire type is not present in the loaded spec.
11
+ UnknownNotification = Data.define(:header, :payload)
12
+
13
+ # Thread-safe queue shared by a family's blocking and asynchronous facades.
14
+ class NotificationChannel
15
+ def initialize(capacity:)
16
+ raise ArgumentError, 'notification capacity must be positive' if capacity && capacity <= 0
17
+
18
+ @capacity = capacity
19
+ @mutex = Mutex.new
20
+ @condition = ConditionVariable.new
21
+ @queue = []
22
+ @error = nil
23
+ @closed = false
24
+ end
25
+
26
+ # Adds a notification without ever blocking the socket receive loop.
27
+ # Returns false after the channel has been closed.
28
+ def push(notification)
29
+ @mutex.synchronize do
30
+ return false if @closed
31
+
32
+ if @capacity && @queue.length >= @capacity
33
+ lose!(NotificationLossError.new('notification queue capacity exceeded'))
34
+ else
35
+ wake = @queue.empty? && !@error
36
+ @queue << notification
37
+ @condition.signal if wake
38
+ end
39
+ end
40
+ true
41
+ end
42
+
43
+ # Records a broken notification boundary while allowing later delivery to
44
+ # resume after the consumer observes the error and resynchronizes state.
45
+ def fail(error)
46
+ @mutex.synchronize do
47
+ return false if @closed
48
+
49
+ lose!(error)
50
+ end
51
+ true
52
+ end
53
+
54
+ def pop(timeout: nil)
55
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout if timeout
56
+
57
+ @mutex.synchronize do
58
+ loop do
59
+ if error = @error
60
+ @error = nil
61
+ raise error
62
+ end
63
+ return @queue.shift unless @queue.empty?
64
+ raise ClosedError, 'notification channel is closed' if @closed
65
+
66
+ remaining = deadline && deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
67
+ raise TimeoutError, 'notification receive timed out' if remaining && remaining <= 0
68
+
69
+ @condition.wait(@mutex, remaining)
70
+ end
71
+ end
72
+ end
73
+
74
+ def empty?
75
+ @mutex.synchronize { @queue.empty? && !@error }
76
+ end
77
+
78
+ def close
79
+ @mutex.synchronize do
80
+ return nil if @closed
81
+
82
+ @closed = true
83
+ @queue.clear
84
+ @error = nil
85
+ @condition.broadcast
86
+ end
87
+ nil
88
+ end
89
+
90
+ private def lose!(error)
91
+ @queue.clear
92
+ @error = error
93
+ @condition.signal
94
+ end
95
+ end
96
+
97
+ # An unbounded-in-time, single-family view of unsolicited messages.
98
+ class NotificationStream
99
+ include Enumerable
100
+
101
+ def initialize(&receive)
102
+ @receive = receive
103
+ end
104
+
105
+ def next(timeout: nil)
106
+ @receive.call(timeout)
107
+ end
108
+
109
+ def each
110
+ return enum_for(__method__) unless block_given?
111
+
112
+ loop { yield self.next }
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,85 @@
1
+ require_relative 'core'
2
+ require_relative 'notification'
3
+
4
+ module Nl
5
+ # Routes unsolicited frames to per-family notification channels.
6
+ class NotificationRouter
7
+ Entry = Struct.new(:protocol, :classes, :channel)
8
+ private_constant :Entry
9
+
10
+ def initialize(routing:, capacity:)
11
+ @routing = routing
12
+ @capacity = capacity
13
+ @mutex = Mutex.new
14
+ @entries = {}
15
+ @closed = false
16
+ end
17
+
18
+ def register(protocol, classes)
19
+ @mutex.synchronize do
20
+ raise ClosedError, 'notification router is closed' if @closed
21
+
22
+ key = @routing.family_key(protocol)
23
+ if entry = @entries[key]
24
+ entry.classes.merge!(classes)
25
+ else
26
+ entry = Entry.new(protocol, classes.dup, NotificationChannel.new(capacity: @capacity))
27
+ @entries[key] = entry
28
+ end
29
+ entry.channel
30
+ end
31
+ end
32
+
33
+ def channel(protocol)
34
+ @mutex.synchronize do
35
+ @entries.fetch(@routing.family_key(protocol)).channel
36
+ end
37
+ end
38
+
39
+ # Returns true if the frame belongs to a registered notification family.
40
+ def route(header, payload)
41
+ if header.type == Core::NLMSG_OVERRUN
42
+ entries = @mutex.synchronize { @entries.values.dup }
43
+ entries.each { it.channel.fail(NotificationLossError.new('kernel reported Netlink overrun')) }
44
+ return true
45
+ end
46
+
47
+ entry = @mutex.synchronize do
48
+ @entries[@routing.frame_key(header)]
49
+ end
50
+ return false unless entry
51
+ return false unless entry.protocol.notification_frame?(header, payload)
52
+
53
+ message_class = entry.protocol.notification_class(header, payload, entry.classes)
54
+ notification = if message_class
55
+ entry.protocol.decode_notification(header, payload, message_class)
56
+ else
57
+ UnknownNotification.new(header:, payload: payload.get_string)
58
+ end
59
+ entry.channel.push(notification)
60
+ true
61
+ rescue Exception => error
62
+ entry&.channel&.fail(error)
63
+ true
64
+ end
65
+
66
+ def close
67
+ entries = @mutex.synchronize do
68
+ return nil if @closed
69
+
70
+ @closed = true
71
+ old = @entries.values
72
+ @entries.clear
73
+ old
74
+ end
75
+ entries.each { it.channel.close }
76
+ nil
77
+ end
78
+
79
+ def lose_all(error)
80
+ entries = @mutex.synchronize { @entries.values.dup }
81
+ entries.each { it.channel.fail(error) }
82
+ nil
83
+ end
84
+ end
85
+ end
@@ -1,52 +1,71 @@
1
+ require_relative '../genl'
2
+ require_relative 'raw'
3
+
1
4
  module Nl
2
5
  module Protocols
3
6
  # The Generic Netlink protocol
4
7
  class Genl < Raw
5
- GenlMsgHdr = ::Struct.new(:cmd, :version, :reserved)
6
- class GenlMsgHdr
7
- FORMAT = Ractor.make_shareable([
8
- Endian::Host::U8,
9
- Endian::Host::U8,
10
- Endian::Host::U16,
11
- ])
12
-
13
- def self.decode(decoder)
14
- new(*decoder.get_values(FORMAT))
15
- end
16
-
17
- def encode(encoder)
18
- encoder.put_values(FORMAT, to_a)
19
- end
8
+ class NotificationRouting
9
+ def family_key(protocol) = protocol.family_key
10
+ def frame_key(header) = header.type
20
11
  end
21
12
 
22
- def initialize(name, family_id: nil)
23
- super(name, Core::NETLINK_GENERIC)
13
+ NOTIFICATION_ROUTING = NotificationRouting.new.freeze
14
+
15
+ def self.protonum = Core::NETLINK_GENERIC
16
+
17
+ def initialize(name, family_id: nil, multicast_groups: {})
18
+ super(name, self.class.protonum)
24
19
  @family_id = family_id || default_family_id(name)
20
+ @multicast_groups = multicast_groups.transform_keys(&:to_sym).freeze
25
21
  end
26
22
 
27
23
  def family_id
28
24
  @family_id or raise NotImplementedError, "Genetlink family ID for '#{name}' must be resolved via nlctrl"
29
25
  end
30
26
 
31
- def encode_message(encoder, message)
32
- cmd = message.nlmsg_header.type
27
+ def encode_message(encoder, request, seq:, pid:)
28
+ message = request.message
29
+ header = Core::NlMsgHdr.new(0, request.type, request.flags, seq, pid)
33
30
  encoder.measure(Endian::Host::U16) do
34
- message.nlmsg_header.type = family_id
35
- message.nlmsg_header.encode(encoder)
36
- message.nlmsg_header.type = cmd
37
- GenlMsgHdr.new(cmd, 1, 0).encode(encoder)
38
- message.fixed_header&.encode(encoder)
39
- message.attributes.encode(encoder)
31
+ header.encode(encoder)
32
+ Nl::Genl::GenlMsgHdr.new(message.class::TYPE, 1, 0).encode(encoder)
33
+ message.encode(encoder)
40
34
  end
41
35
  end
42
36
 
43
37
  class Message < Raw::Message
44
- def self.decode(decoder, header)
45
- genlhdr = GenlMsgHdr.decode(decoder)
46
- super(decoder, header, type: genlhdr.cmd)
38
+ def self.decode(decoder, type:)
39
+ genlhdr = Nl::Genl::GenlMsgHdr.decode(decoder)
40
+ super(decoder, type: genlhdr.cmd)
47
41
  end
48
42
  end
49
43
 
44
+ def family_key = family_id
45
+
46
+ def self.notification_routing = NOTIFICATION_ROUTING
47
+ def notification_routing = self.class.notification_routing
48
+
49
+ def notification_frame?(header, _payload)
50
+ header.type == family_id
51
+ end
52
+
53
+ def notification_class(header, payload, classes)
54
+ return unless notification_frame?(header, payload)
55
+
56
+ command = Nl::Genl::GenlMsgHdr.decode(Decoder.new(payload)).cmd
57
+ classes[command]
58
+ end
59
+
60
+ def multicast_group_id(name, _value)
61
+ @multicast_groups.fetch(name.to_sym) do
62
+ raise UnresolvedMulticastGroupError,
63
+ "Generic Netlink multicast group #{name.inspect} was not resolved"
64
+ end
65
+ end
66
+
67
+ private def frame_type(_message_class) = family_id
68
+
50
69
  private def default_family_id(name)
51
70
  Nl::Genl::GENL_ID_CTRL if name == 'nlctrl'
52
71
  end
@@ -5,11 +5,20 @@ module Nl
5
5
  module Protocols
6
6
  # The raw Netlink protocol
7
7
  class Raw
8
- class Done
9
- end
10
- class Ack
8
+ class NotificationRouting
9
+ def family_key(_protocol) = nil
10
+ def frame_key(_header) = nil
11
11
  end
12
12
 
13
+ NOTIFICATION_ROUTING = NotificationRouting.new.freeze
14
+
15
+ AckFrame = Data.define(:header)
16
+ ErrorFrame = Data.define(:header, :errno)
17
+ DoneFrame = Data.define(:header, :errno)
18
+ UnknownFrame = Data.define(:header)
19
+ DataFrame = Data.define(:header, :message)
20
+ Request = Data.define(:type, :flags, :message)
21
+
13
22
  attr_reader :name, :protonum
14
23
 
15
24
  def initialize(name, protonum)
@@ -17,99 +26,91 @@ module Nl
17
26
  @protonum = protonum
18
27
  end
19
28
 
20
- def encode_message(encoder, message)
21
- message.encode(encoder)
29
+ def encode_message(encoder, request, seq:, pid:)
30
+ header = Core::NlMsgHdr.new(0, request.type, request.flags, seq, pid)
31
+ encoder.measure(Endian::Host::U16) do
32
+ header.encode(encoder)
33
+ request.message.encode(encoder)
34
+ end
22
35
  end
23
36
 
24
37
  def decode_message(decoder, message_class)
25
38
  header = NlMsgHdr.decode(decoder)
26
39
  decoder.limit(header.len - Core::NLMSG_HDRLEN) do
27
- message_class.decode(decoder, header)
40
+ DataFrame.new(header:, message: message_class.decode(decoder, type: header.type))
28
41
  end
29
42
  end
30
43
 
31
- def send_message(socket, message)
32
- seq_pid = socket.complete(message.nlmsg_header)
33
- encoder = Encoder.new
34
- encode_message(encoder, message)
35
- socket.sendmsg(encoder.buffer.get_string, 0, Socket.sockaddr_nl(0, 0))
36
- seq_pid
37
- end
44
+ # Decodes one frame using the reply class associated with its sequence.
45
+ def decode_frame(header, payload, message_class)
46
+ decoder = Decoder.new(payload)
47
+ if header.type < Core::NLMSG_MIN_TYPE
48
+ case header.type
49
+ when Core::NLMSG_ERROR
50
+ errno = decoder.get_value(Endian::Host::SINT)
51
+ if errno.positive?
52
+ raise ProtocolViolation, "expected zero or negative NLMSG_ERROR errno, got #{errno}"
53
+ end
38
54
 
39
- def recv_message(socket, seq_pid, message_class)
40
- data, = socket.recvmsg
41
-
42
- decoder = Decoder.new(IO::Buffer.for(data))
43
- while decoder.available?(Core::NLMSG_HDRLEN)
44
- header = Core::NlMsgHdr.decode(decoder)
45
- decoder.align_to(Core::NLMSG_ALIGNTO)
46
- raise binding.irb unless [header.seq, header.pid] == seq_pid
47
- if header.type < Core::NLMSG_MIN_TYPE
48
- # Control messages
49
- case header.type
50
- when Core::NLMSG_ERROR
51
- errno = decoder.get_value(Endian::Host::SINT)
52
- if errno == 0
53
- yield Ack.new
54
- else
55
- yield SystemCallError.new(-errno)
56
- end
57
- decoder.skip(header.len - Core::NLMSG_HDRLEN - 4)
58
- when Core::NLMSG_DONE
59
- yield Done.new
60
- decoder.skip(header.len - Core::NLMSG_HDRLEN)
61
- else
62
- # just ignore NLMSG_NOOP and other unknown control messages
63
- decoder.skip(header.len - Core::NLMSG_HDRLEN)
55
+ errno.zero? ? AckFrame.new(header:) : ErrorFrame.new(header:, errno: -errno)
56
+ when Core::NLMSG_DONE
57
+ return DoneFrame.new(header:, errno: nil) unless decoder.available?
58
+
59
+ errno = decoder.get_value(Endian::Host::SINT)
60
+ if errno.positive?
61
+ raise ProtocolViolation, "expected zero or negative NLMSG_DONE errno, got #{errno}"
64
62
  end
63
+
64
+ DoneFrame.new(header:, errno: errno.negative? ? -errno : nil)
65
65
  else
66
- # Subsystem-specific messages
67
- decoder.limit(header.len - Core::NLMSG_HDRLEN) do
68
- decoder.align_to(Core::NLMSG_ALIGNTO)
69
- yield message_class.decode(decoder, header)
70
- end
66
+ UnknownFrame.new(header:)
71
67
  end
68
+ else
69
+ raise ArgumentError, 'reply class is required for a data message' unless message_class
70
+
71
+ DataFrame.new(header:, message: message_class.decode(decoder, type: header.type))
72
72
  end
73
73
  end
74
74
 
75
- # @param socket [Socket] Netlink socket
76
- # @param type [:do, :dump] Request type
77
- # @param request_class [Class] Request message class
78
- # @param reply_class [Class] Reply message class
79
- # @param args [Hash] Request arguments
80
- def exchange_message(socket, type, request_class, reply_class, args)
75
+ def send_message(socket, request, seq:, pid:)
76
+ encoder = Encoder.new
77
+ encode_message(encoder, request, seq:, pid:)
78
+ socket.sendmsg(encoder.buffer.get_string, 0, Socket.sockaddr_nl(0, 0))
79
+ nil
80
+ end
81
+
82
+ def build_request(kind, request_class, args)
81
83
  flags = Core::NLM_F_REQUEST
82
- flags |= type == :dump ? Core::NLM_F_DUMP : Core::NLM_F_ACK
83
-
84
- request = request_class.from_params(args)
85
- request.nlmsg_header.flags = flags
86
- seq_pid = send_message(socket, request)
87
-
88
- result = [] unless block_given?
89
-
90
- done = false
91
- begin
92
- recv_message(socket, seq_pid, reply_class) do |message|
93
- case message
94
- when Done, Ack
95
- done = true
96
- when Exception
97
- raise message
98
- else
99
- if block_given?
100
- yield message
101
- else
102
- result << message
103
- end
104
- end
105
- end
106
- end until done
84
+ flags |= kind == :dump ? Core::NLM_F_DUMP : Core::NLM_F_ACK
107
85
 
108
- unless block_given?
109
- type == :dump ? result : result.first
110
- end
86
+ message = request_class.from_params(args)
87
+ Request.new(type: frame_type(request_class), flags:, message:)
88
+ end
89
+
90
+ # Raw Netlink sockets carry a single protocol family.
91
+ def notification_routing = NOTIFICATION_ROUTING
92
+
93
+ # Whether an unsolicited data frame belongs to this protocol.
94
+ def notification_frame?(header, _payload)
95
+ header.type >= Core::NLMSG_MIN_TYPE
111
96
  end
112
97
 
98
+ # Looks up the generated notification class for an unsolicited frame.
99
+ def notification_class(header, _payload, classes)
100
+ classes[header.type]
101
+ end
102
+
103
+ def decode_notification(header, payload, message_class)
104
+ decode_frame(header, payload, message_class).message
105
+ end
106
+
107
+ def multicast_group_id(name, value)
108
+ value or raise UnresolvedMulticastGroupError,
109
+ "multicast group #{name.inspect} has no fixed ID"
110
+ end
111
+
112
+ private def frame_type(message_class) = message_class::TYPE
113
+
113
114
  class AttributeSet
114
115
  Attribute = Struct.new(:value)
115
116
  class Attribute
@@ -159,7 +160,9 @@ module Nl
159
160
  end
160
161
 
161
162
  private def encode1(encoder, attr)
162
- nlattr = Core::NlAttr.new(0, attr.class::TYPE)
163
+ datatype = attr.class::DATATYPE
164
+ type = attr.class::TYPE | datatype.nlattr_type_flags
165
+ nlattr = Core::NlAttr.new(0, type)
163
166
  encoder.measure(Endian::Host::U16) do
164
167
  nlattr.encode(encoder)
165
168
  attr.encode(encoder)
@@ -208,10 +211,9 @@ module Nl
208
211
  end
209
212
 
210
213
  class Message
211
- attr_accessor :nlmsg_header, :fixed_header, :attributes
214
+ attr_accessor :fixed_header, :attributes
212
215
 
213
- def initialize(header, fixed_header = nil, attributes = self.class::ATTRIBUTE_SET.new)
214
- @nlmsg_header = header
216
+ def initialize(fixed_header = nil, attributes = self.class::ATTRIBUTE_SET.new)
215
217
  @fixed_header = fixed_header
216
218
  @attributes = attributes
217
219
  end
@@ -230,8 +232,7 @@ module Nl
230
232
  raise ArgumentError, "unknown parameters: #{unknown.join(', ')}"
231
233
  end
232
234
 
233
- header = Core::NlMsgHdr.new(0, self::TYPE, nil, nil, nil)
234
- new(header, fixed_header, attributes)
235
+ new(fixed_header, attributes)
235
236
  end
236
237
 
237
238
  def append_attribute(attribute)
@@ -239,14 +240,11 @@ module Nl
239
240
  end
240
241
 
241
242
  def encode(encoder)
242
- encoder.measure(Endian::Host::U16) do
243
- @nlmsg_header.encode(encoder)
244
- @fixed_header&.encode(encoder)
245
- @attributes.encode(encoder)
246
- end
243
+ @fixed_header&.encode(encoder)
244
+ @attributes.encode(encoder)
247
245
  end
248
246
 
249
- def self.decode(decoder, header, type: header.type)
247
+ def self.decode(decoder, type:)
250
248
  unless self::TYPE == type
251
249
  raise "Expected message type #{self::TYPE}, got #{type}"
252
250
  end
@@ -257,12 +255,18 @@ module Nl
257
255
 
258
256
  attributes = self::ATTRIBUTE_SET.decode(decoder)
259
257
 
260
- new(header, fixed_header, attributes)
258
+ new(fixed_header, attributes)
261
259
  end
262
260
  end
263
261
 
264
262
  module DataTypes
265
- class Scalar
263
+ class Base
264
+ def nlattr_type_flags
265
+ 0
266
+ end
267
+ end
268
+
269
+ class Scalar < Base
266
270
  def initialize(type, check)
267
271
  @type = type
268
272
  @check = check
@@ -278,7 +282,7 @@ module Nl
278
282
  end
279
283
  end
280
284
 
281
- class String
285
+ class String < Base
282
286
  def initialize(check)
283
287
  @check = check
284
288
  end
@@ -292,7 +296,7 @@ module Nl
292
296
  end
293
297
  end
294
298
 
295
- class Binary
299
+ class Binary < Base
296
300
  def initialize(check)
297
301
  @check = check
298
302
  end
@@ -306,7 +310,7 @@ module Nl
306
310
  end
307
311
  end
308
312
 
309
- class Flag
313
+ class Flag < Base
310
314
  def encode(encoder, value)
311
315
  # flag attribute has no payload; presence encodes true
312
316
  end
@@ -317,7 +321,7 @@ module Nl
317
321
  end
318
322
 
319
323
  # A 32-bit value paired with a selector mask (8 bytes total: value u32 + selector u32)
320
- class Bitfield32
324
+ class Bitfield32 < Base
321
325
  def encode(encoder, value)
322
326
  v, selector = value.is_a?(Array) ? value : [value, 0xFFFFFFFF]
323
327
  encoder.put_value(Endian::Host::U32, v)
@@ -331,7 +335,7 @@ module Nl
331
335
  end
332
336
  end
333
337
 
334
- class Pad
338
+ class Pad < Base
335
339
  def initialize(length = nil)
336
340
  @length = length
337
341
  end
@@ -346,13 +350,21 @@ module Nl
346
350
  end
347
351
  end
348
352
 
349
- class NestedAttributes
353
+ class NestedAttributes < Base
350
354
  def initialize(attribute_set)
351
355
  @attribute_set = attribute_set
352
356
  end
353
357
 
358
+ def nlattr_type_flags
359
+ Core::NLA_F_NESTED
360
+ end
361
+
354
362
  def encode(encoder, value)
355
- @attribute_set.encode(encoder, value)
363
+ unless value.is_a?(@attribute_set)
364
+ raise TypeError, "value must be an instance of #{@attribute_set}"
365
+ end
366
+
367
+ value.encode(encoder)
356
368
  end
357
369
 
358
370
  def decode(decoder)
@@ -360,7 +372,60 @@ module Nl
360
372
  end
361
373
  end
362
374
 
363
- class IndexedArray
375
+ # Nested attributes whose type numbers are keys rather than members of
376
+ # an attribute set. Each level is returned as an Integer-keyed Hash;
377
+ # values at the innermost level are decoded as @attribute_set.
378
+ class NestTypeValue < Base
379
+ def initialize(attribute_set, levels)
380
+ raise ArgumentError, 'levels must be positive' unless levels.positive?
381
+
382
+ @attribute_set = attribute_set
383
+ @levels = levels
384
+ end
385
+
386
+ def encode(encoder, value)
387
+ encode_level(encoder, value, @levels)
388
+ end
389
+
390
+ def decode(decoder)
391
+ decode_level(decoder, @levels)
392
+ end
393
+
394
+ private def encode_level(encoder, values, levels)
395
+ values.each do |type, value|
396
+ nlattr = Core::NlAttr.new(0, type | Core::NLA_F_NESTED)
397
+ encoder.measure(Endian::Host::U16) do
398
+ nlattr.encode(encoder)
399
+ if levels == 1
400
+ value.encode(encoder)
401
+ else
402
+ encode_level(encoder, value, levels - 1)
403
+ end
404
+ end
405
+ encoder.align_to(Core::NLA_ALIGNTO)
406
+ end
407
+ end
408
+
409
+ private def decode_level(decoder, levels)
410
+ result = {}
411
+ while decoder.available?(Core::NLA_HDRLEN)
412
+ nlattr = Core::NlAttr.decode(decoder)
413
+ type = nlattr.type & Core::NLA_TYPE_MASK
414
+ value = decoder.limit(nlattr.len - Core::NLA_HDRLEN) do
415
+ if levels == 1
416
+ @attribute_set.decode(decoder)
417
+ else
418
+ decode_level(decoder, levels - 1)
419
+ end
420
+ end
421
+ decoder.align_to(Core::NLA_ALIGNTO)
422
+ result[type] = value
423
+ end
424
+ result
425
+ end
426
+ end
427
+
428
+ class IndexedArray < Base
364
429
  def initialize(sub_type)
365
430
  @sub_type = sub_type
366
431
  end