feat(net): capsule inbound TCP bind (bind_tcp) (#1230) - #1457
Open
jvsteiner wants to merge 1 commit into
Open
Conversation
Fills in the daemon's stubbed astrid:net bind_tcp host fn so a capsule can bind a loopback TCP listener and accept inbound connections — the missing substrate for capsule-hosted HTTP servers (e.g. an Anthropic-Messages shim Claude Code points ANTHROPIC_BASE_URL at, routed by srouter). Design: - Authorization reuses the existing `net_bind` manifest field, whose own doc already reads "Unix/TCP socket bind addresses". TCP entries are `host:port` / `host:*` patterns matched with the SAME semantics as net_connect; a `unix:*` entry (the CLI proxy) never matches a TCP host:port, so the two socket families share the field without cross-authorizing. New gate method `check_net_tcp_bind(capsule, host, port)`, fail-closed default in the trait, allowlist match in ManifestSecurityGate. - Host fn `bind_tcp`: capability-gate → loopback-confinement rail → tokio bind → resource-table slot. Loopback-only is enforced host-side (is_loopback_bind_host) regardless of the allowlist, mirroring how connect_tcp runs its is_safe_ip airlock AFTER the capability gate. Non-loopback bind is refused (AirlockRejected), not downgraded. - TcpListenerSlot now holds the live Arc<tokio::net::TcpListener>. accept / poll_accept register the accepted stream as a NetStream::Tcp — the SAME representation outbound connect_tcp uses — so every existing read/write/peek/timeout host fn works on accepted connections with no extra wiring. Per-capsule MAX_ACTIVE_STREAMS cap applies; accept sets recv_yielded so a bound accept-loop is not epoch-trapped as a spinner; cancellable so capsule unload wins over a blocked accept. Proven: a probe capsule bound 127.0.0.1:8799, accepted a curl connection, and served an HTTP 200 through the patched daemon (LISTENING → ACCEPTED → wrote 110 bytes). 6 new unit tests (gate host:port matching incl. the unix-entry-doesn't-authorize-TCP case; loopback host classification). POC for the Astrid router work — informs the upstream feature request (gotchas + security posture documented separately). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linked Issue
Closes #1230.
Summary
A capsule can now bind an inbound TCP port and accept connections from its run loop — the runtime primitive the
sroutercapsule needs (loopback ingress on127.0.0.1:8788). This is thebind_tcpconcern extracted from the retired #1321 as a focused, independently-reviewable PR (its other two concerns are #1380 and #1231).Changes
bind_tcp(host, port) -> tcp-listener(net/mod.rs) — validates the host, checks thenet_bindcapability, applies a loopback airlock (non-loopback binds are refused, not downgraded), binds atokio::net::TcpListener, and returns atcp-listenerresource.tcp-listenerresource (tcp_listener.rs) —accept/poll-accept(timeout_ms)/local-addr; accepted connections wrap asNetStream::Tcpand reuse every existing read/write/peek/close host fn.acceptis cancellable on capsule unload and setsrecv_yieldedso a bound accept isn't epoch-trapped as a spinner. Capped by the sharedMAX_ACTIVE_STREAMS.check_net_tcp_bind(security/mod.rstrait default, fail-closed +manifest_gate.rsimpl) — matcheshost:portagainst the manifest's existingnet_bindallowlist with the same matchercheck_net_connectuses. Aunix:*entry never authorizes a TCP bind.net_bindcapability field, its discovery-time merge, and the WIT contract already existed — unchanged.Verification
cargo test -p astrid-capsulegreen.check_net_tcp_bind_*— host:port match, wildcard port, unix-entry-doesn't-authorize-tcp, empty-denies) + 2 loopback-airlock unit tests.Notes
accept/poll-accept), sobind_tcpneeds no run-loop integration. Serving multiple concurrent connections is the separate concurrent-workers concern (feat(capsule): concurrent workers for run-loop TCP-server capsules (single-Store run-loop serializes requests) #1231).MAX_ACTIVE_STREAMS) but not a distinct listener count — noted as a small follow-up.Checklist