lakutata 3.1.2 → 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.
@@ -0,0 +1,261 @@
1
+ # Lakutata Netif
2
+
3
+ The package `@lakutata/netif` lets a server of Node.js listen on a network interface instead of an address. A device
4
+ with several interfaces often serves its management pages on one of them only: the address of that port comes from DHCP
5
+ and changes, or is not there yet when the device starts, and listening on `0.0.0.0` would expose the pages on every
6
+ interface. With the package the server listens on "eth1", whatever its addresses, and keeps listening through their
7
+ changes and through the interface unplugged and plugged back. It is not part of `lakutata`, and it needs no
8
+ application. What it is like:
9
+
10
+ - **Any server**: `http`, `https`, `http2`, `tls`, Express and Fastify take the connections of the interface as their
11
+ own; UDP sockets too, for the services that work on one interface (DHCP, mDNS, SSDP).
12
+ - **The entrypoints of lakutata** take an `interface` option, which serves the controllers of an application on one
13
+ interface (section 5).
14
+ - **The system refuses the other interfaces where it can**: on Linux the sockets are bound to the interface, and a
15
+ connection that reaches its address through another interface is refused. On Windows and macOS the addresses of the
16
+ interface are listened on, and `enforced` tells whether the system refuses those connections.
17
+ - **The interfaces and their changes**: every interface of the machine with its state, and events when its addresses,
18
+ its link or its presence change, from the notifications of the system.
19
+ - **A native addon**, compiled when the package is installed, for Linux, macOS and Windows; it also runs in Bun and in
20
+ the main process of Electron.
21
+
22
+ The README of `@lakutata/netif` describes every function and its options; this guide goes through what the package is
23
+ for, with an example of each use.
24
+
25
+ ```bash
26
+ npm install @lakutata/netif
27
+ ```
28
+
29
+ The addon is compiled when the package is installed, so the machine needs Python 3 and a C++17 compiler (on Alpine Linux
30
+ the headers of the kernel too: `apk add python3 make g++ linux-headers`). The package manager has to run the install
31
+ script of the package: npm runs it, and may warn that the script is "not yet covered by allowScripts" (`npm
32
+ install-scripts approve @lakutata/netif` records that you allow it); Bun and pnpm run it only for the packages they are
33
+ told to trust (`trustedDependencies` in the package.json for Bun, `pnpm approve-builds` for pnpm). Its types refer to
34
+ the ones of `@lakutata/core`, a peer dependency that the package managers install with it; it is not loaded when the
35
+ package runs.
36
+
37
+ ## 1. Listening on an interface
38
+
39
+ `listenOn()` listens on an interface for a server. The interface is given by its name, its hardware address
40
+ (`{mac: '52:54:00:12:34:56'}`, the same whatever the system names it) or its index; on Windows the name is the one of
41
+ the network connection (`'以太网 2'`, `'Ethernet 2'`).
42
+
43
+ ```typescript
44
+ import {createServer} from 'node:http'
45
+ import express from 'express'
46
+ import {type InterfaceListener, listenOn} from '@lakutata/netif'
47
+
48
+ const app = express()
49
+ app.get('/', (request, response) => response.send(`served on ${request.socket.localAddress}`))
50
+
51
+ const listener: InterfaceListener = await listenOn('eth1', createServer(app), {port: 8080})
52
+ listener.on('error', error => console.error(error))
53
+ console.log(listener.port, listener.addresses)
54
+ ```
55
+
56
+ - The listener accepts the connections and gives them to the server, which handles them as its own. The server itself
57
+ does not listen: its `address()` is `null`, and `server.close()` does not stop the listener; use `listener.port`,
58
+ `listener.addresses` and `listener.close()`.
59
+ - The interface may not exist yet or have no address yet: `listenOn()` returns at once, and the interface is listened
60
+ on when it can be. When it is unplugged and plugged back, or when its addresses change, it is listened on again by
61
+ itself; the connections established are left to the server.
62
+ - The first listening throws its errors (the port taken); the later ones are `error` events, retried a little later.
63
+ Handle the `error` event of the listener, as for a `net.Server`.
64
+ - `family` chooses `'IPv4'`, `'IPv6'` or `'both'` (by default); `port: 0` lets the system choose a port, kept when the
65
+ interface is listened on again.
66
+
67
+ ## 2. Which connections are refused
68
+
69
+ A client of another network can send packets for the address of the management port through another interface of the
70
+ device, with a route of its own through it. Whether the system refuses such a connection is what `enforced` tells:
71
+
72
+ | | `enforced` |
73
+ |---|---|
74
+ | Linux | `true`: the sockets are bound to the interface (`SO_BINDTODEVICE`); before Linux 5.7 this needs the capability `CAP_NET_RAW`, without which the addresses are listened on and `enforced` is `false` |
75
+ | Windows | `true` while every other interface receives with the strong host model and does not forward, which is the default |
76
+ | macOS | `false` unless the system is set to refuse those packets (`net.inet.ip.check_interface` set to 2, and `net.inet6.ip6.check_interface` for IPv6) and does not forward |
77
+
78
+ By default the interface is listened on in the best way the system allows, never less than `listen(port, host)` on its
79
+ addresses; `reason` tells why there is no guarantee and how to get it. Where the management port is a boundary of
80
+ security, `strict: true` refuses to listen without the guarantee, and stops the listening while it is lost:
81
+
82
+ ```typescript
83
+ import {createServer} from 'node:http'
84
+ import {type InterfaceListener, NetIfNotEnforcedException, NetworkInterface} from '@lakutata/netif'
85
+
86
+ const management: NetworkInterface = new NetworkInterface({mac: '52:54:00:12:34:56'})
87
+ try {
88
+ const listener: InterfaceListener = await management.listen(createServer(), {port: 443, strict: true})
89
+ listener.on('error', error => console.error(error))
90
+ listener.on('enforced', enforced => console.log(enforced ? 'guaranteed again' : 'stopped: no longer guaranteed'))
91
+ } catch (error) {
92
+ if (error instanceof NetIfNotEnforcedException) console.error(error.message)
93
+ throw error
94
+ }
95
+ ```
96
+
97
+ ## 3. Following an interface
98
+
99
+ A `NetworkInterface` gives the state of an interface now (`present`, `running`, `addresses`…) and an event at each
100
+ change. The events come from the notifications of the system, not from polling.
101
+
102
+ ```typescript
103
+ import {type InterfaceAddress, listInterfaces, NetworkInterface} from '@lakutata/netif'
104
+
105
+ console.log(listInterfaces().map(entry => `${entry.name}: ${entry.running ? 'connected' : 'not connected'}`))
106
+
107
+ const wan: NetworkInterface = new NetworkInterface('eth0')
108
+ wan.on('change', ({added, removed}) => {
109
+ for (const address of added) console.log(`new address ${address.cidr}`)
110
+ for (const address of removed) console.log(`address gone ${address.cidr}`)
111
+ })
112
+ wan.on('down', () => console.log('the cable of eth0 is unplugged'))
113
+ wan.on('up', () => console.log('the cable of eth0 is plugged in'))
114
+ const address: InterfaceAddress = await wan.waitForAddress({family: 'IPv4', timeout: 60000})
115
+ console.log(`reachable at ${address.address}`)
116
+ ```
117
+
118
+ - `listInterfaces()` lists the interfaces without an address too, with their state; `os.networkInterfaces()` lists
119
+ only the ones with an address.
120
+ - An IPv6 address counts once it is usable: about a second after it is added, when its duplicate address detection is
121
+ over.
122
+ - `close()` stops following the interface (and closes its listeners).
123
+
124
+ ## 4. UDP on an interface
125
+
126
+ `createUDPSocket()` gives a UDP socket of the interface, with the methods and the events of a `dgram` socket. A device
127
+ that answers the discovery broadcasts of its management network only:
128
+
129
+ ```typescript
130
+ import {type InterfaceUDPSocket, NetworkInterface} from '@lakutata/netif'
131
+
132
+ const management: NetworkInterface = new NetworkInterface('eth1')
133
+ const socket: InterfaceUDPSocket = await management.createUDPSocket({port: 30303, family: 'IPv4'})
134
+ socket.setBroadcast(true)
135
+ socket.on('message', (message, remote) => {
136
+ const answer: string = `device at ${management.addresses[0]?.address}`
137
+ if (message.toString() === 'who is there') socket.send(answer, remote.port, remote.address)
138
+ })
139
+ socket.on('error', error => console.error(error))
140
+ ```
141
+
142
+ On Linux the socket receives what comes through the interface, its broadcasts and the multicast of the groups joined
143
+ with `addMembership()` included, and nothing of the other interfaces. On Windows and macOS a socket is bound to each
144
+ address of the interface; on macOS such sockets receive no broadcast and no multicast.
145
+
146
+ ## 5. In an application
147
+
148
+ The entrypoints of lakutata that listen on a port take an `interface` option: `expressHTTPEntrypoint()`,
149
+ `fastifyHTTPEntrypoint()`, `httpServiceEntrypoint()`, `socketIOServiceEntrypoint()`, `jsonRPCServiceEntrypoint()`,
150
+ `grpcServiceEntrypoint()` of `@lakutata/grpc` and `httpMCPEntrypoint()` of `@lakutata/mcp`. The actions of the
151
+ controllers are then served on that interface, whatever its addresses:
152
+
153
+ ```typescript
154
+ import {Application} from 'lakutata'
155
+ import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
156
+ import {GET} from 'lakutata/decorator/ctrl'
157
+ import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
158
+
159
+ class ManagementController extends Controller {
160
+ @GET('/status')
161
+ public async status(): Promise<{ ok: boolean }> {
162
+ return {ok: true}
163
+ }
164
+ }
165
+
166
+ Application.run(() => ({
167
+ id: 'device.app',
168
+ name: 'Device',
169
+ components: {
170
+ entrypoint: buildEntrypoints({
171
+ controllers: [ManagementController],
172
+ http: expressHTTPEntrypoint({port: 8080, interface: 'eth1'})
173
+ })
174
+ },
175
+ bootstrap: ['entrypoint']
176
+ }))
177
+ ```
178
+
179
+ - `family`, `strict` and `onError` go with `interface`. Without `onError`, the errors of listening again are written to
180
+ the log of the application. The `listening` callback of the adapter gets the listeners. [HTTP](HTTP.md), section 2,
181
+ describes these options.
182
+ - The application starts while the interface does not exist yet, and its actions can be reached once it appears.
183
+ - The entrypoints listen through `IInterfaceListening`, a contract of `@lakutata/core` that this package implements
184
+ and that they load by default. Another implementation is given in their `interfaceListening` option; the
185
+ `InterfaceListener` of the package is an `IInterfaceListener`.
186
+
187
+ A server that is not an entrypoint belongs to a component: it listens in `startup()` and stops in `destroy()`. The
188
+ interface and the port are the options of the component.
189
+
190
+ ```typescript
191
+ import {createServer} from 'node:http'
192
+ import express from 'express'
193
+ import {Application, Component, DTO, type ILogger} from 'lakutata'
194
+ import {Configurable, Inject} from 'lakutata/decorator/di'
195
+ import {type InterfaceListener, listenOn} from '@lakutata/netif'
196
+
197
+ class ManagementPages extends Component {
198
+ @Inject('log')
199
+ protected readonly log: ILogger
200
+
201
+ @Configurable(DTO.String().required())
202
+ protected readonly interfaceName: string
203
+
204
+ @Configurable(DTO.Number().port().default(8080))
205
+ protected readonly port: number
206
+
207
+ protected listener: InterfaceListener
208
+
209
+ protected async startup(): Promise<void> {
210
+ const app = express()
211
+ app.get('/', (request, response) => response.send('management'))
212
+ this.listener = await listenOn(this.interfaceName, createServer(app), {port: this.port})
213
+ this.listener.on('error', (error: Error): void => this.log.error(error.message))
214
+ this.listener.on('listening', (): void => this.log.info('listening on %o', this.listener.addresses))
215
+ this.log.info('enforced: %s', this.listener.enforced)
216
+ }
217
+
218
+ protected async destroy(): Promise<void> {
219
+ await this.listener?.close()
220
+ }
221
+ }
222
+
223
+ Application.run(() => ({
224
+ id: 'device.app',
225
+ name: 'Device',
226
+ components: {
227
+ management: {class: ManagementPages, interfaceName: 'eth1', port: 8080}
228
+ },
229
+ bootstrap: ['management']
230
+ }))
231
+ ```
232
+
233
+ ## 6. Systems, runtimes and packaging
234
+
235
+ - **Linux** binds the sockets to the interface: the system refuses the other interfaces, an address added is served at
236
+ once, and UDP gets the broadcasts and the multicast of the interface. Before Linux 5.7 give the process `CAP_NET_RAW`
237
+ (`setcap cap_net_raw+ep`, or `AmbientCapabilities=CAP_NET_RAW` in its systemd unit).
238
+ - **Windows** listens on the addresses of the interface; its strong host model refuses the other interfaces unless an
239
+ interface is set to the weak host model (`netsh interface ipv4 set interface "<name>" weakhostreceive=enabled`, which
240
+ some load balancers do) or forwards. `reason` names that interface.
241
+ - **macOS** listens on the addresses of the interface, without the guarantee unless the system is set to it. macOS 27
242
+ hides the hardware addresses from the processes (it gives `02:00:00:00:00:00`): give an interface by its name there.
243
+ - **Bun**: its `https` server answers the connections given to it without TLS, so `listen()` refuses it in Bun (a
244
+ Fastify instance created with `https` too); use `http2.createSecureServer({key, cert, allowHTTP1: true})` or
245
+ `tls.createServer()` there. Its servers do not close the idle keep-alive connections given to them, which the
246
+ entrypoints close themselves. A connection given to a server cannot be upgraded to a WebSocket (seen with Bun 1.4), so
247
+ the entrypoints refuse WebSocket on an interface in Bun.
248
+ - **Electron**: in the main process.
249
+ - The bundles, the executables and the installers made by `lakutata package` carry the compiled addon
250
+ (`build/Release/netif.node`): build them on each target platform, after installing the package there (see
251
+ [Packaging](Packaging.md) and [NativeAddons](NativeAddons.md)).
252
+
253
+ ## 7. Exceptions
254
+
255
+ | Exception | When |
256
+ |---|---|
257
+ | `NetIfNotFoundException` | An index no interface has, a hardware address several interfaces have |
258
+ | `NetIfNotEnforcedException` | `strict` where the system cannot refuse the other interfaces, or the guarantee lost (an `error` event) |
259
+ | `NetIfNotSupportedException` | What the system or the runtime cannot do: `bindSocket()` outside Linux, multicast on a socket bound to an address on macOS, an `https` server in Bun |
260
+ | `NetIfTimeoutException` | `waitForAddress()` past its `timeout` |
261
+ | `NetIfException` | The base of all of them; a refusal of the system has its code as `errno` (`'EADDRINUSE'`, `'EACCES'`…) |
@@ -47,8 +47,10 @@ tool, a web and a desktop application, calls between services, background jobs,
47
47
  Not part of this package: `@lakutata/cli` (the `lakutata` command for creating projects, running migrations and packing
48
48
  applications, see [Command](Command.md)), `@lakutata/grpc` (a gRPC client, server and service transport),
