@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 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The base of the exceptions of the package: catch it for any of them. Its `errno` is the code of the error of the
|
|
3
|
+
* system (`'EADDRINUSE'`, `'EACCES'`…) when the system refused something, `'E_NETIF'` for an error of the package, the
|
|
4
|
+
* one of the subclass otherwise; its `name` is the name of its class.
|
|
5
|
+
*/
|
|
6
|
+
export class NetIfException extends Error {
|
|
7
|
+
/**
|
|
8
|
+
* An exception of a message.
|
|
9
|
+
* @param message The message.
|
|
10
|
+
* @param options The error that caused it (`cause`).
|
|
11
|
+
*/
|
|
12
|
+
constructor(message, options) {
|
|
13
|
+
super(message, options);
|
|
14
|
+
/**
|
|
15
|
+
* `'E_NETIF'`, the code of the error of the system, or the one of the subclass.
|
|
16
|
+
*/
|
|
17
|
+
this.errno = 'E_NETIF';
|
|
18
|
+
this.name = new.target.name;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NetIfException } from './NetIfException.js';
|
|
2
|
+
/**
|
|
3
|
+
* A listening with `strict: true` that the system cannot guarantee: the connections and the datagrams reaching the
|
|
4
|
+
* addresses of the interface through another interface would not be refused. Its message tells why (the system, the
|
|
5
|
+
* interface and its setting, the permission missing) and what to do. Its `errno` is `'E_NETIF_NOT_ENFORCED'`.
|
|
6
|
+
*/
|
|
7
|
+
export class NetIfNotEnforcedException extends NetIfException {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(...arguments);
|
|
10
|
+
/**
|
|
11
|
+
* `'E_NETIF_NOT_ENFORCED'`.
|
|
12
|
+
*/
|
|
13
|
+
this.errno = 'E_NETIF_NOT_ENFORCED';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NetIfException } from './NetIfException.js';
|
|
2
|
+
/**
|
|
3
|
+
* A network interface that cannot be found: an interface given by an index that does not exist (an index is never
|
|
4
|
+
* given again once its interface is gone), or a hardware address that several interfaces have (a bridge and its ports).
|
|
5
|
+
* Its `errno` is `'E_NETIF_NOT_FOUND'`.
|
|
6
|
+
*/
|
|
7
|
+
export class NetIfNotFoundException extends NetIfException {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(...arguments);
|
|
10
|
+
/**
|
|
11
|
+
* `'E_NETIF_NOT_FOUND'`.
|
|
12
|
+
*/
|
|
13
|
+
this.errno = 'E_NETIF_NOT_FOUND';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NetIfException } from './NetIfException.js';
|
|
2
|
+
/**
|
|
3
|
+
* An operation the system, or the way the interface is listened on, cannot do: `bindSocket()` outside Linux, the
|
|
4
|
+
* broadcasts and the multicast of a UDP socket bound to the addresses of an interface on Linux and macOS. Its `errno` is
|
|
5
|
+
* `'E_NETIF_NOT_SUPPORTED'`.
|
|
6
|
+
*/
|
|
7
|
+
export class NetIfNotSupportedException extends NetIfException {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(...arguments);
|
|
10
|
+
/**
|
|
11
|
+
* `'E_NETIF_NOT_SUPPORTED'`.
|
|
12
|
+
*/
|
|
13
|
+
this.errno = 'E_NETIF_NOT_SUPPORTED';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { NetIfException } from './NetIfException.js';
|
|
2
|
+
/**
|
|
3
|
+
* A wait that ended before what it waited for happened: `waitForAddress()` whose interface got no address in time.
|
|
4
|
+
* Its `errno` is `'E_NETIF_TIMEOUT'`.
|
|
5
|
+
*/
|
|
6
|
+
export class NetIfTimeoutException extends NetIfException {
|
|
7
|
+
constructor() {
|
|
8
|
+
super(...arguments);
|
|
9
|
+
/**
|
|
10
|
+
* `'E_NETIF_TIMEOUT'`.
|
|
11
|
+
*/
|
|
12
|
+
this.errno = 'E_NETIF_TIMEOUT';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { NetIfException } from '../exceptions/NetIfException.js';
|
|
2
|
+
export { NetIfNotFoundException } from '../exceptions/NetIfNotFoundException.js';
|
|
3
|
+
export { NetIfTimeoutException } from '../exceptions/NetIfTimeoutException.js';
|
|
4
|
+
export { NetIfNotEnforcedException } from '../exceptions/NetIfNotEnforcedException.js';
|
|
5
|
+
export { NetIfNotSupportedException } from '../exceptions/NetIfNotSupportedException.js';
|
|
6
|
+
export { listenOn, listenOnAll, listInterfaces } from '../lib/Functions.js';
|
|
7
|
+
export { InterfaceListener } from '../lib/InterfaceListener.js';
|
|
8
|
+
export { NetworkInterface } from '../lib/NetworkInterface.js';
|
|
9
|
+
export { InterfaceUDPSocket } from '../lib/InterfaceUDPSocket.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {createRequire} from 'node:module';
|
|
2
|
+
import {fileURLToPath} from 'node:url';
|
|
3
|
+
let loaded;
|
|
4
|
+
try {
|
|
5
|
+
loaded = createRequire(import.meta.url)(fileURLToPath(new URL('../../../build/Release/netif.node', import.meta.url)));
|
|
6
|
+
} catch (error) {
|
|
7
|
+
if (error?.code === 'MODULE_NOT_FOUND') throw new Error("The addon of @lakutata/netif (build/Release/netif.node) is not built: the install script of the package did not run. Bun and pnpm run it only for the dependencies they trust (trustedDependencies in the package.json for Bun, pnpm approve-builds for pnpm): allow it, then install again or run npm rebuild @lakutata/netif.", {cause: error});
|
|
8
|
+
throw error;
|
|
9
|
+
}
|
|
10
|
+
export const addon = loaded;
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { closeSync } from 'node:fs';
|
|
3
|
+
import { NetIfNotEnforcedException } from '../exceptions/NetIfNotEnforcedException.js';
|
|
4
|
+
import { addressEnforcement, LINUX_PERMISSION } from './Enforcement.js';
|
|
5
|
+
import { OBSERVE } from './Internal.js';
|
|
6
|
+
import { nativeBindSocket } from './Native.js';
|
|
7
|
+
const RETRY_FIRST = 1000;
|
|
8
|
+
const RETRY_LAST = 30000;
|
|
9
|
+
//A port 0 shared by the sockets of the families: chosen by the first one, taken by another program for the next one
|
|
10
|
+
const PORT_ATTEMPTS = 3;
|
|
11
|
+
/**
|
|
12
|
+
* The code of the error of the system an error carries
|
|
13
|
+
*/
|
|
14
|
+
export function codeOf(error) {
|
|
15
|
+
return error?.code ?? error?.errno;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Whether an error is the one of an address gone or not usable yet: the next reading of the interfaces settles it
|
|
19
|
+
*/
|
|
20
|
+
function vanished(error) {
|
|
21
|
+
const code = codeOf(error);
|
|
22
|
+
return code === 'EADDRNOTAVAIL' || code === 'ENODEV' || code === 'ENXIO';
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The sockets of an interface kept matching it (the base of the listeners and the UDP sockets): on Linux sockets bound
|
|
26
|
+
* to the interface, one by family, made again when the interface is plugged back (a new index); elsewhere (and on Linux
|
|
27
|
+
* without the permission to bind) a socket by address of the interface, made and closed as its addresses change. The
|
|
28
|
+
* guarantee that the system refuses what comes through another interface (`enforced`), followed; with `strict` the
|
|
29
|
+
* sockets are closed while it is lost. The changes of the interface are followed one at a time.
|
|
30
|
+
*/
|
|
31
|
+
export class Attachment extends EventEmitter {
|
|
32
|
+
#requestedPort;
|
|
33
|
+
#endpoints = new Map();
|
|
34
|
+
#ownsInterface;
|
|
35
|
+
#port;
|
|
36
|
+
//Linux: the sockets bound to the interface until the process may not (then by address)
|
|
37
|
+
#bound = process.platform === 'linux';
|
|
38
|
+
#enforcement;
|
|
39
|
+
#paused = false;
|
|
40
|
+
#closed = false;
|
|
41
|
+
#stopObserving;
|
|
42
|
+
#syncing = Promise.resolve();
|
|
43
|
+
#again = false;
|
|
44
|
+
#retry;
|
|
45
|
+
#retryDelay = RETRY_FIRST;
|
|
46
|
+
#alive;
|
|
47
|
+
/**
|
|
48
|
+
* Internal: the options checked.
|
|
49
|
+
* @param networkInterface The interface.
|
|
50
|
+
* @param options The port, the families and `strict`.
|
|
51
|
+
* @param ownsInterface Whether the interface is closed with it.
|
|
52
|
+
*/
|
|
53
|
+
constructor(networkInterface, options, ownsInterface) {
|
|
54
|
+
super();
|
|
55
|
+
if (!Number.isInteger(options?.port) || options.port < 0 || options.port > 65535)
|
|
56
|
+
throw new TypeError(`${String(options?.port)} is not a port`);
|
|
57
|
+
const family = options.family ?? 'both';
|
|
58
|
+
if (!['IPv4', 'IPv6', 'both'].includes(family))
|
|
59
|
+
throw new TypeError(`${String(family)} is not a family ('IPv4', 'IPv6' or 'both')`);
|
|
60
|
+
this.networkInterface = networkInterface;
|
|
61
|
+
this.families = family === 'both' ? ['IPv4', 'IPv6'] : [family];
|
|
62
|
+
this.strict = options.strict ?? false;
|
|
63
|
+
this.#requestedPort = options.port;
|
|
64
|
+
this.#port = options.port || undefined;
|
|
65
|
+
this.#ownsInterface = ownsInterface;
|
|
66
|
+
this.#enforcement = this.#enforcementNow();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Whether the system refuses, through another interface, what is sent to the addresses of the interface: always on
|
|
70
|
+
* Linux (the sockets are bound to the interface), on Windows while every other interface receives with the strong
|
|
71
|
+
* host model and does not forward, on macOS when it is set to (`net.inet.ip.check_interface` 2, forwarding off).
|
|
72
|
+
*/
|
|
73
|
+
get enforced() {
|
|
74
|
+
return this.#enforcement.enforced;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Why what comes through another interface is not refused, `undefined` while it is.
|
|
78
|
+
*/
|
|
79
|
+
get reason() {
|
|
80
|
+
return this.#enforcement.reason;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The port, `undefined` until a port 0 was given one by the system.
|
|
84
|
+
*/
|
|
85
|
+
get port() {
|
|
86
|
+
return this.#port;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Whether the interface is listened on now (it exists, and has an address where its addresses are listened on).
|
|
90
|
+
*/
|
|
91
|
+
get listening() {
|
|
92
|
+
return this.#endpoints.size > 0;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The addresses it can be reached at now. On Linux the sockets bound to the interface also get what is sent to
|
|
96
|
+
* another address of the machine through the interface.
|
|
97
|
+
*/
|
|
98
|
+
get addresses() {
|
|
99
|
+
if (this.#port === undefined || !this.#endpoints.size)
|
|
100
|
+
return [];
|
|
101
|
+
const families = new Set([...this.#endpoints.values()].map((endpoint) => endpoint.family));
|
|
102
|
+
const addresses = this.#bound
|
|
103
|
+
? this.networkInterface.addresses.filter((address) => families.has(address.family))
|
|
104
|
+
: [...this.#endpoints.values()].map((endpoint) => endpoint.address);
|
|
105
|
+
return addresses.map((address) => {
|
|
106
|
+
const listening = { address: address.address, family: address.family, port: this.#port };
|
|
107
|
+
if (address.family === 'IPv6')
|
|
108
|
+
listening.scopeid = address.scopeid ?? 0;
|
|
109
|
+
return listening;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Whether it was closed.
|
|
114
|
+
*/
|
|
115
|
+
get closed() {
|
|
116
|
+
return this.#closed;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Whether the sockets are bound to the interface (Linux), not to its addresses
|
|
120
|
+
*/
|
|
121
|
+
get boundToInterface() {
|
|
122
|
+
return this.#bound;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The sockets now
|
|
126
|
+
*/
|
|
127
|
+
endpoints() {
|
|
128
|
+
return [...this.#endpoints.values()];
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* A socket made, before it is counted (its settings applied: the broadcasts, the multicast groups)
|
|
132
|
+
*/
|
|
133
|
+
prepare(_endpoint) {
|
|
134
|
+
//Nothing by default
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* A socket closed, no longer counted
|
|
138
|
+
*/
|
|
139
|
+
released(_endpoint) {
|
|
140
|
+
//Nothing by default
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Internal: the first binding, which the method that made it waits for.
|
|
144
|
+
* @returns It.
|
|
145
|
+
*/
|
|
146
|
+
async start() {
|
|
147
|
+
if (this.strict && !this.#enforcement.enforced) {
|
|
148
|
+
this.#finish();
|
|
149
|
+
throw new NetIfNotEnforcedException(`The interface cannot be listened on strictly: ${this.#enforcement.reason}`);
|
|
150
|
+
}
|
|
151
|
+
this.#stopObserving = this.networkInterface[OBSERVE]((event) => {
|
|
152
|
+
if (event === 'close') {
|
|
153
|
+
void this.close();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
this.#sync();
|
|
157
|
+
});
|
|
158
|
+
try {
|
|
159
|
+
await this.#enqueue(() => this.#reconcile(true));
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
this.#finish();
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
if (this.strict && !this.#enforcement.enforced) {
|
|
166
|
+
this.#finish();
|
|
167
|
+
throw new NetIfNotEnforcedException(`The interface cannot be listened on strictly: ${this.#enforcement.reason}`);
|
|
168
|
+
}
|
|
169
|
+
this.#keepAlive();
|
|
170
|
+
return this;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Stops: the sockets closed (the listener does not wait for the connections established, which are left to the
|
|
174
|
+
* server).
|
|
175
|
+
*/
|
|
176
|
+
async close() {
|
|
177
|
+
if (this.#closed)
|
|
178
|
+
return;
|
|
179
|
+
this.#finish();
|
|
180
|
+
if (this.#ownsInterface)
|
|
181
|
+
this.networkInterface.close();
|
|
182
|
+
this.#emit('close');
|
|
183
|
+
}
|
|
184
|
+
#emit(event, ...args) {
|
|
185
|
+
this.emit(event, ...args);
|
|
186
|
+
}
|
|
187
|
+
#finish() {
|
|
188
|
+
this.#closed = true;
|
|
189
|
+
this.#stopObserving?.();
|
|
190
|
+
this.#stopObserving = undefined;
|
|
191
|
+
clearTimeout(this.#retry);
|
|
192
|
+
clearInterval(this.#alive);
|
|
193
|
+
this.#retry = undefined;
|
|
194
|
+
this.#alive = undefined;
|
|
195
|
+
this.#closeAll();
|
|
196
|
+
}
|
|
197
|
+
#closeAll() {
|
|
198
|
+
const endpoints = [...this.#endpoints.values()];
|
|
199
|
+
this.#endpoints.clear();
|
|
200
|
+
for (const endpoint of endpoints) {
|
|
201
|
+
this.closeHandle(endpoint.handle);
|
|
202
|
+
this.released(endpoint);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The interfaces were read again: the sockets made or closed to match them
|
|
207
|
+
*/
|
|
208
|
+
#sync() {
|
|
209
|
+
if (this.#closed || this.#again)
|
|
210
|
+
return;
|
|
211
|
+
this.#again = true;
|
|
212
|
+
void this.#enqueue(async () => {
|
|
213
|
+
this.#again = false;
|
|
214
|
+
if (this.#closed)
|
|
215
|
+
return;
|
|
216
|
+
try {
|
|
217
|
+
await this.#reconcile(false);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
this.failed(error);
|
|
221
|
+
}
|
|
222
|
+
this.#keepAlive();
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* A reconciliation run after the ones queued: its promise (the queue goes on whatever its result)
|
|
227
|
+
*/
|
|
228
|
+
#enqueue(task) {
|
|
229
|
+
const run = this.#syncing.then(task);
|
|
230
|
+
this.#syncing = run.catch(() => undefined);
|
|
231
|
+
return run;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* While nothing is bound (the interface missing, no address, a retry pending), it keeps the process alive as a
|
|
235
|
+
* listening server does
|
|
236
|
+
*/
|
|
237
|
+
#keepAlive() {
|
|
238
|
+
if (this.#closed || this.#endpoints.size) {
|
|
239
|
+
clearInterval(this.#alive);
|
|
240
|
+
this.#alive = undefined;
|
|
241
|
+
}
|
|
242
|
+
else if (!this.#alive) {
|
|
243
|
+
this.#alive = setInterval(() => undefined, 0x7fffffff);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* An error of a binding again, or of a socket: the error event, the binding retried a little later
|
|
248
|
+
*/
|
|
249
|
+
failed(error) {
|
|
250
|
+
if (this.#closed)
|
|
251
|
+
return;
|
|
252
|
+
this.#emit('error', error);
|
|
253
|
+
if (this.#retry)
|
|
254
|
+
return;
|
|
255
|
+
this.#retry = setTimeout(() => {
|
|
256
|
+
this.#retry = undefined;
|
|
257
|
+
this.#sync();
|
|
258
|
+
}, this.#retryDelay);
|
|
259
|
+
this.#retryDelay = Math.min(this.#retryDelay * 2, RETRY_LAST);
|
|
260
|
+
}
|
|
261
|
+
#enforcementNow() {
|
|
262
|
+
if (this.#bound)
|
|
263
|
+
return { enforced: true };
|
|
264
|
+
return addressEnforcement(this.networkInterface.index ?? 0, this.families);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* The guarantee read again: its event when it changed; with strict, the sockets closed while it is lost
|
|
268
|
+
*/
|
|
269
|
+
#checkEnforcement() {
|
|
270
|
+
const now = this.#enforcementNow();
|
|
271
|
+
const changed = now.enforced !== this.#enforcement.enforced;
|
|
272
|
+
this.#enforcement = now;
|
|
273
|
+
if (!changed)
|
|
274
|
+
return;
|
|
275
|
+
this.#emit('enforced', now.enforced);
|
|
276
|
+
if (!this.strict)
|
|
277
|
+
return;
|
|
278
|
+
if (!now.enforced) {
|
|
279
|
+
this.#paused = true;
|
|
280
|
+
this.#closeAll();
|
|
281
|
+
this.#emit('error', new NetIfNotEnforcedException(`The listening stopped: ${now.reason}`));
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
this.#paused = false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
#wanted(state) {
|
|
288
|
+
if (!state || this.#paused)
|
|
289
|
+
return [];
|
|
290
|
+
if (this.#bound)
|
|
291
|
+
return this.families.map((family) => ({ key: `bound|${family}|${state.index}`, family: family }));
|
|
292
|
+
return state.addresses
|
|
293
|
+
.filter((address) => this.families.includes(address.family))
|
|
294
|
+
.map((address) => ({ key: `address|${address.family}|${address.address}|${address.scopeid ?? ''}`, family: address.family, address: address }));
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The sockets made and closed to match the interface; on the first time an error is thrown, later it is retried
|
|
298
|
+
*/
|
|
299
|
+
async #reconcile(first) {
|
|
300
|
+
this.#checkEnforcement();
|
|
301
|
+
for (let attempt = 1;; attempt++) {
|
|
302
|
+
const state = this.networkInterface.info;
|
|
303
|
+
const wanted = this.#wanted(state);
|
|
304
|
+
const keys = new Set(wanted.map((entry) => entry.key));
|
|
305
|
+
for (const [key, endpoint] of [...this.#endpoints]) {
|
|
306
|
+
if (keys.has(key))
|
|
307
|
+
continue;
|
|
308
|
+
this.closeHandle(endpoint.handle);
|
|
309
|
+
this.#endpoints.delete(key);
|
|
310
|
+
this.released(endpoint);
|
|
311
|
+
}
|
|
312
|
+
let opened = false;
|
|
313
|
+
let byAddress = false;
|
|
314
|
+
try {
|
|
315
|
+
for (const entry of wanted) {
|
|
316
|
+
if (this.#closed)
|
|
317
|
+
return;
|
|
318
|
+
if (this.#endpoints.has(entry.key))
|
|
319
|
+
continue;
|
|
320
|
+
const result = await this.#open(entry, state);
|
|
321
|
+
if (result === 'opened')
|
|
322
|
+
opened = true;
|
|
323
|
+
if (result === 'by address') {
|
|
324
|
+
byAddress = true;
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
//A port 0 the system gave the first socket, taken for the next one: all of them again on another port
|
|
331
|
+
if (codeOf(error) === 'EADDRINUSE' && !this.#requestedPort && this.#endpoints.size && attempt < PORT_ATTEMPTS && first) {
|
|
332
|
+
this.#closeAll();
|
|
333
|
+
this.#port = undefined;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
//Linux without the permission to bind to the interface: the addresses bound instead, at once
|
|
339
|
+
if (byAddress)
|
|
340
|
+
continue;
|
|
341
|
+
this.#retryDelay = RETRY_FIRST;
|
|
342
|
+
if (opened)
|
|
343
|
+
this.#emit('listening');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async #open(entry, state) {
|
|
348
|
+
let handle;
|
|
349
|
+
if (this.#bound) {
|
|
350
|
+
let bound;
|
|
351
|
+
try {
|
|
352
|
+
bound = nativeBindSocket(state.name, entry.family === 'IPv4' ? 4 : 6, this.stream, this.#port ?? 0, 511);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
const code = codeOf(error);
|
|
356
|
+
if (code === 'EPERM' || code === 'EACCES') {
|
|
357
|
+
//Linux before 5.7 without CAP_NET_RAW: by address, without the guarantee
|
|
358
|
+
if (this.strict)
|
|
359
|
+
throw new NetIfNotEnforcedException(`The process may not bind a socket to ${state.name}. ${LINUX_PERMISSION}`, { cause: error });
|
|
360
|
+
this.#bound = false;
|
|
361
|
+
this.#closeAll();
|
|
362
|
+
this.#enforcement = { enforced: false, reason: LINUX_PERMISSION };
|
|
363
|
+
this.#emit('enforced', false);
|
|
364
|
+
return 'by address';
|
|
365
|
+
}
|
|
366
|
+
//IPv6 off in the system: IPv4 only for 'both'
|
|
367
|
+
if (code === 'EAFNOSUPPORT' && this.families.length > 1)
|
|
368
|
+
return 'skipped';
|
|
369
|
+
if (vanished(error))
|
|
370
|
+
return 'skipped';
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
handle = await this.openHandle({ fd: bound.fd }, entry.family);
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
try {
|
|
378
|
+
closeSync(bound.fd);
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
//Owned by the socket already
|
|
382
|
+
}
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
const address = entry.address;
|
|
388
|
+
//An IPv6 address with its scope: the index on Windows, the name of the interface elsewhere (libuv)
|
|
389
|
+
const scope = address.family === 'IPv6' && address.scopeid ? `%${process.platform === 'win32' ? address.scopeid : state.name}` : '';
|
|
390
|
+
try {
|
|
391
|
+
handle = await this.openHandle({ host: `${address.address}${scope}`, port: this.#port ?? 0 }, entry.family);
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
if (vanished(error))
|
|
395
|
+
return 'skipped';
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (this.#closed) {
|
|
400
|
+
this.closeHandle(handle);
|
|
401
|
+
return 'skipped';
|
|
402
|
+
}
|
|
403
|
+
if (this.#port === undefined)
|
|
404
|
+
this.#port = this.portOf(handle);
|
|
405
|
+
const endpoint = { key: entry.key, family: entry.family, handle: handle, address: entry.address };
|
|
406
|
+
this.prepare(endpoint);
|
|
407
|
+
this.#endpoints.set(entry.key, endpoint);
|
|
408
|
+
return 'opened';
|
|
409
|
+
}
|
|
410
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { nativeWeakReception } from './Native.js';
|
|
2
|
+
/**
|
|
3
|
+
* Why a socket of Linux could not be bound to the interface
|
|
4
|
+
*/
|
|
5
|
+
export const LINUX_PERMISSION = 'Linux accepts the packets of an address through any interface, and binding a socket to an interface (SO_BINDTODEVICE) needs the capability CAP_NET_RAW before Linux 5.7: give it to the process (setcap cap_net_raw+ep on its executable, AmbientCapabilities=CAP_NET_RAW in its systemd unit) or run a newer kernel';
|
|
6
|
+
/**
|
|
7
|
+
* Whether the system refuses, through the other interfaces, the packets for the addresses of the interface of the index
|
|
8
|
+
* when they are listened on by address: the strong host model of Windows (every other interface receiving with it and
|
|
9
|
+
* not forwarding) and of macOS (net.inet.ip.check_interface set to 2, forwarding off); never on Linux
|
|
10
|
+
*/
|
|
11
|
+
export function addressEnforcement(index, families) {
|
|
12
|
+
if (process.platform !== 'win32' && process.platform !== 'darwin') {
|
|
13
|
+
if (process.platform === 'linux')
|
|
14
|
+
return { enforced: false, reason: LINUX_PERMISSION };
|
|
15
|
+
return { enforced: false, reason: `The interfaces of ${process.platform} are listened on by address, with no guarantee about the other interfaces` };
|
|
16
|
+
}
|
|
17
|
+
for (const family of families) {
|
|
18
|
+
const weak = nativeWeakReception(index, family === 'IPv4' ? 4 : 6);
|
|
19
|
+
if (!weak)
|
|
20
|
+
continue;
|
|
21
|
+
const version = family === 'IPv4' ? 'ipv4' : 'ipv6';
|
|
22
|
+
if (process.platform === 'win32') {
|
|
23
|
+
const setting = weak.setting === 'WeakHostReceive' ? 'weakhostreceive' : 'forwarding';
|
|
24
|
+
return {
|
|
25
|
+
enforced: false,
|
|
26
|
+
reason: `Windows accepts ${family} packets for the addresses of the interface through the interface "${weak.interface}": its ${weak.setting} is enabled. Disable it as an administrator: netsh interface ${version} set interface "${weak.interface}" ${setting}=disabled`
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (weak.setting.endsWith('forwarding')) {
|
|
30
|
+
return {
|
|
31
|
+
enforced: false,
|
|
32
|
+
reason: `macOS forwards ${family} (${weak.setting} is 1, as while a virtual machine or a container shares the network), and does not check the interface packets come through then`
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
enforced: false,
|
|
37
|
+
reason: `macOS accepts the ${family} packets of an address through any interface (${weak.setting} is not 2). Make it refuse them: sudo sysctl -w ${weak.setting}=2, with forwarding off`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return { enforced: true };
|
|
41
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { InterfaceListener } from './InterfaceListener.js';
|
|
2
|
+
import { NetworkInterface } from './NetworkInterface.js';
|
|
3
|
+
import { readInterfaces } from './Snapshot.js';
|
|
4
|
+
/**
|
|
5
|
+
* The network interfaces of the machine, the ones without an address too, with their state: their name, index,
|
|
6
|
+
* hardware address, whether they are enabled and connected, their usable addresses. More than `os.networkInterfaces()`,
|
|
7
|
+
* which lists only the interfaces with an address and no state.
|
|
8
|
+
* @returns The interfaces, in the order of the system.
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import {listInterfaces, type NetworkInterfaceInfo} from '@lakutata/netif'
|
|
12
|
+
*
|
|
13
|
+
* for (const entry of listInterfaces()) {
|
|
14
|
+
* console.log(entry.name, entry.running ? 'connected' : 'not connected', entry.addresses.map(address => address.cidr))
|
|
15
|
+
* }
|
|
16
|
+
* const connected: NetworkInterfaceInfo[] = listInterfaces().filter(entry => entry.running && !entry.loopback)
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function listInterfaces() {
|
|
20
|
+
return readInterfaces().interfaces;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Listens on an interface for a server: `new NetworkInterface(selector).listen(server, options)` in one call, the
|
|
24
|
+
* interface followed until the listener is closed.
|
|
25
|
+
* @param selector The interface: its name, `{mac}` or `{index}`.
|
|
26
|
+
* @param server A `net.Server` or a server built on one (`http`, `https`, `http2`, `tls`), which must not listen itself.
|
|
27
|
+
* @param options The port, the families, `strict` and the backlog.
|
|
28
|
+
* @returns The listener.
|
|
29
|
+
* @throws NetIfNotEnforcedException With `strict`, where the system cannot refuse the connections coming through another
|
|
30
|
+
* interface.
|
|
31
|
+
* @throws NetIfNotFoundException An index or a hardware address that cannot be found.
|
|
32
|
+
* @throws NetIfException The system refused the first listening, its code as `errno`.
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* import {createServer} from 'node:http'
|
|
36
|
+
* import {type InterfaceListener, listenOn} from '@lakutata/netif'
|
|
37
|
+
*
|
|
38
|
+
* const listener: InterfaceListener = await listenOn('eth1', createServer((request, response) => response.end('ok')), {port: 8080})
|
|
39
|
+
* console.log(listener.addresses)
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export async function listenOn(selector, server, options) {
|
|
43
|
+
const networkInterface = new NetworkInterface(selector);
|
|
44
|
+
try {
|
|
45
|
+
return await new InterfaceListener(networkInterface, server, options, true).start();
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
networkInterface.close();
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Listens on several interfaces for a server: `listenOn()` for each of them, one listener by interface, the interfaces
|
|
54
|
+
* followed until their listeners are closed. When one of them cannot be listened on, the ones listened on already are
|
|
55
|
+
* closed and the error is thrown. Several interfaces need a port: 0 lets the system choose one for the first
|
|
56
|
+
* interface, which may not exist yet. With it, the package implements `IInterfaceListening` of `@lakutata/core`: the
|
|
57
|
+
* entrypoints of lakutata listen on interfaces through it.
|
|
58
|
+
* @param selectors An interface (its name, `{mac}` or `{index}`), or several.
|
|
59
|
+
* @param server A `net.Server` or a server built on one (`http`, `https`, `http2`, `tls`), which must not listen itself.
|
|
60
|
+
* @param options The port, the families, `strict` and the backlog, the same for every interface.
|
|
61
|
+
* @returns The listeners, in the order of the interfaces.
|
|
62
|
+
* @throws TypeError No interface, or a port 0 with several of them.
|
|
63
|
+
* @throws NetIfNotEnforcedException With `strict`, where the system cannot refuse the connections coming through another
|
|
64
|
+
* interface.
|
|
65
|
+
* @throws NetIfException The system refused a first listening, its code as `errno`.
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* import {createServer} from 'node:http'
|
|
69
|
+
* import {type InterfaceListener, listenOnAll} from '@lakutata/netif'
|
|
70
|
+
*
|
|
71
|
+
* const server = createServer((request, response) => response.end('ok'))
|
|
72
|
+
* const listeners: InterfaceListener[] = await listenOnAll(['eth1', {mac: '52:54:00:12:34:56'}], server, {port: 8080})
|
|
73
|
+
* for (const listener of listeners) listener.on('error', error => console.error(error))
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export async function listenOnAll(selectors, server, options) {
|
|
77
|
+
const list = Array.isArray(selectors) ? selectors : [selectors];
|
|
78
|
+
if (!list.length)
|
|
79
|
+
throw new TypeError('listenOnAll() takes an interface at least');
|
|
80
|
+
if (list.length > 1 && options?.port === 0)
|
|
81
|
+
throw new TypeError('Several interfaces need a port: 0 lets the system choose one for the first interface, which may not exist yet');
|
|
82
|
+
const listeners = [];
|
|
83
|
+
try {
|
|
84
|
+
for (const selector of list)
|
|
85
|
+
listeners.push(await listenOn(selector, server, options));
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
await Promise.all(listeners.map((listener) => listener.close()));
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
return listeners;
|
|
92
|
+
}
|