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,131 @@
1
+ # rbs_inline: enabled
2
+
3
+ require_relative 'datagram'
4
+ require_relative 'exchange'
5
+ require_relative 'notification_router'
6
+ require_relative 'sequence_allocator'
7
+
8
+ module Nl
9
+ # Drives one exchange at a time with blocking socket operations.
10
+ class BlockingTransport
11
+ class ConcurrentOperationError < StandardError; end
12
+
13
+ class UnexpectedSequenceError < StandardError
14
+ attr_reader :expected, :actual
15
+
16
+ def initialize(expected, actual)
17
+ @expected = expected
18
+ @actual = actual
19
+ super("expected Netlink sequence and port ID #{expected.inspect}, got #{actual.inspect}")
20
+ end
21
+ end
22
+
23
+ def initialize(socket, notifications:)
24
+ @socket = socket
25
+ @sequences = SequenceAllocator.new
26
+ @mutex = Mutex.new
27
+ @notifications = notifications
28
+ end
29
+
30
+ def exchange(protocol, kind, request_class, reply_class, args)
31
+ unless locked = @mutex.try_lock
32
+ raise ConcurrentOperationError, 'BlockingTransport supports only one active operation'
33
+ end
34
+
35
+ request = protocol.build_request(kind, request_class, args)
36
+ seq = @sequences.next
37
+ pid = @socket.local_port_id
38
+ key = [seq, pid]
39
+ protocol.send_message(@socket, request, seq:, pid:)
40
+ exchange = Exchange.new(kind:, expects_reply: !reply_class.nil?)
41
+ result = [] unless block_given?
42
+
43
+ until exchange.complete?
44
+ receive(protocol, key, reply_class) do |message|
45
+ case outcome = exchange.accept(message)
46
+ when Exchange::Item
47
+ block_given? ? yield(outcome.value) : result << outcome.value
48
+ when Exchange::Failure
49
+ raise outcome.exception
50
+ end
51
+ end
52
+ end
53
+
54
+ return if block_given?
55
+
56
+ kind == :dump ? result : exchange.result
57
+ ensure
58
+ @mutex.unlock if locked
59
+ end
60
+
61
+ def async_capable? #: false
62
+ false
63
+ end
64
+
65
+ def receive_notification(protocol, timeout: nil)
66
+ channel = @notifications.channel(protocol)
67
+ return channel.pop(timeout: 0)
68
+ rescue TimeoutError
69
+ unless locked = @mutex.try_lock
70
+ raise ConcurrentOperationError, 'BlockingTransport supports only one active operation'
71
+ end
72
+
73
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout if timeout
74
+ loop do
75
+ begin
76
+ return channel.pop(timeout: 0)
77
+ rescue TimeoutError
78
+ # Read another datagram below.
79
+ end
80
+
81
+ remaining = deadline && deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
82
+ raise TimeoutError, 'notification receive timed out' if remaining && remaining <= 0
83
+ if remaining && !@socket.wait_readable(remaining)
84
+ raise TimeoutError, 'notification receive timed out'
85
+ end
86
+
87
+ receive_notifications
88
+ end
89
+ ensure
90
+ @mutex.unlock if locked
91
+ end
92
+
93
+ def close #: void
94
+ @socket.close unless @socket.closed?
95
+ nil
96
+ end
97
+
98
+ private def receive(protocol, expected_key, reply_class)
99
+ Datagram.each_frame(receive_datagram) do |header, payload|
100
+ actual_key = [header.seq, header.pid]
101
+ if actual_key == expected_key
102
+ yield protocol.decode_frame(header, payload, reply_class)
103
+ elsif header.seq.zero?
104
+ @notifications.route(header, payload)
105
+ else
106
+ raise UnexpectedSequenceError.new(expected_key, actual_key)
107
+ end
108
+ end
109
+ rescue Errno::ENOBUFS
110
+ @notifications.lose_all(NotificationLossError.new('kernel receive buffer overflowed'))
111
+ raise
112
+ end
113
+
114
+ private def receive_notifications
115
+ Datagram.each_frame(receive_datagram) do |header, payload|
116
+ if header.seq.zero?
117
+ @notifications.route(header, payload)
118
+ else
119
+ raise UnexpectedSequenceError.new(nil, [header.seq, header.pid])
120
+ end
121
+ end
122
+ rescue Errno::ENOBUFS
123
+ @notifications.lose_all(NotificationLossError.new('kernel receive buffer overflowed'))
124
+ end
125
+
126
+ private def receive_datagram
127
+ data, = @socket.recvmsg
128
+ IO::Buffer.for(data)
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,63 @@
1
+ # rbs_inline: enabled
2
+
3
+ require_relative 'async'
4
+ require_relative 'blocking_transport'
5
+ require_relative 'notification_router'
6
+ require_relative 'socket'
7
+
8
+ module Nl
9
+ # Owns one Netlink socket and the facilities shared by families using it.
10
+ class Connection
11
+ DEFAULT_NOTIFICATION_CAPACITY = 1_024
12
+
13
+ def initialize(protocol:, executor: nil, notification_capacity: DEFAULT_NOTIFICATION_CAPACITY)
14
+ socket = Socket.new(protocol.protonum)
15
+ socket.bind(Socket.sockaddr_nl(0, 0))
16
+ notifications = NotificationRouter.new(
17
+ routing: protocol.notification_routing,
18
+ capacity: notification_capacity,
19
+ )
20
+
21
+ @socket = socket
22
+ @notifications = notifications
23
+ @transport = if executor
24
+ Async::Dispatcher.new(socket, executor:, notifications:)
25
+ else
26
+ BlockingTransport.new(socket, notifications:)
27
+ end
28
+ rescue Exception
29
+ @transport ? @transport.close : socket&.close
30
+ notifications&.close
31
+ raise
32
+ end
33
+
34
+ def exchange(...) = @transport.exchange(...)
35
+ def exchange_async(...) = @transport.exchange_async(...)
36
+ def async_capable? = @transport.async_capable?
37
+
38
+ def register_notifications(protocol, classes)
39
+ @notifications.register(protocol, classes)
40
+ end
41
+
42
+ def add_memberships(group_ids)
43
+ group_ids.each { @socket.add_membership(it) }
44
+ nil
45
+ end
46
+
47
+ def drop_memberships(group_ids)
48
+ group_ids.each { @socket.drop_membership(it) }
49
+ nil
50
+ end
51
+
52
+ def receive_notification(protocol, timeout: nil)
53
+ @transport.receive_notification(protocol, timeout:)
54
+ end
55
+
56
+ def close
57
+ @transport.close
58
+ nil
59
+ ensure
60
+ @notifications.close
61
+ end
62
+ end
63
+ end
data/lib/nl/core.rb CHANGED
@@ -1,4 +1,4 @@
1
- # Data types and message handling
1
+ # Netlink wire definitions
2
2
 
