consumer: the BIND reply#2 re-entry guard + session hygiene (review-002 C-1, D-1, N-6, S-2)

- C-1: BindSession::await_inbound is take-guarded — the first read
  takes the session's stream; the returned BindDataPlane owns it (the
  TakenInner pairing posture; the session stays the teardown owner),
  so a second call returns the family's spent shape Ok(None) and can
  never re-read the live data plane. A failed read restores the inner
  (the phase is only consumed on success; the producer ends the
  session on every post-reply#1 failure exit, so a re-call EOFs —
  never a live data-plane re-read either way). Pinned by the
  double-await_inbound integration test (the spent shape + the
  data-plane bytes asserted untouched) and the spent-BIND unit test.
- D-1: one normative reply#2 reader (option a) — the session's
  await_inbound; BindReply1::await_inbound is cut and reply1() is
  documented as a view (bind_addr + raw_mut), matching its actual
  usage; the reply1()-twice re-entry path is closed (pinned by the
  view test).
- N-6: the three session constructors' prologues collapsed into one
  private open_and_negotiate helper (teardown-at-birth in one place);
  zero behavior change.
- S-2: the EOF-propagation doc line on all three reap sites (the
  dropped teardown_channel handle is deliberate — the graceful shape;
  clear_all aborts instead).
- BindDataPlane is un-lifetimed (owns the stream); into_inner returns
  the owned Socks5Stream<BiStream>; docs synced (module + session
  docs, consumer.md §The BIND client).

Verification: cargo test 352 green (all-features too), clippy
-D warnings clean, fmt clean, cargo doc clean, both wasm checks clean.
This commit is contained in:
glm-5.3-flash committed 2026-09-24 11:58:55 +00:00
1 parent 6d14d35a54
commit 53739b20cb
4 files changed
+459 -139

No files matched your search

+152 -3
View File
@@ -173,7 +173,7 @@ async fn bind_data_plane_accessors_into_inner_and_socket_mut() {
.write_all(b"raw")
.await
.expect("write");
let inner = plane.into_inner();
let mut inner = plane.into_inner();
let mut buf = [0u8; 6];
tokio::time::timeout(
std::time::Duration::from_secs(10),
@@ -183,7 +183,7 @@ async fn bind_data_plane_accessors_into_inner_and_socket_mut() {
.expect("echo timed out")
.expect("read the echo");
assert_eq!(&buf, b"preraw", "both write paths rode the same pipe");
let _unused = inner;
drop(inner);
assert!(session.close().await, "the entry existed");
no_leaked_channels(&topo).await;
@@ -484,6 +484,155 @@ async fn bind_drop_teardown_reaps_and_connection_stays_healthy() {
fn bind_session_type_is_send() {
fn assert_send<T: Send>() {}
assert_send::<BindSession>();
assert_send::<BindDataPlane>();
assert_send::<BindReply1<'static>>();
assert_send::<BindDataPlane<'static>>();
}
/// The C-1 re-entry guard (review 002): a second `await_inbound` on
/// the same session cannot re-read the data plane — it returns the
/// spent shape (`Ok(None)`), and the data-plane bytes survive
/// untouched (the first plane's echo round-trip is intact after the
/// second call; the pinning test for the silent-corruption path).
#[tokio::test]
async fn double_await_inbound_returns_the_spent_shape_and_bytes_survive() {
let topo = wire_consumer_side_with(
harness::echo_dial(),
None,
Some(harness::echo_accept()),
Some(bind_scoped()),
)
.await;
let mut session = BindSession::bind(
topo.consumer.as_ref(),
("reentry.test", ECHO_BIND_PORT).to_target_addr().unwrap(),
)
.await
.expect("the session binds");
// The first read: the plane exists (and consumes the phase —
// the plane owns the stream; the session stays the teardown
// owner).
let mut plane = session
.await_inbound()
.await
.expect("reply#2 read")
.expect("the plane exists before reply#2");
plane.write_all(b"ping").await.expect("write");
plane.flush().await.expect("flush");
let mut buf = [0u8; 4];
tokio::time::timeout(
std::time::Duration::from_secs(10),
plane.read_exact(&mut buf),
)
.await
.expect("echo timed out")
.expect("read the echo");
assert_eq!(&buf, b"ping", "the first plane's round-trip");
drop(plane);
// The second call: the spent shape — `Ok(None)`, never a
// re-read of the live data plane.
let second = session
.await_inbound()
.await
.expect("the spent shape is an Ok, not an error");
assert!(second.is_none(), "the phase was consumed");
// The data-plane bytes survive: the first plane's echo
// round-trip was complete and no re-read corrupted the pipe —
// the channel is closed with the reap (the session owner's
// `close`), and a fresh bind on the same registration
// round-trips with its own bytes intact.
assert!(session.close().await, "the entry existed");
no_leaked_channels(&topo).await;
let mut fresh = BindSession::bind(
topo.consumer.as_ref(),
("after.test", ECHO_BIND_PORT).to_target_addr().unwrap(),
)
.await
.expect("fresh bind after the spent call");
let mut plane = fresh
.await_inbound()
.await
.expect("reply#2")
.expect("the plane");
plane.write_all(b"again").await.expect("write");
plane.flush().await.expect("flush");
let mut buf = [0u8; 5];
tokio::time::timeout(
std::time::Duration::from_secs(10),
plane.read_exact(&mut buf),
)
.await
.expect("echo timed out")
.expect("read the fresh echo");
assert_eq!(&buf, b"again", "the fresh session's bytes are intact");
drop(plane);
fresh.close().await;
no_leaked_channels(&topo).await;
}
/// The `reply1()`-twice re-entry path (C-1's other door, closed by
/// the D-1 one-reader decision): `reply1()` is a view — fresh handles
/// per call, no read phase — so repeated `reply1()` calls carry the
/// same advertised address without consuming anything, and the one
/// normative reader (`await_inbound`) still yields exactly one plane.
#[tokio::test]
async fn reply1_twice_is_a_view_and_the_reader_yields_one_plane() {
let topo = wire_consumer_side_with(
harness::echo_dial(),
None,
Some(harness::echo_accept()),
Some(bind_scoped()),
)
.await;
let mut session = BindSession::bind(
topo.consumer.as_ref(),
("view.test", ECHO_BIND_PORT).to_target_addr().unwrap(),
)
.await
.expect("the session binds");
let expected = TargetAddr::Ip(std::net::SocketAddr::from(([127, 0, 0, 1], ECHO_BIND_PORT)));
// Repeated views: every handle carries the same advertised
// address; nothing is consumed.
let BindReply1 { bind_addr, .. } = session.reply1().expect("reply#1's handle");
assert_eq!(bind_addr, expected);
let BindReply1 { bind_addr, .. } = session.reply1().expect("reply#1's handle again");
assert_eq!(bind_addr, expected, "a view, not a reader");
// The one normative reader: exactly one plane.
let mut plane = session
.await_inbound()
.await
.expect("reply#2 read")
.expect("the plane");
plane.write_all(b"once").await.expect("write");
plane.flush().await.expect("flush");
let mut buf = [0u8; 4];
tokio::time::timeout(
std::time::Duration::from_secs(10),
plane.read_exact(&mut buf),
)
.await
.expect("echo timed out")
.expect("read the echo");
assert_eq!(&buf, b"once");
drop(plane);
// The re-entry is closed on this door too.
assert!(
session
.await_inbound()
.await
.expect("spent shape")
.is_none(),
"no second plane after the reader ran"
);
session.close().await;
no_leaked_channels(&topo).await;
}