@secure-exec/core 0.1.1-rc.3 → 0.2.0-rc.1

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.
Files changed (102) hide show
  1. package/dist/esm-compiler.d.ts +5 -1
  2. package/dist/esm-compiler.js +5 -1
  3. package/dist/fs-helpers.d.ts +1 -1
  4. package/dist/generated/isolate-runtime.d.ts +15 -15
  5. package/dist/generated/isolate-runtime.js +15 -15
  6. package/dist/index.d.ts +24 -5
  7. package/dist/index.js +23 -3
  8. package/dist/isolate-runtime/apply-custom-global-policy.js +3 -3
  9. package/dist/isolate-runtime/apply-timing-mitigation-freeze.js +2 -2
  10. package/dist/isolate-runtime/apply-timing-mitigation-off.js +2 -2
  11. package/dist/isolate-runtime/bridge-attach.js +2 -2
  12. package/dist/isolate-runtime/bridge-initial-globals.js +145 -6
  13. package/dist/isolate-runtime/eval-script-result.js +1 -1
  14. package/dist/isolate-runtime/global-exposure-helpers.js +2 -2
  15. package/dist/isolate-runtime/init-commonjs-module-globals.js +2 -2
  16. package/dist/isolate-runtime/override-process-cwd.js +1 -1
  17. package/dist/isolate-runtime/override-process-env.js +1 -1
  18. package/dist/isolate-runtime/require-setup.js +1600 -338
  19. package/dist/isolate-runtime/set-commonjs-file-globals.js +2 -2
  20. package/dist/isolate-runtime/set-stdin-data.js +1 -1
  21. package/dist/isolate-runtime/setup-dynamic-import.js +47 -19
  22. package/dist/isolate-runtime/setup-fs-facade.js +62 -23
  23. package/dist/kernel/command-registry.d.ts +44 -0
  24. package/dist/kernel/command-registry.js +114 -0
  25. package/dist/kernel/device-layer.d.ts +12 -0
  26. package/dist/kernel/device-layer.js +262 -0
  27. package/dist/kernel/dns-cache.d.ts +29 -0
  28. package/dist/kernel/dns-cache.js +52 -0
  29. package/dist/kernel/fd-table.d.ts +84 -0
  30. package/dist/kernel/fd-table.js +278 -0
  31. package/dist/kernel/file-lock.d.ts +34 -0
  32. package/dist/kernel/file-lock.js +123 -0
  33. package/dist/kernel/host-adapter.d.ts +50 -0
  34. package/dist/kernel/host-adapter.js +8 -0
  35. package/dist/kernel/index.d.ts +36 -0
  36. package/dist/kernel/index.js +34 -0
  37. package/dist/kernel/inode-table.d.ts +43 -0
  38. package/dist/kernel/inode-table.js +85 -0
  39. package/dist/kernel/kernel.d.ts +9 -0
  40. package/dist/kernel/kernel.js +1396 -0
  41. package/dist/kernel/permissions.d.ts +27 -0
  42. package/dist/kernel/permissions.js +118 -0
  43. package/dist/kernel/pipe-manager.d.ts +64 -0
  44. package/dist/kernel/pipe-manager.js +267 -0
  45. package/dist/kernel/proc-layer.d.ts +11 -0
  46. package/dist/kernel/proc-layer.js +501 -0
  47. package/dist/kernel/process-table.d.ts +124 -0
  48. package/dist/kernel/process-table.js +631 -0
  49. package/dist/kernel/pty.d.ts +108 -0
  50. package/dist/kernel/pty.js +541 -0
  51. package/dist/kernel/socket-table.d.ts +305 -0
  52. package/dist/kernel/socket-table.js +1124 -0
  53. package/dist/kernel/timer-table.d.ts +54 -0
  54. package/dist/kernel/timer-table.js +108 -0
  55. package/dist/kernel/types.d.ts +500 -0
  56. package/dist/kernel/types.js +89 -0
  57. package/dist/kernel/user.d.ts +29 -0
  58. package/dist/kernel/user.js +35 -0
  59. package/dist/kernel/vfs.d.ts +54 -0
  60. package/dist/kernel/vfs.js +8 -0
  61. package/dist/kernel/wait.d.ts +45 -0
  62. package/dist/kernel/wait.js +112 -0
  63. package/dist/kernel/wstatus.d.ts +21 -0
  64. package/dist/kernel/wstatus.js +33 -0
  65. package/dist/module-resolver.d.ts +4 -0
  66. package/dist/module-resolver.js +4 -0
  67. package/dist/package-bundler.d.ts +6 -1
  68. package/dist/runtime-driver.d.ts +3 -1
  69. package/dist/shared/bridge-contract.d.ts +329 -20
  70. package/dist/shared/bridge-contract.js +60 -5
  71. package/dist/shared/console-formatter.js +8 -4
  72. package/dist/shared/global-exposure.js +269 -19
  73. package/dist/shared/in-memory-fs.d.ts +30 -11
  74. package/dist/shared/in-memory-fs.js +383 -109
  75. package/dist/shared/permissions.d.ts +4 -6
  76. package/dist/shared/permissions.js +19 -39
  77. package/dist/types.d.ts +8 -159
  78. package/dist/types.js +5 -0
  79. package/package.json +12 -22
  80. package/dist/bridge/active-handles.d.ts +0 -22
  81. package/dist/bridge/active-handles.js +0 -55
  82. package/dist/bridge/child-process.d.ts +0 -99
  83. package/dist/bridge/child-process.js +0 -670
  84. package/dist/bridge/fs.d.ts +0 -281
  85. package/dist/bridge/fs.js +0 -2235
  86. package/dist/bridge/index.d.ts +0 -10
  87. package/dist/bridge/index.js +0 -41
  88. package/dist/bridge/module.d.ts +0 -75
  89. package/dist/bridge/module.js +0 -308
  90. package/dist/bridge/network.d.ts +0 -350
  91. package/dist/bridge/network.js +0 -2050
  92. package/dist/bridge/os.d.ts +0 -13
  93. package/dist/bridge/os.js +0 -256
  94. package/dist/bridge/polyfills.d.ts +0 -2
  95. package/dist/bridge/polyfills.js +0 -11
  96. package/dist/bridge/process.d.ts +0 -89
  97. package/dist/bridge/process.js +0 -1015
  98. package/dist/bridge.js +0 -12496
  99. package/dist/python-runtime.d.ts +0 -16
  100. package/dist/python-runtime.js +0 -45
  101. package/dist/runtime.d.ts +0 -31
  102. package/dist/runtime.js +0 -69