3
3
  require_relative 'endian'
4
4
 
@@ -0,0 +1,24 @@
1
+ # rbs_inline: enabled
2
+
3
+ require_relative 'core'
4
+ require_relative 'decoder'
5
+
6
+ module Nl
7
+ # Splits Netlink datagrams into aligned header/payload frames.
8
+ module Datagram
9
+ # @rbs (IO::Buffer buffer) { (Core::NlMsgHdr, IO::Buffer) -> void } -> nil
10
+ # | (IO::Buffer buffer) -> Enumerator[[Core::NlMsgHdr, IO::Buffer], nil]
11
+ def self.each_frame(buffer)
12
+ return enum_for(__method__, buffer) unless block_given?
13
+
14
+ decoder = Decoder.new(buffer)
15
+ while decoder.available?(Core::NLMSG_HDRLEN)
16
+ header = Core::NlMsgHdr.decode(decoder)
17
+ payload_size = header.len - Core::NLMSG_HDRLEN
18
+ payload = decoder.get_buffer(payload_size)
19
+ decoder.align_to(Core::NLMSG_ALIGNTO)
20
+ yield header, payload
21
+ end
22
+ end
23
+ end
24
+ end
data/lib/nl/decoder.rb CHANGED
@@ -38,12 +38,20 @@ module Nl
38
38
 
39
39
  def get_string(length = @limit - @position)
40
40
  nposition = @position + length
