nl 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b834199d097a768959ee9b81d4f8e08e504048b9bb537aefbbb3c737dcbc166b
4
+ data.tar.gz: 62fbdaaeb0ea9c4a5aefba149476f32dde99b16e210252566340b9627282bd4e
5
+ SHA512:
6
+ metadata.gz: 218aabff17cc161fbb064f22e50c78c11770bc70df76636434bd648af2e3592bbafdb491b61daf8164be306f5d5bfe1e8e0ad968963c9c17756fcb06e826ed47
7
+ data.tar.gz: 4ff4dc4caf73c20e1b93e090689684534f8f8b184f15538eac47451293c4705918fbea24583949e1454882a850379341784d1226f3c7f8421d362c6b184fcc3e
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # ChangeLog
2
+
3
+ ## Unreleased
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Kasumi Hanazuki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # nl
2
+
3
+ Core library to handle Netlink sockets and data types.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
data/lib/nl/core.rb ADDED
@@ -0,0 +1,94 @@
1
+ # Data types and message handling
2
+
3
+ require_relative 'endian'
4
+
5
+ module Nl
6
+ module Core
7
+ module Constants
8
+ # From include/uapi/linux/netlink.h
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
+ NlMsgHdr = Struct.new(:len, :type, :flags, :seq, :pid)
48
+ # Message header
49
+ class NlMsgHdr
50
+ FORMAT = Ractor.make_shareable([
51
+ Endian::Host::U32,
52
+ Endian::Host::U16,
53
+ Endian::Host::U16,
54
+ Endian::Host::U32,
55
+ Endian::Host::U32,
56
+ ])
57
+ private_constant :FORMAT
58
+
59
+ def self.decode(decoder)
60
+ obj = new(*decoder.get_values(FORMAT))
61
+ decoder.align_to(Constants::NLMSG_ALIGNTO)
62
+ obj
63
+ end
64
+
65
+ def encode(encoder)
66
+ encoder.reserve(Constants::NLMSG_HDRLEN)
67
+ encoder.put_values(FORMAT, to_a)
68
+ encoder.align_to(Constants::NLMSG_ALIGNTO)
69
+ end
70
+ end
71
+
72
+ NlAttr = Struct.new(:len, :type)
73
+ # Attribute header
74
+ class NlAttr
75
+ FORMAT = Ractor.make_shareable([
76
+ Endian::Host::U16,
77
+ Endian::Host::U16,
78
+ ])
79
+ private_constant :FORMAT
80
+
81
+ def self.decode(decoder)
82
+ obj = new(*decoder.get_values(FORMAT))
83
+ decoder.align_to(Constants::NLA_ALIGNTO)
84
+ obj
85
+ end
86
+
87
+ def encode(encoder)
88
+ encoder.reserve(Constants::NLA_HDRLEN)
89
+ encoder.put_values(FORMAT, to_a)
90
+ encoder.align_to(Constants::NLA_ALIGNTO)
91
+ end
92
+ end
93
+ end
94
+ end
data/lib/nl/decoder.rb ADDED
@@ -0,0 +1,86 @@
1
+ module Nl
2
+ class Decoder
3
+ class Error < StandardError; end
4
+ class OutOfBounds < Error
5
+ def initialize(message = 'out of bounds') = super
6
+ end
7
+ class Unterminated < Error
8
+ def initialize(message = 'unterminated string') = super
9
+ end
10
+
11
+ def initialize(buffer, offset = 0, length = buffer.size - offset)
12
+ @buffer = buffer
13
+ @position = offset
14
+ @limit = length
15
+ end
16
+
17
+ def available?(size = 1)
18
+ @position + size <= @limit
19
+ end
20
+
21
+ def limit(size)
22
+ orig_limit = @limit
23
+ @limit = @position + size
24
+ result = yield self
25
+ if @limit != @position
26
+ raise "Not all bytes upto specified limit is consumed"
27
+ end
28
+ result
29
+ ensure
30
+ @limit = orig_limit
31
+ end
32
+
33
+ def skip(length = @limit - @position)
34
+ nposition = @position + length
35
+ raise OutOfBounds if nposition > @limit
36
+ @position = nposition
37
+ end
38
+
39
+ def get_string(length = @limit - @position)
40
+ nposition = @position + length
41
+ raise OutOfBounds if nposition > @limit
42
+ value = @buffer.get_string(@position, length)
43
+ @position = nposition
44
+ value
45
+ end
46
+
47
+ def get_zstring(unterminated_ok: false)
48
+ nposition = @position
49
+ nul_found = false
50
+ while nposition < @limit
51
+ c = @buffer.get_value(:U8, nposition)
52
+ nposition += 1
53
+ if c == 0
54
+ nul_found = true
55
+ break
56
+ end
57
+ end
58
+ raise Unterminated if !nul_found && !unterminated_ok
59
+ value = @buffer.get_string(@position, nposition - @position - (nul_found ? 1 : 0))
60
+ @position = nposition
61
+ value
62
+ end
63
+
64
+ def get_value(type)
65
+ nposition = @position + IO::Buffer.size_of(type)
66
+ raise OutOfBounds if nposition > @limit
67
+ value = @buffer.get_value(type, @position)
68
+ @position = nposition
69
+ value
70
+ end
71
+
72
+ def get_values(types)
73
+ nposition = @position + IO::Buffer.size_of(types)
74
+ raise OutOfBounds if nposition > @limit
75
+ values = @buffer.get_values(types, @position)
76
+ @position = nposition
77
+ values
78
+ end
79
+
80
+ def align_to(alignment)
81
+ nposition = (@position + alignment - 1) & ~(alignment - 1)
82
+ raise OutOfBounds if nposition > @limit
83
+ @position = nposition
84
+ end
85
+ end
86
+ end
data/lib/nl/encoder.rb ADDED
@@ -0,0 +1,65 @@
1
+ module Nl
2
+ class Encoder
3
+ attr_reader :position
4
+
5
+ def initialize(capacity = 4096)
6
+ @buffer = IO::Buffer.new(capacity)
7
+ @position = 0
8
+ end
9
+
10
+ def reserve(size)
11
+ nposition = @position + size
12
+ if @buffer.size < nposition
13
+ @buffer.resize([@buffer.size * 2, nposition].max)
14
+ end
15
+ end
16
+
17
+ def put_string(value)
18
+ reserve(value.bytesize)
19
+ @position = @buffer.set_string(value, @position)
20
+ end
21
+
22
+ def put_zstring(value)
23
+ reserve(value.bytesize + 1)
24
+ @position = @buffer.set_string(value, @position)
25
+ @position = @buffer.set_value(:U8, @position, 0)
26
+ end
27
+
28
+ def put_value(type, value)
29
+ reserve(IO::Buffer.size_of(type))
30
+ @position = @buffer.set_value(type, @position, value)
31
+ end
32
+
33
+ def put_values(types, values)
34
+ reserve(IO::Buffer.size_of(types))
35
+ @position = @buffer.set_values(types, @position, values)
36
+ end
37
+
38
+ def align_to(alignment)
39
+ if alignment > 1
40
+ nposition = (@position + alignment - 1) & ~(alignment - 1)
41
+ reserve(nposition - @position)
42
+ @position = nposition
43
+ end
44
+ end
45
+
46
+ def put_value_at(offset, type, value)
47
+ @buffer.set_value(type, offset, value)
48
+ end
49
+
50
+ def put_values_at(offset, types, values)
51
+ @buffer.set_values(types, offset, values)
52
+ end
53
+
54
+ def buffer
55
+ @buffer.slice(0, @position)
56
+ end
57
+
58
+ def measure(type, offset = 0)
59
+ before = @position
60
+ yield
61
+ after = @position
62
+ put_value_at(before + offset, type, after - before)
63
+ end
64
+ end
65
+ end
data/lib/nl/endian.rb ADDED
@@ -0,0 +1,35 @@
1
+ module Nl
2
+ module Endian
3
+ SIZEOF_INT = [1].pack('i!').bytesize
4
+ SIZEOF_LONG = [1].pack('l!').bytesize
5
+ SIZEOF_LLONG = [1].pack('q!').bytesize
6
+
7
+ module Little
8
+ U8, S8, U16, S16, U32, S32, U64, S64, F32, F64 = :U8, :S8, :u16, :s16, :u32, :s32, :u64, :s64, :f32, :f64
9
+ end
10
+
11
+ module Big
12
+ U8, S8, U16, S16, U32, S32, U64, S64, F32, F64 = :U8, :S8, :U16, :S16, :U32, :S32, :U64, :S64, :F32, :F64
13
+ end
14
+
15
+ module Host
16
+ include (IO::Buffer::HOST_ENDIAN == IO::Buffer::LITTLE_ENDIAN ? Little : Big)
17
+
18
+ UINT, SINT = case SIZEOF_INT
19
+ when 2; [U16, S16]
20
+ when 4; [U32, S32]
21
+ when 8; [U64, S64]
22
+ else raise "Unsupported 'int' size"
23
+ end
24
+ ULONG, SLONG = case SIZEOF_LONG
25
+ when 4; [U32, S32]
26
+ when 8; [U64, S64]
27
+ else raise "Unsupported 'long' size"
28
+ end
29
+ ULLONG, SLLONG = case SIZEOF_LLONG
30
+ when 8; [U64, S64]
31
+ else raise "Unsupported 'long long' size"
32
+ end
33
+ end
34
+ end
35
+ end
data/lib/nl/family.rb ADDED
@@ -0,0 +1,163 @@
1
+ require_relative 'socket'
2
+
3
+ module Nl
4
+ class Family
5
+ def initialize(socket)
6
+ @socket = socket
7
+ end
8
+
9
+ def self.open
10
+ begin
11
+ socket = Socket.new(self::PROTONUM)
12
+ socket.bind(Socket.sockaddr_nl(0, 0))
13
+ if block_given?
14
+ yield new(socket)
15
+ else
16
+ return new(socket)
17
+ end
18
+ ensure
19
+ socket&.close if block_given?
20
+ end
21
+ end
22
+
23
+ def exchange_message(type, request_class, reply_class, args)
24
+ self.class::PROTOCOL.exchange_message(@socket, type, request_class, reply_class, args)
25
+ end
26
+
27
+ class Message
28
+ attr_accessor :header, :fixed_header, :attributes
29
+
30
+ def initialize(header, fixed_header = nil, attributes = [])
31
+ @header = header
32
+ @fixed_header = fixed_header
33
+ @attributes = attributes
34
+ end
35
+
36
+ def self.from_params(params)
37
+ header_params = params.slice(*self::HEADER_PARAMS)
38
+ attribute_params = params.slice(*self::ATTRIBUTE_PARAMS)
39
+ # TODO: Reject unknown params
40
+
41
+ header = Core::NlMsgHdr.new(0, self::TYPE, nil, nil, nil)
42
+ fixed_header = self::FIXED_HEADER&.new(**header_params)
43
+ attributes = self::ATTRIBUTE_SET.build_attributes(**attribute_params)
44
+ new(header, fixed_header, attributes)
45
+ end
46
+
47
+ def append_attribute(attribute)
48
+ @attributes << attribute
49
+ end
50
+
51
+ def encode(encoder)
52
+ validate!
53
+
54
+ encoder.measure(Endian::Host::U16) do
55
+ @header.encode(encoder)
56
+ @fixed_header.encode(encoder) if @fixed_header
57
+ @attributes.each do |attr|
58
+ attr.encode(encoder)
59
+ end
60
+ end
61
+ end
62
+
63
+ def self.decode(decoder, header)
64
+ unless self::TYPE == header.type
65
+ raise "Expected message type #{self::TYPE}, got #{header.type}"
66
+ end
67
+
68
+ if fixed_header_class = self::FIXED_HEADER
69
+ fixed_header = fixed_header_class.decode(decoder)
70
+ end
71
+
72
+ attributes = self::ATTRIBUTE_SET.decode(decoder)
73
+
74
+ new(header, fixed_header, attributes).tap(&:validate!)
75
+ end
76
+
77
+ def validate!
78
+ unless self.class::TYPE == header.type
79
+ raise "Expected message type #{self.class::FIXED_HEADER}, got #{header.type}"
80
+ end
81
+ # TODO: Validate fixed header and attributes
82
+ # @fixed_header&.validate!
83
+ # @attributes.each(&:validate!)
84
+ end
85
+
86
+ def to_h
87
+ to_h_rec(@attributes, @fixed_header&.to_h || {})
88
+ end
89
+
90
+ # FIXME:
91
+ private def to_h_rec(attributes, init = {})
92
+ attributes.each_with_object(init) do |attr, h|
93
+ if attr.class::DATATYPE.is_a?(Protocols::Raw::DataTypes::NestedAttributes)
94
+ h[attr.class::NAME] = to_h_rec(attr.value)
95
+ else
96
+ h[attr.class::NAME] = attr.value
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ class AttributeSet
103
+ Attribute = Struct.new(:value)
104
+ class Attribute
105
+ def self.decode(decoder)
106
+ value = self::DATATYPE.decode(decoder)
107
+ new(value)
108
+ end
109
+
110
+ def encode(encoder)
111
+ self.class::DATATYPE.encode(encoder, self.value)
112
+ end
113
+ end
114
+
115
+ class << self
116
+ private def decode1(decoder)
117
+ nlattr = Core::NlAttr.decode(decoder)
118
+ attr = decoder.limit(nlattr.len - Core::NLA_HDRLEN) do
119
+ if attr_class = self::BY_TYPE[nlattr.type & Core::NLA_TYPE_MASK]
120
+ attr_class.decode(decoder)
121
+ else
122
+ decoder.skip
123
+ nil
124
+ end
125
+ end
126
+ decoder.align_to(Core::NLA_ALIGNTO)
127
+ attr
128
+ end
129
+
130
+ def decode(decoder)
131
+ attrs = []
132
+ while decoder.available?
133
+ attr = decode1(decoder)
134
+ attrs << attr
135
+ end
136
+ attrs.compact
137
+ end
138
+
139
+ private def encode1(encoder, attr)
140
+ nlattr = Core::NlAttr.new(attr.class::TYPE, 0)
141
+ encoder.measure(Endian::Host::U16) do
142
+ nlattr.encode(encoder)
143
+ attr.encode(encoder)
144
+ end
145
+ encoder.align_to(Core::NLA_ALIGNTO)
146
+ end
147
+
148
+ def encode(encoder, attrs)
149
+ attrs.each do |attr|
150
+ encode1(encoder, attr)
151
+ end
152
+ end
153
+
154
+ def build_attributes(**params)
155
+ params.map do |name, value|
156
+ attr_class = self::BY_NAME[name] or raise "Unknown attribute #{name}"
157
+ attr_class.new(value)
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
data/lib/nl/genl.rb ADDED
@@ -0,0 +1,65 @@
1
+ # General Netlink message handling
2
+
3
+ require_relative 'core'
4
+ require_relative 'endian'
5
+
6
+ module Nl
7
+ module Genl
8
+ module Constants
9
+ # From include/uapi/linux/genetlink.
10
+ GENL_NAMSIZ = 16
11
+ GENL_MIN_ID = Core::NLMSG_MIN_TYPE
12
+ GENL_MAX_ID = 1023
13
+
14
+ GENL_HDRLEN = 4
15
+
16
+ GENL_ID_GENERATE = 0
17
+ GENL_ID_CTRL = Core::NLMSG_MIN_TYPE
18
+ GENL_ID_VFS_DQUOT = Core::NLMSG_MIN_TYPE + 1
19
+ GENL_ID_PMCRAID = Core::NLMSG_MIN_TYPE + 2
20
+
21
+ CTRL_CMD_UNSPEC = 0
22
+ CTRL_CMD_NEWFAMILY = 1
23
+ CTRL_CMD_DELFAMILY = 2
24
+ CTRL_CMD_GETFAMILY = 3
25
+ CTRL_CMD_NEWOPS = 4
26
+ CTRL_CMD_DELOPS = 5
27
+ CTRL_CMD_GETOPS = 6
28
+ CTRL_CMD_NEWMCAST_GRP = 7
29
+ CTRL_CMD_DELMCAST_GRP = 8
30
+ CTRL_CMD_GETMCAST_GRP = 9
31
+
32
+ CTRL_ATTR_UNSPEC = 0
33
+ CTRL_ATTR_FAMILY_ID = 1
34
+ CTRL_ATTR_FAMILY_NAME = 2
35
+ CTRL_ATTR_VERSION = 3
36
+ CTRL_ATTR_HDRSIZE = 4
37
+ CTRL_ATTR_MAXATTR = 5
38
+ CTRL_ATTR_OPS = 6
39
+ CTRL_ATTR_MCAST_GROUPS = 7
40
+
41
+ CTRL_ATTR_OP_UNSPEC = 0
42
+ CTRL_ATTR_OP_ID = 1
43
+ CTRL_ATTR_OP_FLAGS = 2
44
+
45
+ CTRL_ATTR_MCAST_GRP_UNSPEC = 0
46
+ CTRL_ATTR_MCAST_GRP_NAME = 1
47
+ CTRL_ATTR_MCAST_GRP_ID = 2
48
+ end
49
+ include Constants
50
+
51
+ GenlMsgHdr ||= Data.define(:cmd, :version, :reserved)
52
+ class GenlMsgHdr
53
+ GENLMSGHDR_FMT = Ractor.make_shareable([
54
+ Endian::Host::U8,
55
+ Endian::Host::U8,
56
+ Endian::Host::U16,
57
+ ])
58
+ private_constant :GENLMSGHDR_FMT
59
+
60
+ def self.parse(buffer, offset)
61
+ new(*buffer.get_values(GENLMSGHDR_FMT, offset))
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,15 @@
1
+ module Nl
2
+ module Protocols
3
+ # The Generic Netlink protocol
4
+ class Genl < Raw # TODO: Implement
5
+ def initialize(name)
6
+ super(name, NETLINK_GENERIC)
7
+ end
8
+
9
+ def parse(buffer, offset)
10
+ nlmsg = super(buffer, offset)
11
+ genlmsg = GenlMsgHdr.parse(nlmsg.data, 0)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,169 @@
1
+ module Nl
2
+ module Protocols
3
+ # The raw Netlink protocol
4
+ class Raw
5
+ class Done
6
+ end
7
+ class Ack
8
+ end
9
+
10
+ attr_reader :name, :protonum
11
+
12
+ def initialize(name, protonum)
13
+ @name = name
14
+ @protonum = protonum
15
+ end
16
+
17
+ def encode_message(encoder, message)
18
+ message.encode(encoder)
19
+ end
20
+
21
+ def decode_message(decoder, message_class)
22
+ header = NlMsgHdr.decode(decoder)
23
+ decoder.limit(header.len - Core::NLMSG_HDRLEN) do
24
+ message_class.decode(decoder, header)
25
+ end
26
+ end
27
+
28
+ def send_message(socket, message)
29
+ seq_pid = socket.complete(message.header)
30
+ encoder = Encoder.new
31
+ encode_message(encoder, message)
32
+ socket.sendmsg(encoder.buffer.get_string, 0, Socket.sockaddr_nl(0, 0))
33
+ seq_pid
34
+ end
35
+
36
+ def recv_message(socket, seq_pid, message_class)
37
+ data, = socket.recvmsg
38
+
39
+ decoder = Decoder.new(IO::Buffer.for(data))
40
+ while decoder.available?(Core::NLMSG_HDRLEN)
41
+ header = Core::NlMsgHdr.decode(decoder)
42
+ decoder.align_to(Core::NLMSG_ALIGNTO)
43
+ raise binding.irb unless [header.seq, header.pid] == seq_pid
44
+ if header.type < Core::NLMSG_MIN_TYPE
45
+ # Control messages
46
+ case header.type
47
+ when Core::NLMSG_ERROR
48
+ errno = decoder.get_value(Endian::Host::SINT)
49
+ if errno == 0
50
+ yield Ack.new
51
+ else
52
+ yield SystemCallError.new(-errno)
53
+ end
54
+ decoder.skip(header.len - Core::NLMSG_HDRLEN - 4)
55
+ when Core::NLMSG_DONE
56
+ yield Done.new
57
+ decoder.skip(header.len - Core::NLMSG_HDRLEN)
58
+ else
59
+ # just ignore NLMSG_NOOP and other unknown control messages
60
+ decoder.skip(header.len - Core::NLMSG_HDRLEN)
61
+ end
62
+ else
63
+ # Subsystem-specific messages
64
+ decoder.limit(header.len - Core::NLMSG_HDRLEN) do
65
+ decoder.align_to(Core::NLMSG_ALIGNTO)
66
+ yield message_class.decode(decoder, header)
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ # @param socket [Socket] Netlink socket
73
+ # @param type [:do, :dump] Request type
74
+ # @param request_class [Class] Request message class
75
+ # @param reply_class [Class] Reply message class
76
+ # @param args [Hash] Request arguments
77
+ def exchange_message(socket, type, request_class, reply_class, args)
78
+ flags = Core::NLM_F_REQUEST | Core::NLM_F_ACK
79
+ flags |= Core::NLM_F_DUMP if type == :dump
80
+
81
+ request = request_class.from_params(args)
82
+ request.header.flags = flags
83
+ seq_pid = send_message(socket, request)
84
+
85
+ result = []
86
+
87
+ done = false
88
+ acked = false
89
+ begin
90
+ recv_message(socket, seq_pid, reply_class) do |message|
91
+ case message
92
+ when Done
93
+ done = true
94
+ when Exception
95
+ raise
96
+ when Ack
97
+ acked = true
98
+ else
99
+ result << message
100
+ done = true if type == :do
101
+ end
102
+ end
103
+ end until done
104
+
105
+ result
106
+ end
107
+
108
+ module DataTypes
109
+ class Scalar
110
+ def initialize(type, check)
111
+ @type = type
112
+ @check = check
113
+ end
114
+
115
+ def encode(encoder, value)
116
+ value ||= 0
117
+ encoder.put_value(@type, value.tap(&@check))
118
+ end
119
+
120
+ def decode(decoder)
121
+ value = decoder.get_value(@type).tap(&@check)
122
+ end
123
+ end
124
+
125
+ class String
126
+ def initialize(check)
127
+ @check = check
128
+ end
129
+
130
+ def encode(encoder, value)
131
+ encoder.put_zstring(value)
132
+ end
133
+
134
+ def decode(decoder)
135
+ decoder.get_zstring
136
+ end
137
+ end
138
+
139
+ class Binary
140
+ def initialize(check)
141
+ @check = check
142
+ end
143
+
144
+ def encode(encoder, value)
145
+ encoder.put_string(value)
146
+ end
147
+
148
+ def decode(decoder)
149
+ decoder.get_string
150
+ end
151
+ end
152
+
153
+ class NestedAttributes
154
+ def initialize(attribute_set)
155
+ @attribute_set = attribute_set
156
+ end
157
+
158
+ def encode(encoder, value)
159
+ @attribute_set.encode(encoder, value)
160
+ end
161
+
162
+ def decode(decoder)
163
+ @attribute_set.decode(decoder)
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
data/lib/nl/socket.rb ADDED
@@ -0,0 +1,53 @@
1
+ # Netlink sockets
2
+
3
+ require 'socket'
4
+
5
+ module Nl
6
+ # Netlink socket
7
+ class Socket < ::Socket
8
+ module Constants
9
+ # From include/linux/socket.h
10
+ PF_NETLINK = AF_NETLINK = 16
11
+ end
12
+ include Constants
13
+
14
+ class << self
15
+ def pack_sockaddr_nl(pid, groups) = [Socket::AF_NETLINK, 0, pid, groups].pack('S!S!LL')
16
+ alias sockaddr_nl pack_sockaddr_nl
17
+
18
+ def unpack_sockaddr_nl(sockaddr) = sockaddr.unpack('S!S!LL')[2..3]
19
+ end
20
+
21
+ # @param protonum [Integer] Netlink protocol number
22
+ def initialize(protonum)
23
+ super(PF_NETLINK, SOCK_RAW, protonum)
24
+ @seq = 0 # last-used sequence number
25
+ end
26
+
27
+ def self.open(protonum)
28
+ return new(protonum) unless block_given?
29
+ begin
30
+ socket = new(protonum)
31
+ yield socket
32
+ ensure
33
+ socket&.close
34
+ end
35
+ end
36
+
37
+ # XXX: Should Protocol manage next_seq?
38
+
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
44
+ end
45
+
46
+ def complete(hdr)
47
+ [
48
+ hdr.seq ||= next_seq,
49
+ hdr.pid ||= Socket.unpack_sockaddr_nl(local_address.to_sockaddr).first,
50
+ ]
51
+ end
52
+ end
53
+ end
data/lib/nl/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module Nl
2
+ VERSION = '0.1.0'
3
+ end
data/lib/nl.rb ADDED
@@ -0,0 +1,14 @@
1
+ require_relative 'nl/version'
2
+ require_relative 'nl/core'
3
+ require_relative 'nl/genl'
4
+ require_relative 'nl/socket'
5
+ require_relative 'nl/family'
6
+ require_relative 'nl/protocols/raw'
7
+ require_relative 'nl/protocols/genl'
8
+
9
+ module Nl
10
+ class Error < StandardError; end
11
+
12
+ include Core
13
+ include Genl
14
+ end
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nl
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kasumi Hanazuki
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Linux Netlink client
13
+ email:
14
+ - kasumi@rollingapple.net
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - ".rspec"
20
+ - CHANGELOG.md
21
+ - LICENSE.txt
22
+ - README.md
23
+ - Rakefile
24
+ - lib/nl.rb
25
+ - lib/nl/core.rb
26
+ - lib/nl/decoder.rb
27
+ - lib/nl/encoder.rb
28
+ - lib/nl/endian.rb
29
+ - lib/nl/family.rb
30
+ - lib/nl/genl.rb
31
+ - lib/nl/protocols/genl.rb
32
+ - lib/nl/protocols/raw.rb
33
+ - lib/nl/socket.rb
34
+ - lib/nl/version.rb
35
+ homepage: https://github.com/hanazuki/nl
36
+ licenses:
37
+ - MIT
38
+ metadata:
39
+ homepage_uri: https://github.com/hanazuki/nl
40
+ source_code_uri: https://github.com/hanazuki/nl/tree/v0.1.0
41
+ changelog_uri: https://github.com/hanazuki/nl/blob/master/CHANGELOG.md
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '3.1'
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubygems_version: 3.6.8
57
+ specification_version: 4
58
+ summary: Linux Netlink client
59
+ test_files: []