@ai-ecoverse/slicc-kernel 1.95.3 → 1.97.0
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/README.md +20 -2
- package/dist/index.js +71 -8
- package/dist/index.js.map +2 -2
- package/dist/kernel-worker.js +556 -178
- package/dist/kernel-worker.js.map +4 -4
- package/dist/node-process-worker.js +710 -604
- package/dist/node-process-worker.js.map +4 -4
- package/dist/node.js +552 -212
- package/dist/node.js.map +4 -4
- package/dist/process-worker.js +783 -677
- package/dist/process-worker.js.map +4 -4
- package/dist/testing.js +45 -1
- package/dist/testing.js.map +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -282,7 +282,7 @@ On top of preview1 a WASI program gets:
|
|
|
282
282
|
- **Job control:** `^Z`, `SIGTSTP`, `SIGTTIN` and `SIGTTOU` stop a WASIX program at once, unless it has a handler for them. A stopped program that is busy computing is stopped for job control (`jobs` shows it, `fg`, `bg` and `SIGCONT` work), but its worker keeps a core busy until its next system call, since a worker's running code cannot be frozen from outside. Agents should `kill` such a job rather than leave it stopped. An Emscripten program stops at its next kernel call as well, and at its next write, read or sleep that its glue answers without the kernel (a write to its own `/dev/null`, as in `yes > /dev/null`), so a stopped one keeps no core busy unless it computes without any of these calls. A stopped program doesn't move on while stopped: a `sleep` (or a blocked read) that would end meanwhile ends after `SIGCONT`. A signal for such a program is also delivered at those glue-answered calls, so a handler runs in a loop that only writes to `/dev/null`. For the same reason, a signal with a handler or a terminating default (`^C`, `SIGTERM`) reaches a program that is busy computing only at its next system call, so a CPU-bound loop may not end on `^C`; `kill -9` always ends it. A pending signal is delivered at the program's next WASI or WASIX call, including the calls its worker answers without the kernel (`random_get`, `args_get`, …), except the exempt ones: `clock_time_get` (a tight loop of it measured up to 30% slower), `proc_exit`, `proc_exit2`, `thread_exit`, `thread_spawn_v2` and `wasi.thread-spawn` (wasi-threads), the calls that install handlers (`callback_signal`, `slicc.sigaction_set`), and calls made while Asyncify unwinds or rewinds the stack for `fork` or `longjmp`. A signal that arrives while a thread computes is thus delivered at its next call; when that call is a wait (`poll_oneoff` with a timeout or a descriptor, so `nanosleep` and `poll`/`select` too, `sigmask_poll`, or a futex wait), the handler has run during the call, so the wait ends at once: `EINTR` for a libc of -22 or later (an older one sees its clocks fire, as for any interruption), and a futex wait returns woken. A thread blocked in the kernel or waiting on a lock (`pthread_cond_wait`, a futex) is woken by a signal for it (a futex wait checks for signals every 50 ms, so each waiting thread wakes briefly that often), and `pthread_kill` runs the handler on the thread it names. A sleep, or a clock in `poll_oneoff`, never ends before the time asked (it can end later, never earlier). A signal that ends a sleep or a `poll`/`select` with a timeout makes the call fail with `EINTR` in a program whose libc says it is wasix-sysroot 2025.9.30-22 or later (its `slicc.libc` custom section). Older libcs turn that answer into `ENOTSUP` from `nanosleep` (up to -18), so for a program without the section the wait ends as if it had timed out, unless it imports `fd_fdflags_set` (then `EINTR`, as before).
|
|
283
283
|
- **Blocked signals** (`sigprocmask`, `pthread_sigmask`, `sigpending`): a program whose libc imports `slicc.sigmask_set(how, set_lo, set_hi, old_ptr)` and `slicc.sigpending_get(ptr)` (wasix-sysroot 2025.9.30-28 and later) has a blocked mask per thread. `how` is musl's `SIG_BLOCK` 0, `SIG_UNBLOCK` 1 or `SIG_SETMASK` 2, the set is the 64-bit `sigset_t` (bit `sig - 1`), and the old mask is written as a u64 at `old_ptr`; any other `how` is `EINVAL`. `SIGKILL` and `SIGSTOP` cannot be blocked. A blocked signal is not delivered, does not interrupt a sleep or a blocked call, and its default action (terminate or stop) waits: it stays pending, `sigpending` shows it (the thread's pending signals and the process's), and it is delivered when it is unblocked, before `sigmask_set` returns. A signal ignored with `SIG_IGN` is discarded even while blocked. A forked child (with the mask of the thread that forked), the program an `exec` starts and a spawned child keep the mask, all 64 bits of it, and so does a new thread; `/proc/<pid>/status` shows the main thread's as `SigBlk`. The mask is kept in a word of the worker's shared header, so changing it costs no kernel call. A program without the import (an older libc) has no mask and gets every signal at once, as before, and one it starts by `exec` with a mask set starts with none. A signal for the whole process goes to the main thread, even if the main thread blocks it and another thread does not. A signal sent to one thread (`pthread_kill`) that it blocks waits for that thread, and if it has no handler, the thread's unblock ends the whole process by the signal, whatever the other threads block. `slicc.sigmask_poll(set_lo, set_hi, in_ptr, out_ptr, nsubscriptions, nevents_ptr)` is `pselect`, `ppoll` and `sigsuspend`: it swaps the thread's mask to the given set for the call and puts the old one back before it returns. It delivers a pending signal the new mask unblocks, without polling, or polls as `poll_oneoff` does, under the new mask, so a signal it unblocks interrupts the wait with no window between the swap and the wait; with no subscriptions it waits until a signal is delivered. An interrupted call returns `EINTR` with no events, after the handler has run.
|
|
284
284
|
- **Pipes** have no file type for WASI and WASIX programs' `fstat` (`st_mode` type 0), because WASI has no FIFO file type. A socket type, as kernels 1.47.3 to 1.48.1 gave them, made Ruby treat a `popen` pipe as a socket (`close_write` then never ends the child's input). The kernel's `slicc_fs.fd_mode` answers `S_IFIFO | 0600` (Linux numbering) for a pipe, and wasix-sysroot 2025.9.30-21 and later take the type from it, so programs built on it see a FIFO (`S_ISFIFO`); older libcs keep only the permission bits. Until then Python's asyncio refuses a pipe for a subprocess (`Pipe transport is only for pipes, sockets and character devices`). They are read and written as pipes; socket calls on them fail with `ENOTSOCK`, and `isatty()` is false. Emscripten programs, bash included, see a FIFO. The same holds for any kernel descriptor that is neither seekable nor a terminal or device, such as captured output.
|
|
285
|
-
- **Sockets**: the WASIX socket calls (`sock_open`, `sock_pair` for an `AF_UNIX` stream pair, `bind`, `listen`, `connect`, `accept`, options, local and peer names) are the kernel's loopback sockets, and preview1's `sock_recv` / `sock_send` / `sock_shutdown` work on them and on inherited ones. Writing to a socket whose peer is gone fails with `EPIPE`, through `fd_write` as through `sock_send`, as if `MSG_NOSIGNAL` were set, since a WASI program cannot ignore `SIGPIPE`; a pipe or terminal whose reader is gone still ends the program with status 141. HTTP and HTTPS leave through the realm proxy that `https_proxy` names, as for any other program; `resolve` answers `/etc/hosts` names and literal addresses, and the [uplink](#uplink)'s names.
|
|
285
|
+
- **Sockets**: the WASIX socket calls (`sock_open`, `sock_pair` for an `AF_UNIX` stream pair, `bind`, `listen`, `connect`, `accept`, options, local and peer names) are the kernel's loopback sockets, and preview1's `sock_recv` / `sock_send` / `sock_shutdown` work on them and on inherited ones. Writing to a socket whose peer is gone fails with `EPIPE`, through `fd_write` as through `sock_send`, as if `MSG_NOSIGNAL` were set, since a WASI program cannot ignore `SIGPIPE`; a pipe or terminal whose reader is gone still ends the program with status 141. HTTP and HTTPS leave through the realm proxy that `https_proxy` names, as for any other program; `resolve` answers `/etc/hosts` names and literal addresses, and the [uplink](#uplink)'s names. `sock_open(SOCK_DGRAM)` on `AF_INET`/`AF_INET6` gives a [datagram socket](#datagram-sockets-udp); `sock_send_to` and `sock_recv_from` carry its addresses, and `RECV_DATA_TRUNCATED` is set in `ro_flags` when a datagram did not fit.
|
|
286
286
|
- **Diagnostics**: a trap ends the program with 134 and its message on stderr; with `SLICC_WASM_BACKTRACE=1` the wasm frames follow, named from the module's name section, or from a sidecar for a module shipped without one (`<module>.names` beside it, or the same path in a `<package>-names` package). `SLICC_WASI_STATS=1` makes a program print its calls, counted and timed, as it ends.
|
|
287
287
|
|
|
288
288
|
### Program imports
|
|
@@ -359,9 +359,21 @@ await kernel.run(['curl', '-sS', 'https://registry.npmjs.org/@ai-ecoverse/wasm-b
|
|
|
359
359
|
```
|
|
360
360
|
|
|
361
361
|
- **Sockets**: `AF_INET` stream sockets on `127.x` and `AF_UNIX` sockets are kernel descriptors on one loopback network per kernel, so processes can serve and connect to each other. They survive `dup`, `fork` and `exec` and work with `select`, `poll` and WASIX epoll, like pipe ends. A bind of an `AF_UNIX` socket creates its path in OPFS. Other addresses are unreachable (`ENETUNREACH`) unless the [uplink](#uplink) routes them: otherwise programs reach the outside world only through the proxy.
|
|
362
|
+
|
|
363
|
+
#### Datagram sockets (UDP)
|
|
364
|
+
|
|
365
|
+
WASIX programs get `AF_INET`/`AF_INET6` `SOCK_DGRAM` sockets (`AF_UNIX` datagrams are `EPROTONOSUPPORT`). They are kernel descriptors like stream sockets: they survive `dup`, `fork` and `exec`, and work with `select`, `poll`, WASIX epoll and `fstat` (`SOCK_DGRAM`).
|
|
366
|
+
- **Addresses:** `bind` follows the stream rules: loopback or `0.0.0.0`/`::`, port `0` for an ephemeral port, the kernel's reserved ports refused (`EADDRINUSE`), any other address `EADDRNOTAVAIL`. The first send binds an unbound socket. UDP and TCP ports are separate.
|
|
367
|
+
- **Where a datagram goes** is decided as for a TCP `connect`, by the same route table: loopback (`127.0.0.0/8`, `::1`, `0.0.0.0`) is delivered inside the kernel, an address the uplink routes goes through `uplink.udp` when `traits.udp` is set, and everything else is `ENETUNREACH`: `host.slicc.internal`, link-local (`169.254.0.0/16`, so the metadata service too), reserved, multicast and broadcast (`224.0.0.0/4`, `240.0.0.0/4`, `255.255.255.255`, `ff00::/8`), with or without an uplink; an unspecified destination (`0.0.0.0`, `::`) is the kernel's loopback, as for TCP. Such a datagram never reaches the uplink.
|
|
368
|
+
- **One datagram per call:** `recv`/`read` returns one datagram, and a buffer that is too short gets its first bytes and loses the rest. `MSG_PEEK` leaves it queued. A datagram is at most 65 507 bytes (65 527 to an IPv6 address); a larger send is `EMSGSIZE`. A send with no address on an unconnected socket is `EDESTADDRREQ`.
|
|
369
|
+
- **`connect`** sets the default destination and keeps only datagrams from it. On a connected socket, a datagram to a loopback port nobody bound, or an uplink's report that the destination is unreachable, fails the next send or receive once with `ECONNREFUSED` (or the uplink's `EACCES`, `ENETUNREACH`, `EHOSTUNREACH`), and the socket polls readable until then (epoll reports `EPOLLERR` with `EPOLLIN`), as on Linux. An unconnected socket is told nothing.
|
|
370
|
+
- **Queues:** at most 512 datagrams or 256 KiB wait to be read; more are dropped. A socket polls writable while less than 256 KiB is on its way through the uplink.
|
|
371
|
+
- **Options:** `SO_TYPE`, `SO_ERROR`, `SO_DOMAIN`, `SO_RCVBUF` and `SO_SNDBUF` answer; `SO_BROADCAST` can't be turned on (`EOPNOTSUPP`), and the `IP_MULTICAST_*` options are `ENOPROTOOPT`. There is no multicast or broadcast.
|
|
372
|
+
- Emscripten programs have no datagram sockets yet.
|
|
362
373
|
- **The proxy** listens on `127.0.0.1:3128` from the first connection on. It takes absolute-form `http:` requests and `CONNECT` tunnels, which it terminates with a certificate for the requested host, issued by the kernel's own CA, then hands each request to the transport. Programs start with `http_proxy`, `https_proxy` (and the upper-case names) pointing at it, `no_proxy` covering loopback and the [node name](#node-name), and `SSL_CERT_FILE`, `CURL_CA_BUNDLE` and `GIT_SSL_CAINFO` pointing at the CA bundle in `/etc/ssl/certs/slicc-kernel-ca.pem`; variables in `env` override them. The CA's key never leaves WebCrypto; it is kept in IndexedDB, in `<metadata>-ca`, or in `slicc-kernel-ca` when `metadata` is the default or `false`, so kernels on one origin share one CA whether or not they keep metadata. `ca` names another database, and `ca: false` keeps a CA in memory for the kernel's lifetime. Each kernel adds its CA to the bundle instead of replacing it, so every kernel on the origin stays trusted; the bundle keeps the newest 16.
|
|
363
374
|
- **TLS** needs [`@ai-ecoverse/wasm-tls-engine`](https://www.npmjs.com/package/@ai-ecoverse/wasm-tls-engine) installed under `modules`, like a command. Without it, `CONNECT` to port 443 is answered `501` with the reason. A tunnel to another port is accepted, and what the program sends first decides: plain HTTP is served as `http://host:port`, without TLS, and a TLS handshake is terminated as above, or the tunnel closes when it cannot be.
|
|
364
375
|
- **The transport** is an object on the page with `traits` (`manualRedirects`, `encodedBodies`, `maxRequestBody`, and `crossOrigin`: `'cors'` or `'any'`) and `fetch(request)`, which answers `{ status, statusText, headers, body, cancel }` with `body` an async iterable of `Uint8Array`, and optionally `url` and `redirected: true` when it followed redirects itself (`fetchTransport()` does), so a JS program's `fetch` reports where it ended. The kernel pulls the body one chunk at a time, so a slow program slows the download. A rejection with a numeric `status` is answered with that status; any other with `502`. `fetchTransport()` is the plain `fetch` of the page, bound by CORS; its `hint` option is appended to the message of a request it could not make, so a program's 502 can name a way around CORS (`registry.npmjs.org` and jsDelivr allow it); an embedder with a way around CORS passes its own. A response body that delivers no data for `bodyIdleMs` (default 5 minutes; `0` or `Infinity` turns it off) fails with `ETIMEDOUT` instead of waiting forever, which matters because Chrome under heavy load can stop handing a page the rest of a body. A slow body that keeps delivering is never cut off.
|
|
376
|
+
- **Traits of the embedder's own:** `traits` may carry more fields than the kernel reads. The kernel keeps them, and attached clients get them as they are (`client.transport.traits`, `onTraits`). In TypeScript, `NetworkTraits` types its own fields and leaves any other as `unknown`. One is typed: `exit`, the default exit an embedder routes through, `{ kind, caps }` (`NetworkExit`). `kind` is `'local'`, `'remote'` or `'tailnet'` (or another string), `caps` lists what the exit carries (`['net', 'http', 'ws']`), and further fields are `unknown`. When traits change later, call `kernel.updateTransportTraits()`.
|
|
365
377
|
- **Redirect hops** ([#302](https://github.com/ai-ecoverse/slicc-kernel/issues/302)): a transport that follows redirects but can see them (a browser extension's `webRequest.onBeforeRedirect` can) sets `traits.redirectHops` and answers `redirects: [{ status, url, headers }]`, one entry per 3xx it followed, in order (`NetworkRedirectHop`). The kernel then treats it as one that does not follow. Programs get each 3xx as it was answered: through the proxy, curl, requests and `hf` see the real redirect, `X-Repo-Commit` and `ETag` included, and `ctx.fetch` with `redirect: 'manual'` answers it. The rest of the chain is held for the process that asked, at most 30 s and 8 chains per process (64 in all), with the final body unread. A follow-up from that process, with the method the redirect leads to and the same `Range`, `If-None-Match`, `If-Modified-Since`, `If-Range` and `Accept-Encoding` (and an `Authorization` if the first had one), is answered from it without a second request; any other request goes to the transport. A chain that expires or is pushed out is cancelled, and so is one whose follow-up arrives and does not match. After a redirect to another origin the follow-up is expected without `Authorization`, which HTTP clients drop when they follow one; a `Location` matches without its `#fragment`. The page's own `fetch` cannot show a redirect, so `fetchTransport()` in a page stays a transport that follows, and `redirect: 'manual'` fails there as before. `fetchTransport({ redirect: 'manual' })` hands redirects back unfollowed where the platform's `fetch` shows them (Node; a browser answers an opaque status 0), and `nodeTransport()` does so. A kernel socket also records who connected it (`SO_PEERCRED`): the connecting process's pid, uid and gid on the accepting side, and the listener's on the connecting side, kept when the socket passes to another process. The proxy uses this to keep each process's held chains its own.
|
|
366
378
|
- **WebSockets:** a `GET` with `Upgrade: websocket`, in absolute form or inside a tunnel, is bridged to the transport's optional `websocket({ url, protocols, headers, signal })`. It answers `{ protocol, buffered, send(data), messages, close(code, reason), closed }`, with `messages` an async iterable of strings and `Uint8Array`s and `closed` resolving with `{ code, reason }`; a transport that has it says so with `traits.websocket`. The program gets `101 Switching Protocols` with the far end's protocol, then its frames go out as messages and the far end's come back as frames. Pings are answered by the proxy, and close codes pass both ways (a code that may not go on the wire becomes 1000 or 1011). A message over 64 MiB, or more than 16 MiB of payload from the far end waiting for the program, closes with 1009. The proxy stops reading the program while 1 MiB of its messages wait to be sent. A refused connection is `502` with the reason. A transport without WebSockets answers `501` with a message naming the option below, and the kernel logs a warning once, so a program never hangs. `fetchTransport({ webSocket: true })` opens them with the page's `WebSocket` (or pass a constructor); they are off by default because a browser WebSocket carries the user's cookies for the target site, which `fetch` here never sends (`credentials: 'omit'`), so a program could reach a site as the user. A page also cannot send headers on a WebSocket, and an `https:` page cannot open `ws:` to anything but loopback, which is a `502` that says so. `nodeTransport()` has WebSockets on (Node keeps no cookies) and sends the request's headers. An open fails as soon as Node's `WebSocket` reports an error, also on Node 22, which never follows the error with a close. A transport's `websocket()` may reject with an `Error` whose `code` is an errno, and the kernel hands that on to the program. Node's `WebSocket` doesn't say why an open failed, so when one fails, `nodeTransport()` finds out: it looks up the host's name (`EAI_NONAME` when that fails), then makes one TCP connection to the target, or a TLS one for `wss:`, for at most 3 s, and closes it at once. A refusal there is `ECONNREFUSED`, an unreachable host `EHOSTUNREACH` or `ENETUNREACH`, no answer `ETIMEDOUT`, and a TLS failure (a bad certificate) or a server that takes the connection but refused the upgrade `EPROTO`. A server that refused the upgrade therefore sees a second connection. The diagnosis never runs when an open succeeds, and never in a page (`fetchTransport()`), whose failed opens stay `ECONNREFUSED`. The credential-free path for a page is the coming slicc-node relay (`localProxyTransport()`, phase 2 of [#204](https://github.com/ai-ecoverse/slicc-kernel/issues/204)), which has no WebSockets yet.
|
|
367
379
|
- **`localProxyTransport({ url, key })`** sends every request to a local proxy on loopback, such as [slicc-node](https://github.com/ai-ecoverse/slicc-node) or slicc-swift, which fetches it without CORS. Redirects reach the program unfollowed, with every `Set-Cookie`; bodies arrive decoded. `probeLocalProxy({ url, key })` resolves with the proxy's capabilities, or `null` when it is gone or refuses the key, so a page can fall back to `fetchTransport()`. `checkLocalProxy({ url, key })` says why: it resolves with `{ state }`, which is `ready` (with `probe`), `blocked` (the user denied Chrome's Local Network Access permission, `loopback-network`), `unreachable` (nothing answered; its `permission` is `granted`, `prompt` or `unknown`, and with `prompt` the page cannot tell a dismissed prompt from a proxy that is not running, since Chrome asks only once it has a connection), `refused` (with `status` and `error`, such as a stale key) or `incompatible`. The permission is not consulted when the page itself is on loopback. Both take a `fetch` option, and the transport `maxRequestBody` (default 64 MiB) and `bodyIdleMs` like `fetchTransport`.
|
|
@@ -381,7 +393,7 @@ The kernel has one node name, `slicc` unless `createKernel({ hostname })` (or `c
|
|
|
381
393
|
|
|
382
394
|
`createKernel({ network: { uplink } })` (or `createNodeKernel`) gives the kernel a page-owned way into another network, such as a tailnet. It is the whole kernel's: while the page passes an uplink, every process gets its names. The uplink is an object with:
|
|
383
395
|
|
|
384
|
-
- `traits`: `{ tcp: true, udp
|
|
396
|
+
- `traits`: `{ tcp: true, udp, ipv6 }`. Without `ipv6: true`, the kernel asks only for, and keeps only, IPv4 addresses. With `udp: true` and a `udp` function, routed datagrams go through it.
|
|
385
397
|
- `routes`: the first route table, `{ prefixes, exit }`, which `kernel.setRoutes` replaces when it changes. The kernel matches it itself, so a destination outside it costs no round trip.
|
|
386
398
|
- `resolve(name, family, signal)`: resolves with the name's addresses, as a list or as `{ addresses, ttl }` (seconds). `[]` means the name is not the uplink's. `family` is `4`, `6` or `0` (either), and `signal` aborts when the kernel stops waiting.
|
|
387
399
|
- `dial({ network: 'tcp', host, port, signal })`: opens a TCP connection and resolves with `{ localAddr, remoteAddr, read(), write(bytes), closeWrite(), close() }`.
|
|
@@ -404,6 +416,12 @@ The kernel has one node name, `slicc` unless `createKernel({ hostname })` (or `c
|
|
|
404
416
|
3. An address the route table matches goes through `dial`.
|
|
405
417
|
4. Anything else is `ENETUNREACH`.
|
|
406
418
|
|
|
419
|
+
- `udp({ family, signal })` (optional): opens a datagram socket for `family` `4` or `6` and resolves with `{ localAddr, send(bytes, host, port), messages, close() }`.
|
|
420
|
+
- The kernel opens one per program socket and family on its first routed send, and closes it with the socket.
|
|
421
|
+
- `send` resolves once the datagram is on its way. A rejection's `code` (`ENETUNREACH`, `EHOSTUNREACH`, `EACCES`, `EMSGSIZE`) is what the program sees, and any other is `EHOSTUNREACH`.
|
|
422
|
+
- `messages` yields `{ bytes, host, port }` for each datagram that arrives, and may yield `{ error, host, port }` when a destination turned out unreachable (`error` is `EACCES`, `ENETUNREACH` or `EHOSTUNREACH`). Its end makes the next send open a new one.
|
|
423
|
+
- `host` is always an address: the kernel resolves names before it sends.
|
|
424
|
+
|
|
407
425
|
`bind()` still takes only loopback and `0.0.0.0`, so nothing listens on an uplink address.
|
|
408
426
|
|
|
409
427
|
An uplink connection is an ordinary socket fd: it survives `dup`, `fork` and `exec`, and works with `select`, `poll` and WASIX epoll.
|
package/dist/index.js
CHANGED
|
@@ -562,6 +562,9 @@ var Routes = class {
|
|
|
562
562
|
};
|
|
563
563
|
|
|
564
564
|
// src/kernel/net/uplink.ts
|
|
565
|
+
function carriesUdp(uplink) {
|
|
566
|
+
return uplink.traits.udp === true && typeof uplink.udp === "function";
|
|
567
|
+
}
|
|
565
568
|
function answerOf(result) {
|
|
566
569
|
const answer = Array.isArray(result) ? { addresses: result } : result;
|
|
567
570
|
const addresses = Array.isArray(answer?.addresses) ? answer.addresses.filter((a) => typeof a === "string") : [];
|
|
@@ -599,12 +602,7 @@ function serveUplink(peer, uplink) {
|
|
|
599
602
|
return found;
|
|
600
603
|
};
|
|
601
604
|
const dial = async (call, signal) => {
|
|
602
|
-
|
|
603
|
-
if (kind === "loopback" || kind === "host" || kind === "reserved") {
|
|
604
|
-
throw Object.assign(new Error(`the uplink does not carry ${call.host}`), {
|
|
605
|
-
code: "ENETUNREACH"
|
|
606
|
-
});
|
|
607
|
-
}
|
|
605
|
+
refuseLocal(call.host);
|
|
608
606
|
const { host, port } = call;
|
|
609
607
|
const opened = await uplink.dial({ network: "tcp", host, port, signal });
|
|
610
608
|
if (signal.aborted || closed) {
|
|
@@ -621,6 +619,7 @@ function serveUplink(peer, uplink) {
|
|
|
621
619
|
conns.delete(cid);
|
|
622
620
|
found?.close();
|
|
623
621
|
};
|
|
622
|
+
const udp = new UdpServer(uplink, () => closed);
|
|
624
623
|
return {
|
|
625
624
|
answer(call) {
|
|
626
625
|
switch (call.uplink) {
|
|
@@ -648,18 +647,82 @@ function serveUplink(peer, uplink) {
|
|
|
648
647
|
}));
|
|
649
648
|
case "close-write":
|
|
650
649
|
return conns.get(call.cid)?.closeWrite();
|
|
651
|
-
|
|
650
|
+
case "close":
|
|
652
651
|
return drop(call.cid);
|
|
652
|
+
case "udp-close":
|
|
653
|
+
return udp.drop(call.cid);
|
|
654
|
+
default:
|
|
655
|
+
return run(call.uid, (signal) => udp.answer(call, signal));
|
|
653
656
|
}
|
|
654
657
|
},
|
|
655
658
|
close() {
|
|
656
659
|
closed = true;
|
|
657
660
|
for (const abort of pending.values()) abort.abort();
|
|
658
661
|
for (const cid of [...conns.keys()]) drop(cid);
|
|
662
|
+
udp.close();
|
|
659
663
|
}
|
|
660
664
|
};
|
|
661
665
|
}
|
|
666
|
+
function copied(message) {
|
|
667
|
+
if ("bytes" in message) return { ...message, bytes: message.bytes.slice() };
|
|
668
|
+
return { error: String(message.error), host: message.host, port: message.port };
|
|
669
|
+
}
|
|
670
|
+
function refuseLocal(host) {
|
|
671
|
+
const kind = NEVER.classify(host);
|
|
672
|
+
if (kind === "loopback" || kind === "host" || kind === "reserved") {
|
|
673
|
+
throw Object.assign(new Error(`the uplink does not carry ${host}`), { code: "ENETUNREACH" });
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
var UdpServer = class {
|
|
677
|
+
sockets = /* @__PURE__ */ new Map();
|
|
678
|
+
next = 0;
|
|
679
|
+
uplink;
|
|
680
|
+
closed;
|
|
681
|
+
constructor(uplink, closed) {
|
|
682
|
+
this.uplink = uplink;
|
|
683
|
+
this.closed = closed;
|
|
684
|
+
}
|
|
685
|
+
socket(cid) {
|
|
686
|
+
const found = this.sockets.get(cid);
|
|
687
|
+
if (!found) throw Object.assign(new Error("no such uplink socket"), { code: "EBADF" });
|
|
688
|
+
return found;
|
|
689
|
+
}
|
|
690
|
+
async answer(call, signal) {
|
|
691
|
+
if (call.uplink === "udp-open") {
|
|
692
|
+
if (!this.uplink.udp || this.uplink.traits.udp !== true) {
|
|
693
|
+
throw Object.assign(new Error("the uplink carries no datagrams"), { code: "ENETUNREACH" });
|
|
694
|
+
}
|
|
695
|
+
const opened = await this.uplink.udp({ family: call.family, signal });
|
|
696
|
+
if (signal.aborted || this.closed()) {
|
|
697
|
+
opened.close();
|
|
698
|
+
return void 0;
|
|
699
|
+
}
|
|
700
|
+
const cid = ++this.next;
|
|
701
|
+
this.sockets.set(cid, { udp: opened, messages: opened.messages[Symbol.asyncIterator]() });
|
|
702
|
+
return { uplink: "udp-opened", uid: call.uid, cid, localAddr: opened.localAddr };
|
|
703
|
+
}
|
|
704
|
+
if (call.uplink === "udp-send") {
|
|
705
|
+
refuseLocal(call.host);
|
|
706
|
+
await this.socket(call.cid).udp.send(call.bytes, call.host, call.port);
|
|
707
|
+
return { uplink: "udp-sent", uid: call.uid };
|
|
708
|
+
}
|
|
709
|
+
const next = await this.socket(call.cid).messages.next();
|
|
710
|
+
const message = next.done ? null : copied(next.value);
|
|
711
|
+
return { uplink: "udp-message", uid: call.uid, message };
|
|
712
|
+
}
|
|
713
|
+
drop(cid) {
|
|
714
|
+
const found = this.sockets.get(cid);
|
|
715
|
+
this.sockets.delete(cid);
|
|
716
|
+
found?.udp.close();
|
|
717
|
+
}
|
|
718
|
+
close() {
|
|
719
|
+
for (const cid of [...this.sockets.keys()]) this.drop(cid);
|
|
720
|
+
}
|
|
721
|
+
};
|
|
662
722
|
function transferOf(reply) {
|
|
723
|
+
if (reply.uplink === "udp-message" && reply.message && "bytes" in reply.message) {
|
|
724
|
+
return [reply.message.bytes.buffer];
|
|
725
|
+
}
|
|
663
726
|
if (reply.uplink !== "data" || !reply.bytes) return void 0;
|
|
664
727
|
return [reply.bytes.buffer];
|
|
665
728
|
}
|
|
@@ -2123,7 +2186,7 @@ function dialer(call) {
|
|
|
2123
2186
|
}
|
|
2124
2187
|
function uplinkInit(uplink) {
|
|
2125
2188
|
if (!uplink) return {};
|
|
2126
|
-
const traits = { tcp: true, udp:
|
|
2189
|
+
const traits = { tcp: true, udp: carriesUdp(uplink), ipv6: uplink.traits.ipv6 === true };
|
|
2127
2190
|
return { uplink: { traits, ...uplink.routes ? { routes: uplink.routes } : {} } };
|
|
2128
2191
|
}
|
|
2129
2192
|
function initCall(options, transport) {
|