41
- raise OutOfBounds if nposition > @limit
41
+ raise OutOfBounds if length.negative? || nposition > @limit
42
42
  value = @buffer.get_string(@position, length)
43
43
  @position = nposition
44
44
  value
45
45
  end
46
46
 
47
+ def get_buffer(length = @limit - @position)
48
+ nposition = @position + length
49
+ raise OutOfBounds if length.negative? || nposition > @limit
50
+ value = @buffer.slice(@position, length)
51
+ @position = nposition
52
+ value
53
+ end
54
+
47
55
  def get_zstring(unterminated_ok: false)
48
56
  nposition = @position
49
57
  nul_found = false
data/lib/nl/error.rb ADDED
@@ -0,0 +1,10 @@
1
+ module Nl
2
+ class Error < StandardError; end
3
+ class ProtocolViolation < Error; end
4
+ class ExhaustedSequenceNumber < Error; end
5
+ class TimeoutError < Error; end
6
+ class ClosedError < Error; end
7
+ class NotificationLossError < Error; end
8
+ class UnknownMulticastGroupError < Error; end
9
+ class UnresolvedMulticastGroupError < Error; end
10
+ end
@@ -0,0 +1,102 @@
1
+ # rbs_inline: enabled
2
+
3
+ require_relative 'error'
4
+ require_relative 'protocols/raw'
5
+
6
+ module Nl
7
+ # State machine for one Netlink request/reply exchange.
8
+ class Exchange
9
+ Item = Data.define(:value)
10
+ Complete = Data.define
11
+ COMPLETE = Complete.new
12
+ Failure = Data.define(:exception)
13
+
14
+ attr_reader :kind
15
+
16
+ def initialize(kind:, expects_reply:)
17
+ @kind = kind
18
+ @expects_reply = expects_reply
19
+ @reply = nil
20
+ @state = kind == :dump ? :multi : :initial
21
+ @acked = false
22
+ @cancelled = false
23
+ @complete = false
24
+ @result = nil
25
+ @mutex = Mutex.new
26
+ end
27
+
28
+ def accept(frame)
29
+ @mutex.synchronize do
30
+ return if @complete
31
+
32
+ case frame
33
+ when Protocols::Raw::UnknownFrame
34
+ nil
35
+ when Protocols::Raw::ErrorFrame
36
+ @cancelled ? complete(nil) : fail_with(SystemCallError.new(frame.errno))
37
+ when Protocols::Raw::DoneFrame
38
+ if frame.errno && !@cancelled
39
+ fail_with(SystemCallError.new(frame.errno))
40
+ else
41
+ complete(@reply)
42
+ end
43
+ when Protocols::Raw::AckFrame
44
+ accept_ack
45
+ when Protocols::Raw::DataFrame
46
+ accept_reply(frame)
47
+ else
48
+ raise ArgumentError, "unexpected exchange input: #{frame.inspect}"
49
+ end
50
+ end
51
+ end
52
+
53
+ def cancel
54
+ @mutex.synchronize { @cancelled = true unless @complete }
55
+ nil
56
+ end
57
+
58
+ def cancelled? = @mutex.synchronize { @cancelled }
59
+ def complete? = @mutex.synchronize { @complete }
60
+ def result = @mutex.synchronize { @result }
61
+ def expects_reply? = @expects_reply
62
+
63
+ private def accept_ack
64
+ @acked = true
65
+ if @cancelled || !@expects_reply || @state == :single
66
+ complete(@reply)
67
+ end
68
+ end
69
+
70
+ private def accept_reply(frame)
71
+ return if @cancelled
72
+
73
+ multipart = (frame.header.flags.to_i & Core::NLM_F_MULTI) != 0
74
+ case @state
75
+ when :initial
76
+ @state = multipart ? :multi : :single
77
+ when :single
78
+ return fail_with(ProtocolViolation.new('more than one data message in a non-multipart Netlink response'))
79
+ when :multi
80
+ unless multipart
81
+ return fail_with(ProtocolViolation.new('multipart Netlink response contains data without NLM_F_MULTI'))
82
+ end
83
+ end
84
+
85
+ return Item.new(frame.message) if @kind == :dump
86
+
87
+ @reply ||= frame.message
88
+ complete(frame.message) if @acked && @state == :single
89
+ end
90
+
91
+ private def complete(value)
92
+ @complete = true
93
+ @result = value
94
+ COMPLETE
95
+ end
96
+
97
+ private def fail_with(exception)
98
+ @complete = true
99
+ Failure.new(exception)
100
+ end
101
+ end
102
+ end
data/lib/nl/family.rb CHANGED
@@ -1,37 +1,154 @@
1
1
  #--
