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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/Rakefile +7 -0
  4. data/lib/nl/async/dispatcher.rb +199 -0
  5. data/lib/nl/async/driver.rb +37 -0
  6. data/lib/nl/async/mailbox.rb +81 -0
  7. data/lib/nl/async/operation.rb +277 -0
  8. data/lib/nl/async.rb +13 -0
  9. data/lib/nl/attribute_set.rb +173 -0
  10. data/lib/nl/bitfield32.rb +86 -0
  11. data/lib/nl/blocking_transport.rb +131 -0
  12. data/lib/nl/connection.rb +61 -0
  13. data/lib/nl/datagram.rb +22 -0
  14. data/lib/nl/datatypes.rb +454 -0
  15. data/lib/nl/decoder.rb +13 -1
  16. data/lib/nl/endian.rb +10 -0
  17. data/lib/nl/error.rb +10 -0
  18. data/lib/nl/exchange.rb +124 -0
  19. data/lib/nl/family.rb +117 -23
  20. data/lib/nl/genl/client.rb +104 -0
  21. data/lib/nl/genl/protocol.rb +59 -0
  22. data/lib/nl/genl/wire.rb +94 -0
  23. data/lib/nl/genl.rb +60 -73
  24. data/lib/nl/notification.rb +112 -0
  25. data/lib/nl/notification_router.rb +92 -0
  26. data/lib/nl/raw/client.rb +83 -0
  27. data/lib/nl/raw/protocol.rb +112 -0
  28. data/lib/nl/raw/wire.rb +146 -0
  29. data/lib/nl/raw.rb +104 -0
  30. data/lib/nl/sequence_allocator.rb +34 -0
  31. data/lib/nl/socket.rb +15 -12
  32. data/lib/nl/structured_payload.rb +61 -0
  33. data/lib/nl/sub_message.rb +94 -0
  34. data/lib/nl/version.rb +1 -1
  35. data/lib/nl.rb +10 -12
  36. data/sig/generated/nl/async/dispatcher.rbs +53 -0
  37. data/sig/generated/nl/async/driver.rbs +21 -0
  38. data/sig/generated/nl/async/mailbox.rbs +32 -0
  39. data/sig/generated/nl/async/operation.rbs +123 -0
  40. data/sig/generated/nl/async.rbs +9 -0
  41. data/sig/generated/nl/attribute_set.rbs +50 -0
  42. data/sig/generated/nl/bitfield32.rbs +41 -0
  43. data/sig/generated/nl/blocking_transport.rbs +33 -0
  44. data/sig/generated/nl/connection.rbs +26 -0
  45. data/sig/generated/nl/datagram.rbs +11 -0
  46. data/sig/generated/nl/datatypes.rbs +181 -0
  47. data/sig/generated/nl/decoder.rbs +38 -0
  48. data/sig/generated/nl/encoder.rbs +29 -0
  49. data/sig/generated/nl/endian.rbs +28 -0
  50. data/sig/generated/nl/error.rbs +27 -0
  51. data/sig/generated/nl/exchange.rbs +75 -0
  52. data/sig/generated/nl/family.rbs +71 -0
  53. data/sig/generated/nl/genl/client.rbs +74 -0
  54. data/sig/generated/nl/genl/protocol.rbs +37 -0
  55. data/sig/generated/nl/genl/wire.rbs +113 -0
  56. data/sig/generated/nl/genl.rbs +50 -0
  57. data/sig/generated/nl/notification.rbs +53 -0
  58. data/sig/generated/nl/notification_router.rbs +30 -0
  59. data/sig/generated/nl/raw/client.rbs +50 -0
  60. data/sig/generated/nl/raw/protocol.rbs +122 -0
  61. data/sig/generated/nl/raw/wire.rbs +151 -0
  62. data/sig/generated/nl/raw.rbs +70 -0
  63. data/sig/generated/nl/sequence_allocator.rbs +15 -0
  64. data/sig/generated/nl/socket.rbs +40 -0
  65. data/sig/generated/nl/structured_payload.rbs +25 -0
  66. data/sig/generated/nl/sub_message.rbs +74 -0
  67. data/sig/generated/nl/version.rbs +5 -0
  68. data/sig/generated/nl.rbs +2 -0
  69. metadata +65 -9
  70. data/lib/nl/core.rb +0 -94
  71. data/lib/nl/protocols/genl.rb +0 -55
  72. 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
@@ -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
- # XXX: Should Protocol manage next_seq?
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
- # @return [Integer] Get next sequence number
40
- def next_seq
41
- nseq = (@seq + 1) & 0xFFFFFFFF
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
- def complete(hdr)
47
- [
48
- hdr.seq ||= next_seq,
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