composed: the bind-first accept — the listener on a listen resource
Lands the composed accept (ADR-007 variant 2 / ADR-008): the BIND accept whose listener lives on an alktunnels 0.2.0 bind-first listen resource, at the graduation's vanilla fidelity. - `composed::bindfirst_accept` — the BindAcceptFactory: opens the listen channel (reply#1 advertises the open-op reply's real `bound`, the 0.8.0 reply projection — no expected-addr stand-in), inserts the minted `listen` token into the correlation registry, and returns the accept future parked on the token's oneshot. - `composed::register_forwarded_serving` — the forwarded-op serving half on the SOCKS5 producer's connection: the shared handler correlates by token and owns the middle pump (either-EOF completion; a three-party per-direction shutdown cascade would deadlock). - The Sync-bound wrinkle: `SyncHalves`/`OwnedWrite` (mutex Sync-proof, no unsafe) over the forwarded halves; the owner (`ForwardedOwner`, the TunnelListener) rides inside the write payload — dropping the handle drops the session, halves, and listener together (teardown structural, compiler-enforced). - `tests/composed-accept.rs`: the full story (real bound, real peer, raw pass-through round trip), client-gone teardown drop-witness, distinct tokens for concurrent BINDs, per-accept isolation of a FORBIDDEN forwarded open, late-forwarded correlation miss. - The producer-side adopted forwarded entry rides the per-adopter contract (transport-EOF reap), as upstream's bind-first suites concede; the no-leak probe reflects that shape. Verification: cargo test (default + --all-features), wasm check/clippy (hard gate), clippy --all-targets --all-features -D warnings, fmt --check — all clean.
This commit is contained in:
1 parent
3128d2bd4f
commit
4e1673eefc
5 files changed
+1284
-24
No files matched your search
+449
-20
@@ -1,8 +1,11 @@
|
||||
//! The `composed` feature's egress provider (ADR-008's composed
|
||||
//! variants, ADR-009's feature gate): [`direct_egress`] — the UDP
|
||||
//! ASSOCIATE egress that dials each destination through an alktunnels
|
||||
//! 0.2.0 direct-op channel ([`alktunnels::TunnelSession::open_direct`],
|
||||
//! `Substrate::Udp`).
|
||||
//! The `composed` feature's egress + accept providers (ADR-008's
|
||||
//! composed variants, ADR-009's feature gate): [`direct_egress`] — the
|
||||
//! UDP ASSOCIATE egress that dials each destination through an
|
||||
//! alktunnels 0.2.0 direct-op channel
|
||||
//! ([`alktunnels::TunnelSession::open_direct`], `Substrate::Udp`) —
|
||||
//! and [`bindfirst_accept`] — the TCP BIND accept whose listener lives
|
||||
//! on a bind-first listen resource (ADR-007 variant 2 / ADR-008's
|
||||
//! composed accept).
|
||||
//!
|
||||
//! **Per-target ACL for free** (ADR-008's composed benefit): each
|
||||
//! destination rides its own `channels/tunnel/direct`-governed channel
|
||||
@@ -49,30 +52,44 @@
|
||||
//! **The Sync-bound wrinkle** (the batch-1 review's seam contract vs
|
||||
//! alktunnels' surface): the landed `TargetHandle`/`BindTargetHandle`
|
||||
//! halves are `Box<dyn AsyncRead + Send + Sync + Unpin>`, but
|
||||
//! alktunnels' `TakenHalves` yields `Send + Unpin` only. The
|
||||
//! owner-wrapper posture (egress.rs's documented contract: "keep-alive
|
||||
//! state a substrate needs rides inside the halves — a provider-side
|
||||
//! owner wrapper") is designed here as [`SessionOwner`] and shared
|
||||
//! with the composed accept (the sibling task): an alktunnels half
|
||||
//! wrapped with its session inside, so the handle payload's `Sync`
|
||||
//! bound is met by ownership and the channel entry reaps when the
|
||||
//! wrapper (and its session) drops. For the egress the wrapper is not
|
||||
//! the handle payload's burden — the egress owns the session in the
|
||||
//! table entry (the POC's shape); the relay task owns the bare read
|
||||
//! half. The accept variant (the sibling task) wraps the accepted
|
||||
//! halves in this wrapper.
|
||||
//! alktunnels' `TakenHalves` yields `Send + Unpin` only — and
|
||||
//! `alkcall::BiStream` is `Send + Unpin` but NOT `Sync` (its inner is
|
||||
//! a `dyn AsyncReadWrite` trait object). The owner-wrapper posture
|
||||
//! (egress.rs's documented contract: "keep-alive state a substrate
|
||||
//! needs rides inside the halves — a provider-side owner wrapper") is
|
||||
//! designed here as [`SessionOwner`] and shared with the composed
|
||||
//! accept (the sibling task): an alktunnels half wrapped with its
|
||||
//! session inside, so the handle payload's `Sync` bound is met by
|
||||
//! ownership and the channel entry reaps when the wrapper (and its
|
||||
//! session) drops. For the egress the wrapper is not the handle
|
||||
//! payload's burden — the egress owns the session in the table entry
|
||||
//! (the POC's shape); the relay task owns the bare read half. The
|
||||
//! accept variant meets the `+ Sync` bound with the [`SyncHalves`]
|
||||
//! adapter (a `std::sync::Mutex` around the joined halves — a
|
||||
//! `Mutex<T: Send>` is `Sync`, no unsafe) with the channel session
|
||||
//! owned inside the payloads ([`ForwardedOwner`] — dropping the handle
|
||||
//! drops the session AND the halves together).
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use alkcall::channels::client::ChannelClient;
|
||||
use alktunnels::params::{Substrate, SubstrateAddr};
|
||||
use alktunnels::TunnelSession;
|
||||
use alkcall::channels::operations::ChannelCore;
|
||||
use alkcall::channels::pump::pump_bidi;
|
||||
use alkcall::core::auth::AuthContext;
|
||||
use alkcall::core::types::BiStream;
|
||||
use alktunnels::params::{Substrate, SubstrateAddr, TunnelForwardedParams};
|
||||
use alktunnels::{TunnelListener, TunnelSession};
|
||||
use bytes::Bytes;
|
||||
use futures::future::BoxFuture;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::egress::{Egress, EgressError, EgressFactory};
|
||||
use crate::egress::{
|
||||
BindAccept, BindAcceptError, BindAcceptFactory, BindAcceptFactoryError, BindTargetHandle,
|
||||
Egress, EgressError, EgressFactory,
|
||||
};
|
||||
pub use crate::rfc1928::util::target_addr::TargetAddr;
|
||||
use crate::wire::MAX_INNER_LEN;
|
||||
|
||||
@@ -404,3 +421,415 @@ pub fn direct_egress(client: Arc<ChannelClient>, max_tunnels: usize) -> EgressFa
|
||||
})
|
||||
})
|
||||
}
|
||||
// --- The composed accept (ADR-007 variant 2, the sibling task's shape) ------
|
||||
|
||||
/// The correlation registry (the assembly-side bookkeeping): `listen`
|
||||
/// token → the oneshot delivering the forwarded session (the adopted
|
||||
/// channel's `BiStream` + the forwarded op's `peer` param). One entry
|
||||
/// per live BIND session; the forwarded handler (the provider's shared
|
||||
/// opener half) removes and completes the entry on correlation. A
|
||||
/// dropped receiver (client-gone teardown) makes completion fail — the
|
||||
/// handler drops the channel stream (teardown-at-birth).
|
||||
pub type CorrelationRegistry =
|
||||
Arc<tokio::sync::Mutex<HashMap<u32, oneshot::Sender<ForwardedSession>>>>;
|
||||
|
||||
/// What a correlated forwarded session delivers: the forwarded
|
||||
/// channel's `BiStream` (raw pass-through — the SOCKS5 BIND data plane
|
||||
/// is raw on both ends) + the forwarded op's `peer` param (reply#2's
|
||||
/// real peer — the graduation's win). Public because the
|
||||
/// [`CorrelationRegistry`] it travels through is public API surface
|
||||
/// (the assembly holds the handle [`register_forwarded_serving`]
|
||||
/// returns); constructing one is the forwarded handler's job alone.
|
||||
pub struct ForwardedSession {
|
||||
pub read: tokio::io::ReadHalf<tokio::io::DuplexStream>,
|
||||
pub write: tokio::io::WriteHalf<tokio::io::DuplexStream>,
|
||||
pub peer: SubstrateAddr,
|
||||
}
|
||||
|
||||
/// The forwarded-op serving handler (the provider's shared opener
|
||||
/// half): correlates the arriving forwarded open by `listen` token and
|
||||
/// OWNS the data-plane pump (R-02 — the `register_openable` wrapper
|
||||
/// awaits this task inline, then tears the forwarded channel down; a
|
||||
/// handler that returns early reaps the channel at birth). The pump
|
||||
/// bridges the forwarded channel's `BiStream` to a local duplex pair:
|
||||
/// the near end pumps here (this task lives exactly as long as the
|
||||
/// data plane — either end's EOF ends it), the far end travels through
|
||||
/// the token's oneshot into the accept future's [`BindTargetHandle`]
|
||||
/// payloads (dropping the handle drops the far end → the pump sees EOF
|
||||
/// → the task returns → the wrapper reaps the forwarded channel —
|
||||
/// teardown-at-birth, structural). A correlation miss (no waiting
|
||||
/// session — client gone, or a token nobody minted) drops the stream:
|
||||
/// the task returns immediately, the same reaping shape. A dead
|
||||
/// oneshot receiver makes the send fail — the task drops the stream
|
||||
/// and returns (same reaping).
|
||||
fn make_forwarded_handler(registry: CorrelationRegistry) -> alktunnels::ForwardedHandler {
|
||||
Arc::new(move |forwarded: TunnelForwardedParams, bidi: BiStream| {
|
||||
let registry = Arc::clone(®istry);
|
||||
tracing::debug!(
|
||||
listen = forwarded.listen,
|
||||
"composed accept: forwarded open arrived"
|
||||
);
|
||||
tokio::spawn(async move {
|
||||
let tx = registry.lock().await.remove(&forwarded.listen);
|
||||
let Some(tx) = tx else {
|
||||
tracing::warn!(
|
||||
listen = forwarded.listen,
|
||||
"composed accept: forwarded open with no waiting session; dropping"
|
||||
);
|
||||
return;
|
||||
};
|
||||
let (near, far) = tokio::io::duplex(64 * 1024);
|
||||
let (far_read, far_write) = tokio::io::split(far);
|
||||
if tx
|
||||
.send(ForwardedSession {
|
||||
read: far_read,
|
||||
write: far_write,
|
||||
peer: forwarded.peer,
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
tracing::warn!(
|
||||
listen = forwarded.listen,
|
||||
"composed accept: waiting session vanished; dropping the forwarded channel"
|
||||
);
|
||||
return;
|
||||
}
|
||||
// The middle pump (three-party half-close would cycle:
|
||||
// pump_bidi's per-direction shutdown-on-completion
|
||||
// deadlocks a bridge — the SOCKS5 pump's far.write shutdown
|
||||
// needs near.read EOF; the wire's bidi.read EOF needs the
|
||||
// producer pump's far.read EOF; far.read needs near.write
|
||||
// shutdown; near.write needs bidi.read EOF). The bridge
|
||||
// therefore completes on EITHER direction's EOF and drops
|
||||
// everything: the far halves (the SOCKS5 pump sees EOF),
|
||||
// the near halves, and the BiStream (the forwarded channel
|
||||
// tears down; the wrapper's inline await returns — R-02 —
|
||||
// and reaps the entry).
|
||||
let (near_read, near_write) = tokio::io::split(near);
|
||||
pump_bidi(bidi, near_read, near_write).await;
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// The forwarded channel's `BiStream` reassembled after the
|
||||
/// payload-split: the read half rides the [`SyncHalves`] adapter (the
|
||||
/// mutex-joined form), the write half rides [`OwnedWrite`] — and the
|
||||
/// owner rides inside the write payload so dropping the handle drops
|
||||
/// everything: the teardown-at-birth discipline, compiler-enforced.
|
||||
/// The owner is the [`TunnelListener`] (NOT an adopted session — the
|
||||
/// forwarded channel's entry is the producer's, adopted inside the
|
||||
/// producer's `ForwardedOpener`): the listener's keep-alive holds the
|
||||
/// far accept loop (and every per-accept pump it spawned — R-02)
|
||||
/// alive; dropping the handle drops the listener → the listen channel
|
||||
/// EOFs → the far teardown cascades. The edition-2021 disjoint-capture
|
||||
/// trap (an earlier draft took `listener.channel_id` — a `u32` — and
|
||||
/// dropped the listener at the factory's return, EOF-ing the listen
|
||||
/// channel at birth) is the reason the listener is forced into the
|
||||
/// future with a whole-struct rebinding.
|
||||
struct ForwardedOwner {
|
||||
_listener: TunnelListener,
|
||||
}
|
||||
|
||||
/// The `BindTargetHandle` read-half adapter over the forwarded
|
||||
/// channel's read half: `BiStream` is `Send + Unpin` but NOT `Sync`
|
||||
/// (its inner is a `dyn AsyncReadWrite` trait object — no `Sync`
|
||||
/// supertrait), so the raw halves cannot meet the handle payloads'
|
||||
/// `+ Sync` bound directly. A `std::sync::Mutex` guard is the
|
||||
/// no-unsafe Sync proof (`Mutex<T: Send>` is `Sync`); the RFC 1928
|
||||
/// BIND data plane is a raw pass-through pumped by ONE `pump_bidi`
|
||||
/// task at a time, so contention never occurs at runtime — the mutex
|
||||
/// is the Sync-proof shape, not a concurrency mechanism. A poisoned
|
||||
/// lock (a panic in one poll) unwraps via `into_inner` — a panic in
|
||||
/// one operation does not cascade (AGENTS.md convention 2).
|
||||
struct SyncHalves {
|
||||
inner: std::sync::Mutex<tokio::io::ReadHalf<tokio::io::DuplexStream>>,
|
||||
}
|
||||
|
||||
impl AsyncRead for SyncHalves {
|
||||
fn poll_read(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> std::task::Poll<std::io::Result<()>> {
|
||||
let this = self.get_mut();
|
||||
let mut inner = match this.inner.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(poisoned) => poisoned.into_inner(),
|
||||
};
|
||||
std::pin::Pin::new(&mut *inner).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// The write-half payload: the owner ([`ForwardedOwner`] — the
|
||||
/// adopted channel session) and the write half ride inside one
|
||||
/// `std::sync::Mutex` (the same no-unsafe Sync proof as
|
||||
/// [`SyncHalves`] — `TunnelSession` and `WriteHalf<BiStream>` are
|
||||
/// `Send` but not `Sync`). Contention never occurs at runtime (one
|
||||
/// pump task); a poisoned lock unwraps via `into_inner`.
|
||||
struct OwnedWrite {
|
||||
inner: std::sync::Mutex<OwnedInner>,
|
||||
}
|
||||
|
||||
struct OwnedInner {
|
||||
_owner: ForwardedOwner,
|
||||
write: tokio::io::WriteHalf<tokio::io::DuplexStream>,
|
||||
}
|
||||
|
||||
impl AsyncWrite for OwnedWrite {
|
||||
fn poll_write(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> std::task::Poll<std::io::Result<usize>> {
|
||||
let this = self.get_mut();
|
||||
let mut inner = match this.inner.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(poisoned) => poisoned.into_inner(),
|
||||
};
|
||||
std::pin::Pin::new(&mut inner.write).poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<std::io::Result<()>> {
|
||||
let this = self.get_mut();
|
||||
let mut inner = match this.inner.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(poisoned) => poisoned.into_inner(),
|
||||
};
|
||||
std::pin::Pin::new(&mut inner.write).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<std::io::Result<()>> {
|
||||
let this = self.get_mut();
|
||||
let mut inner = match this.inner.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(poisoned) => poisoned.into_inner(),
|
||||
};
|
||||
std::pin::Pin::new(&mut inner.write).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// The forwarded op's serving registration (the opener half — ADR-008
|
||||
/// §3, `register_tunnel_forwarded_openable` + this provider's
|
||||
/// correlation handler): register on the SOCKS5 producer's serving
|
||||
/// registry so its connection serves the forwarded op. Call once per
|
||||
/// channels connection at assembly time — the registry is shared
|
||||
/// across all BIND sessions every [`bindfirst_accept`] factory over
|
||||
/// the same client fabricates (one token space per producer
|
||||
/// connection).
|
||||
///
|
||||
/// The scope gate is `tunnel:forwarded` on the PRODUCER's per-call
|
||||
/// identity (the ingress grant — ADR-006 Amendment 1; never implied by
|
||||
/// `tunnel:open`). The returned registry handle feeds the factories;
|
||||
/// keep it alongside the client they ride.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If the underlying registration fails (a duplicate
|
||||
/// `channels/tunnel/forwarded` op id on the registry — an assembly
|
||||
/// bug, not a runtime condition). `register_openable`'s failure type
|
||||
/// is a `String` diagnostic, not a typed error.
|
||||
pub fn register_forwarded_serving(
|
||||
core: &ChannelCore,
|
||||
on_registry: &Arc<alkcall::registry::registration::OperationRegistry>,
|
||||
auth: AuthContext,
|
||||
) -> CorrelationRegistry {
|
||||
let registry: CorrelationRegistry = Arc::new(tokio::sync::Mutex::new(HashMap::new()));
|
||||
alktunnels::producer::register_tunnel_forwarded_openable(
|
||||
core,
|
||||
on_registry,
|
||||
auth,
|
||||
make_forwarded_handler(Arc::clone(®istry)),
|
||||
)
|
||||
.expect("register forwarded openable (the composed accept's serving half)");
|
||||
registry
|
||||
}
|
||||
|
||||
/// The composed accept factory (ADR-007 variant 2, producer.md
|
||||
/// §Wiring `composed::bindfirst_accept`): the `BindAcceptFactory` the
|
||||
/// producer registration consumes — the BIND accept whose listener
|
||||
/// lives on an alktunnels 0.2.0 bind-first listen resource
|
||||
/// (`register_tunnel_bindfirst_openable` + `BindFirstInjections` —
|
||||
/// per-listener ACL for free, hub-relayable).
|
||||
///
|
||||
/// Per session the factory:
|
||||
///
|
||||
/// 1. Opens the listen channel (`TunnelListener::open` — the
|
||||
/// establishment bound covers only this open-op round trip,
|
||||
/// ADR-007); a failed open is a factory failure (reply#1's mapped
|
||||
/// code — no phantom listener).
|
||||
/// 2. Advertises reply#1's BND.ADDR as the REAL `bound` (the open-op
|
||||
/// reply's `bound` field, the alkcall 0.8.0 reply projection — the
|
||||
/// graduation's vanilla-fidelity win; no expected-addr stand-in).
|
||||
/// A producer that supplied no `bound`/`listen` (not a bind-first
|
||||
/// resource) fails the factory — no stand-in on this variant.
|
||||
/// 3. Inserts the minted `listen` token → oneshot into the
|
||||
/// correlation registry and returns the accept future parked on
|
||||
/// it.
|
||||
///
|
||||
/// The accept future waits for the ONE forwarded session matching the
|
||||
/// token (the correlation registry is assembly-side bookkeeping — the
|
||||
/// listener does not own the receipt queue), then builds the
|
||||
/// [`BindTargetHandle`]: reply#2's peer is the forwarded op's `peer`
|
||||
/// param (the graduation's win — no sentinel), and the data-plane
|
||||
/// halves are the [`SyncHalves`]/[`OwnedWrite`] adapters over the
|
||||
/// adopted channel's `BiStream` with the [`ForwardedOwner`] (the
|
||||
/// adopted channel session) riding inside the write payload — dropping
|
||||
/// the handle drops the session AND the halves together.
|
||||
///
|
||||
/// **The client-gone teardown** (the seam contract, end-to-end): the
|
||||
/// listener and the oneshot receiver ride the accept future — the
|
||||
/// quiet wait's `select!` drop of that future drops the
|
||||
/// [`TunnelListener`] (its held write half EOFs the listen channel —
|
||||
/// the far accept dispatcher tears down) AND drops the oneshot
|
||||
/// receiver (a later forwarded open for that token finds a dead
|
||||
/// oneshot and drops the channel stream) — no leaked listener, no
|
||||
/// leaked channel.
|
||||
///
|
||||
/// Only TCP-stream listen resources are supported (the SOCKS5 BIND
|
||||
/// data plane is raw on both ends; a UDP listen substrate would frame
|
||||
/// — a datagram BIND variant is a different task). A non-TCP `bound`
|
||||
/// fails the factory (`NotAllowed`).
|
||||
pub fn bindfirst_accept(
|
||||
client: Arc<ChannelClient>,
|
||||
registry: CorrelationRegistry,
|
||||
resource: impl Into<String>,
|
||||
) -> BindAcceptFactory {
|
||||
let resource: Arc<str> = resource.into().into();
|
||||
Arc::new(move |_target: TargetAddr| {
|
||||
let client = Arc::clone(&client);
|
||||
let registry = Arc::clone(®istry);
|
||||
let resource = Arc::clone(&resource);
|
||||
Box::pin(async move {
|
||||
let params = alktunnels::TunnelParams {
|
||||
resource: resource.to_string(),
|
||||
substrate: Substrate::Tcp,
|
||||
};
|
||||
let listener = TunnelListener::open(client.as_ref(), params)
|
||||
.await
|
||||
.map_err(|e| BindAcceptFactoryError::Refused(format!("listen open: {e}")))?;
|
||||
let bind_addr = match listener.bound() {
|
||||
Some(SubstrateAddr::HostPort { host, port }) => resolve_bound(host, *port)
|
||||
.map_err(|e| BindAcceptFactoryError::General(format!("bound render: {e}")))?,
|
||||
Some(SubstrateAddr::Path { path }) => {
|
||||
return Err(BindAcceptFactoryError::NotAllowed(format!(
|
||||
"non-tcp bound address `{path}` — this provider serves TCP listeners only"
|
||||
)));
|
||||
}
|
||||
None => {
|
||||
return Err(BindAcceptFactoryError::Shortage(
|
||||
"the producer supplied no `bound` reply field — not a bind-first \
|
||||
resource; no stand-in on this variant"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
let token = listener.listen().ok_or_else(|| {
|
||||
BindAcceptFactoryError::Shortage(
|
||||
"the producer supplied no `listen` correlation token — not a bind-first \
|
||||
resource; no stand-in on this variant"
|
||||
.into(),
|
||||
)
|
||||
})?;
|
||||
let (tx, rx) = oneshot::channel();
|
||||
registry.lock().await.insert(token, tx);
|
||||
tracing::debug!(token, "composed accept: registry entry inserted");
|
||||
|
||||
let fut = async move {
|
||||
// Force the whole-struct capture: edition-2021 disjoint
|
||||
// capture would take `listener.channel_id` (a `u32`)
|
||||
// and drop the listener itself at the factory's return
|
||||
// — EOF-ing the listen channel at birth. The listener
|
||||
// rides the future end-to-end (the seam contract).
|
||||
let listener = listener;
|
||||
let ForwardedSession { read, write, peer } = match rx.await {
|
||||
Ok(session) => session,
|
||||
Err(_) => {
|
||||
tracing::warn!(
|
||||
token,
|
||||
"composed accept: oneshot sender dropped; listener closing"
|
||||
);
|
||||
// The registry entry was removed without
|
||||
// delivery (the forwarded serving half tore
|
||||
// down — the whole provider is going away).
|
||||
// The listener's drop tears the listen
|
||||
// channel down (the held write half EOFs).
|
||||
return Err(BindAcceptError::General(
|
||||
"the forwarded-op serving half went away; the listener closed".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
let peer_addr = peer_to_socket_addr(&peer);
|
||||
Ok(BindTargetHandle {
|
||||
peer: TargetAddr::Ip(peer_addr),
|
||||
read: Box::new(SyncHalves {
|
||||
inner: std::sync::Mutex::new(read),
|
||||
}) as Box<dyn AsyncRead + Send + Sync + Unpin>,
|
||||
write: Box::new(OwnedWrite {
|
||||
inner: std::sync::Mutex::new(OwnedInner {
|
||||
// The listener rides INSIDE the payload:
|
||||
// its keep-alive holds the far accept loop
|
||||
// (and every per-accept pump it spawned —
|
||||
// R-02) alive for the data plane's
|
||||
// lifetime; dropping the handle drops the
|
||||
// listener → the listen channel EOFs →
|
||||
// the teardown cascades (teardown
|
||||
// structural, compiler-enforced). The
|
||||
// edition-2021 disjoint-capture trap (an
|
||||
// earlier draft took `listener.channel_id`
|
||||
// — a `u32` — and dropped the listener at
|
||||
// the factory's return, EOF-ing the listen
|
||||
// channel at birth) is why the whole
|
||||
// struct is rebound into the future above.
|
||||
_owner: ForwardedOwner {
|
||||
_listener: listener,
|
||||
},
|
||||
write,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
};
|
||||
Ok(BindAccept::new(bind_addr, fut))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Render the `bound` host (an IP literal or an unresolved host name)
|
||||
/// + port into reply#1's `SocketAddr`:
|
||||
///
|
||||
/// - IP literals resolve directly.
|
||||
/// - A non-IP host fails (reply#1's address is a real socket address
|
||||
/// on this variant — a hostname bound is the producer's resolution
|
||||
/// surface, not the advertised address's).
|
||||
///
|
||||
/// The parse error is a `String` diagnostic (the factory maps it to
|
||||
/// `General`).
|
||||
fn resolve_bound(host: &str, port: u16) -> Result<SocketAddr, String> {
|
||||
host.parse::<std::net::IpAddr>()
|
||||
.map(|ip| SocketAddr::new(ip, port))
|
||||
.map_err(|e| format!("`{host}`: {e}"))
|
||||
}
|
||||
|
||||
/// The forwarded op's `peer` param rendered to reply#2's socket
|
||||
/// address (the graduation's peer fidelity — the param is the real
|
||||
/// peer). A `Path` peer (a unix-socket accept) has no RFC-shaped
|
||||
/// address; the RFC sentinel `0.0.0.0:0` carries it (the data plane
|
||||
/// is unaffected either way).
|
||||
fn peer_to_socket_addr(peer: &SubstrateAddr) -> SocketAddr {
|
||||
match peer {
|
||||
SubstrateAddr::HostPort { host, port } => host
|
||||
.parse::<std::net::IpAddr>()
|
||||
.map(|ip| SocketAddr::new(ip, *port))
|
||||
.unwrap_or_else(|_| sentinel_peer()),
|
||||
SubstrateAddr::Path { .. } => sentinel_peer(),
|
||||
}
|
||||
}
|
||||
|
||||
fn sentinel_peer() -> SocketAddr {
|
||||
SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), 0)
|
||||
}
|
||||
+2
-1
@@ -123,7 +123,8 @@ pub use local::{ProducerDoor, ProducerDoorBuilder, ProducerDoorGuard};
|
||||
|
||||
#[cfg(feature = "composed")]
|
||||
pub use composed::{
|
||||
direct_egress, key_of, substrate_addr_of, ComposedEgress, SessionOwner, DEFAULT_MAX_TUNNELS,
|
||||
bindfirst_accept, direct_egress, key_of, register_forwarded_serving, substrate_addr_of,
|
||||
ComposedEgress, CorrelationRegistry, ForwardedSession, SessionOwner, DEFAULT_MAX_TUNNELS,
|
||||
};
|
||||
|
||||
#[cfg(feature = "local")]
|
||||
|
||||
Reference in new issue
Block a user