2
2
  # rbs_inline: enabled
3
- require_relative 'socket'
3
+ require_relative 'connection'
4
+ require_relative 'notification'
4
5
 
5
6
  module Nl
7
+ # @rbs!
8
+ # type executor = :thread | :fiber
9
+ #
10
+ # interface _Connection
11
+ # def exchange: (
12
+ # Protocols::Raw protocol,
13
+ # Symbol kind,
14
+ # Class request_class,
15
+ # Class reply_class,
16
+ # Hash[Symbol, untyped] args
17
+ # ) ?{ (untyped) -> void } -> untyped
18
+ # def exchange_async: (
19
+ # Protocols::Raw protocol,
20
+ # Symbol kind,
21
+ # Class request_class,
22
+ # Class reply_class,
23
+ # Hash[Symbol, untyped] args,
24
+ # ?stream_capacity: Integer?
25
+ # ) -> (Async::Future[untyped] | Async::Stream[untyped])
26
+ # def async_capable?: () -> bool
27
+ # def register_notifications: (Protocols::Raw, Hash[Integer, Class]) -> NotificationChannel
28
+ # def add_memberships: (Array[Integer]) -> nil
29
+ # def drop_memberships: (Array[Integer]) -> nil
30
+ # def receive_notification: (Protocols::Raw, ?timeout: Numeric?) -> untyped
31
+ # def close: () -> nil
32
+ # end
33
+
6
34
  class Family
35
+ DEFAULT_NOTIFICATION_CAPACITY = Connection::DEFAULT_NOTIFICATION_CAPACITY
36
+
37
+ module Session
38
+ def close #: nil
39
+ @connection.close
40
+ end
41
+ end
42
+
7
43
  #--
8
- # @rbs socket: Socket
44
+ # @rbs connection: _Connection
9
45
  # @rbs protocol: Protocol
10
46
  # @rbs return: instance
11
- def initialize(socket, protocol: self.class::PROTOCOL)
12
- @socket = socket
47
+ def initialize(connection, protocol: self.class::PROTOCOL)
13
48
  @protocol = protocol
49
+ @connection = connection
50
+ @connection.register_notifications(@protocol, notification_classes)
51
+ @notification_stream = NotificationStream.new do |timeout|
52
+ @connection.receive_notification(@protocol, timeout:)
53
+ end
14
54
  end
15
55
 
16
56
  #--
17
- # @rbs () -> instance
18
- # | [R] () { (instance) -> R } -> R
19
- def self.open
57
+ # @rbs (?executor: executor?, ?notification_capacity: Integer?) -> (Session & instance)
58
+ # | [R] (?executor: executor?, ?notification_capacity: Integer?) { (instance) -> R } -> R
59
+ def self.open(executor: nil, notification_capacity: DEFAULT_NOTIFICATION_CAPACITY)
60
+ session = build_session(executor:, notification_capacity:)
61
+ return session unless block_given?
62
+
20
63
  begin
21
- socket = Socket.new(self::PROTOCOL.protonum)
22
- socket.bind(Socket.sockaddr_nl(0, 0))
23
- if block_given?
24
- yield new(socket)
25
- else
26
- return new(socket)
27
- end
64
+ yield session
28
65
  ensure