49
49
  `@lakutata/nats` (a NATS component and service transport), `@lakutata/dbus` (D-Bus), `@lakutata/mcp` (the Model
50
- Context Protocol), `@lakutata/webrtc` (WebRTC: the signaling, a TURN server, a media server, peer connections) and
51
- `@lakutata/ffi` (the C functions of native libraries).
50
+ Context Protocol), `@lakutata/webrtc` (WebRTC: the signaling, a TURN server, a media server, peer connections),
51
+ `@lakutata/ffi` (the C functions of native libraries), `@lakutata/math` (symbolic and numeric mathematics, as functions
52
+ that need no lakutata application) and `@lakutata/netif` (servers and UDP sockets listening on a network interface
53
+ whatever its addresses, the interfaces of the machine and their changes).
52
54
 
53
55
  ## The guides
54
56
 
@@ -93,6 +95,8 @@ Context Protocol), `@lakutata/webrtc` (WebRTC: the signaling, a TURN server, a m
93
95
  | [MCP](MCP.md) | The tools, resources and prompts of the Model Context Protocol (`@lakutata/mcp`), over stdio and HTTP |
94
96
  | [WebRTC](WebRTC.md) | Peer-to-peer connections and a media server (`@lakutata/webrtc`): the signaling, the TURN server, browsers and applications, the actions of an application called through data channels, WHIP and WHEP |
95
97
  | [FFI](FFI.md) | The C functions of native libraries (`@lakutata/ffi`): declarations copied from headers, structs, callbacks, memory, packaging |
98
+ | [Math](Math.md) | Mathematics (`@lakutata/math`): formulas and exact results, equations, decimals of any precision, matrices, statistics, numeric solutions, units |
99
+ | [Netif](Netif.md) | Network interfaces (`@lakutata/netif`): a server listening on an interface whatever its addresses, the connections of the other interfaces refused, the interfaces and their changes, UDP on one interface |
96
100
  | [Command](Command.md) | The `lakutata` command: creating projects, migrations, translations, environment variables |
97
101
  | [Workspaces](Workspaces.md) | Projects of several packages: running their scripts, giving them a version, adding packages |
98
102
  | [NativeAddons](NativeAddons.md) | Native addons in C++ or Rust: creating them, linking libraries, shipping them |
@@ -197,7 +197,7 @@ pages of a web UI during its development. The Electron templates add it in devel
197
197
  | `host` | all the interfaces | The address to listen on |
198
198
  | `path` | `'/'` | The path of the requests; the events are under it, at `<path>/events` |
199
199
  | `limit` | `10485760` (10 MiB) | The largest body, in bytes |
200
- | `listening` | | `(server) => …`, called once the server listens |
200
+ | `listening` | | `(server, listeners) => …`, called once the server listens (`listeners`, with `interface`: see below) |
201
201
 
202
202
  - A request is a `POST` to `path`, its body the message as JSON. It is answered with 200 and the envelope, whatever the
203
203
  result of the action. A body that is not JSON is answered with 400, a body over `limit` with 413, another method or
@@ -284,7 +284,7 @@ connected.
284
284
  | `options` | | The options of the Socket.IO server: `cors`, `path`, `maxHttpBufferSize`… |
285
285
  | `server` | | An HTTP server to share, or a function returning it (below) |
286
286
  | `io` | | An existing Socket.IO server, used as it is |
287
- | `listening` | | `(io, server) => …`, called once the adapter is ready |
287
+ | `listening` | | `(io, server, listeners) => …`, called once the adapter is ready |
288
288
 
289
289
  ```typescript
290
290
  import {io, type Socket} from 'socket.io-client'
@@ -340,7 +340,7 @@ the tools that speak the protocol, and the parent processes over the standard in
340
340
  | `input`, `output` | `process.stdin`, `process.stdout` | The streams of stdio |
341
341
  | `end` | `'application'` | When the input of stdio ends: `'application'` exits the application once the requests in progress are answered, `'none'` does nothing, or a function |
342
342
  | `heartbeat` | `30000` | The interval of the pings of WebSocket, in milliseconds; `0` sends none |
343
- | `listening` | | `(server) => …`, called once the server of HTTP or WebSocket listens |
343
+ | `listening` | | `(server, listeners) => …`, called once the server of HTTP or WebSocket listens |
344
344
 
345
345
  The method and the params of a request make the message: `{"method": "order.get", "params": {"id": 1}}` is the message
346
346
  `{method: 'order.get', id: 1}`, so the action declares the method in its pattern:
@@ -417,6 +417,25 @@ $ echo '{"jsonrpc": "2.0", "id": 1, "method": "sum", "params": {"values": [1, 2,
417
417
 
418
418
  The application exits once its input ends and its requests are answered (`end`).
419
419
 
420
+ ### On a network interface
421
+
422
+ `httpServiceEntrypoint()`, `socketIOServiceEntrypoint()` with its own server, and `jsonRPCServiceEntrypoint()` over HTTP
423
+ or WebSocket can listen on a network interface instead of an address, whatever its addresses, with the package
424
+ `@lakutata/netif` installed apart. They take the options `interface`, `family`, `strict`, `onError` and
425
+ `interfaceListening` of the HTTP adapters (see [HTTP](HTTP.md), section 2), and their `listening` callback gets the
426
+ listeners of the interface as its last argument:
427
+
428
+ ```typescript
429
+ import {httpServiceEntrypoint} from 'lakutata/com/entrypoint'
430
+
431
+ //The service on eth1 only, whatever its address
432
+ export const service = httpServiceEntrypoint({port: 3000, interface: 'eth1'})
433
+ ```
434
+
435
+ In Bun, a connection given to a server cannot be upgraded to a WebSocket (seen with Bun 1.4). There the start fails for
436
+ JSON-RPC over WebSocket, and for Socket.IO unless it uses polling only (`options: {transports: ['polling']}`, and the
437
+ same on the clients).
438
+
420
439
  ### Electron
421
440
 
422
441
  `electronIPCServiceEntrypoint()` serves the actions to the windows of an Electron application, in its main process.
package/doc/zh/GRPC.md CHANGED
@@ -52,10 +52,13 @@ Application.run(() => ({
52
52
  | `tls` | 无(明文 HTTP/2) | `{cert, key}` 启用 TLS;再加上 `ca` 时,客户端必须出示由它签发的证书(双向 TLS) |
53
53
  | `maxMessageSize` | 4 MiB | 请求的最大大小(解压前后都算);超过时以 `RESOURCE_EXHAUSTED` 失败 |
54
54
  | `compression` | `'identity'` | 设为 `'gzip'` 或 `'deflate'` 时,对支持的客户端压缩响应。压缩过的请求总是可以接收 |
55
- | `listening` | | `(address) => void`,服务开始监听后调用;启动过程会等它完成 |
55
+ | `listening` | | `(address, listeners) => void`,服务开始监听后调用;启动过程会等它完成 |
56
+ | `interface`、`family`、`strict`、`onError` | | 按网口监听,而不是按地址监听,用法和 HTTP 适配器一样(见 [HTTP](HTTP.md) 第 2 节);不能和 `host` 或 `socketPath` 一起设置 |
56
57
  | `middlewares` | | 通过 gRPC 调用的动作专用的中间件,在入口的中间件之后执行 |
57
58
  | `name` | | 适配器的名字,用来区分多个适配器 |
58
59
 
60
+ 设置 `interface` 时需要安装 `@lakutata/netif` 包。这时传给 `listening` 的 `address` 是第一个监听的地址;网口还不存在或还没有地址时是 `null`;`listeners` 是网口的监听器。
61
+
59
62
  服务端不提供反射服务,也不提供健康检查服务。
60
63
 
61
64
  ## 2. 从另一个应用调用
@@ -223,4 +226,4 @@ print(json.loads(response.payload) if response.success else json.loads(response.
223
226
 
224
227
  ## 8. 底层组件
225
228
 
226
- 这个包也提供服务所依赖的底层组件,脱离 lakutata 也能使用:`GRPCConnection` 是可以调用任何 gRPC 服务端的客户端(一元调用、服务端流、双向流、元数据、TLS);`GRPCServer` 是服务端;`encode`/`decode` 是基于 schema 的 protobuf 编解码器。详见 `@lakutata/grpc` 的 README。
229
+ 这个包也提供服务所依赖的底层组件,脱离 lakutata 也能使用:`GRPCConnection` 是可以调用任何 gRPC 服务端的客户端(一元调用、服务端流、双向流、元数据、TLS);`GRPCServer` 是服务端;`encode`/`decode` 是基于 schema 的 protobuf 编解码器。详见 `@lakutata/grpc` 的 README。`GRPCServer.listenWith()` 也能像适配器一样,通过网口的监听器(`@lakutata/netif` 的 `listenOnAll()`)按网口监听。
package/doc/zh/HTTP.md CHANGED
@@ -119,6 +119,8 @@ HTTP/1.1 400 Bad Request
119
119
  | `fallback` | | 响应动作路径之外的请求,比如 Web 界面的页面(第 7 节) |
120
120
  | `stop` | | 适配器停止、服务器关闭之后调用 |
121
121
  | `heartbeat` | `15000` | 每隔多久发送一条注释,让事件流保持连接,单位毫秒(第 5 节) |
122
+ | `listening` | | `(server, listeners) => …`:服务器开始监听后调用;按网口监听时,在网口的监听器建好后调用(见下文) |
123
+ | `interface` | | 按网口监听,而不是按地址监听,可以再配上 `family`、`strict` 和 `onError`(见下文) |
122
124
 
123
125
  `fastifyHTTPEntrypoint()` 的选项相同,只是没有 `server` 和 `app`,取而代之的是 `fastify`:一个 Fastify 实例,或者要创建的实例的选项(`bodyLimit`,默认 1 MiB;`trustProxy`;`connectionTimeout`、`keepAliveTimeout`、`requestTimeout`;`logger`……)。它的 `body` 选项只有 `text`,大小上限是实例的 `bodyLimit`。
124
126
 
@@ -136,6 +138,33 @@ HTTP/1.1 400 Bad Request
136
138
 
137
139
  应用停止时(见 [Entrypoint](Entrypoint.md) 第 4 节),适配器不再接受新的连接,并结束事件流;Fastify 还会对已打开的连接上仍然到达的请求返回 503。正在处理的请求可以在入口的 `shutdownTimeout` 内完成(超时后以 503 响应),之后最多再有一秒来发送响应,然后剩下的连接被关闭,WebSocket 连接也一样。
138
140
 
141
+ ### 按网口监听
142
+
143
+ 有多个网口的设备,常常只在其中一个网口上提供页面,而这个网口的地址来自 DHCP,会变。设置 `interface` 后,适配器监听的是这个网口,而不是某个地址:不管网口现在是什么地址,地址变化、网口拔掉再插回,都会继续监听。默认用的是 `@lakutata/netif` 包,需要另外安装,它会编译一个原生插件(见 [Netif](Netif.md))。
144
+
145
+ ```typescript
146
+ import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
147
+
148
+ //页面只在 eth1 上提供,不管它是什么地址
149
+ export const http = expressHTTPEntrypoint({
150
+ port: 8080,
151
+ interface: 'eth1',
152
+ listening: (_server, listeners): void => console.log(listeners?.map(listener => listener.addresses))
153
+ })
154
+ ```
155
+
156
+ | 选项 | 默认值 | 含义 |
157
+ |---|---|---|
158
+ | `interface` | | 网口的名字(`'eth1'`;Windows 上是网络连接的名字)、`{mac: '52:54:00:12:34:56'}` 或 `{index: 3}`;多个网口放在数组里,这时 `port` 不能是 `0`。不能和 `host` 一起设置 |
159
+ | `family` | `'both'` | `'IPv4'`、`'IPv6'` 或两者都监听 |
160
+ | `strict` | `false` | 如果系统挡不住从别的网口发往这个网口地址的连接,就让启动失败,并在挡不住的期间停止监听(见 [Netif](Netif.md) 第 2 节) |
161
+ | `onError` | 应用的日志 | 接收应用运行期间重新监听时的错误,比如端口被别的程序占用;适配器会自己重试 |
162
+ | `interfaceListening` | `@lakutata/netif` | 换用另一种按网口监听的实现:一个 `IInterfaceListening`,也就是 lakutata 定义、`@lakutata/netif` 实现的接口 |
163
+
164
+ - 应用启动时网口可能还不存在(比如 USB 网卡还没插上):启动照样成功,网口出现后适配器就开始监听。在那之前,监听器的 `addresses` 是空的。
165
+ - 服务器自己不监听,它的 `address()` 是 `null`:端口和地址从监听器上读取。
166
+ - 没有设置 `interface` 却设置了 `family`、`strict`、`onError` 或 `interfaceListening`,或者同时设置了 `interface` 和 `host`,创建适配器时就会报错。
167
+
139
168
  ## 3. 路由
140
169
 
141
170
  | 装饰器 | 方法 |
package/doc/zh/MCP.md CHANGED
@@ -224,7 +224,8 @@ Application.run(() => ({
224
224
  |---|---|---|
225
225
  | `port`、`host`、`path` | `3000`、`'127.0.0.1'`、`'/mcp'` | 监听的位置 |
226
226
  | `authenticate` | | `(request) => MCPAuthInfo \| undefined`:客户端的身份认证;抛出错误时以 401 响应,返回 `undefined` 时不认证,照常处理请求 |
227
- | `allowedHosts`、`allowedOrigins` | 在本机地址上时为本机的名字 | 接受的 `Host` 和 `Origin` 请求头(否则为 403);`false` 表示不检查 |
227
+ | `allowedHosts`、`allowedOrigins` | 在本机地址或回环网口上时为本机的名字 | 接受的 `Host` 和 `Origin` 请求头(否则为 403);`false` 表示不检查 |
228
+ | `interface`、`family`、`strict`、`onError` | | 按网口监听,代替 `host`,需要安装 `@lakutata/netif` 包(见 [HTTP](HTTP.md) 第 2 节) |
228
229
  | `legacy` | `'stateless'` | 2025 年协议版本的客户端:以无会话的方式服务,或者用 `'reject'` 拒绝 |
229
230
  | `maxRequestBodySize` | 4 MiB | 更大的请求体以 413 响应 |
230
231
  | `bus` | 内存中 | 通知的去向,用于在应用的多个实例之间共享 |
@@ -232,7 +233,7 @@ Application.run(() => ({
232
233
  | `serverName`、`serverVersion`、`instructions`、`server`、`listening`、`name`、`middlewares` | | 与 stdio 相同;`server` 是 `node:http` 服务器的选项 |
233
234
 
234
235
  - 它通过 Streamable HTTP 提供协议:2026-07-28 版本,以及无会话方式的 2025 年版本。
235
- - 在本机地址上,`Host` 和 `Origin` 请求头会与本机的名字比对,防止 DNS 重绑定攻击:在代理之后时,要列出客户端使用的名字。在其他地址上,除非列出名字,否则不做检查。
236
+ - 在本机地址上,`Host` 和 `Origin` 请求头会与本机的名字比对,防止 DNS 重绑定攻击:在代理之后时,要列出客户端使用的名字。在其他地址上,除非列出名字,否则不做检查。设置 `interface` 时也一样:在回环网口上与本机的名字比对;在其他网口上,网络中的客户端都能访问这个服务器,所以要设置 `allowedHosts` 和 `authenticate`。
236
237
  - 应用停止时,正在处理的调用会得到响应,然后端口关闭。
237
238
 
238
239
  **与 HTTP 适配器共用端口。** 设置 `listen: false`,在 Express 应用的 `before` 钩子中挂载适配器的 `handler`: