Skip to content

Kernel UDP host-relay abstraction#530

Open
mho22 wants to merge 5 commits into
mainfrom
split/kernel-udp-host-relay
Open

Kernel UDP host-relay abstraction#530
mho22 wants to merge 5 commits into
mainfrom
split/kernel-udp-host-relay

Conversation

@mho22

@mho22 mho22 commented May 20, 2026

Copy link
Copy Markdown
Collaborator

First of three split PRs from #60 (emdash/explore-doom-webrtc-8179f). Adds the kernel surface and host wiring that lets the kernel route non-loopback UDP datagrams through a host-owned transport, then exposes the symmetric inbound path so the host can post datagrams back to a bound SOCK_DGRAM. No WebRTC code here — that lands in the follow-up PR.

What changes

Kernel (crates/kernel/, abi/snapshot.json)

  • HostIO::send_dgram trait method on process.rs with a default -ENETUNREACH impl. Overridden only by WasmHostIO. Every existing concrete impl (MockHostIO, etc.) compiles unchanged.
  • sys_sendto now dispatches non-loopback destinations through host.send_dgram instead of returning ENETUNREACH. Loopback FIFO branch is untouched.
  • wasm_api.rs gains a host_send_dgram import (value-returning errno convention) and a new kernel_inject_datagram export. The export scans proc.sockets for a DGRAM in Bound/Connected state with a matching bind_port and pushes onto dgram_queue; returns -ESRCH / -ECONNREFUSED on failure.
  • DoS cap: kernel_inject_datagram is bounded at MAX_INJECT_DGRAM_QUEUE_LEN = 256 per matching socket. Sized to absorb a few seconds of net-tick bursts while still bounding kernel-heap growth under a misbehaving remote feeding the queue at line rate.
  • The per-process logic is extracted into syscalls::inject_datagram_into so it's unit-testable from a Process fixture; the wasm export wrapper is the only piece that still touches PROCESS_TABLE.

Host (host/src/)

  • host/src/browser-kernel-protocol.tsInjectDatagramMessage (main → kernel-worker) and HostSendDgramMessage (kernel-worker → main).
  • host/src/browser-kernel-host.tsBrowserKernel.injectDatagram() and onHostSendDgram(handler) accessors. Both are fire-and-forget; UDP drops are indistinguishable from wire loss.
  • host/src/kernel.tsKernelCallbacks gains optional onHostSendDgram; the wasm imports table binds the kernel's host_send_dgram import to a private hostSendDgram() that forwards to the callback (returns -ENETUNREACH when unset).
  • host/src/kernel-worker.tsCentralizedKernelWorker registers an onHostSendDgram default that returns -ENETUNREACH, matching the kernel-side trait default.
  • host/src/browser-kernel-worker-entry.ts — adds the RelayHostShim class (postMessages host_send_dgram up to the main thread) and handleInjectDatagram for the inject_datagram message dispatch.

Dual-host parity (CLAUDE.md §"Two hosts")

  • Node: no host_send_dgram registration needed — the kernel-worker default returns -ENETUNREACH (matches the kernel-side trait default). node-kernel-worker-entry.ts has no inject_datagram handler (Node has no RTCDataChannel).
  • Browser: full wiring via RelayHostShim + handleInjectDatagram. The page-side RelayChannel that consumes these hooks lands in the follow-up PR.

ABI

