--- id: tunnels/local-socket-halves name: "local feature — real socket halves functions (TCP dial, UDP connect, unix)" status: pending depends_on: [tunnels/producer-open-op, tunnels/wire-codec] scope: moderate risk: medium impact: component level: implementation tags: [local, backend, sockets, feature-gate] --- ## Description Add the `local` feature module (`src/local/`) per overview.md §Feature Gates + ADR-004: the real-socket halves functions the assembly layer injects as `DialFn`/`AcceptFn` closures. The POCs' substrate adapters are the reference (they ran all of this); the task is packaging them into the feature-gated module with the crate's conventions. ### Surface ```rust // src/local/mod.rs (feature = "local"; non-wasm by design) pub async fn dial_tcp(target: &str) -> Result; // TcpStream::connect (5s timeout per the POCs) → into_split → boxed halves pub async fn connect_udp(target: &str) -> Result; // UdpSocket::bind(("0.0.0.0", 0)) → connect(target) → the FRAMED adapter // (UdpHalf + codec — ADR-003; truncation fail-loud per OQ-TN-13) pub async fn dial_unix(path: &str) -> Result; // UnixStream::connect → into_split (OQ-TN-14: ships with local v1 — cheap, // same halves shape as TCP) pub struct TcpListenerHalves { listener: tokio::net::TcpListener } impl TcpListenerHalves { pub async fn bind(addr: &str) -> Result; pub fn accept_fn(&self, queue: &AcceptQueue) -> impl Future; // the accept loop feeding the queue } pub struct UdpAssociateHalves { sock: Arc } // the POC's associate shape ``` - **UDP adapter (`UdpHalf`)**: port from the forward POC (`params.rs`'s `UdpHalfRead`/`UdpHalfWrite`) with the truncation fix: `poll_recv` into a too-small caller buffer must fail loud (OQ-TN-13's resolved posture — ADR-003). The codec wraps at this boundary; the pump never sees UDP specifics. - **Error mapping** to `TunnelEstablishError` variants (the POC's `Into` path). - `TargetHandle` re-used from producer.rs (the halves type lives in producer.rs; local/ depends on it, never vice versa). ### Feature wiring ```toml [features] default = [] local = ["tokio/net"] ``` - `tokio/net` is the only dep addition (no new external crates; the POCs proved `rt+sync+io-util+macros+time+net` covers everything). - The default crate stays wasm-clean: `local`-gated code must not be importable from `producer.rs`/`consumer.rs` (convention 16 — backend modules are never imported from the shared/producer/consumer modules). - Unix is IN v1 (OQ-TN-14's lean-yes posture; same halves shape as TCP); stdio bridging is NOT (different lifecycle — deferred). ### Tests Behind the feature: TCP dial round-trip through the real establisher path, UDP associate + datagram round-trip with the framed adapter, unix dial round-trip, truncation fail-loud (a short recv surfaces as an error), the 1400-byte MTU datagram through bounded buffers (the POC's sizing test). ## Acceptance Criteria - [ ] `cargo test --features local` green; `cargo test` (default) unaffected - [ ] `cargo test --all-features` green (AGENTS.md convention 14) - [ ] wasm32 check on the DEFAULT crate passes; `local` is non-wasm by design (documented) - [ ] Truncation fails loud (the OQ-TN-13 test) - [ ] No socket type appears outside `src/local/` - [ ] Clippy/fmt clean ## References - docs/architecture/overview.md §Feature Gates, §Dependencies - docs/architecture/decisions/004-no-backend-trait.md (the injection point), 003 (the framed adapter) - POC reference: `/workspace/alktunnels-udp-poc/src/producer.rs` (`UdpHalf`, the dial paths), `params.rs` (the UDP adapter) ## Notes > Agent fills during implementation. ## Summary > Agent fills this on completion.