29
- socket&.close if block_given?
66
+ session.close
67
+ end
68
+ end
69
+
70
+ class << self
71
+ # @rbs (?executor: executor?, notification_capacity: Integer) -> (Session & instance)
72
+ private def build_session(executor: nil, notification_capacity:)
73
+ protocol = self::PROTOCOL
74
+ connection = Connection.new(protocol:, executor:, notification_capacity:)
75
+ new(connection).extend(Session)
76
+ rescue Exception
77
+ connection&.close
78
+ raise
79
+ end
80
+ end
81
+
82
+ private def exchange_message(kind, request_class, reply_class, args, &block)
83
+ @connection.exchange(@protocol, kind, request_class, reply_class, args, &block)
84
+ end
85
+
86
+ def async_capable? #: bool
87
+ @connection.async_capable?
88
+ end
89
+
90
+ # Adds multicast memberships to the existing family socket. Membership is
91
+ # additive and remains active until explicitly removed or the owner closes.
92
+ def subscribe(*groups)
93
+ @connection.add_memberships(multicast_group_ids(groups))
94
+ self
95
+ end
96
+
97
+ def unsubscribe(*groups)
98
+ @connection.drop_memberships(multicast_group_ids(groups))
99
+ self
100
+ end
101
+
102
+ def receive_notification(timeout: nil)
103
+ @notification_stream.next(timeout:)
104
+ end
105
+
106
+ def each_notification(&block)
107
+ return @notification_stream.each unless block
108
+
109
+ @notification_stream.each(&block)
110
+ end
111
+
112
+ # Builds a generated asynchronous-operation facade with a narrowly scoped
113
+ # callback, so the facade does not need access to Family's private API.
114
+ #--
115
+ # @rbs operations_class: Class
116
+ # @rbs stream_capacity: Integer?
117
+ # @rbs return: untyped
118
+ private def build_async_facade(operations_class, stream_capacity: nil)
119
+ unless async_capable?
120
+ raise Async::UnavailableError, 'async operations require an executor'
30
121
  end
122
+
123
+ operations_class.new do |kind, request_class, reply_class, args|
124
+ exchange_message_async(kind, request_class, reply_class, args, stream_capacity:)
125
+ end
126
+ end
127
+
128
+ private def exchange_message_async(kind, request_class, reply_class, args, stream_capacity: nil)
129
+ @connection.exchange_async(@protocol, kind, request_class, reply_class, args, stream_capacity:)
130
+ end
131
+
132
+ private def notification_classes
133
+ self.class.const_get(:NOTIFICATIONS, false)
134
+ rescue NameError
135
+ {}
31
136
  end
32
137
 
33
- private def exchange_message(type, request_class, reply_class, args, &block)
34
- @protocol.exchange_message(@socket, type, request_class, reply_class, args, &block)
138
+ private def multicast_groups
139
+ self.class.const_get(:MCAST_GROUPS, false)
140
+ rescue NameError
141
+ {}
142
+ end
143
+
144
+ private def multicast_group_ids(names)
145
+ names.map do |name|
146
+ key = name.to_sym
147
+ group = multicast_groups.fetch(key) do
148
+ raise UnknownMulticastGroupError, "unknown multicast group #{name.inspect} for #{@protocol.name}"
149
+ end
150
+ @protocol.multicast_group_id(group.name, group.id)
151
+ end
35
152
  end
36
153
  end
37
154
  end