Tests added

  • test_udp_non_loopback_routes_to_host (kernel) — covers the new sys_sendto branch via a MockHostIO that captures (src_port, dst_ip, dst_port, data).
  • test_inject_datagram_no_bound_socket-ECONNREFUSED.
  • test_inject_datagram_pushes_and_recvfrom_delivers — successful push, sys_recvfrom returns the data, src_addr/src_port restored.
  • test_inject_datagram_caps_queue_and_silently_drops_overflow — 256 pushes succeed, overflow attempts return 0, queue stays at exactly 256.
  • host/test/sendto-non-loopback.test.ts — boots NodeKernelHost + a tiny C program that bind() + sendto(10.0.0.1:1234) and asserts errno == ENETUNREACH. Closes the Node-host parity gap (the kernel-side test uses MockHostIO and never exercises kernel-worker.ts's actual default).

Stacked-PR series

This PR is the first of three split from the original branch. The follow-ups depend on this one being merged first:

  1. [this PR] Kernel UDP host-relay abstraction — kernel + host plumbing.
  2. WebRTC DataChannel relay for browser demos (split/webrtc-data-channel-relay) — standalone WebRTC chat demo + page-side RelayChannel that bridges RTCDataChannel to the abstraction landed here.
  3. Multiplayer DOOM over WebRTC (split/doom-multiplayer-over-webrtc) — chocolate-doom net stack + POSIX i_net + doom-mp demo page.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented May 21, 2026

Copy link
Copy Markdown

Phase B-1 matrix build status — pr-530-staging

ABI v12. 44 built, 0 failed, 44 total.

Package Arch Status Sha
bash wasm32 built d3f62e36
bc wasm32 built 3f9d8b42
bzip2 wasm32 built 00594792
coreutils wasm32 built 2c7d33d1
curl wasm32 built 4146a843
dash wasm32 built 21fd2b13
dinit wasm32 built fef7e94b
file wasm32 built 4d577cd7
git wasm32 built adac2e4d
grep wasm32 built 3ee0e9c4
gzip wasm32 built c6ac4ceb
kandelo-sdk wasm32 built 7d2fa7e1
lamp wasm32 built 192bc0bd
less wasm32 built 431586da
lsof wasm32 built 2c7ed293
m4 wasm32 built 6dcaf0a4
make wasm32 built b066d56d
mariadb-test wasm32 built 07f15fd5
mariadb-vfs wasm32 built 9b1847a5
mariadb-vfs wasm64 built d58f3d27
msmtpd wasm32 built 541a8631
nano wasm32 built 2b4c6f4c
nethack-browser-bundle wasm32 built 813f4cad
nethack wasm32 built f42afa6b
nginx wasm32 built 1aaf2c60
node-vfs wasm32 built 9b3f10d0
node wasm32 built d61cb6e7
php wasm32 built 894abbfb
posix-utils-lite wasm32 built 7fea317d
rootfs wasm32 built cbbee4f3
sed wasm32 built 06df07c9
shell wasm32 built 48783990
spidermonkey-node wasm32 built 4dc4e333
spidermonkey wasm32 built 98906836
tar wasm32 built fa00ec94
tcl wasm32 built d418f58d
unzip wasm32 built ae858c13
vim-browser-bundle wasm32 built 928b3750
vim wasm32 built 097341d8
wget wasm32 built b8a0829b
wordpress wasm32 built 63105217
xz wasm32 built 4e40c108
zip wasm32 built d4ca2188
zstd wasm32 built ecf8718d

Auto-generated; replaced on each push. Raw data in the publish-status workflow artifact.

mho22 and others added 5 commits May 29, 2026 19:53
Add the kernel surface needed to route UDP datagrams between user
programs and a host-owned transport (in v1, the browser's WebRTC
RelayHostShim — wiring lands in subsequent tasks).

* HostIO::send_dgram trait method (process.rs) with a default
  ENETUNREACH impl, overridden only by WasmHostIO. All existing
  concrete impls (the in-test MockHostIO and others) compile
  unchanged.
* sys_sendto (syscalls.rs) now dispatches non-loopback destinations
  through host.send_dgram instead of returning ENETUNREACH. Loopback
  FIFO branch is untouched; test_udp_loopback still passes and grew a
  one-line guard asserting the host relay stays empty on the loopback
  path. New test_udp_non_loopback_routes_to_host covers the new
  branch via a MockHostIO that captures (src_port, dst_ip, dst_port,
  data) tuples.
* wasm_api.rs gains a host_send_dgram import, the WasmHostIO wrapper
  using the value-returning errno convention, and a new
  kernel_inject_datagram export. The export scans proc.sockets for a
  DGRAM in Bound/Connected state with matching bind_port (mirroring
  sys_sendto's loopback scan) and pushes onto dgram_queue; returns
  -ESRCH / -ECONNREFUSED on failure. No wakeup call — sys_recvfrom's
  unconditional-EAGAIN behaviour is a documented follow-up and DOOM
  tolerates polling.
* abi/snapshot.json regenerated. The single added kernel_exports
  entry (kernel_inject_datagram) is additive under the post-PR-#490
  policy in docs/abi-versioning.md — ABI_VERSION stays at 11.
  scripts/check-abi-version.sh reports
  "additive-compatible change: added kernel_exports entry
  kernel_inject_datagram" and "ABI_VERSION may stay unchanged".
  The new host_send_dgram import is not tracked by the snapshot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the host-side abstraction for the kernel's UDP host-relay hooks:
BrowserKernel grows a fire-and-forget `injectDatagram()` and an
`onHostSendDgram()` subscription, and the main↔kernel-worker message
protocol gains the two new types.

* host/src/browser-kernel-protocol.ts — `InjectDatagramMessage`
  (main → kernel-worker) and `HostSendDgramMessage` (kernel-worker
  → main).
* host/src/browser-kernel-host.ts — `injectDatagram()` and
  `onHostSendDgram(handler)` accessors on BrowserKernel. The page-side
  WebRTC relay (added in a follow-up PR) uses these to bridge a
  RTCDataChannel to the kernel.

UDP drops are indistinguishable from wire-level loss, so both APIs are
fire-and-forget — no requestId, no result to await (~35 Hz cadence in
the consuming demo).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the kernel-worker side of the UDP host-relay hooks from the
previous commit. With this, an outbound `sendto()` from a user program
reaches the page-side host via a postMessage; an inbound datagram
posted via `BrowserKernel.injectDatagram` reaches the bound DGRAM
socket.

* host/src/kernel.ts — `KernelCallbacks` gains optional
  `onHostSendDgram`; the wasm imports table binds the kernel's
  `host_send_dgram` import to a private `hostSendDgram()` that reads
  the data slice out of wasm memory and forwards to the callback.
  Returns -101 (-ENETUNREACH) when no callback is registered,
  matching the kernel-side `HostIO::send_dgram` trait default.
* host/src/kernel-worker.ts — `CentralizedKernelWorker` registers an
  `onHostSendDgram` default that returns -101 (-ENETUNREACH). Inline
  comment names the override site so the next reader sees the Node
  behavior without grepping.
* host/src/browser-kernel-worker-entry.ts — adds the `RelayHostShim`
  class: a thin wrapper that postMessages
  `{ type: "host_send_dgram", ... }` up to the main thread.
  Unconditional instantiation (idle until called); wired through the
  `kw.kernel.callbacks` override block alongside `onNetListen`. Adds
  `handleInjectDatagram` + "inject_datagram" message dispatch: copies
  payload into kernel scratch, calls `kernel_inject_datagram`.

Dual-host parity (CLAUDE.md):
- Node host: no `host_send_dgram` registration needed — the
  kernel-worker default returns -ENETUNREACH (matches kernel-side
  trait default). No "inject_datagram" handler in
  node-kernel-worker-entry.ts — Node has no RelayChannel and never
  sends one.
- Browser host: full wiring via `RelayHostShim` + `handleInjectDatagram`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The kernel_inject_datagram export (cd7620f / earlier branch commit)
pushed onto proc.sockets[*].dgram_queue with no cap. Pre-WebRTC, only
intra-process loopback fed this queue, so a misbehaving local program
was the only threat model. With the WebRTC relay landed, a remote peer
over an RTCDataChannel can fill the queue at line rate; a stalled
userspace reader then grows the kernel heap without bound. One-line
DoS surface for any "play a stranger" link.

Fix:
  • Cap at MAX_INJECT_DGRAM_QUEUE_LEN = 256 per matching socket. Sized
    to absorb a few seconds of fbDOOM net-tick bursts (~35 Hz × a
    handful of stalled accepters) while still bounding kernel-heap
    growth at ~256 datagrams per process. Overflow returns 0 (UDP is
    best-effort; the upstream host has no recourse for a queue-full
    signal, and an errno would just propagate noise).
  • Extract the per-process logic into syscalls::inject_datagram_into
    so it's unit-testable with a local Process — the wasm export
    wrapper (kernel_inject_datagram in wasm_api.rs) is the only piece
    that still touches PROCESS_TABLE. (The wasm_api module only
    compiles for wasm32/wasm64; tests live in syscalls.rs which
    compiles for the host target too.)

Three new unit tests pin the contract (F3 from session-13 audit):
  • test_inject_datagram_no_bound_socket → -ECONNREFUSED
  • test_inject_datagram_pushes_and_recvfrom_delivers — successful
    push, sys_recvfrom returns the data, src_addr/src_port restored
    on the wire
  • test_inject_datagram_caps_queue_and_silently_drops_overflow —
    256 pushes succeed, 16 overflow attempts return 0, queue stays
    at exactly 256

The ESRCH path in kernel_inject_datagram (pid not in PROCESS_TABLE)
is the standard match on `table.get_mut(pid)` used by every other
kernel_* export; covered by inspection rather than by polluting
GLOBAL_PROCESS_TABLE from cargo tests that run in parallel.

ABI: kernel_inject_datagram export signature unchanged → ABI snapshot
still additive-compatible vs origin/main; ABI_VERSION unchanged.

Tested on both hosts: pure-kernel change inside wasm. cargo: 841/0
(+3). libc-test / posix-test boot the rebuilt kernel.wasm and pass.
Browser end-to-end (DOOM session) still works via the unchanged
RelayHostShim path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Rust kernel side of UDP non-loopback routing is covered by
test_udp_non_loopback_routes_to_host (syscalls.rs), but that test
overrides host_send_dgram via MockHostIO and never exercises the
actual default that ships with the Node kernel-worker
(host/src/kernel-worker.ts:1139-1154, returning -101/-ENETUNREACH).
This was DA #3 from the 2026-05-20 session-13 audit:

  > The browser side is the one we actually use the relay through,
  > and the user's manual two-browser verification covers it. The
  > Node default — which programs running under NodeKernelHost see —
  > had zero coverage. CLAUDE.md §"Two hosts" calls out this exact
  > failure mode (PR #388 / #410 — a Node-only fix breaks the
  > browser, or vice versa).

New test boots a real NodeKernelHost via the runCentralizedProgram
helper, runs a tiny C program that bind()s a DGRAM socket and
sendto()s to 10.0.0.1:1234 (non-loopback, non-broadcast, non-zero),
and asserts:
  • sendto returns -1
  • errno == ENETUNREACH (101)
  • program exits with status 0 (the C program reports PASS/FAIL)

scripts/build-programs.sh auto-picks the new programs/*.c file, so
no build-script change is needed. The resolver under local-binaries/
takes precedence over the published-binaries/ tree so a fresh
worktree's `scripts/build-programs.sh` run is sufficient.

Tested on both hosts: this test is the Node-side coverage; the
browser-side path is exercised by the page-level
relay-network-backend.test.ts (RelayHostShim → RelayChannel
envelope) plus the user's manual two-browser DOOM session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@brandonpayton brandonpayton force-pushed the split/kernel-udp-host-relay branch from 797bd63 to 0cee0e6 Compare May 29, 2026 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant