node-netlink 0.0.3 → 0.0.5

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,74 @@
1
+ import { type TNetlinkMessage } from "./message.js";
2
+ import { type TNetlinkStructures } from "./structures.js";
3
+ type TNetlinkAddress = {
4
+ nl_pid: bigint;
5
+ nl_groups: bigint;
6
+ };
7
+ /**
8
+ * A netlink socket owned by the caller, opened and bound outside of this library.
9
+ *
10
+ * This is the seam between the netlink protocol handling and the operating system.
11
+ * `createPo6NetlinkTransport()` adapts a file descriptor, tests inject fakes.
12
+ */
13
+ type TNetlinkTransport = {
14
+ address: TNetlinkAddress;
15
+ send: (args: {
16
+ data: Uint8Array;
17
+ }) => void;
18
+ listen: (args: {
19
+ onData: (args: {
20
+ data: Uint8Array;
21
+ }) => void;
22
+ onError: (args: {
23
+ error: Error;
24
+ }) => void;
25
+ }) => {
26
+ stop: () => void;
27
+ };
28
+ };
29
+ type TRequestHeader = {
30
+ nlmsg_type: bigint;
31
+ nlmsg_flags?: bigint;
32
+ };
33
+ type TSendHeader = TRequestHeader & {
34
+ nlmsg_seq?: bigint;
35
+ nlmsg_pid?: bigint;
36
+ };
37
+ type TTalkArgs = {
38
+ header: TRequestHeader;
39
+ payload: Uint8Array;
40
+ timeoutMs?: number;
41
+ };
42
+ type TTryTalkResult = {
43
+ errno: number | undefined;
44
+ messages: TNetlinkMessage[];
45
+ };
46
+ type TNetlinkSocket = {
47
+ nl_pid: bigint;
48
+ nl_groups: bigint;
49
+ send: (args: {
50
+ header: TSendHeader;
51
+ payload: Uint8Array;
52
+ }) => {
53
+ nlmsg_seq: bigint;
54
+ };
55
+ tryTalk: (args: TTalkArgs) => Promise<TTryTalkResult>;
56
+ talk: (args: TTalkArgs) => Promise<TNetlinkMessage[]>;
57
+ detach: () => void;
58
+ };
59
+ type TCreateNetlinkSocketArgs = {
60
+ transport: TNetlinkTransport;
61
+ onMessage?: (args: {
62
+ message: TNetlinkMessage;
63
+ }) => void;
64
+ onError?: (args: {
65
+ error: Error;
66
+ }) => void;
67
+ structures?: TNetlinkStructures;
68
+ };
69
+ declare const seqAfter: ({ seq }: {
70
+ seq: bigint;
71
+ }) => bigint;
72
+ declare const createNetlinkSocket: ({ transport, onMessage, onError, structures, }: TCreateNetlinkSocketArgs) => TNetlinkSocket;
73
+ export { createNetlinkSocket, seqAfter, };
74
+ export type { TNetlinkAddress, TNetlinkTransport, TNetlinkSocket, TCreateNetlinkSocketArgs, TTalkArgs, TTryTalkResult, TRequestHeader, TSendHeader, };
@@ -0,0 +1,314 @@
1
+ import { NLM_F_ACK, NLM_F_REQUEST } from "./constants.js";
2
+ import {
3
+ errnoOfMessage,
4
+ formatMessage,
5
+ isTerminatingMessage,
6
+ parseMessages,
7
+ /*type TNetlinkMessage
8
+ */} from "./message.js";
9
+ import { createErrorFromErrno } from "./errno.js";
10
+ import { hostStructures, /*type TNetlinkStructures */} from "./structures.js";
11
+
12
+ /*type TNetlinkAddress = {
13
+ nl_pid: bigint;
14
+ nl_groups: bigint;
15
+ };*/
16
+
17
+ /**
18
+ * A netlink socket owned by the caller, opened and bound outside of this library.
19
+ *
20
+ * This is the seam between the netlink protocol handling and the operating system.
21
+ * `createPo6NetlinkTransport()` adapts a file descriptor, tests inject fakes.
22
+ */
23
+ /*type TNetlinkTransport = {
24
+ // the address the socket is bound to
25
+ address: TNetlinkAddress;
26
+ // sends a datagram to the kernel, throws on failure
27
+ send: (args: { data: Uint8Array }) => void;
28
+ // starts passing received datagrams to onData until stop() is called
29
+ listen: (args: {
30
+ onData: (args: { data: Uint8Array }) => void;
31
+ onError: (args: { error: Error }) => void;
32
+ }) => { stop: () => void };
33
+ };*/
34
+
35
+ const DEFAULT_TIMEOUT_MS = 10_000;
36
+ const MAX_SEQ = 0xFFFF_FFFFn;
37
+
38
+ /*type TRequestHeader = {
39
+ nlmsg_type: bigint;
40
+ nlmsg_flags?: bigint;
41
+ };*/
42
+
43
+ /*type TSendHeader = TRequestHeader & {
44
+ nlmsg_seq?: bigint;
45
+ nlmsg_pid?: bigint;
46
+ };*/
47
+
48
+ /*type TTalkArgs = {
49
+ header: TRequestHeader;
50
+ payload: Uint8Array;
51
+ timeoutMs?: number;
52
+ };*/
53
+
54
+ /*type TTryTalkResult = {
55
+ // undefined if the request succeeded
56
+ errno: number | undefined;
57
+ // all responses except the terminating NLMSG_ERROR or NLMSG_DONE
58
+ messages: TNetlinkMessage[];
59
+ };*/
60
+
61
+ /*type TNetlinkSocket = {
62
+ // port id the socket is bound to
63
+ nl_pid: bigint;
64
+ // multicast groups the socket is bound to
65
+ nl_groups: bigint;
66
+
67
+ // sends a message without waiting for a response, returns the sequence number used
68
+ send: (args: { header: TSendHeader, payload: Uint8Array }) => { nlmsg_seq: bigint };
69
+ // sends a request and resolves with all responses, or the errno the kernel reported
70
+ tryTalk: (args: TTalkArgs) => Promise<TTryTalkResult>;
71
+ // like tryTalk, but rejects if the kernel reported an error
72
+ talk: (args: TTalkArgs) => Promise<TNetlinkMessage[]>;
73
+ // stops listening on the transport and rejects pending requests, the transport itself is left open
74
+ detach: () => void;
75
+ };*/
76
+
77
+ /*type TCreateNetlinkSocketArgs = {
78
+ transport: TNetlinkTransport;
79
+ // called for every received message that is not a response to a pending request
80
+ onMessage?: (args: { message: TNetlinkMessage }) => void;
81
+ // called for errors while receiving, if omitted such errors are thrown asynchronously
82
+ onError?: (args: { error: Error }) => void;
83
+ // layouts of the kernel structures, the ones of the host by default
84
+ structures?: TNetlinkStructures;
85
+ };*/
86
+
87
+ /*type TPendingRequest = {
88
+ messages: TNetlinkMessage[];
89
+ resolve: (result: TTryTalkResult) => void;
90
+ reject: (error: Error) => void;
91
+ timeout: ReturnType<typeof setTimeout>;
92
+ };*/
93
+
94
+ // sequence numbers are 32 bit, 0 is skipped as it is used by notifications
95
+ const seqAfter = ({ seq }/*: { seq: bigint }*/) => {
96
+ return seq === MAX_SEQ ? 1n : seq + 1n;
97
+ };
98
+
99
+ // requests sent to us by other sockets are never responses, even if their sequence number matches
100
+ const isRequest = ({ message }/*: { message: TNetlinkMessage }*/) => {
101
+ return (message.header.nlmsg_flags & NLM_F_REQUEST) !== 0n;
102
+ };
103
+
104
+ // errors without an onError handler must not go unnoticed, so they surface as uncaught exceptions
105
+ const throwAsynchronously = ({ error }/*: { error: Error }*/) => {
106
+ queueMicrotask(() => {
107
+ throw error;
108
+ });
109
+ };
110
+
111
+ const createSeqAllocator = () => {
112
+ let nextSeq = 1n;
113
+
114
+ return () => {
115
+ const seq = nextSeq;
116
+ nextSeq = seqAfter({ seq });
117
+ return seq;
118
+ };
119
+ };
120
+
121
+ const createNetlinkSocket = ({
122
+ transport,
123
+ onMessage,
124
+ onError = throwAsynchronously,
125
+ structures = hostStructures,
126
+ }/*: TCreateNetlinkSocketArgs*/)/*: TNetlinkSocket*/ => {
127
+
128
+ let detached = false;
129
+ let pendingRequests/*: Record<string, TPendingRequest>*/ = {};
130
+
131
+ const removePendingRequest = ({ seq }/*: { seq: bigint }*/) => {
132
+ const { [seq.toString()]: request, ...otherRequests } = pendingRequests;
133
+ pendingRequests = otherRequests;
134
+
135
+ clearTimeout(request.timeout);
136
+ return request;
137
+ };
138
+
139
+ const finishRequest = ({ request, message }/*: { request: TPendingRequest, message: TNetlinkMessage }*/) => {
140
+ let errno/*: number | undefined*/;
141
+
142
+ try {
143
+ errno = errnoOfMessage({ message, structures });
144
+ } catch (ex) {
145
+ request.reject(ex /*as Error*/);
146
+ return;
147
+ }
148
+
149
+ request.resolve({ errno, messages: request.messages });
150
+ };
151
+
152
+ const deliverUnsolicited = ({ message }/*: { message: TNetlinkMessage }*/) => {
153
+ if (onMessage !== undefined) {
154
+ onMessage({ message });
155
+ }
156
+ };
157
+
158
+ const dispatchMessage = ({ message }/*: { message: TNetlinkMessage }*/) => {
159
+ const { nlmsg_seq } = message.header;
160
+ const request = pendingRequests[nlmsg_seq.toString()];
161
+
162
+ if (request === undefined || isRequest({ message })) {
163
+ deliverUnsolicited({ message });
164
+ return;
165
+ }
166
+
167
+ if (!isTerminatingMessage({ message })) {
168
+ pendingRequests = {
169
+ ...pendingRequests,
170
+ [nlmsg_seq.toString()]: { ...request, messages: [...request.messages, message] },
171
+ };
172
+ return;
173
+ }
174
+
175
+ removePendingRequest({ seq: nlmsg_seq });
176
+ finishRequest({ request, message });
177
+ };
178
+
179
+ const onData = ({ data }/*: { data: Uint8Array }*/) => {
180
+ let messages/*: TNetlinkMessage[]*/;
181
+
182
+ try {
183
+ messages = parseMessages({ data, structures });
184
+ } catch (ex) {
185
+ onError({ error: ex /*as Error*/ });
186
+ return;
187
+ }
188
+
189
+ // a handler may detach the socket, the remaining messages are dropped then
190
+ messages.forEach((message) => {
191
+ if (!detached) {
192
+ dispatchMessage({ message });
193
+ }
194
+ });
195
+ };
196
+
197
+ const listener = transport.listen({ onData, onError });
198
+
199
+ const allocateSeq = createSeqAllocator();
200
+
201
+ const assertAttached = () => {
202
+ if (detached) {
203
+ throw Error("netlink socket is detached");
204
+ }
205
+ };
206
+
207
+ const send/*: TNetlinkSocket["send"]*/ = ({ header, payload }) => {
208
+ assertAttached();
209
+
210
+ const {
211
+ nlmsg_type,
212
+ nlmsg_flags = NLM_F_REQUEST,
213
+ nlmsg_seq = allocateSeq(),
214
+ nlmsg_pid = 0n,
215
+ } = header;
216
+
217
+ const data = formatMessage({
218
+ message: {
219
+ header: { nlmsg_type, nlmsg_flags, nlmsg_seq, nlmsg_pid },
220
+ payload,
221
+ },
222
+ structures,
223
+ });
224
+
225
+ transport.send({ data });
226
+
227
+ return { nlmsg_seq };
228
+ };
229
+
230
+ const tryTalk/*: TNetlinkSocket["tryTalk"]*/ = ({ header, payload, timeoutMs = DEFAULT_TIMEOUT_MS }) => {
231
+ return new Promise((resolve, reject) => {
232
+ assertAttached();
233
+
234
+ const seq = allocateSeq();
235
+
236
+ const timeout = setTimeout(() => {
237
+ removePendingRequest({ seq });
238
+ reject(Error(`netlink request timed out after ${timeoutMs} ms`));
239
+ }, timeoutMs);
240
+
241
+ pendingRequests = {
242
+ ...pendingRequests,
243
+ [seq.toString()]: { messages: [], resolve, reject, timeout },
244
+ };
245
+
246
+ try {
247
+ send({
248
+ header: {
249
+ nlmsg_type: header.nlmsg_type,
250
+ nlmsg_flags: (header.nlmsg_flags ?? 0n) | NLM_F_REQUEST | NLM_F_ACK,
251
+ nlmsg_seq: seq,
252
+ },
253
+ payload,
254
+ });
255
+ } catch (ex) {
256
+ removePendingRequest({ seq });
257
+ throw ex;
258
+ }
259
+ });
260
+ };
261
+
262
+ const talk/*: TNetlinkSocket["talk"]*/ = async (args) => {
263
+ const { errno, messages } = await tryTalk(args);
264
+
265
+ if (errno !== undefined) {
266
+ throw createErrorFromErrno({ operation: `netlink request of type ${args.header.nlmsg_type}`, errno });
267
+ }
268
+
269
+ return messages;
270
+ };
271
+
272
+ const detach = () => {
273
+ if (detached) {
274
+ return;
275
+ }
276
+
277
+ detached = true;
278
+ listener.stop();
279
+
280
+ const requests = Object.values(pendingRequests);
281
+ pendingRequests = {};
282
+
283
+ requests.forEach((request) => {
284
+ clearTimeout(request.timeout);
285
+ request.reject(Error("netlink socket was detached while waiting for a response"));
286
+ });
287
+ };
288
+
289
+ return {
290
+ nl_pid: transport.address.nl_pid,
291
+ nl_groups: transport.address.nl_groups,
292
+
293
+ send,
294
+ tryTalk,
295
+ talk,
296
+ detach,
297
+ };
298
+ };
299
+
300
+ export {
301
+ createNetlinkSocket,
302
+ seqAfter,
303
+ };
304
+
305
+ /*export type {
306
+ TNetlinkAddress,
307
+ TNetlinkTransport,
308
+ TNetlinkSocket,
309
+ TCreateNetlinkSocketArgs,
310
+ TTalkArgs,
311
+ TTryTalkResult,
312
+ TRequestHeader,
313
+ TSendHeader,
314
+ };*/
@@ -0,0 +1,30 @@
1
+ import type { TPo6Api, TPo6KernelAbi } from "po6";
2
+ import type { TNetlinkTransport } from "./netlink-socket.js";
3
+ import { type TNetlinkStructures } from "./structures.js";
4
+ type TPo6NetlinkSyscalls = Pick<TPo6Api, "getsockname" | "sendmsg" | "recvmsg" | "createErrorFromErrno">;
5
+ type TPoller = {
6
+ armOnce: (events: {
7
+ readable: () => void;
8
+ error: (args: {
9
+ errorCode: number;
10
+ }) => void;
11
+ }) => void;
12
+ close: () => void;
13
+ };
14
+ type TCreatePoller = (args: {
15
+ fd: number;
16
+ }) => TPoller;
17
+ type TCreatePo6NetlinkTransportArgs = {
18
+ fd: number;
19
+ po6: TPo6NetlinkSyscalls;
20
+ kernelAbi: Pick<TPo6KernelAbi, "constants" | "errnoCodes">;
21
+ createPoller: TCreatePoller;
22
+ structures?: TNetlinkStructures;
23
+ };
24
+ /**
25
+ * Adapts a netlink socket file descriptor to a transport, using the given po6 syscalls and poller.
26
+ * The socket is neither opened nor closed here, it is owned by the caller.
27
+ */
28
+ declare const createPo6NetlinkTransport: ({ fd, po6, kernelAbi, createPoller, structures, }: TCreatePo6NetlinkTransportArgs) => TNetlinkTransport;
29
+ export { createPo6NetlinkTransport, };
30
+ export type { TPo6NetlinkSyscalls, TPoller, TCreatePoller, TCreatePo6NetlinkTransportArgs, };
@@ -0,0 +1,175 @@
1
+ /*import type { TPo6Api, TPo6KernelAbi } from "po6";*/
2
+ import { AF_NETLINK } from "./constants.js";
3
+ /*import type { TNetlinkAddress, TNetlinkTransport } from "./netlink-socket.ts";*/
4
+ import { formatNetlinkAddress, hostStructures, /*type TNetlinkStructures */} from "./structures.js";
5
+
6
+ /*type TPo6NetlinkSyscalls = Pick<TPo6Api, "getsockname" | "sendmsg" | "recvmsg" | "createErrorFromErrno">;*/
7
+
8
+ /*type TPoller = {
9
+ armOnce: (events: {
10
+ readable: () => void;
11
+ error: (args: { errorCode: number }) => void;
12
+ }) => void;
13
+ close: () => void;
14
+ };*/
15
+
16
+ /*type TCreatePoller = (args: { fd: number }) => TPoller;*/
17
+
18
+ /*type TCreatePo6NetlinkTransportArgs = {
19
+ // a netlink socket opened and bound by the caller, who also closes it
20
+ fd: number;
21
+ po6: TPo6NetlinkSyscalls;
22
+ kernelAbi: Pick<TPo6KernelAbi, "constants" | "errnoCodes">;
23
+ createPoller: TCreatePoller;
24
+ structures?: TNetlinkStructures;
25
+ };*/
26
+
27
+ /*type TReceiveResult = {
28
+ errno: number;
29
+ data: undefined;
30
+ } | {
31
+ errno: undefined;
32
+ data: Uint8Array;
33
+ };*/
34
+
35
+ /**
36
+ * Adapts a netlink socket file descriptor to a transport, using the given po6 syscalls and poller.
37
+ * The socket is neither opened nor closed here, it is owned by the caller.
38
+ */
39
+ const createPo6NetlinkTransport = ({
40
+ fd,
41
+ po6,
42
+ kernelAbi,
43
+ createPoller,
44
+ structures = hostStructures,
45
+ }/*: TCreatePo6NetlinkTransportArgs*/)/*: TNetlinkTransport*/ => {
46
+
47
+ const { MSG_DONTWAIT, MSG_PEEK, MSG_TRUNC } = kernelAbi.constants;
48
+ const { EAGAIN, ENOBUFS } = kernelAbi.errnoCodes;
49
+
50
+ const queryAddress = ()/*: TNetlinkAddress*/ => {
51
+ const { errno, sockaddr } = po6.getsockname({ fd });
52
+
53
+ if (errno !== undefined) {
54
+ throw po6.createErrorFromErrno({ operation: "getsockname()", errno });
55
+ }
56
+
57
+ const { nl_family, nl_pid, nl_groups } = structures.sockaddrNl.parse({ data: sockaddr });
58
+
59
+ if (nl_family !== AF_NETLINK) {
60
+ throw Error(`file descriptor ${fd} is not a netlink socket, its address family is ${nl_family}`);
61
+ }
62
+
63
+ return { nl_pid, nl_groups };
64
+ };
65
+
66
+ const address = queryAddress();
67
+ const kernelAddress = formatNetlinkAddress({ address: { nl_pid: 0n, nl_groups: 0n }, structures });
68
+
69
+ const receiveDatagram = ()/*: TReceiveResult*/ => {
70
+ // with MSG_TRUNC, netlink reports the full length of the datagram, even if the buffer is smaller
71
+ const { errno: peekErrno, bytesReceived: datagramLength } = po6.recvmsg({
72
+ fd,
73
+ data: new Uint8Array(1),
74
+ flags: MSG_PEEK | MSG_TRUNC | MSG_DONTWAIT,
75
+ });
76
+
77
+ if (peekErrno !== undefined) {
78
+ return { errno: peekErrno, data: undefined };
79
+ }
80
+
81
+ const data = new Uint8Array(Math.max(datagramLength, 1));
82
+ const { errno, bytesReceived } = po6.recvmsg({ fd, data, flags: MSG_DONTWAIT });
83
+
84
+ if (errno !== undefined) {
85
+ return { errno, data: undefined };
86
+ }
87
+
88
+ return { errno: undefined, data: data.subarray(0, bytesReceived) };
89
+ };
90
+
91
+ const send/*: TNetlinkTransport["send"]*/ = ({ data }) => {
92
+ const { errno } = po6.sendmsg({ fd, data, msghdr: { msg_name: kernelAddress } });
93
+
94
+ if (errno !== undefined) {
95
+ throw po6.createErrorFromErrno({ operation: "sendmsg()", errno });
96
+ }
97
+ };
98
+
99
+ const listen/*: TNetlinkTransport["listen"]*/ = ({ onData, onError }) => {
100
+ const poller = createPoller({ fd });
101
+ let stopped = false;
102
+
103
+ const receiveAndDispatch = ()/*: "continue" | "wait" | "stop"*/ => {
104
+ const { errno, data } = receiveDatagram();
105
+
106
+ if (errno === undefined) {
107
+ onData({ data });
108
+ return "continue";
109
+ }
110
+
111
+ if (errno === EAGAIN) {
112
+ return "wait";
113
+ }
114
+
115
+ onError({ error: po6.createErrorFromErrno({ operation: "recvmsg()", errno }) });
116
+
117
+ // ENOBUFS: the receive buffer overflowed and messages were lost, the socket is still usable
118
+ return errno === ENOBUFS ? "continue" : "stop";
119
+ };
120
+
121
+ // receives until the socket is empty, returns whether to wait for more data
122
+ const drain = () => {
123
+ let state = receiveAndDispatch();
124
+
125
+ while (state === "continue" && !stopped) {
126
+ state = receiveAndDispatch();
127
+ }
128
+
129
+ return state === "wait" && !stopped;
130
+ };
131
+
132
+ const arm = () => {
133
+ poller.armOnce({
134
+ readable: () => {
135
+ if (drain()) {
136
+ arm();
137
+ }
138
+ },
139
+ error: ({ errorCode }) => {
140
+ onError({ error: Error(`polling netlink socket failed with libuv error ${errorCode}`) });
141
+ },
142
+ });
143
+ };
144
+
145
+ arm();
146
+
147
+ return {
148
+ stop: () => {
149
+ if (stopped) {
150
+ return;
151
+ }
152
+
153
+ stopped = true;
154
+ poller.close();
155
+ },
156
+ };
157
+ };
158
+
159
+ return {
160
+ address,
161
+ send,
162
+ listen,
163
+ };
164
+ };
165
+
166
+ export {
167
+ createPo6NetlinkTransport,
168
+ };
169
+
170
+ /*export type {
171
+ TPo6NetlinkSyscalls,
172
+ TPoller,
173
+ TCreatePoller,
174
+ TCreatePo6NetlinkTransportArgs,
175
+ };*/