review-003 N-8: the worthwhile residue probed — the BIND Domain-reply wire helper + table, the refusal-recode unit pins, the serve() wrappers, the len/is_empty accessors, the bridge later-batch garbage

This commit is contained in:
glm-5.3-flash committed 2026-09-27 10:45:38 +00:00
1 parent b43bf5462b
commit b43cbd24ba
3 files changed
+293 -7

No files matched your search

+163 -7
View File
@@ -639,7 +639,23 @@ async fn pipe_bind_plane(
session.close().await;
return DoorExit::PlaneEnded;
};
let wire = match &bind_addr {
let wire = bind_reply1_wire(&bind_addr);
if write_vanilla_reply(&mut stream, &wire).await.is_err() {
session.close().await;
return DoorExit::ClientGone;
}
bind_quiet_and_pipe(stream, session).await
}
/// Reply#1's vanilla wire form, re-encoded from the in-band reply's
/// advertised address: an IP BND.ADDR rides the codec (`new_reply`);
/// a Domain BND.ADDR is the hand-written domain frame (the forked
/// codec's `new_reply` takes a `SocketAddr` only — the domain form
/// has no codec path, so the frame is assembled here; the byte shape
/// is pinned by the unit table, review 003 N-8).
fn bind_reply1_wire(bind_addr: &TargetAddr) -> Vec<u8> {
match bind_addr {
TargetAddr::Ip(sa) => new_reply(&ReplyError::Succeeded, *sa),
TargetAddr::Domain(name, port) => {
let mut wire = vec![
@@ -653,13 +669,7 @@ async fn pipe_bind_plane(
wire.extend_from_slice(&port.to_be_bytes());
wire
}
};
if write_vanilla_reply(&mut stream, &wire).await.is_err() {
session.close().await;
return DoorExit::ClientGone;
}
bind_quiet_and_pipe(stream, session).await
}
/// The BIND door's quiet wait + reply#2 + the data plane: await the
@@ -1178,3 +1188,149 @@ mod tests {
feeder.await.expect("feeder");
}
}
#[cfg(test)]
mod refusal_and_wire_unit_tests {
use super::*;
use crate::SocksOpenError;
use alkcall::channels::client::ChannelOpenError;
use alkcall::protocol::wire::CallError;
use tokio::io::AsyncReadExt;
/// A connected TcpStream pair (the loopback listener shape —
/// `TcpStream` has no `pair` constructor; the listener dial is
/// the portable way).
async fn socket_pair() -> (tokio::net::TcpStream, tokio::net::TcpStream) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind");
let addr = listener.local_addr().expect("addr");
let client = tokio::net::TcpStream::connect(addr).await.expect("connect");
let (server, _peer) = listener.accept().await.expect("accept");
(client, server)
}
/// The BIND reply#1 wire table (review 003 N-8: the Domain arm was
/// hand-written wire code with no test): the IP form decodes
/// through the forked codec (`new_reply`'s own table covers it);
/// the Domain form's bytes are pinned exactly — VER, REP, RSV,
/// ATYP=domain, len, name, port.
#[test]
fn bind_reply1_wire_forms() {
let ip = std::net::SocketAddr::from(([127, 0, 0, 1], 2121));
let ip_wire = bind_reply1_wire(&TargetAddr::Ip(ip));
assert_eq!(ip_wire[0], consts::SOCKS5_VERSION);
assert_eq!(ip_wire[1], consts::SOCKS5_REPLY_SUCCEEDED);
assert_eq!(ip_wire[3], consts::SOCKS5_ADDR_TYPE_IPV4);
assert_eq!(&ip_wire[4..8], &[127, 0, 0, 1]);
assert_eq!(&ip_wire[8..10], &2121u16.to_be_bytes());
let domain_wire = bind_reply1_wire(&TargetAddr::Domain("bind.test".into(), 2121));
let expected = {
let mut v = vec![
consts::SOCKS5_VERSION,
consts::SOCKS5_REPLY_SUCCEEDED,
0x00,
consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME,
9,
];
v.extend_from_slice(b"bind.test");
v.extend_from_slice(&2121u16.to_be_bytes());
v
};
assert_eq!(domain_wire, expected, "the domain BND.ADDR frame");
}
/// `write_refusal_reply`'s non-reply-code arm (review 003 N-8): an
/// open/transport-shaped session error stays a bare close — no
/// bytes are written to the vanilla client.
#[tokio::test]
async fn write_refusal_reply_skips_non_reply_code_shapes() {
let (mut client, mut server) = socket_pair().await;
let open_err =
SocksSessionError::Open(SocksOpenError::Open(ChannelOpenError::CallFailed {
error: CallError::new("channel:open_failed", "refused", false),
}));
write_refusal_reply(&mut server, &open_err).await;
// Nothing written: the client's read finds no bytes (the bare
// close is the door's caller's act — the helper itself is
// silent). A short window proves no reply bytes exist.
let mut probe = [0u8; 16];
let n = tokio::time::timeout(
std::time::Duration::from_millis(200),
client.read(&mut probe),
)
.await;
assert!(
matches!(n, Err(_) | Ok(Ok(0))),
"no reply bytes for a non-reply-code error: {n:?}"
);
}
/// The refusal re-encode's wire shape (the F-1 fix's helper,
/// byte-pinned end to end): a reply-code-shaped session error
/// reaches the vanilla client as the RFC reply with the code +
/// the placeholder BND.ADDR.
#[tokio::test]
async fn write_refusal_reply_encodes_the_mapped_code() {
let (mut client, mut server) = socket_pair().await;
let refused =
SocksSessionError::Rfc(SocksError::ReplyError(ReplyError::ConnectionNotAllowed));
write_refusal_reply(&mut server, &refused).await;
let mut wire = [0u8; 10];
tokio::time::timeout(
std::time::Duration::from_secs(5),
client.read_exact(&mut wire),
)
.await
.expect("the reply arrives")
.expect("read");
assert_eq!(wire[0], consts::SOCKS5_VERSION);
assert_eq!(wire[1], consts::SOCKS5_REPLY_CONNECTION_NOT_ALLOWED);
assert_eq!(&wire[4..8], &[0, 0, 0, 0], "the placeholder BND.ADDR");
assert_eq!(&wire[8..10], &0u16.to_be_bytes());
}
}
#[cfg(test)]
mod bridge_later_batch_tests {
use super::*;
use tokio::io::AsyncWriteExt;
/// A decode failure in a *later* batch frame (review 003 N-8's
/// residual: the unit tests pinned the happy shapes — a garbage
/// frame following a good one in the same chunk surfaces the
/// error, the good frame is NOT silently delivered first).
#[tokio::test]
async fn garbage_in_a_later_batch_frame_fails_loud() {
let (mut writer, reader) = tokio::io::duplex(1024);
let (mut channel_read, _channel_write) = tokio::io::split(reader);
let addr = TargetAddr::Ip("10.0.0.7:53".parse().expect("addr"));
let mut wire = crate::wire::frame(&addr, b"good").unwrap().to_vec();
// A later frame whose length field promises more bytes than
// the chunk carries, then the stream ends — the reader's
// state machine fails loud on the truncated later frame.
wire.extend_from_slice(&[0xff, 0xff]);
wire.extend_from_slice(b"short");
let mut bridge = InbandBridge::new();
writer.write_all(&wire).await.expect("feed");
writer.flush().await.expect("flush");
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let _ = writer.shutdown().await;
let first = bridge.recv(&mut channel_read).await;
assert!(
first.is_err() || matches!(&first, Ok(Some((src, payload))) if &payload[..] == b"good"),
"the first frame or the loud error: {first:?}"
);
if matches!(first, Ok(Some(_))) {
// The good frame delivered, the truncated later frame
// fails loud at EOF (the reader's mid-frame state).
let second = bridge.recv(&mut channel_read).await;
assert!(
second.is_err(),
"the truncated later batch frame fails loud: {second:?}"
);
}
}
}
+84
View File
@@ -104,6 +104,14 @@ impl VanillaClient {
}
}
/// The dial without the panic (the `serve` wrapper's bind races
/// the test's connect — the retry loop's probe).
async fn try_dial(addr: SocketAddr) -> std::io::Result<Self> {
Ok(Self {
stream: TcpStream::connect(addr).await?,
})
}
/// Greeting `05 02 00` (password auth offered — the cred-bearing
/// door's shape) → the method-selection read.
async fn greet_password(&mut self) -> u8 {
@@ -1018,3 +1026,79 @@ async fn associate_session_with_token_round_trip() {
);
session.shutdown().await.expect("shutdown");
}
/// The `serve()` wrapper (review 003 N-8: every test drove `serve_on`
/// — the bind-then-serve wrapper round-trips too; the NoAuth raw-pipe
/// shape, the port probed up front so the wrapper's own bind lands on
/// it).
#[tokio::test]
async fn front_door_serve_wrapper_round_trips() {
let echo = echo_tcp_server().await;
let dial = alksocks::tcp_dial();
let topo = harness::wire_consumer_side_with(dial, None, None, None).await;
// A free loopback port, probed up front (the door binds exactly
// it — the wrapper's own bind decision). The connect retries:
// the wrapper's bind races the probe's teardown (the OS may hold
// the freed port briefly).
let mut addr = None;
for _ in 0..50 {
let probe = TcpListener::bind("127.0.0.1:0").await.expect("probe bind");
let candidate = probe.local_addr().expect("probe addr");
drop(probe);
tokio::time::sleep(Duration::from_millis(10)).await;
if TcpStream::connect(candidate).await.is_err() {
addr = Some(candidate);
break;
}
}
let addr = addr.expect("a genuinely free port");
let door = FrontDoor::builder()
.bind(addr)
.build()
.expect("the door builds");
let serve = tokio::spawn({
let client = Arc::clone(&topo.consumer);
async move { door.serve(client).await.expect("the door serves") }
});
let mut vanilla = loop {
if let Ok(v) = VanillaClient::try_dial(addr).await {
break v;
}
tokio::time::sleep(Duration::from_millis(20)).await;
};
let method = timeout(BOUND, vanilla.greet_no_auth())
.await
.expect("greeting");
assert_eq!(method, consts::SOCKS5_AUTH_METHOD_NONE);
let code = timeout(BOUND, vanilla.connect_v4(echo))
.await
.expect("CONNECT");
assert_eq!(code, consts::SOCKS5_REPLY_SUCCEEDED);
vanilla.stream.write_all(b"served").await.unwrap();
let mut buf = [0u8; 6];
timeout(BOUND, vanilla.stream.read_exact(&mut buf))
.await
.expect("echo")
.expect("read");
assert_eq!(&buf, b"served");
serve.abort();
}
/// The credential table's observability accessors (review 003 N-8:
/// `len`/`is_empty` had no caller — one probe pins both).
#[test]
fn credential_table_len_and_is_empty_accessors() {
let empty = alksocks::CredentialTable::new(vec![]);
assert!(empty.is_empty());
assert_eq!(empty.len(), 0);
let one = alksocks::CredentialTable::new(vec![alksocks::DoorCredential {
username: "u".to_string(),
password: "p".to_string(),
identity: harness::consumer_identity(),
}]);
assert!(!one.is_empty());
assert_eq!(one.len(), 1);
}
+46
View File
@@ -455,3 +455,49 @@ async fn fresh_association_succeeds_after_teardown() {
assert_eq!(payload, b"fresh");
serve2.abort();
}
/// The `serve()` wrapper (review 003 N-8: every test drove `serve_on`
/// — the bind-then-serve wrapper round-trips too; the port is
/// configured explicitly so the vanilla client can address the
/// relay).
#[tokio::test]
async fn udp_door_serve_wrapper_round_trips() {
init_trace();
let echo_addr = udp_echo_server().await;
let topo = harness::wire_consumer_side_with(
echo_dial(),
Some(alksocks::udp_egress()),
None,
Some(harness::consumer_identity()),
)
.await;
// A free loopback port, probed up front (the door binds exactly
// it — the wrapper's own bind decision).
let probe = UdpSocket::bind("127.0.0.1:0").await.expect("probe bind");
let relay_addr = probe.local_addr().expect("probe addr");
drop(probe);
let client = UdpSocket::bind("127.0.0.1:0").await.expect("client socket");
let door = UdpFrontDoor::builder()
.bind(relay_addr)
.build()
.expect("the door builds");
let _serve = tokio::spawn({
let client = Arc::clone(&topo.consumer);
async move { door.serve(client).await.expect("the door serves") }
});
client
.send_to(&rfc_datagram(echo_addr, b"wrapped"), relay_addr)
.await
.expect("vanilla sendto the relay");
let mut rx = vec![0u8; 65535];
let (n, from) = timeout(BOUND, client.recv_from(&mut rx))
.await
.expect("reply within bound")
.expect("recv");
assert_eq!(from, relay_addr, "the reply comes from the wrapper's bind");
let (_, payload) = parse_rfc_datagram(&rx[..n]);
assert_eq!(payload, b"wrapped");
}