- src/local/mod.rs (feature = "local" -> tokio/net): dial_tcp (5s timeout), connect_udp (ephemeral bind + connect + the FRAMED UdpHalf adapter), dial_unix (OQ-TN-14 in v1), bind_tcp + TcpListenerHalves::accept_loop (the listen shape's assembly half), local_dial() (one DialFn covering all three substrates). - UdpHalf truncation fails loud (OQ-TN-13): poll_read recvs into a scratch buffer first and size-checks against the caller's buffer — an over-size datagram is io::ErrorKind::InvalidData, never a silent truncation (tokio's poll_recv-into-ReadBuf would truncate). - No socket type appears outside src/local/ (convention 16); the default crate stays wasm-clean; stdio bridging is NOT here (alktty owns process stdio). - Tests (tests/local_halves.rs, 7, feature-gated): TCP/UDP/unix dial round-trips through the real establisher path, the 1400-byte MTU datagram, the truncation fail-loud probe, dial-refusal -> dial_failed, listen producer over a real TCP listener end-to-end. Verified: cargo test green (default + --all-features, 44 + 7), clippy -D warnings (default + all-features + wasm32), fmt clean, cargo check --target wasm32-unknown-unknown passes.
154 lines
6.6 KiB
Markdown
154 lines
6.6 KiB
Markdown
---
|
|
id: tunnels/local-socket-halves
|
|
name: "local feature — real socket halves functions (TCP dial, UDP connect, unix)"
|
|
status: completed
|
|
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<TargetHandle, TunnelEstablishError>;
|
|
// TcpStream::connect (5s timeout per the POCs) → into_split → boxed halves
|
|
|
|
pub async fn connect_udp(target: &str) -> Result<TargetHandle, TunnelEstablishError>;
|
|
// 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<TargetHandle, TunnelEstablishError>;
|
|
// 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<Self, ...>;
|
|
pub fn accept_fn(&self, queue: &AcceptQueue) -> impl Future; // the accept loop feeding the queue
|
|
}
|
|
|
|
pub struct UdpAssociateHalves { sock: Arc<UdpSocket> } // 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.
|
|
- **Unix**: `dial_unix` ships (OQ-TN-14 resolved — same halves shape
|
|
as TCP). **Stdio does NOT** — a spawned process's stdio is alktty's
|
|
pipe mode (exit codes + signals are tty semantics, not tunnel
|
|
semantics); remote command execution composes via alktty, not here.
|
|
- **Error mapping** to `TunnelEstablishError` variants (the POC's
|
|
`Into<EstablishmentError>` 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.
|
|
|
|
- **`bind_tcp` + `TcpListenerHalves::accept_loop(queue)`** complete the
|
|
listen side (the task surface sketched `bind`/`accept_fn`): the
|
|
accept loop is a spawned task feeding an [`AcceptQueue`] — the
|
|
protocol-side queue contract from producer-listen. The assembly
|
|
layer aborts the task on teardown; accept errors end the loop.
|
|
- **`UdpHalf` truncation probe** (OQ-TN-13): `poll_read` recvs into a
|
|
64KiB scratch buffer first, then size-checks against the caller's
|
|
buffer — a >buffer datagram is `io::ErrorKind::InvalidData`, never
|
|
a silent truncation. (`tokio`'s `poll_recv`-into-ReadBuf truncates
|
|
silently, which is exactly the invariant F-2/OQ-TN-13 forbids.)
|
|
WouldBlock polls `poll_recv_ready` for waker registration.
|
|
- **The task sketch's `TcpListenerHalves::accept_fn(&self, queue)`
|
|
became `accept_loop(&self, queue)`** (an owned async loop to spawn)
|
|
— the `accept_fn` borrow-shape fought the spawn model; the loop
|
|
returning on listener failure is the assembly-abort contract.
|
|
- **Test plumbing note** (tests): the listen-over-real-socket test
|
|
drives the client socket via std `try_clone` BEFORE `from_std`
|
|
(O_NONBLOCK is shared across dup — both ends go async); the session
|
|
uses borrowed halves (the producer's wrapper-managed pump already
|
|
pumps channel <-> accepted handle; `pump_against` there would loop
|
|
the same socket back into itself).
|
|
|
|
## Summary
|
|
|
|
> Agent fills this on completion.
|
|
|
|
Implemented the `local` feature (src/local/mod.rs, feature =
|
|
`["tokio/net"]`): `dial_tcp` (5s timeout), `connect_udp` (ephemeral
|
|
bind + connect + the FRAMED `UdpHalf` adapter — truncation fails
|
|
loud), `dial_unix` (OQ-TN-14 in v1), `bind_tcp`/`TcpListenerHalves::
|
|
accept_loop` (the listen shape's assembly half), and `local_dial()`
|
|
(the one `DialFn` covering all three substrates). No socket type
|
|
appears outside `src/local/` (convention 16: the module is never
|
|
imported from producer/consumer/shared; `TargetHandle` re-used from
|
|
producer.rs). Stdio bridging is NOT here (alktty's pipe mode owns
|
|
process stdio).
|
|
|
|
Tests: `tests/local_halves.rs` (7, `#![cfg(feature = "local")]`) —
|
|
TCP dial round-trip through the real establisher path, UDP framed
|
|
round-trip, the 1400-byte MTU datagram, unix dial round-trip, the
|
|
OQ-TN-13 truncation fail-loud probe (60k datagram vs 16KiB read →
|
|
InvalidData), dial-refusal → `dial_failed`, and the listen producer
|
|
over a real TCP listener end-to-end.
|
|
|
|
Verified: `cargo test` (default) and `cargo test --all-features`
|
|
green (44 + 7), clippy `-D warnings` clean (default + all-features +
|
|
wasm32), fmt clean, wasm32 check passes (default crate stays
|
|
wasm-clean; `local` is non-wasm by design). |