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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/Rakefile +7 -0
  4. data/lib/nl/async/dispatcher.rb +16 -16
  5. data/lib/nl/async/driver.rb +0 -2
  6. data/lib/nl/async/mailbox.rb +0 -2
  7. data/lib/nl/async/operation.rb +0 -2
  8. data/lib/nl/async.rb +0 -2
  9. data/lib/nl/attribute_set.rb +173 -0
  10. data/lib/nl/bitfield32.rb +86 -0
  11. data/lib/nl/blocking_transport.rb +12 -12
  12. data/lib/nl/connection.rb +7 -9
  13. data/lib/nl/datagram.rb +7 -9
  14. data/lib/nl/datatypes.rb +454 -0
  15. data/lib/nl/decoder.rb +4 -0
  16. data/lib/nl/endian.rb +9 -0
  17. data/lib/nl/exchange.rb +40 -18
  18. data/lib/nl/family.rb +50 -55
  19. data/lib/nl/genl/client.rb +105 -0
  20. data/lib/nl/genl/protocol.rb +59 -0
  21. data/lib/nl/genl/wire.rb +95 -0
  22. data/lib/nl/genl.rb +60 -62
  23. data/lib/nl/notification.rb +3 -6
  24. data/lib/nl/notification_router.rb +30 -23
  25. data/lib/nl/raw/client.rb +83 -0
  26. data/lib/nl/raw/protocol.rb +112 -0
  27. data/lib/nl/raw/wire.rb +147 -0
  28. data/lib/nl/raw.rb +104 -0
  29. data/lib/nl/sequence_allocator.rb +0 -2
  30. data/lib/nl/socket.rb +21 -5
  31. data/lib/nl/structured_payload.rb +61 -0
  32. data/lib/nl/sub_message.rb +94 -0
  33. data/lib/nl/version.rb +1 -1
  34. data/lib/nl.rb +5 -6
  35. data/sig/generated/nl/async/dispatcher.rbs +53 -0
  36. data/sig/generated/nl/async/driver.rbs +21 -0
  37. data/sig/generated/nl/async/mailbox.rbs +32 -0
  38. data/sig/generated/nl/async/operation.rbs +123 -0
  39. data/sig/generated/nl/async.rbs +9 -0
  40. data/sig/generated/nl/attribute_set.rbs +50 -0
  41. data/sig/generated/nl/bitfield32.rbs +41 -0
  42. data/sig/generated/nl/blocking_transport.rbs +33 -0
  43. data/sig/generated/nl/connection.rbs +26 -0
  44. data/sig/generated/nl/datagram.rbs +11 -0
  45. data/sig/generated/nl/datatypes.rbs +181 -0
  46. data/sig/generated/nl/decoder.rbs +38 -0
  47. data/sig/generated/nl/encoder.rbs +29 -0
  48. data/sig/generated/nl/endian.rbs +29 -0
  49. data/sig/generated/nl/error.rbs +27 -0
  50. data/sig/generated/nl/exchange.rbs +75 -0
  51. data/sig/generated/nl/family.rbs +89 -0
  52. data/sig/generated/nl/genl/client.rbs +74 -0
  53. data/sig/generated/nl/genl/protocol.rbs +37 -0
  54. data/sig/generated/nl/genl/wire.rbs +113 -0
  55. data/sig/generated/nl/genl.rbs +50 -0
  56. data/sig/generated/nl/notification.rbs +53 -0
  57. data/sig/generated/nl/notification_router.rbs +30 -0
  58. data/sig/generated/nl/raw/client.rbs +49 -0
  59. data/sig/generated/nl/raw/protocol.rbs +122 -0
  60. data/sig/generated/nl/raw/wire.rbs +151 -0
  61. data/sig/generated/nl/raw.rbs +70 -0
  62. data/sig/generated/nl/sequence_allocator.rbs +15 -0
  63. data/sig/generated/nl/socket.rbs +59 -0
  64. data/sig/generated/nl/structured_payload.rbs +25 -0
  65. data/sig/generated/nl/sub_message.rbs +74 -0
  66. data/sig/generated/nl/version.rbs +5 -0
  67. data/sig/generated/nl.rbs +2 -0
  68. metadata +48 -7
  69. data/lib/nl/core.rb +0 -94
  70. data/lib/nl/genl/connection.rb +0 -89
  71. data/lib/nl/protocols/genl.rb +0 -74
  72. data/lib/nl/protocols/raw.rb +0 -460
@@ -1,64 +1,70 @@
1
- require_relative 'core'
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(:protocol, :classes, :channel)
7
+ Entry = Struct.new(:endpoint, :classes, :channel)
8
8
  private_constant :Entry
9
9
 
10
- def initialize(routing:, capacity:)
11
- @routing = routing
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(protocol, classes)
19
+ def register(endpoint, classes)
19
20
  @mutex.synchronize do
20
21
  raise ClosedError, 'notification router is closed' if @closed
21
22
 
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
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(protocol)
41
+ def channel(endpoint)
34
42
  @mutex.synchronize do
35
- @entries.fetch(@routing.family_key(protocol)).channel
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 == Core::NLMSG_OVERRUN
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
- @entries[@routing.frame_key(header)]
56
+ @routes[@protocol.notification_frame_key(header)]
49
57
  end
50
58
  return false unless entry
51
- return false unless entry.protocol.notification_frame?(header, payload)
59
+ return false unless @protocol.notification_frame?(entry.endpoint, header, payload)
52
60
 
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
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 Exception => error
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
@@ -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
@@ -1,5 +1,3 @@
1
- # rbs_inline: enabled
2
-
3
1
  require_relative 'error'
4
2
 
5
3
  module Nl
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 include/linux/socket.h
7
+ # From linux/socket.h
8
+ #-
10
9
  PF_NETLINK = AF_NETLINK = 16
11
10
 
12
- # From include/uapi/linux/netlink.h
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 protonum [Integer] Netlink protocol number
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