@lakutata/netif 0.0.0-stage → 3.1.4
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,92 @@
|
|
|
1
|
+
import { type Server } from 'node:net';
|
|
2
|
+
import type { IInterfaceListener } from '@lakutata/core';
|
|
3
|
+
import { Attachment, type Target } from './Attachment.js';
|
|
4
|
+
import { type Family } from './Enforcement.js';
|
|
5
|
+
import { type NetworkInterface } from './NetworkInterface.js';
|
|
6
|
+
/**
|
|
7
|
+
* The options of `listen()`.
|
|
8
|
+
*/
|
|
9
|
+
export interface ListenOptions {
|
|
10
|
+
/**
|
|
11
|
+
* The port. 0: one the system chooses, kept when the interface is listened on again.
|
|
12
|
+
*/
|
|
13
|
+
port: number;
|
|
14
|
+
/**
|
|
15
|
+
* The families listened on: `'IPv4'`, `'IPv6'` or `'both'` (by default). With `'both'`, a system without IPv6 is
|
|
16
|
+
* listened on in IPv4.
|
|
17
|
+
*/
|
|
18
|
+
family?: 'IPv4' | 'IPv6' | 'both';
|
|
19
|
+
/**
|
|
20
|
+
* `true`: the listening is refused (a `NetIfNotEnforcedException`) where the system cannot refuse the connections
|
|
21
|
+
* reaching the addresses of the interface through another interface, and it stops while that guarantee is lost.
|
|
22
|
+
* `false` by default: the best the system allows, told by `enforced`.
|
|
23
|
+
*/
|
|
24
|
+
strict?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The length of the queue of the connections not accepted yet, 511 by default.
|
|
27
|
+
*/
|
|
28
|
+
backlog?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The events of an `InterfaceListener` and their arguments.
|
|
32
|
+
*/
|
|
33
|
+
export interface InterfaceListenerEvents {
|
|
34
|
+
/**
|
|
35
|
+
* The interface is listened on, after `listen()` and each time it is listened on again (an address added, the
|
|
36
|
+
* interface plugged back).
|
|
37
|
+
*/
|
|
38
|
+
listening: [];
|
|
39
|
+
/**
|
|
40
|
+
* An error of a listening again (the port taken meanwhile…), retried a little later; with `strict`, the guarantee
|
|
41
|
+
* lost (a `NetIfNotEnforcedException`). Without a listener of `error`, the error is thrown, as for a `net.Server`.
|
|
42
|
+
*/
|
|
43
|
+
error: [error: Error];
|
|
44
|
+
/**
|
|
45
|
+
* Whether the connections coming through another interface are refused changed: its new value.
|
|
46
|
+
*/
|
|
47
|
+
enforced: [enforced: boolean];
|
|
48
|
+
/**
|
|
49
|
+
* The listener was closed.
|
|
50
|
+
*/
|
|
51
|
+
close: [];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The listening of a server on a network interface: `NetworkInterface.listen()`. The listener accepts the connections
|
|
55
|
+
* and gives them to the server (`server.emit('connection', socket)`), which handles them as its own: the server itself
|
|
56
|
+
* does not listen (its `address()` is `null`, its `close()` does not stop the listener; use the ones of the listener).
|
|
57
|
+
* The interface is listened on again when its addresses change and when it is unplugged and plugged back; the
|
|
58
|
+
* connections established are left to the server. It implements `IInterfaceListener` of `@lakutata/core`.
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* import {createServer} from 'node:http'
|
|
62
|
+
* import {type InterfaceListener, NetworkInterface} from '@lakutata/netif'
|
|
63
|
+
*
|
|
64
|
+
* const server = createServer((request, response) => response.end('ok'))
|
|
65
|
+
* const listener: InterfaceListener = await new NetworkInterface('eth1').listen(server, {port: 8080})
|
|
66
|
+
* console.log(listener.enforced, listener.addresses)
|
|
67
|
+
* listener.on('error', error => console.error(error))
|
|
68
|
+
* await listener.close()
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export declare class InterfaceListener extends Attachment<InterfaceListenerEvents, Server> implements IInterfaceListener {
|
|
72
|
+
#private;
|
|
73
|
+
/**
|
|
74
|
+
* The server the connections are given to.
|
|
75
|
+
*/
|
|
76
|
+
readonly server: Server;
|
|
77
|
+
protected readonly stream: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Internal: made by `NetworkInterface.listen()` and `listenOn()`; `start()` listens.
|
|
80
|
+
* @param networkInterface The interface.
|
|
81
|
+
* @param server The server.
|
|
82
|
+
* @param options The options of `listen()`.
|
|
83
|
+
* @param ownsInterface Whether the interface is closed with the listener (`listenOn()`).
|
|
84
|
+
*/
|
|
85
|
+
constructor(networkInterface: NetworkInterface, server: Server, options: ListenOptions, ownsInterface?: boolean);
|
|
86
|
+
/**
|
|
87
|
+
* Listens on the target: a server of the listener, the connections it accepts given to the server of the user
|
|
88
|
+
*/
|
|
89
|
+
protected openHandle(target: Target, _family: Family): Promise<Server>;
|
|
90
|
+
protected portOf(handle: Server): number | undefined;
|
|
91
|
+
protected closeHandle(handle: Server): void;
|
|
92
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InterfaceListener = void 0;
|
|
4
|
+
const node_http_1 = require("node:http");
|
|
5
|
+
const node_net_1 = require("node:net");
|
|
6
|
+
const NetIfNotSupportedException_js_1 = require("../exceptions/NetIfNotSupportedException.js");
|
|
7
|
+
const Attachment_js_1 = require("./Attachment.js");
|
|
8
|
+
/**
|
|
9
|
+
* Bun 1.4: whether its http server is an https one (Bun's https.Server is its http.Server, which answers a connection
|
|
10
|
+
* given to it in plain HTTP even when it was made with keys: the connections of an https server cannot be given to it).
|
|
11
|
+
* An internal symbol tells: its TLS options, null for a plain server; undefined when Bun has no such symbol.
|
|
12
|
+
*/
|
|
13
|
+
function bunTLS(server) {
|
|
14
|
+
const symbol = Object.getOwnPropertySymbols(server).find((candidate) => candidate.description === 'tls');
|
|
15
|
+
if (!symbol)
|
|
16
|
+
return undefined;
|
|
17
|
+
return !!server[symbol];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The listening of a server on a network interface: `NetworkInterface.listen()`. The listener accepts the connections
|
|
21
|
+
* and gives them to the server (`server.emit('connection', socket)`), which handles them as its own: the server itself
|
|
22
|
+
* does not listen (its `address()` is `null`, its `close()` does not stop the listener; use the ones of the listener).
|
|
23
|
+
* The interface is listened on again when its addresses change and when it is unplugged and plugged back; the
|
|
24
|
+
* connections established are left to the server. It implements `IInterfaceListener` of `@lakutata/core`.
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* import {createServer} from 'node:http'
|
|
28
|
+
* import {type InterfaceListener, NetworkInterface} from '@lakutata/netif'
|
|
29
|
+
*
|
|
30
|
+
* const server = createServer((request, response) => response.end('ok'))
|
|
31
|
+
* const listener: InterfaceListener = await new NetworkInterface('eth1').listen(server, {port: 8080})
|
|
32
|
+
* console.log(listener.enforced, listener.addresses)
|
|
33
|
+
* listener.on('error', error => console.error(error))
|
|
34
|
+
* await listener.close()
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
class InterfaceListener extends Attachment_js_1.Attachment {
|
|
38
|
+
#backlog;
|
|
39
|
+
/**
|
|
40
|
+
* Internal: made by `NetworkInterface.listen()` and `listenOn()`; `start()` listens.
|
|
41
|
+
* @param networkInterface The interface.
|
|
42
|
+
* @param server The server.
|
|
43
|
+
* @param options The options of `listen()`.
|
|
44
|
+
* @param ownsInterface Whether the interface is closed with the listener (`listenOn()`).
|
|
45
|
+
*/
|
|
46
|
+
constructor(networkInterface, server, options, ownsInterface = false) {
|
|
47
|
+
if (!server || typeof server.emit !== 'function')
|
|
48
|
+
throw new TypeError('listen() takes a net.Server (http, https, http2, tls…)');
|
|
49
|
+
if (process.versions.bun && server instanceof node_http_1.Server && bunTLS(server) !== false) {
|
|
50
|
+
throw new NetIfNotSupportedException_js_1.NetIfNotSupportedException(bunTLS(server)
|
|
51
|
+
? 'The https server of Bun answers the connections given to it without TLS: in Bun, listen with http2.createSecureServer({key, cert, allowHTTP1: true}) or a tls.Server instead'
|
|
52
|
+
: 'Whether this server of Bun uses TLS cannot be told, and the https server of Bun answers the connections given to it without TLS: in Bun, listen with an http server of a known version, http2.createSecureServer() or a tls.Server');
|
|
53
|
+
}
|
|
54
|
+
super(networkInterface, options, ownsInterface);
|
|
55
|
+
this.stream = true;
|
|
56
|
+
this.server = server;
|
|
57
|
+
this.#backlog = options.backlog ?? 511;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Listens on the target: a server of the listener, the connections it accepts given to the server of the user
|
|
61
|
+
*/
|
|
62
|
+
openHandle(target, _family) {
|
|
63
|
+
const settings = this.server;
|
|
64
|
+
const inner = (0, node_net_1.createServer)({ allowHalfOpen: !!settings.allowHalfOpen, pauseOnConnect: !!settings.pauseOnConnect, noDelay: true }, (socket) => {
|
|
65
|
+
this.server.emit('connection', socket);
|
|
66
|
+
});
|
|
67
|
+
if (typeof this.server.maxConnections === 'number')
|
|
68
|
+
inner.maxConnections = this.server.maxConnections;
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const failed = (error) => {
|
|
71
|
+
inner.off('listening', listened);
|
|
72
|
+
inner.close();
|
|
73
|
+
reject(error);
|
|
74
|
+
};
|
|
75
|
+
const listened = () => {
|
|
76
|
+
inner.off('error', failed);
|
|
77
|
+
//Later errors of the socket: the ones of the listener
|
|
78
|
+
inner.on('error', (error) => this.failed(error));
|
|
79
|
+
resolve(inner);
|
|
80
|
+
};
|
|
81
|
+
inner.once('error', failed);
|
|
82
|
+
inner.once('listening', listened);
|
|
83
|
+
if ('fd' in target) {
|
|
84
|
+
inner.listen({ fd: target.fd, backlog: this.#backlog });
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
inner.listen({ host: target.host, port: target.port, backlog: this.#backlog, exclusive: true });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
portOf(handle) {
|
|
92
|
+
const address = handle.address();
|
|
93
|
+
return address && typeof address === 'object' ? address.port : undefined;
|
|
94
|
+
}
|
|
95
|
+
closeHandle(handle) {
|
|
96
|
+
handle.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
exports.InterfaceListener = InterfaceListener;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { type RemoteInfo, type Socket } from 'node:dgram';
|
|
2
|
+
import { Attachment, type Endpoint, type Target } from './Attachment.js';
|
|
3
|
+
import { type Family } from './Enforcement.js';
|
|
4
|
+
import { type NetworkInterface } from './NetworkInterface.js';
|
|
5
|
+
/**
|
|
6
|
+
* The options of `createUDPSocket()`.
|
|
7
|
+
*/
|
|
8
|
+
export interface UDPSocketOptions {
|
|
9
|
+
/**
|
|
10
|
+
* The port. 0: one the system chooses, kept when the socket is bound again.
|
|
11
|
+
*/
|
|
12
|
+
port: number;
|
|
13
|
+
/**
|
|
14
|
+
* The families: `'IPv4'`, `'IPv6'` or `'both'` (by default, a socket of each family inside). With `'both'`, a system
|
|
15
|
+
* without IPv6 is bound in IPv4.
|
|
16
|
+
*/
|
|
17
|
+
family?: 'IPv4' | 'IPv6' | 'both';
|
|
18
|
+
/**
|
|
19
|
+
* `true`: refused (a `NetIfNotEnforcedException`) where the system cannot refuse the datagrams reaching the addresses
|
|
20
|
+
* of the interface through another interface, and stopped while that guarantee is lost. `false` by default.
|
|
21
|
+
*/
|
|
22
|
+
strict?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The events of an `InterfaceUDPSocket` and their arguments.
|
|
26
|
+
*/
|
|
27
|
+
export interface InterfaceUDPSocketEvents {
|
|
28
|
+
/**
|
|
29
|
+
* A datagram received: its content, and where it came from (as `dgram` gives it).
|
|
30
|
+
*/
|
|
31
|
+
message: [message: Buffer, remote: RemoteInfo];
|
|
32
|
+
/**
|
|
33
|
+
* The interface is bound, after `createUDPSocket()` and each time it is bound again (an address added, the interface
|
|
34
|
+
* plugged back).
|
|
35
|
+
*/
|
|
36
|
+
listening: [];
|
|
37
|
+
/**
|
|
38
|
+
* An error of a binding again, retried a little later, or of a socket; with `strict`, the guarantee lost (a
|
|
39
|
+
* `NetIfNotEnforcedException`). Without a listener of `error`, the error is thrown, as for a `dgram` socket.
|
|
40
|
+
*/
|
|
41
|
+
error: [error: Error];
|
|
42
|
+
/**
|
|
43
|
+
* Whether the datagrams coming through another interface are refused changed: its new value.
|
|
44
|
+
*/
|
|
45
|
+
enforced: [enforced: boolean];
|
|
46
|
+
/**
|
|
47
|
+
* The socket was closed.
|
|
48
|
+
*/
|
|
49
|
+
close: [];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A UDP socket of a network interface: `NetworkInterface.createUDPSocket()`. Its methods and events are the ones of a
|
|
53
|
+
* `dgram` socket. On Linux its sockets (one by family) are bound to the interface: they receive what comes through it,
|
|
54
|
+
* its broadcasts and the multicast of the groups joined there, whatever its addresses, and send through it. On Windows
|
|
55
|
+
* and macOS a socket is bound to each address of the interface, again when they change. The datagrams that arrive while
|
|
56
|
+
* the socket is bound again are lost (UDP never guarantees their delivery).
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* import {type InterfaceUDPSocket, NetworkInterface} from '@lakutata/netif'
|
|
60
|
+
*
|
|
61
|
+
* const lan: NetworkInterface = new NetworkInterface('eth1')
|
|
62
|
+
* const socket: InterfaceUDPSocket = await lan.createUDPSocket({port: 5353})
|
|
63
|
+
* socket.addMembership('224.0.0.251')
|
|
64
|
+
* socket.on('message', (message, remote) => console.log(`${remote.address}:${remote.port}`, message.length))
|
|
65
|
+
* socket.send('hello', 5353, '224.0.0.251')
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare class InterfaceUDPSocket extends Attachment<InterfaceUDPSocketEvents, Socket> {
|
|
69
|
+
#private;
|
|
70
|
+
protected readonly stream: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Internal: made by `NetworkInterface.createUDPSocket()`; `start()` binds it.
|
|
73
|
+
* @param networkInterface The interface.
|
|
74
|
+
* @param options The options of `createUDPSocket()`.
|
|
75
|
+
* @param ownsInterface Whether the interface is closed with the socket.
|
|
76
|
+
*/
|
|
77
|
+
constructor(networkInterface: NetworkInterface, options: UDPSocketOptions, ownsInterface?: boolean);
|
|
78
|
+
protected openHandle(target: Target, family: Family): Promise<Socket>;
|
|
79
|
+
protected portOf(handle: Socket): number | undefined;
|
|
80
|
+
protected closeHandle(handle: Socket): void;
|
|
81
|
+
/**
|
|
82
|
+
* A socket made: the broadcasts and the groups of the socket applied to it
|
|
83
|
+
*/
|
|
84
|
+
protected prepare(endpoint: Endpoint<Socket>): void;
|
|
85
|
+
/**
|
|
86
|
+
* A socket closed: the groups it joined for its family joined by another one
|
|
87
|
+
*/
|
|
88
|
+
protected released(endpoint: Endpoint<Socket>): void;
|
|
89
|
+
/**
|
|
90
|
+
* Sends a datagram through the interface, from the socket of the family of the address (from the address of the
|
|
91
|
+
* interface in the network of the destination, where its addresses are bound).
|
|
92
|
+
* @param message The content.
|
|
93
|
+
* @param port The port of the destination.
|
|
94
|
+
* @param address The address of the destination (a broadcast address with `setBroadcast(true)`, a multicast group).
|
|
95
|
+
* @param callback Called once it is sent, with the error if it could not be; without it, the error is an `error`
|
|
96
|
+
* event.
|
|
97
|
+
*/
|
|
98
|
+
send(message: string | Uint8Array, port: number, address: string, callback?: (error: Error | null, bytes?: number) => void): void;
|
|
99
|
+
/**
|
|
100
|
+
* Whether the datagrams may be sent to broadcast addresses (`SO_BROADCAST`), kept when the socket is bound again.
|
|
101
|
+
* Where the addresses are bound (macOS, and Linux without the permission to bind to the interface) the broadcasts can
|
|
102
|
+
* be sent, not received.
|
|
103
|
+
* @param flag Whether they may.
|
|
104
|
+
*/
|
|
105
|
+
setBroadcast(flag: boolean): void;
|
|
106
|
+
/**
|
|
107
|
+
* Joins a multicast group on the interface, kept when the socket is bound again: its datagrams are received.
|
|
108
|
+
* @param group The address of the group: `'224.0.0.251'`, `'ff02::fb'`.
|
|
109
|
+
* @throws NetIfNotSupportedException Where the addresses are bound on macOS and Linux: a socket bound to an address
|
|
110
|
+
* receives no multicast.
|
|
111
|
+
* @throws NetIfException The system refused, its code as `errno`.
|
|
112
|
+
*/
|
|
113
|
+
addMembership(group: string): void;
|
|
114
|
+
/**
|
|
115
|
+
* Leaves a multicast group joined.
|
|
116
|
+
* @param group The address of the group.
|
|
117
|
+
*/
|
|
118
|
+
dropMembership(group: string): void;
|
|
119
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InterfaceUDPSocket = void 0;
|
|
4
|
+
const node_dgram_1 = require("node:dgram");
|
|
5
|
+
const node_net_1 = require("node:net");
|
|
6
|
+
const NetIfException_js_1 = require("../exceptions/NetIfException.js");
|
|
7
|
+
const NetIfNotSupportedException_js_1 = require("../exceptions/NetIfNotSupportedException.js");
|
|
8
|
+
const Attachment_js_1 = require("./Attachment.js");
|
|
9
|
+
const Native_js_1 = require("./Native.js");
|
|
10
|
+
/**
|
|
11
|
+
* Whether an IPv4 address is in the network of an address of the interface
|
|
12
|
+
*/
|
|
13
|
+
function sameNetwork(destination, address) {
|
|
14
|
+
const toNumber = (text) => text.split('.').reduce((value, part) => value * 256 + Number(part), 0);
|
|
15
|
+
const mask = toNumber(address.netmask);
|
|
16
|
+
return (toNumber(destination) & mask) >>> 0 === (toNumber(address.address) & mask) >>> 0 || destination === '255.255.255.255';
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A UDP socket of a network interface: `NetworkInterface.createUDPSocket()`. Its methods and events are the ones of a
|
|
20
|
+
* `dgram` socket. On Linux its sockets (one by family) are bound to the interface: they receive what comes through it,
|
|
21
|
+
* its broadcasts and the multicast of the groups joined there, whatever its addresses, and send through it. On Windows
|
|
22
|
+
* and macOS a socket is bound to each address of the interface, again when they change. The datagrams that arrive while
|
|
23
|
+
* the socket is bound again are lost (UDP never guarantees their delivery).
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* import {type InterfaceUDPSocket, NetworkInterface} from '@lakutata/netif'
|
|
27
|
+
*
|
|
28
|
+
* const lan: NetworkInterface = new NetworkInterface('eth1')
|
|
29
|
+
* const socket: InterfaceUDPSocket = await lan.createUDPSocket({port: 5353})
|
|
30
|
+
* socket.addMembership('224.0.0.251')
|
|
31
|
+
* socket.on('message', (message, remote) => console.log(`${remote.address}:${remote.port}`, message.length))
|
|
32
|
+
* socket.send('hello', 5353, '224.0.0.251')
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
class InterfaceUDPSocket extends Attachment_js_1.Attachment {
|
|
36
|
+
#broadcast;
|
|
37
|
+
#groups;
|
|
38
|
+
#descriptors;
|
|
39
|
+
//By address: the socket of each family that joined the groups (on every socket, a datagram of a group would be
|
|
40
|
+
//received once by each of them)
|
|
41
|
+
#members;
|
|
42
|
+
/**
|
|
43
|
+
* Internal: made by `NetworkInterface.createUDPSocket()`; `start()` binds it.
|
|
44
|
+
* @param networkInterface The interface.
|
|
45
|
+
* @param options The options of `createUDPSocket()`.
|
|
46
|
+
* @param ownsInterface Whether the interface is closed with the socket.
|
|
47
|
+
*/
|
|
48
|
+
constructor(networkInterface, options, ownsInterface = false) {
|
|
49
|
+
super(networkInterface, options, ownsInterface);
|
|
50
|
+
this.stream = false;
|
|
51
|
+
this.#broadcast = false;
|
|
52
|
+
this.#groups = new Set();
|
|
53
|
+
this.#descriptors = new WeakMap();
|
|
54
|
+
//By address: the socket of each family that joined the groups (on every socket, a datagram of a group would be
|
|
55
|
+
//received once by each of them)
|
|
56
|
+
this.#members = new Map();
|
|
57
|
+
}
|
|
58
|
+
openHandle(target, family) {
|
|
59
|
+
const socket = (0, node_dgram_1.createSocket)({ type: family === 'IPv4' ? 'udp4' : 'udp6' });
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const failed = (error) => {
|
|
62
|
+
try {
|
|
63
|
+
socket.close();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
//Not bound
|
|
67
|
+
}
|
|
68
|
+
reject(error);
|
|
69
|
+
};
|
|
70
|
+
const bound = () => {
|
|
71
|
+
socket.off('error', failed);
|
|
72
|
+
socket.on('error', (error) => this.failed(error));
|
|
73
|
+
socket.on('message', (message, remote) => {
|
|
74
|
+
this.emit('message', message, remote);
|
|
75
|
+
});
|
|
76
|
+
resolve(socket);
|
|
77
|
+
};
|
|
78
|
+
socket.once('error', failed);
|
|
79
|
+
if ('fd' in target) {
|
|
80
|
+
this.#descriptors.set(socket, target.fd);
|
|
81
|
+
socket.bind({ fd: target.fd }, bound);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
socket.bind({ address: target.host, port: target.port, exclusive: true }, bound);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
portOf(handle) {
|
|
89
|
+
return handle.address().port;
|
|
90
|
+
}
|
|
91
|
+
closeHandle(handle) {
|
|
92
|
+
try {
|
|
93
|
+
handle.close();
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
//Closed already
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* A socket made: the broadcasts and the groups of the socket applied to it
|
|
101
|
+
*/
|
|
102
|
+
prepare(endpoint) {
|
|
103
|
+
if (endpoint.family === 'IPv4' && this.#broadcast)
|
|
104
|
+
endpoint.handle.setBroadcast(true);
|
|
105
|
+
this.#join(endpoint);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* A socket closed: the groups it joined for its family joined by another one
|
|
109
|
+
*/
|
|
110
|
+
released(endpoint) {
|
|
111
|
+
if (this.#members.get(endpoint.family) !== endpoint)
|
|
112
|
+
return;
|
|
113
|
+
this.#members.delete(endpoint.family);
|
|
114
|
+
const next = this.endpoints().find((candidate) => candidate.family === endpoint.family);
|
|
115
|
+
if (next)
|
|
116
|
+
this.#join(next);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The groups of the family of a socket joined on it, unless another socket of the family has them already
|
|
120
|
+
*/
|
|
121
|
+
#join(endpoint) {
|
|
122
|
+
if (this.#members.has(endpoint.family))
|
|
123
|
+
return;
|
|
124
|
+
this.#members.set(endpoint.family, endpoint);
|
|
125
|
+
for (const group of this.#groups) {
|
|
126
|
+
if (((0, node_net_1.isIP)(group) === 4 ? 'IPv4' : 'IPv6') !== endpoint.family)
|
|
127
|
+
continue;
|
|
128
|
+
try {
|
|
129
|
+
this.#membership(endpoint, group, true);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
this.failed(error);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
#membership(endpoint, group, join) {
|
|
137
|
+
if (this.boundToInterface) {
|
|
138
|
+
(0, Native_js_1.nativeMembership)(this.#descriptors.get(endpoint.handle), group, this.networkInterface.index ?? 0, join);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
//Windows: the group on the interface of the address of the socket (IPv6: by the index of the interface)
|
|
142
|
+
const via = endpoint.family === 'IPv4' ? endpoint.address.address : `::%${this.networkInterface.index ?? 0}`;
|
|
143
|
+
if (join) {
|
|
144
|
+
endpoint.handle.addMembership(group, via);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
endpoint.handle.dropMembership(group, via);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Sends a datagram through the interface, from the socket of the family of the address (from the address of the
|
|
152
|
+
* interface in the network of the destination, where its addresses are bound).
|
|
153
|
+
* @param message The content.
|
|
154
|
+
* @param port The port of the destination.
|
|
155
|
+
* @param address The address of the destination (a broadcast address with `setBroadcast(true)`, a multicast group).
|
|
156
|
+
* @param callback Called once it is sent, with the error if it could not be; without it, the error is an `error`
|
|
157
|
+
* event.
|
|
158
|
+
*/
|
|
159
|
+
send(message, port, address, callback) {
|
|
160
|
+
const family = (0, node_net_1.isIP)(address.split('%')[0]) === 6 ? 'IPv6' : 'IPv4';
|
|
161
|
+
const endpoints = this.endpoints().filter((endpoint) => endpoint.family === family);
|
|
162
|
+
let chosen = endpoints[0];
|
|
163
|
+
if (!this.boundToInterface && endpoints.length > 1) {
|
|
164
|
+
if (family === 'IPv4') {
|
|
165
|
+
chosen = endpoints.find((endpoint) => sameNetwork(address, endpoint.address)) ?? chosen;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const linkLocal = address.toLowerCase().startsWith('fe80:');
|
|
169
|
+
chosen = endpoints.find((endpoint) => endpoint.address.address.startsWith('fe80:') === linkLocal) ?? chosen;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!chosen) {
|
|
173
|
+
const error = new NetIfException_js_1.NetIfException(`No ${family} socket is bound to the interface now: it does not exist${this.boundToInterface ? '' : ` or has no ${family} address`}`);
|
|
174
|
+
if (callback) {
|
|
175
|
+
queueMicrotask(() => callback(error));
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
queueMicrotask(() => {
|
|
179
|
+
this.emit('error', error);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
chosen.handle.send(message, port, address, (error, bytes) => {
|
|
185
|
+
if (callback) {
|
|
186
|
+
callback(error, bytes);
|
|
187
|
+
}
|
|
188
|
+
else if (error) {
|
|
189
|
+
this.emit('error', error);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Whether the datagrams may be sent to broadcast addresses (`SO_BROADCAST`), kept when the socket is bound again.
|
|
195
|
+
* Where the addresses are bound (macOS, and Linux without the permission to bind to the interface) the broadcasts can
|
|
196
|
+
* be sent, not received.
|
|
197
|
+
* @param flag Whether they may.
|
|
198
|
+
*/
|
|
199
|
+
setBroadcast(flag) {
|
|
200
|
+
this.#broadcast = flag;
|
|
201
|
+
for (const endpoint of this.endpoints()) {
|
|
202
|
+
if (endpoint.family === 'IPv4')
|
|
203
|
+
endpoint.handle.setBroadcast(flag);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Joins a multicast group on the interface, kept when the socket is bound again: its datagrams are received.
|
|
208
|
+
* @param group The address of the group: `'224.0.0.251'`, `'ff02::fb'`.
|
|
209
|
+
* @throws NetIfNotSupportedException Where the addresses are bound on macOS and Linux: a socket bound to an address
|
|
210
|
+
* receives no multicast.
|
|
211
|
+
* @throws NetIfException The system refused, its code as `errno`.
|
|
212
|
+
*/
|
|
213
|
+
addMembership(group) {
|
|
214
|
+
if (!(0, node_net_1.isIP)(group))
|
|
215
|
+
throw new TypeError(`${group} is not the address of a multicast group`);
|
|
216
|
+
if (!this.boundToInterface && process.platform !== 'win32') {
|
|
217
|
+
throw new NetIfNotSupportedException_js_1.NetIfNotSupportedException(`A UDP socket bound to the addresses of an interface receives no multicast on ${process.platform === 'darwin' ? 'macOS' : process.platform}: only Linux binds a socket to the interface itself`);
|
|
218
|
+
}
|
|
219
|
+
if (this.#groups.has(group))
|
|
220
|
+
return;
|
|
221
|
+
this.#groups.add(group);
|
|
222
|
+
const family = (0, node_net_1.isIP)(group) === 4 ? 'IPv4' : 'IPv6';
|
|
223
|
+
const member = this.#members.get(family);
|
|
224
|
+
if (member)
|
|
225
|
+
this.#membership(member, group, true);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Leaves a multicast group joined.
|
|
229
|
+
* @param group The address of the group.
|
|
230
|
+
*/
|
|
231
|
+
dropMembership(group) {
|
|
232
|
+
if (!this.#groups.delete(group))
|
|
233
|
+
return;
|
|
234
|
+
const member = this.#members.get((0, node_net_1.isIP)(group) === 4 ? 'IPv4' : 'IPv6');
|
|
235
|
+
if (!member)
|
|
236
|
+
return;
|
|
237
|
+
try {
|
|
238
|
+
this.#membership(member, group, false);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
//Left already (the socket made again since)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
exports.InterfaceUDPSocket = InterfaceUDPSocket;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The key of the internal method of a NetworkInterface its listeners follow it by: the observer given is called with
|
|
3
|
+
* 'update' after each reading of the interfaces, with 'close' when the NetworkInterface is closed; the function returned
|
|
4
|
+
* removes it
|
|
5
|
+
*/
|
|
6
|
+
export declare const OBSERVE: unique symbol;
|
|
7
|
+
/**
|
|
8
|
+
* An observer of a NetworkInterface
|
|
9
|
+
*/
|
|
10
|
+
export type Observer = (event: 'update' | 'close') => void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OBSERVE = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The key of the internal method of a NetworkInterface its listeners follow it by: the observer given is called with
|
|
6
|
+
* 'update' after each reading of the interfaces, with 'close' when the NetworkInterface is closed; the function returned
|
|
7
|
+
* removes it
|
|
8
|
+
*/
|
|
9
|
+
exports.OBSERVE = Symbol('lakutata.netif.observe');
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An address of an interface as the addon reads it: `tentative` while an IPv6 address is in its duplicate address
|
|
3
|
+
* detection (not usable yet)
|
|
4
|
+
*/
|
|
5
|
+
export interface NativeAddress {
|
|
6
|
+
address: string;
|
|
7
|
+
family: 'IPv4' | 'IPv6';
|
|
8
|
+
prefixLength: number;
|
|
9
|
+
tentative: boolean;
|
|
10
|
+
scopeid?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* An interface as the addon reads it
|
|
14
|
+
*/
|
|
15
|
+
export interface NativeInterface {
|
|
16
|
+
name: string;
|
|
17
|
+
index: number;
|
|
18
|
+
mac?: string;
|
|
19
|
+
up: boolean;
|
|
20
|
+
running: boolean;
|
|
21
|
+
loopback: boolean;
|
|
22
|
+
addresses: NativeAddress[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The interfaces of the machine as the addon reads them
|
|
26
|
+
*/
|
|
27
|
+
export declare function nativeList(): NativeInterface[];
|
|
28
|
+
/**
|
|
29
|
+
* Starts the notifications of the changes of the interfaces: changed() called at each one (once by thread)
|
|
30
|
+
*/
|
|
31
|
+
export declare function nativeWatch(changed: () => void): void;
|
|
32
|
+
/**
|
|
33
|
+
* Stops the notifications
|
|
34
|
+
*/
|
|
35
|
+
export declare function nativeUnwatch(): void;
|
|
36
|
+
/**
|
|
37
|
+
* A socket bound to the interface of the name (Linux): its descriptor and port
|
|
38
|
+
*/
|
|
39
|
+
export declare function nativeBindSocket(name: string, family: 4 | 6, stream: boolean, port: number, backlog: number): {
|
|
40
|
+
fd: number;
|
|
41
|
+
port: number;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* The socket of the descriptor joins or leaves the multicast group on the interface of the index (Linux)
|
|
45
|
+
*/
|
|
46
|
+
export declare function nativeMembership(fd: number, group: string, index: number, join: boolean): void;
|
|
47
|
+
/**
|
|
48
|
+
* The setting that makes the system accept, through another interface, the packets for the addresses of the interface of
|
|
49
|
+
* the index; null when it refuses them (the strong host model of Windows and macOS)
|
|
50
|
+
*/
|
|
51
|
+
export declare function nativeWeakReception(index: number, family: 4 | 6): {
|
|
52
|
+
interface: string;
|
|
53
|
+
setting: string;
|
|
54
|
+
} | null;
|