@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
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023-present Lakutata
|
|
4
|
+
|
|
5
|
+
All rights reserved
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,300 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lakutata/netif
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
English | [中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
Listen on a network interface instead of an address. `listen(port, host)` of Node.js listens on an address: the address
|
|
6
|
+
of a management port changes (DHCP) or is not there yet when the machine starts, and listening on `0.0.0.0` exposes the
|
|
7
|
+
service on every interface. With this package a server listens on "eth1", whatever its addresses: it is listened on
|
|
8
|
+
again when they change and when the interface is unplugged and plugged back, and on Linux the system refuses the
|
|
9
|
+
connections that come through another interface. Any server of Node.js works (`http`, `https`, `http2`, `tls`, Express,
|
|
10
|
+
Fastify), and UDP sockets too. The package also lists the interfaces of the machine with their state and tells when they
|
|
11
|
+
change. It is a native addon for Linux, macOS and Windows, compiled when the package is installed. It belongs to the
|
|
12
|
+
[lakutata](https://github.com/lakutata/lakutata) family but needs no lakutata application: import it and use it.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```shell
|
|
17
|
+
npm install @lakutata/netif
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- The addon is compiled during the installation (node-gyp), so the machine needs Python 3 and a C++17 compiler: the
|
|
21
|
+
Command Line Tools of Xcode on macOS, `build-essential` on Debian and Ubuntu, the Build Tools of Visual Studio
|
|
22
|
+
("Desktop development with C++") on Windows. Alpine Linux also needs the headers of the kernel:
|
|
23
|
+
`apk add python3 make g++ linux-headers`. If compiling fails, the installation fails.
|
|
24
|
+
- The package manager has to run the install script of the package. npm runs it, and may warn that the script is
|
|
25
|
+
"not yet covered by allowScripts": `npm install-scripts approve @lakutata/netif` records that you allow it. Bun and
|
|
26
|
+
pnpm run it only for the dependencies they are told to trust (`trustedDependencies` in the package.json for Bun,
|
|
27
|
+
`pnpm approve-builds` for pnpm); without it, loading the package throws an error telling how to build the addon.
|
|
28
|
+
- Node.js `>=18.10.0`. The addon uses Node-API, so Bun and the main process of Electron load the addon
|
|
29
|
+
node-gyp built for Node.js, without a rebuild.
|
|
30
|
+
- `@lakutata/core` is a peer dependency, which the package managers install with it: the types of the package refer to
|
|
31
|
+
the contract of the listening on network interfaces that core defines. It is not loaded when the package runs.
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
### Getting started
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import {createServer} from 'node:http'
|
|
39
|
+
import express from 'express'
|
|
40
|
+
import {type InterfaceListener, listenOn} from '@lakutata/netif'
|
|
41
|
+
|
|
42
|
+
const app = express()
|
|
43
|
+
app.get('/', (request, response) => response.send('the management interface'))
|
|
44
|
+
|
|
45
|
+
const listener: InterfaceListener = await listenOn('eth1', createServer(app), {port: 8080})
|
|
46
|
+
listener.on('error', error => console.error(error))
|
|
47
|
+
console.log(listener.enforced, listener.addresses) //true [{address: '192.168.10.2', family: 'IPv4', port: 8080}, …]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The interface may not exist yet (a USB adapter not plugged in) or have no address yet (DHCP not done): `listenOn()`
|
|
51
|
+
returns at once and the interface is listened on as soon as it can be. While it waits, the listener keeps the process
|
|
52
|
+
alive, as a listening server does.
|
|
53
|
+
|
|
54
|
+
The entrypoints of lakutata (Express, Fastify, the service adapters, gRPC, MCP) do this through their `interface`
|
|
55
|
+
option: `expressHTTPEntrypoint({port: 8080, interface: 'eth1'})`. The package implements `IInterfaceListening` of
|
|
56
|
+
`@lakutata/core`, the contract they listen through, and its `InterfaceListener` is an `IInterfaceListener`.
|
|
57
|
+
|
|
58
|
+
### Choosing the interface
|
|
59
|
+
|
|
60
|
+
An interface is given by its name, its hardware address or its index:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import {NetworkInterface} from '@lakutata/netif'
|
|
64
|
+
|
|
65
|
+
const byName: NetworkInterface = new NetworkInterface('eth1')
|
|
66
|
+
const byHardwareAddress: NetworkInterface = new NetworkInterface({mac: '52:54:00:12:34:56'})
|
|
67
|
+
const byIndex: NetworkInterface = new NetworkInterface({index: 3})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- **The name**: `'eth1'`, `'enp3s0'`, `'en0'`. On Windows it is the name of the network connection (`'以太网 2'`,
|
|
71
|
+
`'Ethernet 2'`), in the language of the system.
|
|
72
|
+
- **The hardware address** stays the same whatever the system names the interface: the one to give a management port
|
|
73
|
+
by. Several interfaces with the same address (a bridge and its ports) throw a `NetIfNotFoundException`: give the name
|
|
74
|
+
then.
|
|
75
|
+
macOS 27 gives every process without a special entitlement `02:00:00:00:00:00` as the hardware address of each
|
|
76
|
+
interface, so there an interface has no `mac` and cannot be found by it.
|
|
77
|
+
- **The index** changes when the interface is unplugged and plugged again: an index no interface has throws a
|
|
78
|
+
`NetIfNotFoundException`, and the object does not follow the interface once it is unplugged.
|
|
79
|
+
|
|
80
|
+
`listenOn(selector, server, options)` is `new NetworkInterface(selector).listen(server, options)`, the interface
|
|
81
|
+
followed until the listener is closed.
|
|
82
|
+
|
|
83
|
+
### Which connections are refused
|
|
84
|
+
|
|
85
|
+
When the interface is listened on, the clients of its network reach the server at its addresses, and nothing is listened
|
|
86
|
+
on at the addresses of the other interfaces. A client of another network can still send packets for an address of the
|
|
87
|
+
interface through another interface (with a route of its own through the machine). Whether the system refuses those is
|
|
88
|
+
what `enforced` tells:
|
|
89
|
+
|
|
90
|
+
| | How the interface is listened on | `enforced` |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| Linux | Sockets bound to the interface (`SO_BINDTODEVICE`), one by family: whatever its addresses, an address added is served at once | `true`: the kernel refuses the connections that come through another interface |
|
|
93
|
+
| Windows | A socket on each address of the interface, again when they change | `true` while every other interface of the family receives with the strong host model and does not forward (the default): Windows then drops the packets of an address that come through another interface ([the strong host model](https://learn.microsoft.com/en-us/previous-versions/technet-magazine/cc137807(v=msdn.10))) |
|
|
94
|
+
| macOS | A socket on each address of the interface, again when they change | `false`: macOS accepts the packets of an address through any interface. `true` only when the system is set to refuse them (`sudo sysctl -w net.inet.ip.check_interface=2`, and `net.inet6.ip6.check_interface=2` for IPv6) and does not forward (a virtual machine or a container sharing the network turns forwarding on) |
|
|
95
|
+
|
|
96
|
+
By default the interface is listened on in the best way the system allows: it is never less than listening on its
|
|
97
|
+
addresses with `listen(port, host)`, and `enforced` tells whether the guarantee is there (`reason` tells why not, and
|
|
98
|
+
how to get it). With `strict: true`, `listen()` throws a `NetIfNotEnforcedException` where the guarantee is not there,
|
|
99
|
+
and the listener stops (an `error` event) while it is lost, until it is back:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import {createServer} from 'node:http'
|
|
103
|
+
import {type InterfaceListener, NetworkInterface} from '@lakutata/netif'
|
|
104
|
+
|
|
105
|
+
const management: NetworkInterface = new NetworkInterface('eth1')
|
|
106
|
+
const listener: InterfaceListener = await management.listen(createServer(), {port: 443, strict: true})
|
|
107
|
+
listener.on('enforced', enforced => console.log(enforced ? 'guaranteed again' : 'no longer guaranteed'))
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The guarantee is followed while the interface is listened on: on Windows a change of the settings of an interface is
|
|
111
|
+
notified; macOS sends no notification of its settings, which are read again when an interface changes.
|
|
112
|
+
|
|
113
|
+
On Linux, a socket bound to eth1 serves every connection that comes through eth1, whatever address of the machine it was
|
|
114
|
+
sent to; `addresses` lists the ones of eth1.
|
|
115
|
+
|
|
116
|
+
### The server and its listener
|
|
117
|
+
|
|
118
|
+
The listener accepts the connections and gives them to the server (`server.emit('connection', socket)`), which handles
|
|
119
|
+
them as its own: requests, keep-alive, upgrades, TLS. The server itself does not listen, so:
|
|
120
|
+
|
|
121
|
+
- `server.address()` is `null` and `server.listening` is `false`: use `listener.port` and `listener.addresses`.
|
|
122
|
+
- `server.close()` does not stop the listener: use `listener.close()`. Called on a server that never listened,
|
|
123
|
+
`server.close()` of Node.js gives its callback `ERR_SERVER_NOT_RUNNING` (a promisified `close()` rejects);
|
|
124
|
+
`fastify.close()` does not.
|
|
125
|
+
- A server listened on several times (two interfaces, two ports) gets the connections of each listener.
|
|
126
|
+
`listenOnAll(['eth1', 'eth2'], server, {port: 8080})` listens on several interfaces, one listener each; when one of
|
|
127
|
+
them cannot be listened on, the others are closed and the error is thrown. With several interfaces the port cannot
|
|
128
|
+
be 0.
|
|
129
|
+
|
|
130
|
+
For Express, give `createServer(app)`; for Fastify, its `fastify.server` once `await fastify.ready()` has returned:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import Fastify from 'fastify'
|
|
134
|
+
import {type InterfaceListener, NetworkInterface} from '@lakutata/netif'
|
|
135
|
+
|
|
136
|
+
const fastify = Fastify()
|
|
137
|
+
fastify.get('/', async () => 'ok')
|
|
138
|
+
await fastify.ready()
|
|
139
|
+
const listener: InterfaceListener = await new NetworkInterface('eth1').listen(fastify.server, {port: 8080})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The options of `listen()`:
|
|
143
|
+
|
|
144
|
+
| Option | |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `port` | The port. 0: one the system chooses (`listener.port`), kept when the interface is listened on again |
|
|
147
|
+
| `family` | `'IPv4'`, `'IPv6'` or `'both'` (by default). With `'both'`, a system without IPv6 is listened on in IPv4 |
|
|
148
|
+
| `strict` | `true`: refused where the system cannot refuse the connections coming through another interface. `false` by default |
|
|
149
|
+
| `backlog` | The length of the queue of the connections not accepted yet, 511 by default |
|
|
150
|
+
|
|
151
|
+
`listener.close()` stops listening and returns without waiting for the connections established, which are left to the
|
|
152
|
+
server. `NetworkInterface.close()` closes its listeners too.
|
|
153
|
+
|
|
154
|
+
### When the interface changes
|
|
155
|
+
|
|
156
|
+
| What happens | Linux (bound to the interface) | Windows, macOS |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| The interface does not exist | The listener waits; the interface is listened on when it appears | Same |
|
|
159
|
+
| It exists, without an address | Listened on: an address added is served at once | The listener waits for an address |
|
|
160
|
+
| An address is added or removed | Nothing to do | The address is listened on, or no longer. A new IPv6 address is listened on once it is usable (after its duplicate address detection, about a second) |
|
|
161
|
+
| The link goes down | The sockets stay; they serve again when it comes back | Same, while its addresses stay |
|
|
162
|
+
| It is unplugged and plugged back | Listened on again (its index changed): a `listening` event | Listened on at its addresses again |
|
|
163
|
+
| The port is taken | An `error` event, then another try 1 second later, the delay doubling up to 30 seconds | Same |
|
|
164
|
+
|
|
165
|
+
The connections established are left to the server whatever happens. The first listening throws its errors (a port
|
|
166
|
+
taken, a port below 1024 without the permission); the later ones are `error` events of the listener: **handle the
|
|
167
|
+
`error` event of the listener**, as for a `net.Server` (without a handler, the error is thrown and ends the process).
|
|
168
|
+
|
|
169
|
+
### The interfaces and their changes
|
|
170
|
+
|
|
171
|
+
`listInterfaces()` lists every interface of the machine, the ones without an address too, with its state: more than
|
|
172
|
+
`os.networkInterfaces()`, which lists only the interfaces with an address and no state. A `NetworkInterface` follows one
|
|
173
|
+
interface: its state now and an event at each change.
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import {type InterfaceAddress, listInterfaces, NetworkInterface} from '@lakutata/netif'
|
|
177
|
+
|
|
178
|
+
for (const entry of listInterfaces()) {
|
|
179
|
+
console.log(entry.name, entry.index, entry.mac, entry.running, entry.addresses.map(address => address.cidr))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const lan: NetworkInterface = new NetworkInterface('eth0')
|
|
183
|
+
lan.on('change', ({addresses, added, removed}) => console.log('now', addresses, 'added', added, 'removed', removed))
|
|
184
|
+
lan.on('up', () => console.log('cable plugged in'))
|
|
185
|
+
lan.on('down', () => console.log('cable unplugged'))
|
|
186
|
+
lan.on('added', () => console.log('interface plugged'))
|
|
187
|
+
lan.on('removed', () => console.log('interface unplugged'))
|
|
188
|
+
const address: InterfaceAddress = await lan.waitForAddress({family: 'IPv4', timeout: 60000})
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
- `present`, `name`, `index`, `mac`, `up` (enabled), `running` (its link is up), `loopback`, `addresses` and `info` (all
|
|
192
|
+
of them) give the state now; while the interface does not exist, `present` is `false` and `addresses` is empty.
|
|
193
|
+
- The addresses are the usable ones, written as `os.networkInterfaces()` writes them: `address`, `family`, `netmask`,
|
|
194
|
+
`cidr`, and for IPv6 `scopeid` (the index of the interface for a link-local address, which is written
|
|
195
|
+
`fe80::…%<scopeid>` to be connected to). An IPv6 address in its duplicate address detection counts once it is usable.
|
|
196
|
+
- The events come from the notifications of the system (netlink on Linux, the kernel events on macOS, the notifications
|
|
197
|
+
of IP Helper on Windows), not from polling, a few tens of milliseconds after the change. Every `NetworkInterface` of a
|
|
198
|
+
thread shares one watcher of the system, which keeps no event loop alive.
|
|
199
|
+
- `waitForAddress()` gives an address the interface has, or the first one it gets; with a `timeout`, a
|
|
200
|
+
`NetIfTimeoutException` when none comes.
|
|
201
|
+
- `close()` stops following the interface.
|
|
202
|
+
|
|
203
|
+
### UDP
|
|
204
|
+
|
|
205
|
+
`createUDPSocket()` gives a UDP socket of the interface, with the methods and the events of a `dgram` socket:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import {type InterfaceUDPSocket, NetworkInterface} from '@lakutata/netif'
|
|
209
|
+
|
|
210
|
+
const lan: NetworkInterface = new NetworkInterface('eth1')
|
|
211
|
+
const socket: InterfaceUDPSocket = await lan.createUDPSocket({port: 5353})
|
|
212
|
+
socket.addMembership('224.0.0.251')
|
|
213
|
+
socket.on('message', (message, remote) => {
|
|
214
|
+
socket.send(message, remote.port, remote.address)
|
|
215
|
+
})
|
|
216
|
+
socket.on('error', error => console.error(error))
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
- On Linux its sockets (one by family) are bound to the interface: they receive what comes through it, its broadcasts
|
|
220
|
+
and the multicast of the groups joined there included, whatever its addresses, and send through it. DHCP, mDNS or
|
|
221
|
+
SSDP on one interface work this way.
|
|
222
|
+
- On Windows and macOS a socket is bound to each address of the interface; `send()` sends from the one in the network of
|
|
223
|
+
the destination. On macOS a socket bound to an address receives no broadcast and no multicast: `addMembership()`
|
|
224
|
+
throws a `NetIfNotSupportedException` there; on Windows the group is joined on each address. `setBroadcast(true)`
|
|
225
|
+
lets the socket send broadcasts everywhere.
|
|
226
|
+
- The broadcasts and the groups are applied again when the socket is bound again (an address changed, the interface
|
|
227
|
+
plugged back). The datagrams that arrive meanwhile are lost.
|
|
228
|
+
- A link-local IPv6 destination needs its scope, as for `dgram`: `fe80::1%eth1`.
|
|
229
|
+
- `port`, `family` and `strict` are the ones of `listen()`; `enforced`, `reason`, `port`, `addresses`, `listening` and
|
|
230
|
+
the events `listening`, `error`, `enforced` and `close` too.
|
|
231
|
+
|
|
232
|
+
### A socket bound to the interface
|
|
233
|
+
|
|
234
|
+
On Linux, `bindSocket()` gives a socket bound to the interface, for the code that needs the server itself to listen (its
|
|
235
|
+
`address()` and `close()` as usual). The socket is not made again when the interface is plugged back: that is what
|
|
236
|
+
`listen()` does.
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
import {createServer} from 'node:http'
|
|
240
|
+
import {type BoundSocket, NetworkInterface} from '@lakutata/netif'
|
|
241
|
+
|
|
242
|
+
const management: NetworkInterface = new NetworkInterface('eth1')
|
|
243
|
+
const bound: BoundSocket = management.bindSocket({port: 8080, family: 'IPv4', type: 'tcp'})
|
|
244
|
+
createServer((request, response) => response.end('ok')).listen({fd: bound.fd})
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
A TCP socket is returned listening, for `server.listen({fd})` of a `net.Server`; a UDP socket bound, for
|
|
248
|
+
`socket.bind({fd})` of a `dgram` socket. Whatever takes the descriptor owns it; close one you do not use with
|
|
249
|
+
`fs.closeSync(fd)`. macOS and Windows have no such socket: `bindSocket()` throws a `NetIfNotSupportedException` there.
|
|
250
|
+
|
|
251
|
+
### Systems and runtimes
|
|
252
|
+
|
|
253
|
+
- **Linux**: binding a socket to an interface needs no privilege since Linux 5.7. Before it, the process needs the
|
|
254
|
+
capability `CAP_NET_RAW` (`setcap cap_net_raw+ep` on its executable, `AmbientCapabilities=CAP_NET_RAW` in its systemd
|
|
255
|
+
unit); without it the interface is listened on at its addresses, `enforced` is `false`, and `strict` throws.
|
|
256
|
+
- **Windows**: the weak host model is set by interface
|
|
257
|
+
(`netsh interface ipv4 set interface "<name>" weakhostreceive=enabled`), and some software sets it (load balancers).
|
|
258
|
+
`reason` names the interface whose setting takes the guarantee away, and the command that gives it back.
|
|
259
|
+
- **macOS**: see the table above; the listening is the one of `listen(port, host)` by default.
|
|
260
|
+
- **Bun** listens and gives the connections as Node.js does, with three differences. Its `https` server answers a
|
|
261
|
+
connection given to it without TLS, so `listen()` refuses it in Bun (`NetIfNotSupportedException`):
|
|
262
|
+
`http2.createSecureServer({key, cert, allowHTTP1: true})` and `tls.createServer()` take the connections there. Its
|
|
263
|
+
servers do not close the idle keep-alive connections given to them (`keepAliveTimeout`). Its `http` server cannot take
|
|
264
|
+
the descriptor of `bindSocket()`: give it to a `net.Server` or a `dgram` socket.
|
|
265
|
+
- **Electron**: in the main process.
|
|
266
|
+
- **Worker threads**: the package works in each thread, and a thread ends without waiting for it.
|
|
267
|
+
|
|
268
|
+
### Packaging
|
|
269
|
+
|
|
270
|
+
The bundles, the executables and the installers made by `lakutata package` carry the compiled addon
|
|
271
|
+
(`build/Release/netif.node`): build them on each target platform, after installing the package there.
|
|
272
|
+
|
|
273
|
+
## API
|
|
274
|
+
|
|
275
|
+
| Export | What it is |
|
|
276
|
+
|---|---|
|
|
277
|
+
| `listInterfaces()`, `NetworkInterfaceInfo`, `InterfaceAddress` | The interfaces of the machine and their state; an interface (`name`, `index`, `mac`, `up`, `running`, `loopback`, `addresses`); an address (`address`, `family`, `netmask`, `cidr`, `scopeid`) |
|
|
278
|
+
| `NetworkInterface`, `InterfaceSelector`, `NetworkInterfaceEvents`, `AddressChange`, `WaitForAddressOptions` | An interface followed (`list`, `selector`, `present`, `name`, `index`, `mac`, `up`, `running`, `loopback`, `addresses`, `info`, `closed`, `waitForAddress`, `listen`, `createUDPSocket`, `bindSocket`, `close`); how it is given (a name, `{mac}`, `{index}`); its events (`change`, `up`, `down`, `added`, `removed`); a change of its addresses (`addresses`, `added`, `removed`); the options of `waitForAddress()` (`family`, `timeout`) |
|
|
279
|
+
| `listenOn()`, `listenOnAll()`, `InterfaceListener`, `ListenOptions`, `InterfaceListenerEvents`, `ListeningAddress` | A server listening on an interface, or on several: the listener (`server`, `networkInterface`, `enforced`, `reason`, `port`, `listening`, `addresses`, `closed`, `close`), its options (`port`, `family`, `strict`, `backlog`), its events (`listening`, `error`, `enforced`, `close`), an address it is reached at (`address`, `family`, `port`, `scopeid`) |
|
|
280
|
+
| `InterfaceUDPSocket`, `UDPSocketOptions`, `InterfaceUDPSocketEvents` | A UDP socket of an interface (`send`, `setBroadcast`, `addMembership`, `dropMembership`, `enforced`, `reason`, `port`, `listening`, `addresses`, `closed`, `close`), its options (`port`, `family`, `strict`), its events (`message`, `listening`, `error`, `enforced`, `close`) |
|
|
281
|
+
| `BoundSocket`, `BindSocketOptions` | A socket bound to an interface (`fd`, `port`, `family`, `type`) and the options of `bindSocket()` (`port`, `family`, `type`, `backlog`) |
|
|
282
|
+
|
|
283
|
+
The typings document every declaration, with examples.
|
|
284
|
+
|
|
285
|
+
## Errors
|
|
286
|
+
|
|
287
|
+
| Error | When |
|
|
288
|
+
|---|---|
|
|
289
|
+
| `NetIfNotFoundException` | An index no interface has, a hardware address several interfaces have; `bindSocket()` while the interface does not exist |
|
|
290
|
+
| `NetIfNotEnforcedException` | With `strict`, the system cannot refuse what comes through another interface (also when the guarantee is lost: an `error` event); `bindSocket()` without the permission to bind (`CAP_NET_RAW` before Linux 5.7). Its message tells why and what to do |
|
|
291
|
+
| `NetIfNotSupportedException` | `bindSocket()` outside Linux, `addMembership()` where the addresses are bound on macOS (and on Linux without the permission), an `https` server in Bun |
|
|
292
|
+
| `NetIfTimeoutException` | `waitForAddress()` past its `timeout` |
|
|
293
|
+
| `NetIfException` | The base of all of them; a refusal of the system has its code as `errno` (`'EADDRINUSE'`, `'EACCES'`…), and `'E_NETIF'` is an error of the package itself (a `NetworkInterface` closed while `waitForAddress()` waits) |
|
|
294
|
+
|
|
295
|
+
A selector, a port or a family that is not one throws a `TypeError`.
|
|
296
|
+
|
|
297
|
+
## See also
|
|
298
|
+
|
|
299
|
+
- [socket(7)](https://man7.org/linux/man-pages/man7/socket.7.html): `SO_BINDTODEVICE` on Linux.
|
|
300
|
+
- [The strong and weak host models of Windows](https://learn.microsoft.com/en-us/previous-versions/technet-magazine/cc137807(v=msdn.10)).
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# @lakutata/netif
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
按网口监听,而不是按地址监听。Node.js 的 `listen(port, host)` 只能按地址监听:管理网口的地址会变(DHCP),开机时可能还没有地址;监听 `0.0.0.0` 又会把服务暴露在所有网口上。用这个包,服务器监听的是“eth1 这个网口”,不管它现在是什么地址:地址变了、网口拔掉再插回,都会自动重新监听;在 Linux 上,从别的网口进来的连接由系统直接挡掉。Node.js 的任何服务器都能用(`http`、`https`、`http2`、`tls`、Express、Fastify),UDP 套接字也可以。这个包还能列出本机的网口和它们的状态,并在网口变化时发出事件。它是一个原生插件,支持 Linux、macOS 和 Windows,在安装这个包时编译。它属于 [lakutata](https://github.com/lakutata/lakutata) 系列,但不需要 lakutata 应用:导入就能直接使用。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```shell
|
|
10
|
+
npm install @lakutata/netif
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- 原生插件在安装时编译(node-gyp),所以机器上需要 Python 3 和支持 C++17 的编译器:macOS 上是 Xcode 的命令行工具,Debian、Ubuntu 上是 `build-essential`,Windows 上是 Visual Studio 生成工具(“使用 C++ 的桌面开发”)。Alpine Linux 还需要内核的头文件:`apk add python3 make g++ linux-headers`。编译失败时,安装也会失败。
|
|
14
|
+
- 包管理器需要执行这个包的安装脚本。npm 会执行,但可能提示这个脚本“not yet covered by allowScripts”:用 `npm install-scripts approve @lakutata/netif` 记下你允许它执行。Bun 和 pnpm 只为信任的依赖执行,需要先允许它(Bun 在 package.json 的 `trustedDependencies` 里加上它,pnpm 用 `pnpm approve-builds`);没有允许时,加载这个包会抛出错误,说明怎样编译原生插件。
|
|
15
|
+
- Node.js `>=18.10.0`。原生插件用的是 Node-API,所以 Bun 和 Electron 的主进程可以直接加载 node-gyp 为 Node.js 编译的插件,不用重新编译。
|
|
16
|
+
- `@lakutata/core` 是 peer 依赖,包管理器会一起安装:这个包的类型引用了它定义的按网口监听的接口。这个包运行时不会加载它。
|
|
17
|
+
|
|
18
|
+
## 用法
|
|
19
|
+
|
|
20
|
+
### 快速上手
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import {createServer} from 'node:http'
|
|
24
|
+
import express from 'express'
|
|
25
|
+
import {type InterfaceListener, listenOn} from '@lakutata/netif'
|
|
26
|
+
|
|
27
|
+
const app = express()
|
|
28
|
+
app.get('/', (request, response) => response.send('the management interface'))
|
|
29
|
+
|
|
30
|
+
const listener: InterfaceListener = await listenOn('eth1', createServer(app), {port: 8080})
|
|
31
|
+
listener.on('error', error => console.error(error))
|
|
32
|
+
console.log(listener.enforced, listener.addresses) //true [{address: '192.168.10.2', family: 'IPv4', port: 8080}, …]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
网口可以暂时不存在(USB 网卡还没插上),也可以还没有地址(DHCP 还没完成):`listenOn()` 会立即返回,等网口能监听了就开始监听。等待期间,监听器会让进程保持运行,就像一个正在监听的服务器那样。
|
|
36
|
+
|
|
37
|
+
lakutata 的入口(Express、Fastify、各个服务适配器、gRPC、MCP)用 `interface` 选项替你做这件事:`expressHTTPEntrypoint({port: 8080, interface: 'eth1'})`。这个包实现了 `@lakutata/core` 的 `IInterfaceListening`,入口就是通过这个接口监听的;它的 `InterfaceListener` 就是一个 `IInterfaceListener`。
|
|
38
|
+
|
|
39
|
+
### 指定网口
|
|
40
|
+
|
|
41
|
+
网口可以按名字、MAC 地址或序号指定:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import {NetworkInterface} from '@lakutata/netif'
|
|
45
|
+
|
|
46
|
+
const byName: NetworkInterface = new NetworkInterface('eth1')
|
|
47
|
+
const byHardwareAddress: NetworkInterface = new NetworkInterface({mac: '52:54:00:12:34:56'})
|
|
48
|
+
const byIndex: NetworkInterface = new NetworkInterface({index: 3})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- **名字**:`'eth1'`、`'enp3s0'`、`'en0'`。Windows 上是网络连接的名字(`'以太网 2'`、`'Ethernet 2'`),随系统语言变化。
|
|
52
|
+
- **MAC 地址**不随系统给网口起的名字变化,适合用来指定管理网口。如果有几个网口的 MAC 相同(比如网桥和它的成员网口),会抛出 `NetIfNotFoundException`,这时请改用名字。macOS 27 对没有特殊权限的进程,把每个网口的 MAC 都报成 `02:00:00:00:00:00`,所以在这样的 macOS 上网口没有 `mac`,也不能按 MAC 找到。
|
|
53
|
+
- **序号**在网口拔掉再插回后会变:指定一个不存在的序号会抛出 `NetIfNotFoundException`;网口拔掉后,这个对象也不会再跟着它。
|
|
54
|
+
|
|
55
|
+
`listenOn(selector, server, options)` 就是 `new NetworkInterface(selector).listen(server, options)`,网口会一直跟踪到监听器关闭。
|
|
56
|
+
|
|
57
|
+
### 哪些连接会被挡住
|
|
58
|
+
|
|
59
|
+
监听网口后,这个网口所在网络的客户端通过它的地址访问服务器,其他网口的地址上不会监听任何东西。但另一个网络的客户端仍然可以把发往这个网口地址的包,从别的网口送进本机(自己加一条经过本机的路由就行)。系统会不会挡住这样的连接,就是 `enforced` 回答的问题:
|
|
60
|
+
|
|
61
|
+
| | 怎样监听 | `enforced` |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| Linux | 套接字绑定在网口上(`SO_BINDTODEVICE`),每个地址族一个,不管网口有哪些地址;新加的地址立刻就能访问 | `true`:内核挡掉从别的网口进来的连接 |
|
|
64
|
+
| Windows | 在网口的每个地址上各开一个套接字,地址变化时重新监听 | 同一地址族的其他网口都按强主机模型收包、都没开转发时(默认就是这样)为 `true`:这时 Windows 会丢掉发往某个地址、却从别的网口进来的包(见[强主机模型](https://learn.microsoft.com/en-us/previous-versions/technet-magazine/cc137807(v=msdn.10))) |
|
|
65
|
+
| macOS | 在网口的每个地址上各开一个套接字,地址变化时重新监听 | `false`:macOS 从任何网口都接受发往本机地址的包。只有在系统设置为拒绝(`sudo sysctl -w net.inet.ip.check_interface=2`,IPv6 还要 `net.inet6.ip6.check_interface=2`)并且没开转发时才为 `true`(虚拟机或容器共享网络时会打开转发) |
|
|
66
|
+
|
|
67
|
+
默认按系统允许的最好方式监听:无论如何都不会比用 `listen(port, host)` 按地址监听差,`enforced` 告诉你有没有这层保证(`reason` 说明为什么没有,以及怎样才能有)。加上 `strict: true` 后,保证不了时 `listen()` 抛出 `NetIfNotEnforcedException`;运行中保证丢了,监听器会停下来(触发 `error` 事件),等保证恢复后再继续:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import {createServer} from 'node:http'
|
|
71
|
+
import {type InterfaceListener, NetworkInterface} from '@lakutata/netif'
|
|
72
|
+
|
|
73
|
+
const management: NetworkInterface = new NetworkInterface('eth1')
|
|
74
|
+
const listener: InterfaceListener = await management.listen(createServer(), {port: 443, strict: true})
|
|
75
|
+
listener.on('enforced', enforced => console.log(enforced ? 'guaranteed again' : 'no longer guaranteed'))
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
监听期间会一直跟踪这个保证:Windows 上网口设置的变化会有通知;macOS 的这些设置没有变化通知,会在网口变化时重新读取。
|
|
79
|
+
|
|
80
|
+
在 Linux 上,绑定到 eth1 的套接字会接受所有从 eth1 进来的连接,不管连接发往本机的哪个地址;`addresses` 列出的是 eth1 自己的地址。
|
|
81
|
+
|
|
82
|
+
### 服务器和它的监听器
|
|
83
|
+
|
|
84
|
+
监听器接受连接后,交给服务器(`server.emit('connection', socket)`),服务器像处理自己的连接一样处理它们:请求、长连接、协议升级、TLS 都照常。服务器自己并没有在监听,所以:
|
|
85
|
+
|
|
86
|
+
- `server.address()` 是 `null`,`server.listening` 是 `false`:请用 `listener.port` 和 `listener.addresses`。
|
|
87
|
+
- `server.close()` 不能让监听器停下:请用 `listener.close()`。对一个从没监听过的服务器调用 Node.js 的 `server.close()`,会把 `ERR_SERVER_NOT_RUNNING` 交给它的回调(`promisify` 后是 reject);`fastify.close()` 不会。
|
|
88
|
+
- 同一个服务器可以被监听多次(两个网口、两个端口),每个监听器的连接都交给它。`listenOnAll(['eth1', 'eth2'], server, {port: 8080})` 在多个网口上监听,每个网口一个监听器;其中一个无法监听时,已经开始的都会关闭,并抛出错误。多个网口时端口不能是 0。
|
|
89
|
+
|
|
90
|
+
Express 请传 `createServer(app)`;Fastify 请在 `await fastify.ready()` 之后传 `fastify.server`:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import Fastify from 'fastify'
|
|
94
|
+
import {type InterfaceListener, NetworkInterface} from '@lakutata/netif'
|
|
95
|
+
|
|
96
|
+
const fastify = Fastify()
|
|
97
|
+
fastify.get('/', async () => 'ok')
|
|
98
|
+
await fastify.ready()
|
|
99
|
+
const listener: InterfaceListener = await new NetworkInterface('eth1').listen(fastify.server, {port: 8080})
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`listen()` 的选项:
|
|
103
|
+
|
|
104
|
+
| 选项 | |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `port` | 端口。0 表示由系统选一个(见 `listener.port`),重新监听时保持同一个端口 |
|
|
107
|
+
| `family` | `'IPv4'`、`'IPv6'` 或 `'both'`(默认)。用 `'both'` 时,系统没有 IPv6 就只监听 IPv4 |
|
|
108
|
+
| `strict` | `true`:系统挡不住从别的网口进来的连接时拒绝监听。默认 `false` |
|
|
109
|
+
| `backlog` | 还没被接受的连接的队列长度,默认 511 |
|
|
110
|
+
|
|
111
|
+
`listener.close()` 停止监听后立即返回,不等已经建立的连接结束,这些连接交给服务器处理。`NetworkInterface.close()` 也会关闭它的监听器。
|
|
112
|
+
|
|
113
|
+
### 网口变化时
|
|
114
|
+
|
|
115
|
+
| 情况 | Linux(绑定在网口上) | Windows、macOS |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| 网口不存在 | 监听器等待,网口出现时开始监听 | 同左 |
|
|
118
|
+
| 网口存在、没有地址 | 已经在监听:地址一加上就能访问 | 监听器等待地址 |
|
|
119
|
+
| 增加或删除地址 | 什么都不用做 | 开始或停止监听这个地址。新加的 IPv6 地址等它可用后才监听(重复地址检测结束后,大约一秒) |
|
|
120
|
+
| 网线拔掉 | 套接字保留,连通后继续可用 | 同左,只要地址还在 |
|
|
121
|
+
| 网口拔掉再插回 | 重新监听(序号变了),触发 `listening` 事件 | 按地址重新监听 |
|
|
122
|
+
| 端口被占用 | 触发 `error` 事件,1 秒后重试,间隔逐次加倍,最长 30 秒 | 同左 |
|
|
123
|
+
|
|
124
|
+
无论发生什么,已经建立的连接都交给服务器处理。第一次监听的错误(端口被占用、没有权限监听 1024 以下的端口)会直接抛出;之后的错误是监听器的 `error` 事件:**请给监听器加上 `error` 的监听函数**,就像对 `net.Server` 那样(没有的话,这个错误会被抛出,导致进程退出)。
|
|
125
|
+
|
|
126
|
+
### 网口和它们的变化
|
|
127
|
+
|
|
128
|
+
`listInterfaces()` 列出本机所有网口,包括没有地址的,并带上它们的状态:比 `os.networkInterfaces()` 更全,后者只列出有地址的网口,也没有状态。`NetworkInterface` 跟踪一个网口:它现在的状态,以及每次变化时的事件。
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import {type InterfaceAddress, listInterfaces, NetworkInterface} from '@lakutata/netif'
|
|
132
|
+
|
|
133
|
+
for (const entry of listInterfaces()) {
|
|
134
|
+
console.log(entry.name, entry.index, entry.mac, entry.running, entry.addresses.map(address => address.cidr))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const lan: NetworkInterface = new NetworkInterface('eth0')
|
|
138
|
+
lan.on('change', ({addresses, added, removed}) => console.log('now', addresses, 'added', added, 'removed', removed))
|
|
139
|
+
lan.on('up', () => console.log('cable plugged in'))
|
|
140
|
+
lan.on('down', () => console.log('cable unplugged'))
|
|
141
|
+
lan.on('added', () => console.log('interface plugged'))
|
|
142
|
+
lan.on('removed', () => console.log('interface unplugged'))
|
|
143
|
+
const address: InterfaceAddress = await lan.waitForAddress({family: 'IPv4', timeout: 60000})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- `present`、`name`、`index`、`mac`、`up`(是否启用)、`running`(是否连通)、`loopback`、`addresses` 和 `info`(以上全部)给出现在的状态;网口不存在时,`present` 是 `false`,`addresses` 是空数组。
|
|
147
|
+
- 地址只包括可用的,写法和 `os.networkInterfaces()` 相同:`address`、`family`、`netmask`、`cidr`,IPv6 还有 `scopeid`(链路本地地址是网口的序号,连接时写成 `fe80::…%<scopeid>`)。正在做重复地址检测的 IPv6 地址,等它可用后才算。
|
|
148
|
+
- 事件来自系统的通知(Linux 的 netlink、macOS 的内核事件、Windows 的 IP Helper 通知),不靠定时轮询,变化后几十毫秒内触发。同一个线程里的所有 `NetworkInterface` 共用一个系统通知的监听者,它不会让进程一直不退出。
|
|
149
|
+
- `waitForAddress()` 返回网口已有的地址,或者等到它的第一个地址;设了 `timeout` 时,到时还没有地址会抛出 `NetIfTimeoutException`。
|
|
150
|
+
- `close()` 停止跟踪这个网口。
|
|
151
|
+
|
|
152
|
+
### UDP
|
|
153
|
+
|
|
154
|
+
`createUDPSocket()` 返回这个网口的 UDP 套接字,方法和事件和 `dgram` 的套接字一样:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import {type InterfaceUDPSocket, NetworkInterface} from '@lakutata/netif'
|
|
158
|
+
|
|
159
|
+
const lan: NetworkInterface = new NetworkInterface('eth1')
|
|
160
|
+
const socket: InterfaceUDPSocket = await lan.createUDPSocket({port: 5353})
|
|
161
|
+
socket.addMembership('224.0.0.251')
|
|
162
|
+
socket.on('message', (message, remote) => {
|
|
163
|
+
socket.send(message, remote.port, remote.address)
|
|
164
|
+
})
|
|
165
|
+
socket.on('error', error => console.error(error))
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- 在 Linux 上,它的套接字(每个地址族一个)绑定在网口上:只接收从这个网口进来的数据,包括这个网口上的广播和已加入的组播组,不管网口有哪些地址;发出的数据也从这个网口出去。在单个网口上做 DHCP、mDNS、SSDP 就是这样。
|
|
169
|
+
- 在 Windows 和 macOS 上,网口的每个地址各绑一个套接字;`send()` 从和目标同一网段的那个地址发出。macOS 上绑定在地址上的套接字收不到广播和组播,所以在那里调用 `addMembership()` 会抛出 `NetIfNotSupportedException`;Windows 上会在每个地址上加入组播组。`setBroadcast(true)` 在哪里都能让套接字发送广播。
|
|
170
|
+
- 套接字重新绑定时(地址变了、网口插回),广播设置和组播组会重新应用。这期间到达的数据报会丢失。
|
|
171
|
+
- 发往 IPv6 链路本地地址时要带上作用域,和 `dgram` 一样:`fe80::1%eth1`。
|
|
172
|
+
- `port`、`family` 和 `strict` 与 `listen()` 的相同;`enforced`、`reason`、`port`、`addresses`、`listening`,以及事件 `listening`、`error`、`enforced` 和 `close` 也一样。
|
|
173
|
+
|
|
174
|
+
### 绑定网口的套接字
|
|
175
|
+
|
|
176
|
+
在 Linux 上,`bindSocket()` 返回一个绑定在网口上的套接字,给需要服务器自己监听的代码用(服务器的 `address()` 和 `close()` 照常工作)。网口插回后不会重新创建这个套接字:那是 `listen()` 做的事。
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import {createServer} from 'node:http'
|
|
180
|
+
import {type BoundSocket, NetworkInterface} from '@lakutata/netif'
|
|
181
|
+
|
|
182
|
+
const management: NetworkInterface = new NetworkInterface('eth1')
|
|
183
|
+
const bound: BoundSocket = management.bindSocket({port: 8080, family: 'IPv4', type: 'tcp'})
|
|
184
|
+
createServer((request, response) => response.end('ok')).listen({fd: bound.fd})
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
TCP 套接字返回时已经在监听,交给 `net.Server` 的 `server.listen({fd})`;UDP 套接字返回时已经绑定,交给 `dgram` 套接字的 `socket.bind({fd})`。拿到描述符的服务器或套接字就拥有它;不用的话请用 `fs.closeSync(fd)` 关掉。macOS 和 Windows 没有这样的套接字:在那里调用 `bindSocket()` 会抛出 `NetIfNotSupportedException`。
|
|
188
|
+
|
|
189
|
+
### 系统和运行环境
|
|
190
|
+
|
|
191
|
+
- **Linux**:从 Linux 5.7 起,把套接字绑定到网口不需要特权。更早的内核需要进程有 `CAP_NET_RAW` 能力(对可执行文件执行 `setcap cap_net_raw+ep`,或在 systemd 单元里写 `AmbientCapabilities=CAP_NET_RAW`);没有的话,就按地址监听,`enforced` 为 `false`,`strict` 会抛出异常。
|
|
192
|
+
- **Windows**:弱主机模型是按网口设置的(`netsh interface ipv4 set interface "<名字>" weakhostreceive=enabled`),有些软件会打开它(比如负载均衡软件)。`reason` 会说出是哪个网口的设置让保证失效,以及恢复它的命令。
|
|
193
|
+
- **macOS**:见上面的表格;默认就是 `listen(port, host)` 那样的按地址监听。
|
|
194
|
+
- **Bun** 的监听和连接交接和 Node.js 一样,只有三点不同。Bun 的 `https` 服务器收到交过来的连接时不做 TLS,所以在 Bun 上 `listen()` 会拒绝它(`NetIfNotSupportedException`):在 Bun 上请改用 `http2.createSecureServer({key, cert, allowHTTP1: true})` 或 `tls.createServer()`,它们都能正常接收。Bun 的服务器不会关闭交给它的空闲长连接(`keepAliveTimeout` 不起作用)。Bun 的 `http` 服务器不能直接使用 `bindSocket()` 的描述符:请交给 `net.Server` 或 `dgram` 套接字。
|
|
195
|
+
- **Electron**:在主进程里使用。
|
|
196
|
+
- **worker 线程**:每个线程都可以使用这个包,线程结束时不会被它拖住。
|
|
197
|
+
|
|
198
|
+
### 打包
|
|
199
|
+
|
|
200
|
+
`lakutata package` 生成的打包文件、可执行文件和安装包会带上编译好的原生插件(`build/Release/netif.node`):请在每个目标平台上先安装这个包,再在那里打包。
|
|
201
|
+
|
|
202
|
+
## API
|
|
203
|
+
|
|
204
|
+
| 导出 | 说明 |
|
|
205
|
+
|---|---|
|
|
206
|
+
| `listInterfaces()`、`NetworkInterfaceInfo`、`InterfaceAddress` | 本机的网口和它们的状态;一个网口(`name`、`index`、`mac`、`up`、`running`、`loopback`、`addresses`);一个地址(`address`、`family`、`netmask`、`cidr`、`scopeid`) |
|
|
207
|
+
| `NetworkInterface`、`InterfaceSelector`、`NetworkInterfaceEvents`、`AddressChange`、`WaitForAddressOptions` | 被跟踪的网口(`list`、`selector`、`present`、`name`、`index`、`mac`、`up`、`running`、`loopback`、`addresses`、`info`、`closed`、`waitForAddress`、`listen`、`createUDPSocket`、`bindSocket`、`close`);指定网口的方式(名字、`{mac}`、`{index}`);它的事件(`change`、`up`、`down`、`added`、`removed`);地址的变化(`addresses`、`added`、`removed`);`waitForAddress()` 的选项(`family`、`timeout`) |
|
|
208
|
+
| `listenOn()`、`listenOnAll()`、`InterfaceListener`、`ListenOptions`、`InterfaceListenerEvents`、`ListeningAddress` | 在一个或多个网口上监听的服务器:监听器(`server`、`networkInterface`、`enforced`、`reason`、`port`、`listening`、`addresses`、`closed`、`close`),它的选项(`port`、`family`、`strict`、`backlog`),它的事件(`listening`、`error`、`enforced`、`close`),能访问到它的地址(`address`、`family`、`port`、`scopeid`) |
|
|
209
|
+
| `InterfaceUDPSocket`、`UDPSocketOptions`、`InterfaceUDPSocketEvents` | 网口的 UDP 套接字(`send`、`setBroadcast`、`addMembership`、`dropMembership`、`enforced`、`reason`、`port`、`listening`、`addresses`、`closed`、`close`),它的选项(`port`、`family`、`strict`),它的事件(`message`、`listening`、`error`、`enforced`、`close`) |
|
|
210
|
+
| `BoundSocket`、`BindSocketOptions` | 绑定网口的套接字(`fd`、`port`、`family`、`type`),以及 `bindSocket()` 的选项(`port`、`family`、`type`、`backlog`) |
|
|
211
|
+
|
|
212
|
+
类型声明里有每个声明的说明和示例。
|
|
213
|
+
|
|
214
|
+
## 异常
|
|
215
|
+
|
|
216
|
+
| 异常 | 什么时候 |
|
|
217
|
+
|---|---|
|
|
218
|
+
| `NetIfNotFoundException` | 指定的序号没有网口,指定的 MAC 有几个网口;网口不存在时调用 `bindSocket()` |
|
|
219
|
+
| `NetIfNotEnforcedException` | 用了 `strict`,但系统挡不住从别的网口进来的连接或数据(运行中保证丢失时,是一个 `error` 事件);没有绑定网口的权限时调用 `bindSocket()`(Linux 5.7 以前要 `CAP_NET_RAW`)。消息里说明原因和怎么办 |
|
|
220
|
+
| `NetIfNotSupportedException` | 在 Linux 以外调用 `bindSocket()`;在 macOS(以及没有权限的 Linux)上按地址绑定时调用 `addMembership()`;在 Bun 上监听 `https` 服务器 |
|
|
221
|
+
| `NetIfTimeoutException` | `waitForAddress()` 超过了 `timeout` |
|
|
222
|
+
| `NetIfException` | 以上所有异常的基类;系统拒绝时,`errno` 是系统的错误码(`'EADDRINUSE'`、`'EACCES'`…);`'E_NETIF'` 是这个包自己的错误(比如 `waitForAddress()` 等待时 `NetworkInterface` 被关闭) |
|
|
223
|
+
|
|
224
|
+
选择网口的方式、端口或地址族写错时,抛出 `TypeError`。
|
|
225
|
+
|
|
226
|
+
## 另见
|
|
227
|
+
|
|
228
|
+
- [socket(7)](https://man7.org/linux/man-pages/man7/socket.7.html):Linux 的 `SO_BINDTODEVICE`。
|
|
229
|
+
- [Windows 的强主机模型和弱主机模型](https://learn.microsoft.com/en-us/previous-versions/technet-magazine/cc137807(v=msdn.10))。
|
package/binding.gyp
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# The build of the addon of @lakutata/netif by node-gyp, when the package is installed: the sources common to every
|
|
2
|
+
# system (native/), then the ones of the system built on (native/linux, native/darwin, native/windows). C++17 with
|
|
3
|
+
# exceptions: the addon uses the C functions of Node-API (through node-addon-api, a dependency of the package). Linux
|
|
4
|
+
# needs the headers of the kernel (linux/rtnetlink.h): the linux-headers package on Alpine.
|
|
5
|
+
{
|
|
6
|
+
'targets': [
|
|
7
|
+
{
|
|
8
|
+
'target_name': 'netif',
|
|
9
|
+
'sources': ['native/addon.cpp', 'native/Interfaces.cpp'],
|
|
10
|
+
'include_dirs': ['native', "<!(node -p \"require('node-addon-api').include_dir\")"],
|
|
11
|
+
'defines': ['NAPI_VERSION=8', 'NAPI_CPP_EXCEPTIONS', 'NODE_API_SWALLOW_UNTHROWABLE_EXCEPTIONS', 'NODE_ADDON_API_DISABLE_DEPRECATED'],
|
|
12
|
+
'cflags!': ['-fno-exceptions'],
|
|
13
|
+
'cflags_cc!': ['-fno-exceptions', '-fno-rtti', '-std=gnu++20', '-std=gnu++17'],
|
|
14
|
+
'cflags_cc': ['-std=c++17', '-fexceptions'],
|
|
15
|
+
'conditions': [
|
|
16
|
+
['OS=="linux"', {
|
|
17
|
+
'sources': ['native/linux/Interfaces.cpp', 'native/linux/Watcher.cpp', 'native/linux/Sockets.cpp']
|
|
18
|
+
}],
|
|
19
|
+
['OS=="mac"', {
|
|
20
|
+
'sources': ['native/darwin/Interfaces.cpp', 'native/darwin/Watcher.cpp', 'native/darwin/Sockets.cpp'],
|
|
21
|
+
'xcode_settings': {
|
|
22
|
+
'GCC_ENABLE_CPP_EXCEPTIONS': 'YES',
|
|
23
|
+
'CLANG_CXX_LANGUAGE_STANDARD': 'c++17',
|
|
24
|
+
'CLANG_CXX_LIBRARY': 'libc++',
|
|
25
|
+
'MACOSX_DEPLOYMENT_TARGET': '11.0'
|
|
26
|
+
}
|
|
27
|
+
}],
|
|
28
|
+
['OS=="win"', {
|
|
29
|
+
'sources': ['native/windows/Interfaces.cpp', 'native/windows/Watcher.cpp', 'native/windows/Sockets.cpp'],
|
|
30
|
+
'defines': ['_HAS_EXCEPTIONS=1', 'WIN32_LEAN_AND_MEAN', 'NOMINMAX', '_WIN32_WINNT=0x0A00'],
|
|
31
|
+
'libraries': ['iphlpapi.lib', 'ws2_32.lib'],
|
|
32
|
+
'msvs_settings': {
|
|
33
|
+
'VCCLCompilerTool': {
|
|
34
|
+
'ExceptionHandling': 1,
|
|
35
|
+
# The Release configuration of node-gyp asks C++20: the options given last win
|
|
36
|
+
'AdditionalOptions': ['/utf-8', '/std:c++17']
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}]
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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 declare class NetIfException extends Error {
|
|
7
|
+
/**
|
|
8
|
+
* `'E_NETIF'`, the code of the error of the system, or the one of the subclass.
|
|
9
|
+
*/
|
|
10
|
+
errno: string | number;
|
|
11
|
+
/**
|
|
12
|
+
* An exception of a message.
|
|
13
|
+
* @param message The message.
|
|
14
|
+
* @param options The error that caused it (`cause`).
|
|
15
|
+
*/
|
|
16
|
+
constructor(message?: string, options?: ErrorOptions);
|
|
17
|
+
}
|