@@ -0,0 +1,89 @@
1
+ # Generic Netlink connection handling
2
+ #--
3
+ # rbs_inline: enabled
4
+
5
+ require_relative '../connection'
6
+ require_relative '../core'
7
+ require_relative '../protocols/genl'
8
+
9
+ module Nl
10
+ module Genl
11
+ FamilyInfo = Data.define(:id, :multicast_groups) do
12
+ def self.from_reply(reply)
13
+ groups = (reply.mcast_groups || []).to_h do |attributes|
14
+ [attributes[:name].value.to_sym, attributes[:id].value]
15
+ end
16
+ new(id: reply.family_id, multicast_groups: groups.freeze)
17
+ end
18
+ end
19
+
20
+ class Connection
21
+ #--
22
+ # @rbs (resolver: ^(instance, ::String) -> (Integer | FamilyInfo | untyped), ?executor: executor?, ?notification_capacity: Integer?) -> instance
23
+ # | [R] (resolver: ^(instance, ::String) -> (Integer | FamilyInfo | untyped), ?executor: executor?, ?notification_capacity: Integer?) { (instance) -> R } -> R
24
+ def self.open(resolver:, executor: nil, notification_capacity: Nl::Connection::DEFAULT_NOTIFICATION_CAPACITY)
25
+ conn = new(resolver:, executor:, notification_capacity:)
26
+ if block_given?
27
+ begin
28
+ yield conn
29
+ ensure
30
+ conn.close
31
+ end
32
+ else
33
+ conn
34
+ end
35
+ end
36
+
37
+ #--
38
+ # @rbs resolver: ^(instance, ::String) -> (Integer | FamilyInfo | untyped)
39
+ # @rbs executor: executor?
40
+ # @rbs return: instance
41
+ def initialize(resolver:, executor: nil, notification_capacity: Nl::Connection::DEFAULT_NOTIFICATION_CAPACITY)
42
+ @resolver = resolver
43
+ @family_cache = {}
44
+ @family_cache_mutex = Mutex.new
45
+ @connection = Nl::Connection.new(
46
+ protocol: Protocols::Genl,
47
+ executor:,
48
+ notification_capacity:,
49
+ )
50
+ end
51
+
52
+ def family(family_class)
53
+ proto = family_class::PROTOCOL
54
+ info = family_info(proto)
55
+ family_class.new(
56
+ @connection,
57
+ protocol: Protocols::Genl.new(
58
+ proto.name,
59
+ family_id: info.id,
60
+ multicast_groups: info.multicast_groups,
61
+ ),
62
+ )
63
+ end
64
+
65
+ def close
66
+ @connection.close
67
+ end
68
+
69
+ private def family_info(protocol)
70
+ id = protocol.family_id
71
+ FamilyInfo.new(id:, multicast_groups: {}.freeze)
72
+ rescue NotImplementedError
73
+ cached_info = @family_cache_mutex.synchronize { @family_cache[protocol.name] }
74
+ return cached_info if cached_info
75
+
76
+ resolved = @resolver.call(self, protocol.name)
77
+ info = case resolved
78
+ when Integer
79
+ FamilyInfo.new(id: resolved, multicast_groups: {}.freeze)
80
+ when FamilyInfo
81
+ resolved
82
+ else
83
+ FamilyInfo.from_reply(resolved)
84
+ end
85
+ @family_cache_mutex.synchronize { @family_cache[protocol.name] ||= info }
86
+ end
87
+ end
88
+ end
89
+ end
data/lib/nl/genl.rb CHANGED
@@ -1,4 +1,6 @@
1
- # General Netlink message handling
1
+ # Generic Netlink wire definitions
2
+ #--
3
+ # rbs_inline: enabled
2
4
 
3
5
  require_relative 'core'
4
6
  require_relative 'endian'
@@ -48,40 +50,25 @@ module Nl
48
50
  end
49
51
  include Constants
50
52
 
51
- class Connection
52
- def self.open(resolver:)
53
- conn = new(resolver:)
54
- if block_given?
55
- begin
56
- yield conn
57
- ensure
58
- conn.close
59
- end
60
- else
61
- conn
62
- end
63
- end
64
-
65
- def initialize(resolver:)
66
- @socket = Socket.new(Core::NETLINK_GENERIC)
67
- @socket.bind(Socket.sockaddr_nl(0, 0))
68
- @resolver = resolver
69
- @id_cache = {}
70
- end
53
+ GenlMsgHdr = Struct.new(:cmd, :version, :reserved)
54
+ # Generic Netlink message header
55
+ class GenlMsgHdr
56
+ FORMAT = Ractor.make_shareable([
57
+ Endian::Host::U8,
58
+ Endian::Host::U8,
59
+ Endian::Host::U16,
60
+ ])
61
+ private_constant :FORMAT
71
62
 
72
- def open(family_class)
73
- proto = family_class::PROTOCOL
74
- id = @id_cache[proto.name] ||= begin
75
- proto.family_id
76
- rescue NotImplementedError
77
- @resolver.call(@socket, proto.name)
78
- end
79
- family_class.new(@socket, protocol: Protocols::Genl.new(proto.name, family_id: id))
63
+ def self.decode(decoder)
64
+ new(*decoder.get_values(FORMAT))
80
65
  end
81
66
 
82
- def close
83
- @socket.close
67
+ def encode(encoder)
68
+ encoder.put_values(FORMAT, to_a)
84
69
  end
85
70
  end
86
71
  end
87
72
  end
73
+
74
+ require_relative 'genl/connection'