@@ -0,0 +1,1124 @@
1
+ /**
2
+ * Virtual socket table.
3
+ *
4
+ * Manages kernel-level sockets: create, bind, listen, accept, connect,
5
+ * send, recv, close, poll, per-process isolation, and resource limits.
6
+ * Loopback connections are routed entirely in-kernel without touching
7
+ * the host network stack.
8
+ */
9
+ import { WaitQueue } from "./wait.js";
10
+ import { KernelError, SA_RESTART } from "./types.js";
11
+ // ---------------------------------------------------------------------------
12
+ // Socket constants
13
+ // ---------------------------------------------------------------------------
14
+ export const AF_INET = 2;
15
+ export const AF_INET6 = 10;
16
+ export const AF_UNIX = 1;
17
+ export const SOCK_STREAM = 1;
18
+ export const SOCK_DGRAM = 2;
19
+ // Socket option levels
20
+ export const SOL_SOCKET = 1;
21
+ export const IPPROTO_TCP = 6;
22
+ // Socket options (SOL_SOCKET level)
23
+ export const SO_REUSEADDR = 2;
24
+ export const SO_KEEPALIVE = 9;
25
+ export const SO_RCVBUF = 8;
26
+ export const SO_SNDBUF = 7;
27
+ // TCP options (IPPROTO_TCP level)
28
+ export const TCP_NODELAY = 1;
29
+ // Send/recv flags
30
+ export const MSG_PEEK = 0x2;
31
+ export const MSG_DONTWAIT = 0x40;
32
+ export const MSG_NOSIGNAL = 0x4000;
33
+ // UDP limits
34
+ export const MAX_DATAGRAM_SIZE = 65535;
35
+ export const MAX_UDP_QUEUE_DEPTH = 128;
36
+ const EPHEMERAL_PORT_MIN = 49152;
37
+ const EPHEMERAL_PORT_MAX = 65535;
38
+ // File type for socket files in VFS
39
+ export const S_IFSOCK = 0o140000;
40
+ export function isInetAddr(addr) {
41
+ return "host" in addr;
42
+ }
43
+ export function isUnixAddr(addr) {
44
+ return "path" in addr;
45
+ }
46
+ // ---------------------------------------------------------------------------
47
+ // Address key helper
48
+ // ---------------------------------------------------------------------------
49
+ /** Canonical string key for a socket address ("host:port" or unix path). */
50
+ export function addrKey(addr) {
51
+ if (isInetAddr(addr))
52
+ return `${addr.host}:${addr.port}`;
53
+ return addr.path;
54
+ }
55
+ /** Canonical string key for a socket option ("level:optname"). */
56
+ export function optKey(level, optname) {
57
+ return `${level}:${optname}`;
58
+ }
59
+ // ---------------------------------------------------------------------------
60
+ // SocketTable
61
+ // ---------------------------------------------------------------------------
62
+ const DEFAULT_MAX_SOCKETS = 1024;
63
+ export class SocketTable {
64
+ sockets = new Map();
65
+ nextSocketId = 1;
66
+ maxSockets;
67
+ networkCheck;
68
+ hostAdapter;
69
+ vfs;
70
+ getSignalState;
71
+ /** Bound/listening address → socket ID. Used for EADDRINUSE and TCP routing. */
72
+ listeners = new Map();
73
+ /** Bound UDP address → socket ID. Separate from TCP listeners. */
74
+ udpBindings = new Map();
75
+ constructor(options) {
76
+ this.maxSockets = options?.maxSockets ?? DEFAULT_MAX_SOCKETS;
77
+ this.networkCheck = options?.networkCheck;
78
+ this.hostAdapter = options?.hostAdapter;
79
+ this.vfs = options?.vfs;
80
+ this.getSignalState = options?.getSignalState;
81
+ }
82
+ /**
83
+ * Create a new socket owned by the given process.
84
+ * Returns the kernel socket ID.
85
+ */
86
+ create(domain, type, protocol, pid) {
87
+ if (this.sockets.size >= this.maxSockets) {
88
+ throw new KernelError("EMFILE", "too many open sockets");
89
+ }
90
+ const id = this.nextSocketId++;
91
+ const socket = {
92
+ id,
93
+ domain,
94
+ type,
95
+ protocol,
96
+ state: "created",
97
+ nonBlocking: false,
98
+ options: new Map(),
99
+ pid,
100
+ readBuffer: [],
101
+ readWaiters: new WaitQueue(),
102
+ backlog: [],
103
+ backlogLimit: 0,
104
+ acceptWaiters: new WaitQueue(),
105
+ datagramQueue: [],
106
+ };
107
+ this.sockets.set(id, socket);
108
+ return id;
109
+ }
110
+ /**
111
+ * Get a socket by ID. Returns null if not found.
112
+ */
113
+ get(socketId) {
114
+ return this.sockets.get(socketId) ?? null;
115
+ }
116
+ // -------------------------------------------------------------------
117
+ // Network permission check
118
+ // -------------------------------------------------------------------
119
+ /**
120
+ * Check network permission for an operation. Throws EACCES if the
121
+ * configured policy denies the request or if no policy is set
122
+ * (deny-by-default). Loopback callers should skip this method.
123
+ */
124
+ checkNetworkPermission(op, addr) {
125
+ const request = { op };
126
+ if (addr && isInetAddr(addr)) {
127
+ request.hostname = addr.host;
128
+ }
129
+ if (!this.networkCheck) {
130
+ throw new KernelError("EACCES", `network ${op} denied (no permission policy)`);
131
+ }
132
+ const decision = this.networkCheck(request);
133
+ if (!decision?.allow) {
134
+ const reason = decision?.reason ? `: ${decision.reason}` : "";
135
+ throw new KernelError("EACCES", `network ${op} denied${reason}`);
136
+ }
137
+ }
138
+ // -------------------------------------------------------------------
139
+ // Bind / Listen / Accept
140
+ // -------------------------------------------------------------------
141
+ /**
142
+ * Bind a socket to an address. Transitions to 'bound' and registers
143
+ * the address in the listeners map for port reservation.
144
+ *
145
+ * For Unix domain sockets (UnixAddr), creates a socket file in the
146
+ * VFS if one is configured.
147
+ */
148
+ async bind(socketId, addr, options) {
149
+ const socket = this.requireSocket(socketId);
150
+ if (socket.state !== "created") {
151
+ throw new KernelError("EINVAL", "socket must be in created state to bind");
152
+ }
153
+ const boundAddr = this.assignEphemeralPort(addr, socket);
154
+ // Unix domain sockets: check VFS for existing path
155
+ if (isUnixAddr(boundAddr) && this.vfs) {
156
+ if (await this.vfs.exists(boundAddr.path)) {
157
+ throw new KernelError("EADDRINUSE", `address already in use: ${boundAddr.path}`);
158
+ }
159
+ }
160
+ // UDP uses a separate binding map from TCP
161
+ if (socket.type === SOCK_DGRAM) {
162
+ if (this.isUdpAddrInUse(boundAddr, socket)) {
163
+ throw new KernelError("EADDRINUSE", `address already in use: ${addrKey(boundAddr)}`);
164
+ }
165
+ socket.localAddr = boundAddr;
166
+ socket.state = "bound";
167
+ this.udpBindings.set(addrKey(boundAddr), socketId);
168
+ // Create socket file in VFS for Unix dgram sockets
169
+ if (isUnixAddr(boundAddr) && this.vfs) {
170
+ await this.createSocketFile(boundAddr.path, options?.mode);
171
+ }
172
+ return;
173
+ }
174
+ if (this.isAddrInUse(boundAddr, socket)) {
175
+ throw new KernelError("EADDRINUSE", `address already in use: ${addrKey(boundAddr)}`);
176
+ }
177
+ socket.localAddr = boundAddr;
178
+ socket.state = "bound";
179
+ this.listeners.set(addrKey(boundAddr), socketId);
180
+ // Create socket file in VFS for Unix stream sockets
181
+ if (isUnixAddr(boundAddr) && this.vfs) {
182
+ await this.createSocketFile(boundAddr.path, options?.mode);
183
+ }
184
+ }
185
+ /**
186
+ * Mark a bound socket as listening. The socket must already be bound.
187
+ * Checks network permission before transitioning.
188
+ *
189
+ * When `external` is true and a host adapter is available, creates a
190
+ * real TCP listener via `hostAdapter.tcpListen()` and starts an accept
191
+ * pump that feeds incoming connections into the kernel backlog.
192
+ */
193
+ async listen(socketId, backlogSize = 128, options) {
194
+ const socket = this.requireSocket(socketId);
195
+ if (socket.state !== "bound") {
196
+ throw new KernelError("EINVAL", "socket must be bound before listen");
197
+ }
198
+ socket.backlogLimit = Math.max(0, backlogSize);
199
+ // Permission check for listen
200
+ if (this.networkCheck) {
201
+ this.checkNetworkPermission("listen", socket.localAddr);
202
+ }
203
+ // External listen — delegate to host adapter
204
+ if (options?.external && this.hostAdapter && socket.localAddr && isInetAddr(socket.localAddr)) {
205
+ const hostListener = await this.hostAdapter.tcpListen(socket.localAddr.host, socket.requestedEphemeralPort ? 0 : socket.localAddr.port);
206
+ socket.hostListener = hostListener;
207
+ socket.external = true;
208
+ // Update port for ephemeral (port 0) bindings
209
+ if (socket.requestedEphemeralPort || socket.localAddr.port === 0) {
210
+ const oldKey = addrKey(socket.localAddr);
211
+ socket.localAddr = { host: socket.localAddr.host, port: hostListener.port };
212
+ // Re-register in listeners map with actual port
213
+ this.listeners.delete(oldKey);
214
+ this.listeners.set(addrKey(socket.localAddr), socketId);
215
+ }
216
+ socket.state = "listening";
217
+ this.startAcceptPump(socket);
218
+ return;
219
+ }
220
+ socket.state = "listening";
221
+ }
222
+ accept(socketId, options) {
223
+ const socket = this.requireSocket(socketId);
224
+ if (socket.state !== "listening") {
225
+ throw new KernelError("EINVAL", "socket is not listening");
226
+ }
227
+ if (socket.backlog.length === 0 && socket.nonBlocking) {
228
+ throw new KernelError("EAGAIN", "no pending connections on non-blocking socket");
229
+ }
230
+ if (!options?.block) {
231
+ const connId = socket.backlog.shift();
232
+ return connId ?? null;
233
+ }
234
+ return this.acceptBlocking(socket, options.pid);
235
+ }
236
+ /**
237
+ * Find a listening socket that matches the given address.
238
+ * Checks exact match first, then wildcard (0.0.0.0 / ::).
239
+ */
240
+ findListener(addr) {
241
+ if (isInetAddr(addr)) {
242
+ // Exact match
243
+ const sock = this.getListeningSocket(`${addr.host}:${addr.port}`);
244
+ if (sock)
245
+ return sock;
246
+ // Wildcard IPv4
247
+ const wild4 = this.getListeningSocket(`0.0.0.0:${addr.port}`);
248
+ if (wild4)
249
+ return wild4;
250
+ // Wildcard IPv6
251
+ const wild6 = this.getListeningSocket(`:::${addr.port}`);
252
+ if (wild6)
253
+ return wild6;
254
+ return null;
255
+ }
256
+ return this.getListeningSocket(addr.path) ?? null;
257
+ }
258
+ // -------------------------------------------------------------------
259
+ // Shutdown (half-close)
260
+ // -------------------------------------------------------------------
261
+ /**
262
+ * Shut down part of a full-duplex connection.
263
+ * - 'write': peer recv() gets EOF, local send() returns EPIPE
264
+ * - 'read': local recv() returns EOF immediately
265
+ * - 'both': equivalent to shutdown('read') + shutdown('write')
266
+ */
267
+ shutdown(socketId, how) {
268
+ const socket = this.requireSocket(socketId);
269
+ if (socket.state !== "connected" && socket.state !== "write-closed" && socket.state !== "read-closed") {
270
+ throw new KernelError("ENOTCONN", "socket is not connected");
271
+ }
272
+ // Propagate half-close/full-close semantics to real host sockets so
273
+ // external TCP clients observe EOF instead of hanging on response reads.
274
+ socket.hostSocket?.shutdown(how);
275
+ if (how === "both") {
276
+ this.shutdownWrite(socket);
277
+ this.shutdownRead(socket);
278
+ socket.state = "closed";
279
+ return;
280
+ }
281
+ if (how === "write") {
282
+ this.shutdownWrite(socket);
283
+ if (socket.state === "read-closed") {
284
+ socket.state = "closed";
285
+ }
286
+ else {
287
+ socket.state = "write-closed";
288
+ }
289
+ return;
290
+ }
291
+ // how === 'read'
292
+ this.shutdownRead(socket);
293
+ if (socket.state === "write-closed") {
294
+ socket.state = "closed";
295
+ }
296
+ else {
297
+ socket.state = "read-closed";
298
+ }
299
+ }
300
+ /** Signal EOF to the peer by waking their readWaiters. */
301
+ shutdownWrite(socket) {
302
+ if (socket.peerId !== undefined) {
303
+ const peer = this.sockets.get(socket.peerId);
304
+ if (peer) {
305
+ peer.peerWriteClosed = true;
306
+ peer.readWaiters.wakeAll();
307
+ }
308
+ }
309
+ }
310
+ /** Discard unread data and mark the read side as closed. */
311
+ shutdownRead(socket) {
312
+ socket.readBuffer.length = 0;
313
+ socket.readWaiters.wakeAll();
314
+ }
315
+ // -------------------------------------------------------------------
316
+ // Socketpair
317
+ // -------------------------------------------------------------------
318
+ /**
319
+ * Create a pair of connected sockets atomically (for IPC).
320
+ * Returns [socketId1, socketId2]. Both are pre-connected with
321
+ * peerId linking, so data written to one appears in the other's
322
+ * readBuffer via send/recv.
323
+ */
324
+ socketpair(domain, type, protocol, pid) {
325
+ const id1 = this.create(domain, type, protocol, pid);
326
+ const id2 = this.create(domain, type, protocol, pid);
327
+ const sock1 = this.get(id1);
328
+ const sock2 = this.get(id2);
329
+ sock1.peerId = id2;
330
+ sock2.peerId = id1;
331
+ sock1.state = "connected";
332
+ sock2.state = "connected";
333
+ return [id1, id2];
334
+ }
335
+ // -------------------------------------------------------------------
336
+ // Socket options
337
+ // -------------------------------------------------------------------
338
+ /**
339
+ * Set a socket option. Stores the value keyed by "level:optname".
340
+ */
341
+ setsockopt(socketId, level, optname, optval) {
342
+ const socket = this.requireSocket(socketId);
343
+ socket.options.set(optKey(level, optname), optval);
344
+ }
345
+ /** Toggle non-blocking behavior for an existing socket. */
346
+ setNonBlocking(socketId, nonBlocking) {
347
+ const socket = this.requireSocket(socketId);
348
+ socket.nonBlocking = nonBlocking;
349
+ }
350
+ /**
351
+ * Get a socket option. Returns the value, or undefined if not set.
352
+ */
353
+ getsockopt(socketId, level, optname) {
354
+ const socket = this.requireSocket(socketId);
355
+ return socket.options.get(optKey(level, optname));
356
+ }
357
+ /** Get the bound/local address for a socket. */
358
+ getLocalAddr(socketId) {
359
+ const socket = this.requireSocket(socketId);
360
+ if (!socket.localAddr) {
361
+ throw new KernelError("EINVAL", "socket has no local address");
362
+ }
363
+ return socket.localAddr;
364
+ }
365
+ /** Get the connected peer address for a socket. */
366
+ getRemoteAddr(socketId) {
367
+ const socket = this.requireSocket(socketId);
368
+ if (!socket.remoteAddr) {
369
+ throw new KernelError("ENOTCONN", "socket is not connected");
370
+ }
371
+ return socket.remoteAddr;
372
+ }
373
+ // -------------------------------------------------------------------
374
+ // Connect (loopback routing)
375
+ // -------------------------------------------------------------------
376
+ /**
377
+ * Connect a socket to a remote address. For loopback (addr matches a
378
+ * kernel listener), creates a paired server-side socket and queues it
379
+ * in the listener's backlog — loopback is always allowed regardless of
380
+ * permission policy. External addresses are checked against the network
381
+ * permission policy and routed through the host adapter.
382
+ */
383
+ async connect(socketId, addr) {
384
+ const socket = this.requireSocket(socketId);
385
+ if (socket.state !== "created" && socket.state !== "bound") {
386
+ throw new KernelError("EINVAL", "socket must be in created or bound state to connect");
387
+ }
388
+ // Mirror POSIX auto-bind behavior so connected client sockets always
389
+ // expose a concrete local address/port to both peers.
390
+ if (!socket.localAddr && isInetAddr(addr)) {
391
+ socket.localAddr = this.assignEphemeralPort({
392
+ host: addr.host.includes(":") ? "::1" : "127.0.0.1",
393
+ port: 0,
394
+ }, socket);
395
+ }
396
+ // Unix domain sockets: check VFS for socket file existence
397
+ if (isUnixAddr(addr) && this.vfs) {
398
+ if (!await this.vfs.exists(addr.path)) {
399
+ throw new KernelError("ECONNREFUSED", `connection refused: ${addr.path}`);
400
+ }
401
+ }
402
+ const listener = this.findListener(addr);
403
+ if (!listener) {
404
+ // External connection — check permission (throws EACCES if denied)
405
+ if (this.networkCheck) {
406
+ this.checkNetworkPermission("connect", addr);
407
+ }
408
+ // Route through host adapter if available
409
+ if (this.hostAdapter && isInetAddr(addr)) {
410
+ if (socket.nonBlocking) {
411
+ socket.state = "connecting";
412
+ socket.remoteAddr = addr;
413
+ this.startExternalConnect(socket, addr);
414
+ throw new KernelError("EINPROGRESS", `connection in progress: ${addrKey(addr)}`);
415
+ }
416
+ const hostSocket = await this.hostAdapter.tcpConnect(addr.host, addr.port);
417
+ socket.state = "connected";
418
+ socket.external = true;
419
+ socket.remoteAddr = addr;
420
+ socket.hostSocket = hostSocket;
421
+ this.startReadPump(socket);
422
+ return;
423
+ }
424
+ throw new KernelError("ECONNREFUSED", `connection refused: ${addrKey(addr)}`);
425
+ }
426
+ // Loopback — always allowed, no permission check
427
+ if (listener.backlog.length >= listener.backlogLimit) {
428
+ throw new KernelError("ECONNREFUSED", `connection refused: backlog full for ${addrKey(addr)}`);
429
+ }
430
+ // Create server-side socket paired with the client
431
+ const serverSockId = this.create(listener.domain, listener.type, listener.protocol, listener.pid);
432
+ const serverSock = this.get(serverSockId);
433
+ // Set addresses
434
+ socket.remoteAddr = addr;
435
+ serverSock.localAddr = listener.localAddr;
436
+ serverSock.remoteAddr = socket.localAddr;
437
+ // Link peers
438
+ socket.peerId = serverSockId;
439
+ serverSock.peerId = socketId;
440
+ // Transition both to connected
441
+ socket.state = "connected";
442
+ serverSock.state = "connected";
443
+ // Queue server socket in listener's backlog
444
+ listener.backlog.push(serverSockId);
445
+ listener.acceptWaiters.wakeOne();
446
+ }
447
+ // -------------------------------------------------------------------
448
+ // Send / Recv
449
+ // -------------------------------------------------------------------
450
+ /**
451
+ * Send data to the connected peer. Writes to the peer's readBuffer
452
+ * and wakes one pending reader. Returns bytes written.
453
+ *
454
+ * Flags: MSG_NOSIGNAL suppresses SIGPIPE — returns EPIPE error
455
+ * instead of raising SIGPIPE on a broken connection.
456
+ *
457
+ * For external sockets, checks network permission before sending.
458
+ */
459
+ send(socketId, data, flags = 0) {
460
+ const socket = this.requireSocket(socketId);
461
+ const nosignal = (flags & MSG_NOSIGNAL) !== 0;
462
+ if (socket.state === "write-closed" || socket.state === "closed") {
463
+ throw new KernelError("EPIPE", nosignal
464
+ ? "broken pipe (MSG_NOSIGNAL)"
465
+ : "broken pipe: write side shut down");
466
+ }
467
+ if (socket.state !== "connected" && socket.state !== "read-closed") {
468
+ throw new KernelError("ENOTCONN", "socket is not connected");
469
+ }
470
+ // Permission check for external sockets
471
+ if (socket.external && this.networkCheck) {
472
+ this.checkNetworkPermission("connect", socket.remoteAddr);
473
+ }
474
+ // External socket: write to host socket
475
+ if (socket.external && socket.hostSocket) {
476
+ socket.hostSocket.write(new Uint8Array(data)).catch(() => {
477
+ socket.state = "closed";
478
+ socket.readWaiters.wakeAll();
479
+ });
480
+ return data.length;
481
+ }
482
+ if (socket.peerId === undefined) {
483
+ throw new KernelError("EPIPE", nosignal
484
+ ? "broken pipe (MSG_NOSIGNAL)"
485
+ : "broken pipe: peer closed");
486
+ }
487
+ const peer = this.sockets.get(socket.peerId);
488
+ if (!peer) {
489
+ socket.peerId = undefined;
490
+ throw new KernelError("EPIPE", nosignal
491
+ ? "broken pipe (MSG_NOSIGNAL)"
492
+ : "broken pipe: peer closed");
493
+ }
494
+ // Enforce SO_RCVBUF on the peer's receive buffer
495
+ const rcvBuf = peer.options.get(optKey(SOL_SOCKET, SO_RCVBUF));
496
+ if (rcvBuf !== undefined) {
497
+ let currentSize = 0;
498
+ for (const chunk of peer.readBuffer)
499
+ currentSize += chunk.length;
500
+ if (currentSize >= rcvBuf) {
501
+ throw new KernelError("EAGAIN", "peer receive buffer full");
502
+ }
503
+ }
504
+ // Copy data into peer's read buffer
505
+ peer.readBuffer.push(new Uint8Array(data));
506
+ peer.readWaiters.wakeOne();
507
+ return data.length;
508
+ }
509
+ recv(socketId, maxBytes, flags = 0, options) {
510
+ const socket = this.requireSocket(socketId);
511
+ const peek = (flags & MSG_PEEK) !== 0;
512
+ const dontwait = (flags & MSG_DONTWAIT) !== 0;
513
+ // read-closed or closed → immediate EOF
514
+ if (socket.state === "read-closed" || socket.state === "closed") {
515
+ return null;
516
+ }
517
+ if (socket.state !== "connected" && socket.state !== "write-closed") {
518
+ throw new KernelError("ENOTCONN", "socket is not connected");
519
+ }
520
+ if (socket.readBuffer.length > 0) {
521
+ if (peek) {
522
+ return this.peekFromBuffer(socket, maxBytes);
523
+ }
524
+ return this.consumeFromBuffer(socket, maxBytes);
525
+ }
526
+ // Buffer empty — check for EOF (peer gone or peer shut down write)
527
+ if (socket.peerId === undefined || !this.sockets.has(socket.peerId) || socket.peerWriteClosed) {
528
+ return null;
529
+ }
530
+ // No data available
531
+ if (socket.nonBlocking || dontwait) {
532
+ throw new KernelError("EAGAIN", socket.nonBlocking
533
+ ? "no data available on non-blocking socket"
534
+ : "no data available (MSG_DONTWAIT)");
535
+ }
536
+ if (options?.block) {
537
+ return this.recvBlocking(socket, maxBytes, flags, options.pid);
538
+ }
539
+ return null;
540
+ }
541
+ // -------------------------------------------------------------------
542
+ // UDP: sendTo / recvFrom
543
+ // -------------------------------------------------------------------
544
+ /**
545
+ * Send a datagram to a specific address (UDP only).
546
+ * For loopback, delivers to the kernel-bound UDP socket. For external
547
+ * addresses, routes through the host adapter (fire-and-forget). Sends
548
+ * to unbound ports are silently dropped (UDP semantics).
549
+ *
550
+ * Returns bytes "sent" (always data.length for UDP — drops are silent).
551
+ */
552
+ sendTo(socketId, data, flags, destAddr) {
553
+ const socket = this.requireSocket(socketId);
554
+ if (socket.type !== SOCK_DGRAM) {
555
+ throw new KernelError("EINVAL", "sendTo requires a datagram socket");
556
+ }
557
+ if (data.length > MAX_DATAGRAM_SIZE) {
558
+ throw new KernelError("EMSGSIZE", "datagram too large (max 65535 bytes)");
559
+ }
560
+ // Loopback routing — find a kernel-bound UDP socket at destAddr
561
+ const target = this.findBoundUdp(destAddr);
562
+ if (target) {
563
+ if (target.datagramQueue.length >= MAX_UDP_QUEUE_DEPTH) {
564
+ return data.length; // Silently drop
565
+ }
566
+ const srcAddr = this.getUdpSourceAddr(socket, destAddr);
567
+ target.datagramQueue.push({ data: new Uint8Array(data), srcAddr });
568
+ target.readWaiters.wakeOne();
569
+ return data.length;
570
+ }
571
+ // External routing via host adapter
572
+ if (socket.hostUdpSocket && this.hostAdapter && isInetAddr(destAddr)) {
573
+ if (this.networkCheck) {
574
+ this.checkNetworkPermission("connect", destAddr);
575
+ }
576
+ this.hostAdapter.udpSend(socket.hostUdpSocket, new Uint8Array(data), destAddr.host, destAddr.port).catch(() => { });
577
+ return data.length;
578
+ }
579
+ // No loopback target, no host adapter — silently drop (UDP semantics)
580
+ return data.length;
581
+ }
582
+ getUdpSourceAddr(socket, destAddr) {
583
+ if (!socket.localAddr) {
584
+ return isInetAddr(destAddr)
585
+ ? {
586
+ host: destAddr.host.includes(":") ? "::1" : "127.0.0.1",
587
+ port: 0,
588
+ }
589
+ : { path: destAddr.path };
590
+ }
591
+ if (isInetAddr(socket.localAddr) &&
592
+ isInetAddr(destAddr) &&
593
+ (socket.localAddr.host === "0.0.0.0" || socket.localAddr.host === "::")) {
594
+ return {
595
+ host: destAddr.host,
596
+ port: socket.localAddr.port,
597
+ };
598
+ }
599
+ return socket.localAddr;
600
+ }
601
+ /**
602
+ * Receive a datagram from a UDP socket. Returns the datagram and the
603
+ * source address, or null if no datagram is queued.
604
+ *
605
+ * Message boundaries are preserved: each sendTo produces exactly one
606
+ * recvFrom result. If the datagram exceeds maxBytes, excess is
607
+ * discarded (UDP truncation semantics).
608
+ *
609
+ * Flags: MSG_PEEK reads without consuming, MSG_DONTWAIT throws EAGAIN.
610
+ */
611
+ recvFrom(socketId, maxBytes, flags = 0) {
612
+ const socket = this.requireSocket(socketId);
613
+ if (socket.type !== SOCK_DGRAM) {
614
+ throw new KernelError("EINVAL", "recvFrom requires a datagram socket");
615
+ }
616
+ const peek = (flags & MSG_PEEK) !== 0;
617
+ const dontwait = (flags & MSG_DONTWAIT) !== 0;
618
+ if (socket.datagramQueue.length > 0) {
619
+ if (peek) {
620
+ const dgram = socket.datagramQueue[0];
621
+ const data = dgram.data.length <= maxBytes
622
+ ? new Uint8Array(dgram.data)
623
+ : new Uint8Array(dgram.data.subarray(0, maxBytes));
624
+ return { data, srcAddr: dgram.srcAddr };
625
+ }
626
+ const dgram = socket.datagramQueue.shift();
627
+ const data = dgram.data.length <= maxBytes
628
+ ? dgram.data
629
+ : dgram.data.subarray(0, maxBytes);
630
+ return { data, srcAddr: dgram.srcAddr };
631
+ }
632
+ if (dontwait) {
633
+ throw new KernelError("EAGAIN", "no datagram available (MSG_DONTWAIT)");
634
+ }
635
+ return null;
636
+ }
637
+ /**
638
+ * Set up external UDP routing for a bound datagram socket.
639
+ * Creates a host UDP socket via the host adapter and starts a recv
640
+ * pump that feeds incoming datagrams into the kernel datagramQueue.
641
+ */
642
+ async bindExternalUdp(socketId) {
643
+ const socket = this.requireSocket(socketId);
644
+ if (socket.type !== SOCK_DGRAM) {
645
+ throw new KernelError("EINVAL", "bindExternalUdp requires a datagram socket");
646
+ }
647
+ if (socket.state !== "bound") {
648
+ throw new KernelError("EINVAL", "socket must be bound before external UDP bind");
649
+ }
650
+ if (!this.hostAdapter || !socket.localAddr || !isInetAddr(socket.localAddr)) {
651
+ throw new KernelError("EINVAL", "host adapter and inet address required");
652
+ }
653
+ if (this.networkCheck) {
654
+ this.checkNetworkPermission("listen", socket.localAddr);
655
+ }
656
+ const hostUdpSocket = await this.hostAdapter.udpBind(socket.localAddr.host, socket.localAddr.port);
657
+ socket.hostUdpSocket = hostUdpSocket;
658
+ socket.external = true;
659
+ this.startUdpRecvPump(socket);
660
+ }
661
+ // -------------------------------------------------------------------
662
+ // Close / Cleanup
663
+ // -------------------------------------------------------------------
664
+ /**
665
+ * Close a socket. The caller must own the socket (per-process isolation).
666
+ * Wakes all pending waiters and frees resources.
667
+ */
668
+ close(socketId, pid) {
669
+ const socket = this.requireSocket(socketId);
670
+ if (socket.pid !== pid) {
671
+ throw new KernelError("EBADF", `socket ${socketId} not owned by pid ${pid}`);
672
+ }
673
+ this.destroySocket(socket);
674
+ }
675
+ /**
676
+ * Poll a socket for readability, writability, and hangup.
677
+ */
678
+ poll(socketId) {
679
+ const socket = this.requireSocket(socketId);
680
+ const closed = socket.state === "closed";
681
+ const readClosed = socket.state === "read-closed";
682
+ const writeClosed = socket.state === "write-closed";
683
+ // UDP: readable when datagramQueue has data
684
+ const readable = socket.type === SOCK_DGRAM
685
+ ? socket.datagramQueue.length > 0 || closed
686
+ : socket.readBuffer.length > 0 || closed || readClosed;
687
+ const writable = socket.state === "connected" ||
688
+ socket.state === "created" ||
689
+ socket.state === "read-closed" ||
690
+ (socket.type === SOCK_DGRAM && socket.state === "bound");
691
+ const hangup = closed || readClosed || writeClosed;
692
+ return { readable, writable, hangup };
693
+ }
694
+ /**
695
+ * Clean up all sockets owned by a process (called on process exit).
696
+ */
697
+ closeAllForProcess(pid) {
698
+ for (const socket of this.sockets.values()) {
699
+ if (socket.pid === pid) {
700
+ this.destroySocket(socket);
701
+ }
702
+ }
703
+ }
704
+ /**
705
+ * Clean up all sockets (called on kernel dispose).
706
+ */
707
+ disposeAll() {
708
+ for (const socket of this.sockets.values()) {
709
+ socket.readWaiters.wakeAll();
710
+ socket.acceptWaiters.wakeAll();
711
+ if (socket.hostSocket) {
712
+ socket.hostSocket.close().catch(() => { });
713
+ }
714
+ if (socket.hostListener) {
715
+ socket.hostListener.close().catch(() => { });
716
+ }
717
+ if (socket.hostUdpSocket) {
718
+ socket.hostUdpSocket.close().catch(() => { });
719
+ }
720
+ }
721
+ this.sockets.clear();
722
+ this.listeners.clear();
723
+ this.udpBindings.clear();
724
+ }
725
+ /** Number of open sockets. */
726
+ get size() {
727
+ return this.sockets.size;
728
+ }
729
+ // -----------------------------------------------------------------------
730
+ // Internal helpers
731
+ // -----------------------------------------------------------------------
732
+ /** Create a socket file in the VFS with S_IFSOCK mode. */
733
+ async createSocketFile(path, mode = 0o755) {
734
+ if (!this.vfs)
735
+ return;
736
+ await this.vfs.writeFile(path, new Uint8Array(0));
737
+ await this.vfs.chmod(path, S_IFSOCK | (mode & 0o777));
738
+ }
739
+ requireSocket(socketId) {
740
+ const socket = this.sockets.get(socketId);
741
+ if (!socket) {
742
+ throw new KernelError("EBADF", `socket ${socketId} not found`);
743
+ }
744
+ return socket;
745
+ }
746
+ /** Wait for an inbound connection, restarting when SA_RESTART applies. */
747
+ async acceptBlocking(socket, pid) {
748
+ while (true) {
749
+ const connId = socket.backlog.shift();
750
+ if (connId !== undefined)
751
+ return connId;
752
+ await this.waitForSocketWake(socket.acceptWaiters, pid, "accept");
753
+ if (socket.state !== "listening") {
754
+ throw new KernelError("EINVAL", "socket is not listening");
755
+ }
756
+ }
757
+ }
758
+ destroySocket(socket) {
759
+ // Propagate EOF to peer: clear peer link and wake readers
760
+ if (socket.peerId !== undefined) {
761
+ const peer = this.sockets.get(socket.peerId);
762
+ if (peer) {
763
+ peer.peerId = undefined;
764
+ peer.readWaiters.wakeAll();
765
+ }
766
+ }
767
+ // Close host socket for external connections
768
+ if (socket.hostSocket) {
769
+ socket.hostSocket.close().catch(() => { });
770
+ socket.hostSocket = undefined;
771
+ }
772
+ // Close host listener for external-facing server sockets
773
+ if (socket.hostListener) {
774
+ socket.hostListener.close().catch(() => { });
775
+ socket.hostListener = undefined;
776
+ }
777
+ // Close host UDP socket for external datagram sockets
778
+ if (socket.hostUdpSocket) {
779
+ socket.hostUdpSocket.close().catch(() => { });
780
+ socket.hostUdpSocket = undefined;
781
+ }
782
+ // Free listener/binding registration if this socket was bound
783
+ if (socket.localAddr) {
784
+ const key = addrKey(socket.localAddr);
785
+ if (this.listeners.get(key) === socket.id) {
786
+ this.listeners.delete(key);
787
+ }
788
+ if (this.udpBindings.get(key) === socket.id) {
789
+ this.udpBindings.delete(key);
790
+ }
791
+ }
792
+ socket.state = "closed";
793
+ socket.readBuffer.length = 0;
794
+ socket.datagramQueue.length = 0;
795
+ socket.readWaiters.wakeAll();
796
+ socket.acceptWaiters.wakeAll();
797
+ this.sockets.delete(socket.id);
798
+ }
799
+ /** Background pump: reads from host socket and feeds kernel readBuffer. */
800
+ startReadPump(socket) {
801
+ if (!socket.hostSocket)
802
+ return;
803
+ const hostSocket = socket.hostSocket;
804
+ const pump = async () => {
805
+ try {
806
+ while (socket.state !== "closed" && socket.hostSocket === hostSocket) {
807
+ const data = await hostSocket.read();
808
+ if (data === null) {
809
+ // EOF from host
810
+ socket.peerWriteClosed = true;
811
+ socket.readWaiters.wakeAll();
812
+ break;
813
+ }
814
+ socket.readBuffer.push(data);
815
+ socket.readWaiters.wakeOne();
816
+ }
817
+ }
818
+ catch {
819
+ // Connection error — mark as closed
820
+ if (socket.state !== "closed") {
821
+ socket.peerWriteClosed = true;
822
+ socket.readWaiters.wakeAll();
823
+ }
824
+ }
825
+ };
826
+ pump();
827
+ }
828
+ /** Complete a non-blocking external connect in the background. */
829
+ startExternalConnect(socket, addr) {
830
+ if (!this.hostAdapter)
831
+ return;
832
+ this.hostAdapter.tcpConnect(addr.host, addr.port).then(hostSocket => {
833
+ const current = this.sockets.get(socket.id);
834
+ if (!current || current !== socket || current.state === "closed") {
835
+ hostSocket.close().catch(() => { });
836
+ return;
837
+ }
838
+ current.state = "connected";
839
+ current.external = true;
840
+ current.remoteAddr = addr;
841
+ current.hostSocket = hostSocket;
842
+ this.startReadPump(current);
843
+ }).catch(() => {
844
+ const current = this.sockets.get(socket.id);
845
+ if (!current || current !== socket || current.state === "closed") {
846
+ return;
847
+ }
848
+ current.state = "created";
849
+ current.remoteAddr = undefined;
850
+ current.external = false;
851
+ current.hostSocket = undefined;
852
+ current.readWaiters.wakeAll();
853
+ });
854
+ }
855
+ /** Background pump: accepts incoming connections from host listener and feeds kernel backlog. */
856
+ startAcceptPump(socket) {
857
+ if (!socket.hostListener)
858
+ return;
859
+ const hostListener = socket.hostListener;
860
+ const pump = async () => {
861
+ try {
862
+ while (socket.state === "listening" && socket.hostListener === hostListener) {
863
+ const hostSocket = await hostListener.accept();
864
+ if (socket.backlog.length >= socket.backlogLimit) {
865
+ hostSocket.close().catch(() => { });
866
+ continue;
867
+ }
868
+ // Create a kernel socket for this incoming connection
869
+ const connId = this.create(socket.domain, socket.type, socket.protocol, socket.pid);
870
+ const connSock = this.get(connId);
871
+ connSock.state = "connected";
872
+ connSock.external = true;
873
+ connSock.hostSocket = hostSocket;
874
+ connSock.localAddr = socket.localAddr;
875
+ // Start read pump for the accepted socket
876
+ this.startReadPump(connSock);
877
+ // Queue in listener's backlog
878
+ socket.backlog.push(connId);
879
+ socket.acceptWaiters.wakeOne();
880
+ }
881
+ }
882
+ catch {
883
+ // Listener closed or error — stop pump
884
+ }
885
+ };
886
+ pump();
887
+ }
888
+ /** Look up a listening socket by exact address key. */
889
+ getListeningSocket(key) {
890
+ const id = this.listeners.get(key);
891
+ if (id === undefined)
892
+ return null;
893
+ const sock = this.sockets.get(id);
894
+ if (!sock || sock.state !== "listening")
895
+ return null;
896
+ return sock;
897
+ }
898
+ /** Peek up to maxBytes from a socket's readBuffer without consuming. */
899
+ peekFromBuffer(socket, maxBytes) {
900
+ const chunks = [];
901
+ let totalLen = 0;
902
+ for (const chunk of socket.readBuffer) {
903
+ if (totalLen >= maxBytes)
904
+ break;
905
+ const remaining = maxBytes - totalLen;
906
+ if (chunk.length <= remaining) {
907
+ chunks.push(chunk);
908
+ totalLen += chunk.length;
909
+ }
910
+ else {
911
+ chunks.push(chunk.subarray(0, remaining));
912
+ totalLen += remaining;
913
+ }
914
+ }
915
+ if (chunks.length === 1)
916
+ return new Uint8Array(chunks[0]);
917
+ const result = new Uint8Array(totalLen);
918
+ let offset = 0;
919
+ for (const c of chunks) {
920
+ result.set(c, offset);
921
+ offset += c.length;
922
+ }
923
+ return result;
924
+ }
925
+ /** Consume up to maxBytes from a socket's readBuffer. */
926
+ consumeFromBuffer(socket, maxBytes) {
927
+ const chunks = [];
928
+ let totalLen = 0;
929
+ while (socket.readBuffer.length > 0 && totalLen < maxBytes) {
930
+ const chunk = socket.readBuffer[0];
931
+ const remaining = maxBytes - totalLen;
932
+ if (chunk.length <= remaining) {
933
+ chunks.push(chunk);
934
+ totalLen += chunk.length;
935
+ socket.readBuffer.shift();
936
+ }
937
+ else {
938
+ chunks.push(chunk.subarray(0, remaining));
939
+ socket.readBuffer[0] = chunk.subarray(remaining);
940
+ totalLen += remaining;
941
+ }
942
+ }
943
+ if (chunks.length === 1)
944
+ return chunks[0];
945
+ const result = new Uint8Array(totalLen);
946
+ let offset = 0;
947
+ for (const c of chunks) {
948
+ result.set(c, offset);
949
+ offset += c.length;
950
+ }
951
+ return result;
952
+ }
953
+ /** Wait for readable data, restarting when SA_RESTART applies. */
954
+ async recvBlocking(socket, maxBytes, flags, pid) {
955
+ while (true) {
956
+ const result = this.recv(socket.id, maxBytes, flags);
957
+ if (result !== null)
958
+ return result;
959
+ if (!this.canBlockForRecv(socket))
960
+ return null;
961
+ await this.waitForSocketWake(socket.readWaiters, pid, "recv");
962
+ }
963
+ }
964
+ /** Check whether recv() could still yield data later instead of EOF. */
965
+ canBlockForRecv(socket) {
966
+ if (socket.state === "read-closed" || socket.state === "closed") {
967
+ return false;
968
+ }
969
+ if (socket.readBuffer.length > 0) {
970
+ return false;
971
+ }
972
+ if (socket.external) {
973
+ return !socket.peerWriteClosed;
974
+ }
975
+ return socket.peerId !== undefined && this.sockets.has(socket.peerId) && !socket.peerWriteClosed;
976
+ }
977
+ /** Wait for socket readiness or an interrupting signal. */
978
+ async waitForSocketWake(waiters, pid, op) {
979
+ const signalState = this.getSignalState?.(pid);
980
+ if (!signalState) {
981
+ const handle = waiters.enqueue();
982
+ await handle.wait();
983
+ waiters.remove(handle);
984
+ return;
985
+ }
986
+ const startSeq = signalState.deliverySeq;
987
+ const socketHandle = waiters.enqueue();
988
+ const signalHandle = signalState.signalWaiters.enqueue();
989
+ if (signalState.deliverySeq !== startSeq) {
990
+ signalHandle.wake();
991
+ }
992
+ try {
993
+ const winner = await Promise.race([
994
+ socketHandle.wait().then(() => "socket"),
995
+ signalHandle.wait().then(() => "signal"),
996
+ ]);
997
+ if (winner === "signal" && signalState.deliverySeq !== startSeq) {
998
+ if ((signalState.lastDeliveredFlags & SA_RESTART) !== 0) {
999
+ return;
1000
+ }
1001
+ throw new KernelError("EINTR", `${op} interrupted by signal ${signalState.lastDeliveredSignal ?? "unknown"}`);
1002
+ }
1003
+ }
1004
+ finally {
1005
+ waiters.remove(socketHandle);
1006
+ signalState.signalWaiters.remove(signalHandle);
1007
+ }
1008
+ }
1009
+ /** Find a bound UDP socket that matches the given address (exact + wildcard). */
1010
+ findBoundUdp(addr) {
1011
+ if (isInetAddr(addr)) {
1012
+ const sock = this.getBoundUdpSocket(`${addr.host}:${addr.port}`);
1013
+ if (sock)
1014
+ return sock;
1015
+ const wild4 = this.getBoundUdpSocket(`0.0.0.0:${addr.port}`);
1016
+ if (wild4)
1017
+ return wild4;
1018
+ const wild6 = this.getBoundUdpSocket(`:::${addr.port}`);
1019
+ if (wild6)
1020
+ return wild6;
1021
+ return null;
1022
+ }
1023
+ return this.getBoundUdpSocket(addr.path) ?? null;
1024
+ }
1025
+ /** Look up a bound UDP socket by exact address key. */
1026
+ getBoundUdpSocket(key) {
1027
+ const id = this.udpBindings.get(key);
1028
+ if (id === undefined)
1029
+ return null;
1030
+ const sock = this.sockets.get(id);
1031
+ if (!sock || sock.type !== SOCK_DGRAM)
1032
+ return null;
1033
+ return sock;
1034
+ }
1035
+ /** Check if a UDP address conflicts with an existing UDP binding. */
1036
+ isUdpAddrInUse(addr, socket) {
1037
+ if (!isInetAddr(addr)) {
1038
+ return this.udpBindings.has(addr.path);
1039
+ }
1040
+ if (socket.options.get(optKey(SOL_SOCKET, SO_REUSEADDR)) === 1)
1041
+ return false;
1042
+ if (this.udpBindings.has(addrKey(addr)))
1043
+ return true;
1044
+ const isWildcard = addr.host === "0.0.0.0" || addr.host === "::";
1045
+ for (const existingId of this.udpBindings.values()) {
1046
+ const existing = this.sockets.get(existingId);
1047
+ if (!existing?.localAddr || !isInetAddr(existing.localAddr))
1048
+ continue;
1049
+ if (existing.localAddr.port !== addr.port)
1050
+ continue;
1051
+ const existingIsWildcard = existing.localAddr.host === "0.0.0.0" || existing.localAddr.host === "::";
1052
+ if (isWildcard || existingIsWildcard)
1053
+ return true;
1054
+ }
1055
+ return false;
1056
+ }
1057
+ /** Background pump: receives datagrams from host UDP socket and feeds kernel datagramQueue. */
1058
+ startUdpRecvPump(socket) {
1059
+ if (!socket.hostUdpSocket)
1060
+ return;
1061
+ const hostUdpSocket = socket.hostUdpSocket;
1062
+ const pump = async () => {
1063
+ try {
1064
+ while (socket.state !== "closed" && socket.hostUdpSocket === hostUdpSocket) {
1065
+ const result = await hostUdpSocket.recv();
1066
+ if (socket.datagramQueue.length < MAX_UDP_QUEUE_DEPTH) {
1067
+ socket.datagramQueue.push({
1068
+ data: result.data,
1069
+ srcAddr: { host: result.remoteAddr.host, port: result.remoteAddr.port },
1070
+ });
1071
+ socket.readWaiters.wakeOne();
1072
+ }
1073
+ }
1074
+ }
1075
+ catch {
1076
+ // Socket closed or error — stop pump
1077
+ }
1078
+ };
1079
+ pump();
1080
+ }
1081
+ /** Check if an address conflicts with an existing TCP binding. */
1082
+ isAddrInUse(addr, socket) {
1083
+ if (!isInetAddr(addr)) {
1084
+ return this.listeners.has(addr.path);
1085
+ }
1086
+ // SO_REUSEADDR on the new socket skips the check
1087
+ if (socket.options.get(optKey(SOL_SOCKET, SO_REUSEADDR)) === 1)
1088
+ return false;
1089
+ // Exact match
1090
+ if (this.listeners.has(addrKey(addr)))
1091
+ return true;
1092
+ // Wildcard overlap: same port, either side is wildcard
1093
+ const isWildcard = addr.host === "0.0.0.0" || addr.host === "::";
1094
+ for (const existingId of this.listeners.values()) {
1095
+ const existing = this.sockets.get(existingId);
1096
+ if (!existing?.localAddr || !isInetAddr(existing.localAddr))
1097
+ continue;
1098
+ if (existing.localAddr.port !== addr.port)
1099
+ continue;
1100
+ const existingIsWildcard = existing.localAddr.host === "0.0.0.0" || existing.localAddr.host === "::";
1101
+ if (isWildcard || existingIsWildcard)
1102
+ return true;
1103
+ }
1104
+ return false;
1105
+ }
1106
+ /** Assign a kernel-managed ephemeral port for bind(port=0). */
1107
+ assignEphemeralPort(addr, socket) {
1108
+ if (!isInetAddr(addr) || addr.port !== 0) {
1109
+ socket.requestedEphemeralPort = false;
1110
+ return addr;
1111
+ }
1112
+ socket.requestedEphemeralPort = true;
1113
+ for (let port = EPHEMERAL_PORT_MIN; port <= EPHEMERAL_PORT_MAX; port++) {
1114
+ const candidate = { host: addr.host, port };
1115
+ const inUse = socket.type === SOCK_DGRAM
1116
+ ? this.isUdpAddrInUse(candidate, socket)
1117
+ : this.isAddrInUse(candidate, socket);
1118
+ if (!inUse) {
1119
+ return candidate;
1120
+ }
1121
+ }
1122
+ throw new KernelError("EADDRINUSE", "no ephemeral ports available");
1123
+ }
1124
+ }