@lakutata/netif 0.0.0-stage → 3.1.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.
- package/LICENSE +23 -0
- package/README.md +299 -2
- package/README.zh-CN.md +229 -0
- package/binding.gyp +43 -0
- package/dist/cjs/exceptions/NetIfException.d.ts +17 -0
- package/dist/cjs/exceptions/NetIfException.js +24 -0
- package/dist/cjs/exceptions/NetIfNotEnforcedException.d.ts +12 -0
- package/dist/cjs/exceptions/NetIfNotEnforcedException.js +19 -0
- package/dist/cjs/exceptions/NetIfNotFoundException.d.ts +12 -0
- package/dist/cjs/exceptions/NetIfNotFoundException.js +19 -0
- package/dist/cjs/exceptions/NetIfNotSupportedException.d.ts +12 -0
- package/dist/cjs/exceptions/NetIfNotSupportedException.js +19 -0
- package/dist/cjs/exceptions/NetIfTimeoutException.d.ts +11 -0
- package/dist/cjs/exceptions/NetIfTimeoutException.js +18 -0
- package/dist/cjs/exports/Netif.d.ts +11 -0
- package/dist/cjs/exports/Netif.js +26 -0
- package/dist/cjs/lib/Addon.d.ts +5 -0
- package/dist/cjs/lib/Addon.js +9 -0
- package/dist/cjs/lib/Attachment.d.ts +152 -0
- package/dist/cjs/lib/Attachment.js +415 -0
- package/dist/cjs/lib/Enforcement.d.ts +22 -0
- package/dist/cjs/lib/Enforcement.js +45 -0
- package/dist/cjs/lib/Functions.d.ts +66 -0
- package/dist/cjs/lib/Functions.js +97 -0
- package/dist/cjs/lib/InterfaceListener.d.ts +92 -0
- package/dist/cjs/lib/InterfaceListener.js +99 -0
- package/dist/cjs/lib/InterfaceUDPSocket.d.ts +119 -0
- package/dist/cjs/lib/InterfaceUDPSocket.js +245 -0
- package/dist/cjs/lib/Internal.d.ts +10 -0
- package/dist/cjs/lib/Internal.js +9 -0
- package/dist/cjs/lib/Native.d.ts +54 -0
- package/dist/cjs/lib/Native.js +83 -0
- package/dist/cjs/lib/NetworkInterface.d.ts +313 -0
- package/dist/cjs/lib/NetworkInterface.js +391 -0
- package/dist/cjs/lib/Registry.d.ts +26 -0
- package/dist/cjs/lib/Registry.js +103 -0
- package/dist/cjs/lib/Snapshot.d.ts +17 -0
- package/dist/cjs/lib/Snapshot.js +84 -0
- package/dist/cjs/lib/Types.d.ts +63 -0
- package/dist/cjs/lib/Types.js +2 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/exceptions/NetIfException.js +20 -0
- package/dist/esm/exceptions/NetIfNotEnforcedException.js +15 -0
- package/dist/esm/exceptions/NetIfNotFoundException.js +15 -0
- package/dist/esm/exceptions/NetIfNotSupportedException.js +15 -0
- package/dist/esm/exceptions/NetIfTimeoutException.js +14 -0
- package/dist/esm/exports/Netif.js +9 -0
- package/dist/esm/lib/Addon.js +10 -0
- package/dist/esm/lib/Attachment.js +410 -0
- package/dist/esm/lib/Enforcement.js +41 -0
- package/dist/esm/lib/Functions.js +92 -0
- package/dist/esm/lib/InterfaceListener.js +95 -0
- package/dist/esm/lib/InterfaceUDPSocket.js +241 -0
- package/dist/esm/lib/Internal.js +6 -0
- package/dist/esm/lib/Native.js +75 -0
- package/dist/esm/lib/NetworkInterface.js +386 -0
- package/dist/esm/lib/Registry.js +100 -0
- package/dist/esm/lib/Snapshot.js +79 -0
- package/dist/esm/lib/Types.js +1 -0
- package/dist/types/exceptions/NetIfException.d.ts +17 -0
- package/dist/types/exceptions/NetIfNotEnforcedException.d.ts +12 -0
- package/dist/types/exceptions/NetIfNotFoundException.d.ts +12 -0
- package/dist/types/exceptions/NetIfNotSupportedException.d.ts +12 -0
- package/dist/types/exceptions/NetIfTimeoutException.d.ts +11 -0
- package/dist/types/exports/Netif.d.ts +11 -0
- package/dist/types/lib/Addon.d.ts +5 -0
- package/dist/types/lib/Attachment.d.ts +152 -0
- package/dist/types/lib/Enforcement.d.ts +22 -0
- package/dist/types/lib/Functions.d.ts +66 -0
- package/dist/types/lib/InterfaceListener.d.ts +92 -0
- package/dist/types/lib/InterfaceUDPSocket.d.ts +119 -0
- package/dist/types/lib/Internal.d.ts +10 -0
- package/dist/types/lib/Native.d.ts +54 -0
- package/dist/types/lib/NetworkInterface.d.ts +313 -0
- package/dist/types/lib/Registry.d.ts +26 -0
- package/dist/types/lib/Snapshot.d.ts +17 -0
- package/dist/types/lib/Types.d.ts +63 -0
- package/native/Interfaces.cpp +34 -0
- package/native/Interfaces.h +47 -0
- package/native/Sockets.h +36 -0
- package/native/Watcher.h +27 -0
- package/native/addon.cpp +195 -0
- package/native/darwin/Interfaces.cpp +150 -0
- package/native/darwin/Sockets.cpp +44 -0
- package/native/darwin/Watcher.cpp +113 -0
- package/native/linux/Interfaces.cpp +133 -0
- package/native/linux/RouteNetlink.h +38 -0
- package/native/linux/Sockets.cpp +89 -0
- package/native/linux/Watcher.cpp +70 -0
- package/native/windows/Interfaces.cpp +78 -0
- package/native/windows/Sockets.cpp +56 -0
- package/native/windows/Watcher.cpp +53 -0
- package/package.json +81 -4
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NetworkInterface = void 0;
|
|
4
|
+
exports.changesOf = changesOf;
|
|
5
|
+
const node_events_1 = require("node:events");
|
|
6
|
+
const NetIfException_js_1 = require("../exceptions/NetIfException.js");
|
|
7
|
+
const NetIfNotEnforcedException_js_1 = require("../exceptions/NetIfNotEnforcedException.js");
|
|
8
|
+
const NetIfNotFoundException_js_1 = require("../exceptions/NetIfNotFoundException.js");
|
|
9
|
+
const NetIfNotSupportedException_js_1 = require("../exceptions/NetIfNotSupportedException.js");
|
|
10
|
+
const NetIfTimeoutException_js_1 = require("../exceptions/NetIfTimeoutException.js");
|
|
11
|
+
const Enforcement_js_1 = require("./Enforcement.js");
|
|
12
|
+
const InterfaceListener_js_1 = require("./InterfaceListener.js");
|
|
13
|
+
const InterfaceUDPSocket_js_1 = require("./InterfaceUDPSocket.js");
|
|
14
|
+
const Internal_js_1 = require("./Internal.js");
|
|
15
|
+
const Native_js_1 = require("./Native.js");
|
|
16
|
+
const Registry_js_1 = require("./Registry.js");
|
|
17
|
+
const Snapshot_js_1 = require("./Snapshot.js");
|
|
18
|
+
const MAC = /^[0-9a-f]{2}([:-][0-9a-f]{2}){5}$/i;
|
|
19
|
+
/**
|
|
20
|
+
* The key of an address in a comparison
|
|
21
|
+
*/
|
|
22
|
+
function keyOf(address) {
|
|
23
|
+
return `${address.family}|${address.address}|${address.scopeid ?? ''}`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* What changed between two states of an interface: whether it appeared or disappeared, its link came up or went down,
|
|
27
|
+
* and its addresses added and removed
|
|
28
|
+
*/
|
|
29
|
+
function changesOf(previous, next) {
|
|
30
|
+
const before = new Map((previous?.addresses ?? []).map((address) => [keyOf(address), address]));
|
|
31
|
+
const after = new Map((next?.addresses ?? []).map((address) => [keyOf(address), address]));
|
|
32
|
+
const wasRunning = previous?.running ?? false;
|
|
33
|
+
const isRunning = next?.running ?? false;
|
|
34
|
+
return {
|
|
35
|
+
appeared: !previous && !!next,
|
|
36
|
+
disappeared: !!previous && !next,
|
|
37
|
+
up: !wasRunning && isRunning,
|
|
38
|
+
down: wasRunning && !isRunning,
|
|
39
|
+
added: [...after].filter(([key]) => !before.has(key)).map(([, address]) => ({ ...address })),
|
|
40
|
+
removed: [...before].filter(([key]) => !after.has(key)).map(([, address]) => ({ ...address }))
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A network interface of the machine, followed: its state and addresses now, events when they change, and the servers
|
|
45
|
+
* listening on it. The interface may not exist yet (a USB adapter not plugged in, a system starting): the object follows
|
|
46
|
+
* it once it appears, and again when it is unplugged and plugged back.
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* import {type InterfaceAddress, NetworkInterface} from '@lakutata/netif'
|
|
50
|
+
*
|
|
51
|
+
* const management: NetworkInterface = new NetworkInterface('eth1')
|
|
52
|
+
* console.log(management.present, management.running, management.addresses.map(address => address.address))
|
|
53
|
+
* management.on('change', ({added, removed}) => console.log('added', added, 'removed', removed))
|
|
54
|
+
* management.on('down', () => console.log('the cable of eth1 is unplugged'))
|
|
55
|
+
* const address: InterfaceAddress = await management.waitForAddress({family: 'IPv4', timeout: 30000})
|
|
56
|
+
* console.log(`http://${address.address}:8080`)
|
|
57
|
+
* management.close()
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
class NetworkInterface extends node_events_1.EventEmitter {
|
|
61
|
+
#state;
|
|
62
|
+
#unsubscribe;
|
|
63
|
+
#waits = new Set();
|
|
64
|
+
#observers = new Set();
|
|
65
|
+
/**
|
|
66
|
+
* Follows an interface.
|
|
67
|
+
* @param selector Its name, `{mac}` or `{index}`.
|
|
68
|
+
* @throws NetIfNotFoundException An index that no interface has (an index is never given again once its interface is
|
|
69
|
+
* gone), or a hardware address that several interfaces have now (a bridge and its ports: give the name then).
|
|
70
|
+
*/
|
|
71
|
+
constructor(selector) {
|
|
72
|
+
super();
|
|
73
|
+
this.selector = NetworkInterface.#normalized(selector);
|
|
74
|
+
this.#unsubscribe = Registry_js_1.registry.subscribe((interfaces) => this.#update(interfaces));
|
|
75
|
+
const { entry, ambiguous } = this.#match(Registry_js_1.registry.current());
|
|
76
|
+
if (ambiguous || (!entry && typeof this.selector === 'object' && 'index' in this.selector)) {
|
|
77
|
+
this.close();
|
|
78
|
+
throw new NetIfNotFoundException_js_1.NetIfNotFoundException(ambiguous
|
|
79
|
+
? `Several interfaces have the hardware address ${this.selector.mac}: give the interface by its name`
|
|
80
|
+
: `No interface has the index ${this.selector.index}`);
|
|
81
|
+
}
|
|
82
|
+
this.#state = entry;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The network interfaces of the machine, as `listInterfaces()` gives them.
|
|
86
|
+
* @returns The interfaces, in the order of the system.
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* import {NetworkInterface} from '@lakutata/netif'
|
|
90
|
+
*
|
|
91
|
+
* const names: string[] = NetworkInterface.list().map(entry => entry.name)
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
static list() {
|
|
95
|
+
return (0, Snapshot_js_1.readInterfaces)().interfaces;
|
|
96
|
+
}
|
|
97
|
+
static #normalized(selector) {
|
|
98
|
+
if (typeof selector === 'string') {
|
|
99
|
+
if (!selector)
|
|
100
|
+
throw new TypeError('The name of an interface is empty');
|
|
101
|
+
return selector;
|
|
102
|
+
}
|
|
103
|
+
if (selector && typeof selector === 'object' && 'mac' in selector) {
|
|
104
|
+
if (typeof selector.mac !== 'string' || !MAC.test(selector.mac))
|
|
105
|
+
throw new TypeError(`${String(selector.mac)} is not a hardware address (aa:bb:cc:dd:ee:ff)`);
|
|
106
|
+
return { mac: selector.mac.toLowerCase().replaceAll('-', ':') };
|
|
107
|
+
}
|
|
108
|
+
if (selector && typeof selector === 'object' && 'index' in selector) {
|
|
109
|
+
if (!Number.isInteger(selector.index) || selector.index <= 0)
|
|
110
|
+
throw new TypeError(`${String(selector.index)} is not the index of an interface`);
|
|
111
|
+
return { index: selector.index };
|
|
112
|
+
}
|
|
113
|
+
throw new TypeError('An interface is given by its name, {mac} or {index}');
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Whether the interface exists now.
|
|
117
|
+
*/
|
|
118
|
+
get present() {
|
|
119
|
+
return !!this.#state;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Its name, `undefined` while it does not exist.
|
|
123
|
+
*/
|
|
124
|
+
get name() {
|
|
125
|
+
return this.#state?.name;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Its index, `undefined` while it does not exist. It changes when the interface is unplugged and plugged again.
|
|
129
|
+
*/
|
|
130
|
+
get index() {
|
|
131
|
+
return this.#state?.index;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Its hardware address, `undefined` while it does not exist or when it has none (and on the macOS versions that hide
|
|
135
|
+
* it).
|
|
136
|
+
*/
|
|
137
|
+
get mac() {
|
|
138
|
+
return this.#state?.mac;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Whether it is enabled (its administrative state); `false` while it does not exist.
|
|
142
|
+
*/
|
|
143
|
+
get up() {
|
|
144
|
+
return this.#state?.up ?? false;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Whether its link is up (a cable plugged in, an association made); `false` while it does not exist.
|
|
148
|
+
*/
|
|
149
|
+
get running() {
|
|
150
|
+
return this.#state?.running ?? false;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Whether it is the loopback interface.
|
|
154
|
+
*/
|
|
155
|
+
get loopback() {
|
|
156
|
+
return this.#state?.loopback ?? false;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Its usable addresses now, an empty array when it has none or does not exist.
|
|
160
|
+
*/
|
|
161
|
+
get addresses() {
|
|
162
|
+
return this.#state ? this.#state.addresses.map((address) => ({ ...address })) : [];
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Its state now as `listInterfaces()` gives it, `undefined` while it does not exist.
|
|
166
|
+
*/
|
|
167
|
+
get info() {
|
|
168
|
+
return this.#state ? { ...this.#state, addresses: this.addresses } : undefined;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Whether the object was closed.
|
|
172
|
+
*/
|
|
173
|
+
get closed() {
|
|
174
|
+
return !this.#unsubscribe;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* An address of the interface: one it has now, or the first one it gets.
|
|
178
|
+
* @param options The family of the address and the milliseconds to wait at most.
|
|
179
|
+
* @returns The address.
|
|
180
|
+
* @throws NetIfTimeoutException No address came in time.
|
|
181
|
+
* @throws NetIfException The object was closed while waiting.
|
|
182
|
+
* @example
|
|
183
|
+
* ```typescript
|
|
184
|
+
* import {type InterfaceAddress, NetworkInterface} from '@lakutata/netif'
|
|
185
|
+
*
|
|
186
|
+
* const wan: NetworkInterface = new NetworkInterface('eth0')
|
|
187
|
+
* const address: InterfaceAddress = await wan.waitForAddress({family: 'IPv4', timeout: 60000})
|
|
188
|
+
* wan.close()
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
waitForAddress(options = {}) {
|
|
192
|
+
const pick = () => this.addresses.find((address) => !options.family || address.family === options.family);
|
|
193
|
+
const found = pick();
|
|
194
|
+
if (found)
|
|
195
|
+
return Promise.resolve(found);
|
|
196
|
+
if (this.closed)
|
|
197
|
+
return Promise.reject(new NetIfException_js_1.NetIfException('The NetworkInterface is closed'));
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
//The wait keeps the process alive, as a server waiting for connections does
|
|
200
|
+
const alive = setInterval(() => undefined, 0x7fffffff);
|
|
201
|
+
let timer;
|
|
202
|
+
const finish = () => {
|
|
203
|
+
clearInterval(alive);
|
|
204
|
+
clearTimeout(timer);
|
|
205
|
+
this.off('change', changed);
|
|
206
|
+
this.#waits.delete(closed);
|
|
207
|
+
};
|
|
208
|
+
const changed = () => {
|
|
209
|
+
const address = pick();
|
|
210
|
+
if (!address)
|
|
211
|
+
return;
|
|
212
|
+
finish();
|
|
213
|
+
resolve(address);
|
|
214
|
+
};
|
|
215
|
+
const closed = (error) => {
|
|
216
|
+
finish();
|
|
217
|
+
reject(error);
|
|
218
|
+
};
|
|
219
|
+
this.on('change', changed);
|
|
220
|
+
this.#waits.add(closed);
|
|
221
|
+
if (options.timeout !== undefined) {
|
|
222
|
+
timer = setTimeout(() => closed(new NetIfTimeoutException_js_1.NetIfTimeoutException(`${this.#described()} got no ${options.family ?? ''} address in ${options.timeout} ms`.replace(' ', ' '))), options.timeout);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Listens on the interface for the server: its connections are accepted by the listener returned and given to the
|
|
228
|
+
* server, which handles them as its own. On Linux the sockets are bound to the interface (whatever its addresses, the
|
|
229
|
+
* system refuses the connections coming through another interface); on Windows and macOS each address of the
|
|
230
|
+
* interface is listened on, and listened on again when they change (`enforced` tells whether the system refuses the
|
|
231
|
+
* connections coming through another interface). The interface may not exist yet, nor have an address: it is listened
|
|
232
|
+
* on once it can be, and again when it is unplugged and plugged back.
|
|
233
|
+
* @param server A `net.Server` or a server built on one: `http`, `https`, `http2`, `tls`; for Express
|
|
234
|
+
* `createServer(app)`, for Fastify `fastify.server`. The server itself must not listen.
|
|
235
|
+
* @param options The port, the families, `strict` and the backlog.
|
|
236
|
+
* @returns The listener, once the interface is listened on (or waited for).
|
|
237
|
+
* @throws NetIfNotEnforcedException With `strict`, where the system cannot refuse the connections coming through
|
|
238
|
+
* another interface.
|
|
239
|
+
* @throws NetIfException The system refused the first listening, its code as `errno` (`'EADDRINUSE'`, `'EACCES'`…).
|
|
240
|
+
* Later refusals are `error` events of the listener, and retried.
|
|
241
|
+
* @example
|
|
242
|
+
* ```typescript
|
|
243
|
+
* import {createServer} from 'node:http'
|
|
244
|
+
* import express from 'express'
|
|
245
|
+
* import {type InterfaceListener, NetworkInterface} from '@lakutata/netif'
|
|
246
|
+
*
|
|
247
|
+
* const app = express()
|
|
248
|
+
* app.get('/', (request, response) => response.send('ok'))
|
|
249
|
+
* const management: NetworkInterface = new NetworkInterface({mac: '52:54:00:12:34:56'})
|
|
250
|
+
* const listener: InterfaceListener = await management.listen(createServer(app), {port: 8080})
|
|
251
|
+
* listener.on('error', error => console.error(error))
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
async listen(server, options) {
|
|
255
|
+
if (this.closed)
|
|
256
|
+
throw new NetIfException_js_1.NetIfException('The NetworkInterface is closed');
|
|
257
|
+
return new InterfaceListener_js_1.InterfaceListener(this, server, options).start();
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* A UDP socket of the interface, with the methods and the events of a `dgram` socket. On Linux it is bound to the
|
|
261
|
+
* interface: it receives what comes through it, broadcasts and the multicast of the groups joined there included,
|
|
262
|
+
* whatever its addresses. On Windows and macOS a socket is bound to each address of the interface (on macOS it
|
|
263
|
+
* receives no broadcast nor multicast then). It is bound again when the addresses change and when the interface is
|
|
264
|
+
* plugged back.
|
|
265
|
+
* @param options The port, the families and `strict`.
|
|
266
|
+
* @returns The socket, once it is bound (or the interface waited for).
|
|
267
|
+
* @throws NetIfNotEnforcedException With `strict`, where the system cannot refuse the datagrams coming through
|
|
268
|
+
* another interface.
|
|
269
|
+
* @throws NetIfException The system refused the first binding, its code as `errno`.
|
|
270
|
+
* @example
|
|
271
|
+
* ```typescript
|
|
272
|
+
* import {type InterfaceUDPSocket, NetworkInterface} from '@lakutata/netif'
|
|
273
|
+
*
|
|
274
|
+
* const socket: InterfaceUDPSocket = await new NetworkInterface('eth1').createUDPSocket({port: 9999, family: 'IPv4'})
|
|
275
|
+
* socket.setBroadcast(true)
|
|
276
|
+
* socket.on('message', (message, remote) => socket.send(message, remote.port, remote.address))
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
async createUDPSocket(options) {
|
|
280
|
+
if (this.closed)
|
|
281
|
+
throw new NetIfException_js_1.NetIfException('The NetworkInterface is closed');
|
|
282
|
+
return new InterfaceUDPSocket_js_1.InterfaceUDPSocket(this, options).start();
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* A socket bound to the interface (Linux, `SO_BINDTODEVICE`), on the port of every address of its family: it gets the
|
|
286
|
+
* connections and the datagrams that come through the interface, whatever the address they were sent to, and the
|
|
287
|
+
* system refuses the other ones. For the users that need the server itself listening (its `address()` and `close()`
|
|
288
|
+
* as usual): the socket is not made again when the interface is unplugged and plugged back, which `listen()` does.
|
|
289
|
+
* In Bun, give the descriptor to a `net.Server` or a `dgram` socket (Bun's `http.Server` does not listen on one).
|
|
290
|
+
* @param options The port, the family, the type and the backlog.
|
|
291
|
+
* @returns The descriptor of the socket and its port.
|
|
292
|
+
* @throws NetIfNotSupportedException Outside Linux: macOS and Windows have no socket bound to an interface.
|
|
293
|
+
* @throws NetIfNotFoundException The interface does not exist now.
|
|
294
|
+
* @throws NetIfNotEnforcedException The process may not bind a socket to an interface (CAP_NET_RAW before Linux 5.7).
|
|
295
|
+
* @throws NetIfException Another refusal of the system, its code as `errno` (`'EADDRINUSE'`…).
|
|
296
|
+
* @example
|
|
297
|
+
* ```typescript
|
|
298
|
+
* import {createServer} from 'node:http'
|
|
299
|
+
* import {type BoundSocket, NetworkInterface} from '@lakutata/netif'
|
|
300
|
+
*
|
|
301
|
+
* const management: NetworkInterface = new NetworkInterface('eth1')
|
|
302
|
+
* const socket: BoundSocket = management.bindSocket({port: 8080, family: 'IPv4', type: 'tcp'})
|
|
303
|
+
* createServer((request, response) => response.end('ok')).listen({fd: socket.fd})
|
|
304
|
+
* ```
|
|
305
|
+
*/
|
|
306
|
+
bindSocket(options = {}) {
|
|
307
|
+
const family = options.family ?? 'IPv4';
|
|
308
|
+
const type = options.type ?? 'tcp';
|
|
309
|
+
if (process.platform !== 'linux')
|
|
310
|
+
throw new NetIfNotSupportedException_js_1.NetIfNotSupportedException(`${process.platform === 'darwin' ? 'macOS' : process.platform === 'win32' ? 'Windows' : process.platform} has no socket bound to an interface: bindSocket() is for Linux; listen() and createUDPSocket() work everywhere`);
|
|
311
|
+
if (!this.#state)
|
|
312
|
+
throw new NetIfNotFoundException_js_1.NetIfNotFoundException(`${this.#described()} does not exist now`);
|
|
313
|
+
try {
|
|
314
|
+
const bound = (0, Native_js_1.nativeBindSocket)(this.#state.name, family === 'IPv4' ? 4 : 6, type === 'tcp', options.port ?? 0, options.backlog ?? 511);
|
|
315
|
+
return { fd: bound.fd, port: bound.port, family: family, type: type };
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
if (error instanceof NetIfException_js_1.NetIfException && (error.errno === 'EPERM' || error.errno === 'EACCES') && /SO_BINDTODEVICE/.test(error.message)) {
|
|
319
|
+
throw new NetIfNotEnforcedException_js_1.NetIfNotEnforcedException(`The process may not bind a socket to ${this.#state.name}. ${Enforcement_js_1.LINUX_PERMISSION}`, { cause: error });
|
|
320
|
+
}
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Stops following the interface: no event any more; its listeners are closed, the waits of `waitForAddress()`
|
|
326
|
+
* rejected.
|
|
327
|
+
*/
|
|
328
|
+
close() {
|
|
329
|
+
if (!this.#unsubscribe)
|
|
330
|
+
return;
|
|
331
|
+
this.#unsubscribe();
|
|
332
|
+
this.#unsubscribe = undefined;
|
|
333
|
+
for (const closed of [...this.#waits])
|
|
334
|
+
closed(new NetIfException_js_1.NetIfException('The NetworkInterface was closed while waiting for an address'));
|
|
335
|
+
for (const observer of [...this.#observers])
|
|
336
|
+
observer('close');
|
|
337
|
+
this.#observers.clear();
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Internal: follows the readings of the interfaces (the listeners of the interface)
|
|
341
|
+
*/
|
|
342
|
+
[Internal_js_1.OBSERVE](observer) {
|
|
343
|
+
this.#observers.add(observer);
|
|
344
|
+
return () => {
|
|
345
|
+
this.#observers.delete(observer);
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
#described() {
|
|
349
|
+
if (typeof this.selector === 'string')
|
|
350
|
+
return `The interface ${this.selector}`;
|
|
351
|
+
return 'mac' in this.selector ? `The interface of ${this.selector.mac}` : `The interface of index ${this.selector.index}`;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* The interface given among the interfaces, and whether several match (a hardware address shared)
|
|
355
|
+
*/
|
|
356
|
+
#match(interfaces) {
|
|
357
|
+
const selector = this.selector;
|
|
358
|
+
const matches = interfaces.filter((entry) => {
|
|
359
|
+
if (typeof selector === 'string')
|
|
360
|
+
return entry.name === selector;
|
|
361
|
+
if ('mac' in selector)
|
|
362
|
+
return entry.mac === selector.mac;
|
|
363
|
+
return entry.index === selector.index;
|
|
364
|
+
});
|
|
365
|
+
return { entry: matches.length === 1 ? matches[0] : undefined, ambiguous: matches.length > 1 };
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* The interfaces read again: the state updated, the events of what changed emitted
|
|
369
|
+
*/
|
|
370
|
+
#update(interfaces) {
|
|
371
|
+
if (this.closed)
|
|
372
|
+
return;
|
|
373
|
+
const previous = this.#state;
|
|
374
|
+
const next = this.#match(interfaces).entry;
|
|
375
|
+
this.#state = next;
|
|
376
|
+
const changes = changesOf(previous, next);
|
|
377
|
+
if (changes.appeared)
|
|
378
|
+
this.emit('added');
|
|
379
|
+
if (changes.up)
|
|
380
|
+
this.emit('up');
|
|
381
|
+
if (changes.down)
|
|
382
|
+
this.emit('down');
|
|
383
|
+
if (changes.added.length || changes.removed.length)
|
|
384
|
+
this.emit('change', { addresses: this.addresses, added: changes.added, removed: changes.removed });
|
|
385
|
+
if (changes.disappeared)
|
|
386
|
+
this.emit('removed');
|
|
387
|
+
for (const observer of [...this.#observers])
|
|
388
|
+
observer('update');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
exports.NetworkInterface = NetworkInterface;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type NetworkInterfaceInfo } from './Types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A subscriber of the changes: called with the interfaces read after each change
|
|
4
|
+
*/
|
|
5
|
+
export type Subscriber = (interfaces: NetworkInterfaceInfo[]) => void;
|
|
6
|
+
/**
|
|
7
|
+
* The interfaces of the machine as the package follows them: the notifications of the system shared by every
|
|
8
|
+
* subscriber of the thread, started with the first one and stopped with the last one. The timers do not keep the event
|
|
9
|
+
* loop alive.
|
|
10
|
+
*/
|
|
11
|
+
declare class Registry {
|
|
12
|
+
#private;
|
|
13
|
+
/**
|
|
14
|
+
* The interfaces read last while followed, read now otherwise
|
|
15
|
+
*/
|
|
16
|
+
current(): NetworkInterfaceInfo[];
|
|
17
|
+
/**
|
|
18
|
+
* Follows the interfaces: the subscriber is called after each change; the function returned unsubscribes it
|
|
19
|
+
*/
|
|
20
|
+
subscribe(subscriber: Subscriber): () => void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The registry of the thread
|
|
24
|
+
*/
|
|
25
|
+
export declare const registry: Registry;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registry = void 0;
|
|
4
|
+
const Native_js_1 = require("./Native.js");
|
|
5
|
+
const Snapshot_js_1 = require("./Snapshot.js");
|
|
6
|
+
//The notifications of the system come in bursts (an interface plugged: its link, then each address): read once after
|
|
7
|
+
const SETTLE = 50;
|
|
8
|
+
//An IPv6 address in its duplicate address detection: read again until it is usable (no notification of it on macOS)
|
|
9
|
+
const TENTATIVE_INTERVAL = 300;
|
|
10
|
+
const TENTATIVE_LIMIT = 15000;
|
|
11
|
+
/**
|
|
12
|
+
* The interfaces of the machine as the package follows them: the notifications of the system shared by every
|
|
13
|
+
* subscriber of the thread, started with the first one and stopped with the last one. The timers do not keep the event
|
|
14
|
+
* loop alive.
|
|
15
|
+
*/
|
|
16
|
+
class Registry {
|
|
17
|
+
#subscribers = new Set();
|
|
18
|
+
#interfaces = [];
|
|
19
|
+
#settle;
|
|
20
|
+
#tentative;
|
|
21
|
+
#tentativeSince = 0;
|
|
22
|
+
/**
|
|
23
|
+
* The interfaces read last while followed, read now otherwise
|
|
24
|
+
*/
|
|
25
|
+
current() {
|
|
26
|
+
return this.#subscribers.size ? this.#interfaces : (0, Snapshot_js_1.readInterfaces)().interfaces;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Follows the interfaces: the subscriber is called after each change; the function returned unsubscribes it
|
|
30
|
+
*/
|
|
31
|
+
subscribe(subscriber) {
|
|
32
|
+
if (!this.#subscribers.size) {
|
|
33
|
+
this.#tentativeSince = Date.now();
|
|
34
|
+
this.#interfaces = this.#read();
|
|
35
|
+
(0, Native_js_1.nativeWatch)(() => this.#changed());
|
|
36
|
+
}
|
|
37
|
+
this.#subscribers.add(subscriber);
|
|
38
|
+
return () => this.#unsubscribe(subscriber);
|
|
39
|
+
}
|
|
40
|
+
#unsubscribe(subscriber) {
|
|
41
|
+
if (!this.#subscribers.delete(subscriber) || this.#subscribers.size)
|
|
42
|
+
return;
|
|
43
|
+
(0, Native_js_1.nativeUnwatch)();
|
|
44
|
+
clearTimeout(this.#settle);
|
|
45
|
+
clearTimeout(this.#tentative);
|
|
46
|
+
this.#settle = undefined;
|
|
47
|
+
this.#tentative = undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A notification of the system: the interfaces read once the burst has settled
|
|
51
|
+
*/
|
|
52
|
+
#changed() {
|
|
53
|
+
if (this.#settle || !this.#subscribers.size)
|
|
54
|
+
return;
|
|
55
|
+
this.#settle = setTimeout(() => {
|
|
56
|
+
this.#settle = undefined;
|
|
57
|
+
this.#tentativeSince = Date.now();
|
|
58
|
+
this.#refresh();
|
|
59
|
+
}, SETTLE);
|
|
60
|
+
this.#settle.unref();
|
|
61
|
+
}
|
|
62
|
+
#refresh() {
|
|
63
|
+
if (!this.#subscribers.size)
|
|
64
|
+
return;
|
|
65
|
+
try {
|
|
66
|
+
this.#interfaces = this.#read();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
//The system cannot be read now: the next notification reads it again
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
for (const subscriber of [...this.#subscribers]) {
|
|
73
|
+
try {
|
|
74
|
+
subscriber(this.#interfaces);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
queueMicrotask(() => {
|
|
78
|
+
throw error;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The interfaces now; while an address is tentative, read again a little later
|
|
85
|
+
*/
|
|
86
|
+
#read() {
|
|
87
|
+
const { interfaces, tentative } = (0, Snapshot_js_1.readInterfaces)();
|
|
88
|
+
clearTimeout(this.#tentative);
|
|
89
|
+
this.#tentative = undefined;
|
|
90
|
+
if (tentative && Date.now() - this.#tentativeSince < TENTATIVE_LIMIT) {
|
|
91
|
+
this.#tentative = setTimeout(() => {
|
|
92
|
+
this.#tentative = undefined;
|
|
93
|
+
this.#refresh();
|
|
94
|
+
}, TENTATIVE_INTERVAL);
|
|
95
|
+
this.#tentative.unref();
|
|
96
|
+
}
|
|
97
|
+
return interfaces;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The registry of the thread
|
|
102
|
+
*/
|
|
103
|
+
exports.registry = new Registry();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type NativeInterface } from './Native.js';
|
|
2
|
+
import { type NetworkInterfaceInfo } from './Types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The mask of a prefix length: `'255.255.255.0'` for 24 in IPv4, `'ffff:ffff:ffff:ffff::'` for 64 in IPv6
|
|
5
|
+
*/
|
|
6
|
+
export declare function netmaskOf(family: 'IPv4' | 'IPv6', prefixLength: number): string;
|
|
7
|
+
/**
|
|
8
|
+
* An interface read by the addon as an interface of the package: its usable addresses only
|
|
9
|
+
*/
|
|
10
|
+
export declare function interfaceOf(entry: NativeInterface): NetworkInterfaceInfo;
|
|
11
|
+
/**
|
|
12
|
+
* The interfaces of the machine now, and whether some address is still tentative (to be read again soon)
|
|
13
|
+
*/
|
|
14
|
+
export declare function readInterfaces(): {
|
|
15
|
+
interfaces: NetworkInterfaceInfo[];
|
|
16
|
+
tentative: boolean;
|
|
17
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.netmaskOf = netmaskOf;
|
|
4
|
+
exports.interfaceOf = interfaceOf;
|
|
5
|
+
exports.readInterfaces = readInterfaces;
|
|
6
|
+
const Native_js_1 = require("./Native.js");
|
|
7
|
+
/**
|
|
8
|
+
* The mask of a prefix length: `'255.255.255.0'` for 24 in IPv4, `'ffff:ffff:ffff:ffff::'` for 64 in IPv6
|
|
9
|
+
*/
|
|
10
|
+
function netmaskOf(family, prefixLength) {
|
|
11
|
+
const bits = family === 'IPv4' ? 32 : 128;
|
|
12
|
+
const length = Math.max(0, Math.min(bits, prefixLength));
|
|
13
|
+
const bytes = [];
|
|
14
|
+
for (let index = 0; index < bits / 8; index++) {
|
|
15
|
+
const taken = Math.max(0, Math.min(8, length - index * 8));
|
|
16
|
+
bytes.push(taken === 0 ? 0 : (0xff << (8 - taken)) & 0xff);
|
|
17
|
+
}
|
|
18
|
+
if (family === 'IPv4')
|
|
19
|
+
return bytes.join('.');
|
|
20
|
+
const groups = [];
|
|
21
|
+
for (let index = 0; index < 16; index += 2)
|
|
22
|
+
groups.push(bytes[index] * 256 + bytes[index + 1]);
|
|
23
|
+
//The longest run of zero groups (two at least) written ::
|
|
24
|
+
let start = -1;
|
|
25
|
+
let size = 0;
|
|
26
|
+
for (let index = 0; index < 8;) {
|
|
27
|
+
if (groups[index] !== 0) {
|
|
28
|
+
index++;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
let end = index;
|
|
32
|
+
while (end < 8 && groups[end] === 0)
|
|
33
|
+
end++;
|
|
34
|
+
if (end - index > size) {
|
|
35
|
+
start = index;
|
|
36
|
+
size = end - index;
|
|
37
|
+
}
|
|
38
|
+
index = end;
|
|
39
|
+
}
|
|
40
|
+
const hex = groups.map((group) => group.toString(16));
|
|
41
|
+
if (size < 2)
|
|
42
|
+
return hex.join(':');
|
|
43
|
+
return `${hex.slice(0, start).join(':')}::${hex.slice(start + size).join(':')}`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* An address read by the addon as an address of the package
|
|
47
|
+
*/
|
|
48
|
+
function addressOf(address) {
|
|
49
|
+
const converted = {
|
|
50
|
+
address: address.address,
|
|
51
|
+
family: address.family,
|
|
52
|
+
netmask: netmaskOf(address.family, address.prefixLength),
|
|
53
|
+
cidr: `${address.address}/${address.prefixLength}`
|
|
54
|
+
};
|
|
55
|
+
if (address.family === 'IPv6')
|
|
56
|
+
converted.scopeid = address.scopeid ?? 0;
|
|
57
|
+
return converted;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* An interface read by the addon as an interface of the package: its usable addresses only
|
|
61
|
+
*/
|
|
62
|
+
function interfaceOf(entry) {
|
|
63
|
+
const converted = {
|
|
64
|
+
name: entry.name,
|
|
65
|
+
index: entry.index,
|
|
66
|
+
up: entry.up,
|
|
67
|
+
running: entry.running,
|
|
68
|
+
loopback: entry.loopback,
|
|
69
|
+
addresses: entry.addresses.filter((address) => !address.tentative).map(addressOf)
|
|
70
|
+
};
|
|
71
|
+
if (entry.mac)
|
|
72
|
+
converted.mac = entry.mac;
|
|
73
|
+
return converted;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The interfaces of the machine now, and whether some address is still tentative (to be read again soon)
|
|
77
|
+
*/
|
|
78
|
+
function readInterfaces() {
|
|
79
|
+
const entries = (0, Native_js_1.nativeList)();
|
|
80
|
+
return {
|
|
81
|
+
interfaces: entries.map(interfaceOf),
|
|
82
|
+
tentative: entries.some((entry) => entry.addresses.some((address) => address.tentative))
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An address of a network interface, written as `os.networkInterfaces()` writes it
|
|
3
|
+
*/
|
|
4
|
+
export interface InterfaceAddress {
|
|
5
|
+
/**
|
|
6
|
+
* The address: `'192.168.10.2'`, `'fd00::2'`, `'fe80::1c82:430f:35a3:800a'` (an IPv6 address without its scope).
|
|
7
|
+
*/
|
|
8
|
+
address: string;
|
|
9
|
+
/**
|
|
10
|
+
* `'IPv4'` or `'IPv6'`.
|
|
11
|
+
*/
|
|
12
|
+
family: 'IPv4' | 'IPv6';
|
|
13
|
+
/**
|
|
14
|
+
* The mask of the network of the address: `'255.255.255.0'`, `'ffff:ffff:ffff:ffff::'`.
|
|
15
|
+
*/
|
|
16
|
+
netmask: string;
|
|
17
|
+
/**
|
|
18
|
+
* The address and the length of its prefix: `'192.168.10.2/24'`.
|
|
19
|
+
*/
|
|
20
|
+
cidr: string;
|
|
21
|
+
/**
|
|
22
|
+
* IPv6 only: the scope of the address, as `os.networkInterfaces()` gives it: the index of the interface for a
|
|
23
|
+
* link-local address (`fe80::…`, written `fe80::…%<scopeid>` to be connected to), `0` for a global one (Windows gives
|
|
24
|
+
* a site-local address, `fec0::…`, the number of its site).
|
|
25
|
+
*/
|
|
26
|
+
scopeid?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A network interface of the machine and its state when it was read
|
|
30
|
+
*/
|
|
31
|
+
export interface NetworkInterfaceInfo {
|
|
32
|
+
/**
|
|
33
|
+
* The name of the interface: `'eth1'`, `'en0'`, on Windows the name of its network connection (`'以太网 2'`,
|
|
34
|
+
* `'Ethernet 2'`, in the language of the system).
|
|
35
|
+
*/
|
|
36
|
+
name: string;
|
|
37
|
+
/**
|
|
38
|
+
* The index the system gives the interface. It changes when the interface is unplugged and plugged again.
|
|
39
|
+
*/
|
|
40
|
+
index: number;
|
|
41
|
+
/**
|
|
42
|
+
* The hardware address (`'52:54:00:12:34:56'`), `undefined` for an interface without one (loopback, tunnels) and on
|
|
43
|
+
* the macOS versions that hide it from the processes (macOS 27 gives `02:00:00:00:00:00` for every interface).
|
|
44
|
+
*/
|
|
45
|
+
mac?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Whether the interface is enabled (its administrative state). On Windows a disabled adapter is not listed at all.
|
|
48
|
+
*/
|
|
49
|
+
up: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Whether its link is up: a cable plugged in, an association made.
|
|
52
|
+
*/
|
|
53
|
+
running: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Whether it is the loopback interface.
|
|
56
|
+
*/
|
|
57
|
+
loopback: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Its usable addresses, an empty array when it has none. An IPv6 address still in its duplicate address detection
|
|
60
|
+
* (about one second after it is added) is not usable yet and not listed.
|
|
61
|
+
*/
|
|
62
|
+
addresses: InterfaceAddress[];
|
|
63
|
+
}
|