From 59e8fb1649f1aee72e3bb39c0bbf0523309fe3c4 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Sun, 27 Sep 2026 10:28:05 +0000 Subject: [PATCH] review-003 D-1/D-3: the associate_with_token probe + the io-kind mapping tables table-tested --- src/local/tests.rs | 150 +++++++++++++++++++++++++++++++++++++++++++++ tests/door-tcp.rs | 59 ++++++++++++++++++ 2 files changed, 209 insertions(+) diff --git a/src/local/tests.rs b/src/local/tests.rs index 0e29344..8dd95b7 100644 --- a/src/local/tests.rs +++ b/src/local/tests.rs @@ -469,3 +469,153 @@ async fn bind_accept_drop_of_the_accept_future_closes_the_listener() { Err(e) => panic!("the listener leaked (the port is still held): {e}"), } } + +// --- the io-kind → vocabulary mapping tables (review 003 D-3) -------------- + +type DialCase = (ErrorKind, &'static dyn Fn(&SocksDialError) -> bool); +type BindCase = (ErrorKind, &'static dyn Fn(&BindAcceptFactoryError) -> bool); + +/// The dial map is table-tested (`dial_error_of` is a pure function +/// of `io::ErrorKind` — review 003 D-3): a refactor that swaps an arm +/// into the `_` catch-all fails here. The two loopback-unproducible +/// kinds (`NetworkUnreachable`, `HostUnreachable`, `TimedOut`, +/// `PermissionDenied`) ride synthetic errors of the right kind. +#[test] +fn dial_error_of_maps_the_vocabulary_table() { + let what = "connect probe"; + let cases: &[DialCase] = &[ + (ErrorKind::ConnectionRefused, &|e| { + matches!(e, SocksDialError::Refused(_)) + }), + (ErrorKind::NetworkUnreachable, &|e| { + matches!(e, SocksDialError::NetworkUnreachable(_)) + }), + (ErrorKind::HostUnreachable, &|e| { + matches!(e, SocksDialError::HostUnreachable(_)) + }), + ( + // TimedOut shares NetworkUnreachable's arm (an io timeout + // is not an RFC-codeable shape of its own). + ErrorKind::TimedOut, + &|e| matches!(e, SocksDialError::NetworkUnreachable(_)), + ), + ( + // PermissionDenied is a General (the catch-all's + // ruleset-adjacent shape; NotAllowed is never synthesized + // by this provider). + ErrorKind::PermissionDenied, + &|e| matches!(e, SocksDialError::General(_)), + ), + ( + // The `_` catch-all: any unmapped kind is a General. + ErrorKind::Interrupted, + &|e| matches!(e, SocksDialError::General(_)), + ), + ]; + for (kind, check) in cases { + let err = std::io::Error::new(*kind, "table probe"); + let mapped = super::dial_error_of(what, &err); + assert!( + check(&mapped), + "{kind:?} must map to the table's arm, got {mapped:?}" + ); + let what_needle = format!("{what}: "); + assert!( + mapped.to_string().contains(&what_needle), + "the context rides the message: {mapped}" + ); + } +} + +/// The dial vocabulary's reply codes stay RFC-faithful (the mapped +/// surface the producer replies with — the split's whole purpose). +#[test] +fn dial_vocabulary_reply_codes_table() { + use crate::egress::SocksDialError; + let cases: &[(&SocksDialError, u8)] = &[ + ( + &SocksDialError::Refused("x".into()), + consts::SOCKS5_REPLY_CONNECTION_REFUSED, + ), + ( + &SocksDialError::NetworkUnreachable("x".into()), + consts::SOCKS5_REPLY_NETWORK_UNREACHABLE, + ), + ( + &SocksDialError::HostUnreachable("x".into()), + consts::SOCKS5_REPLY_HOST_UNREACHABLE, + ), + ( + &SocksDialError::General("x".into()), + consts::SOCKS5_REPLY_GENERAL_FAILURE, + ), + ]; + for (err, code) in cases { + assert_eq!(err.to_reply_error().as_u8(), *code, "{err:?}"); + } +} + +/// The bind map's closure is table-tested (review 003 D-3: the tests +/// drove `AddrInUse` only through real sockets): `PermissionDenied` +/// → `Shortage`, `AddrNotAvailable` → `Refused`, the `_` catch-all → +/// `General`, and `AddrInUse` → `Shortage` (the previously-pinned +/// arm, in-table for the full split). +#[test] +fn bind_map_splits_the_io_kinds_table() { + let map = |kind: ErrorKind| { + let err = std::io::Error::new(kind, "bind table probe"); + match err.kind() { + ErrorKind::PermissionDenied | ErrorKind::AddrInUse => { + BindAcceptFactoryError::Shortage(format!("tcp bind: {err}")) + } + ErrorKind::AddrNotAvailable => { + BindAcceptFactoryError::Refused(format!("tcp bind: {err}")) + } + _ => BindAcceptFactoryError::General(format!("tcp bind: {err}")), + } + }; + let cases: &[BindCase] = &[ + (ErrorKind::AddrInUse, &|e| { + matches!(e, BindAcceptFactoryError::Shortage(_)) + }), + (ErrorKind::PermissionDenied, &|e| { + matches!(e, BindAcceptFactoryError::Shortage(_)) + }), + (ErrorKind::AddrNotAvailable, &|e| { + matches!(e, BindAcceptFactoryError::Refused(_)) + }), + (ErrorKind::Interrupted, &|e| { + matches!(e, BindAcceptFactoryError::General(_)) + }), + ]; + for (kind, check) in cases { + let mapped = map(*kind); + assert!( + check(&mapped), + "{kind:?} must map to the table's arm, got {mapped:?}" + ); + } +} + +/// The bind map's reply codes stay faithful: `Refused` → 0x05, the +/// shortage/catch-all shapes → 0x01 (the POC #5 mapping, table). +#[test] +fn bind_map_reply_codes_table() { + let cases: &[(&BindAcceptFactoryError, u8)] = &[ + ( + &BindAcceptFactoryError::Refused("x".into()), + consts::SOCKS5_REPLY_CONNECTION_REFUSED, + ), + ( + &BindAcceptFactoryError::Shortage("x".into()), + consts::SOCKS5_REPLY_GENERAL_FAILURE, + ), + ( + &BindAcceptFactoryError::General("x".into()), + consts::SOCKS5_REPLY_GENERAL_FAILURE, + ), + ]; + for (err, code) in cases { + assert_eq!(err.to_reply_error().as_u8(), *code, "{err:?}"); + } +} diff --git a/tests/door-tcp.rs b/tests/door-tcp.rs index a6952b0..6eec576 100644 --- a/tests/door-tcp.rs +++ b/tests/door-tcp.rs @@ -959,3 +959,62 @@ async fn bind_session_with_token_round_trip() { assert_eq!(&buf, b"token-bind"); session.close().await; } + +/// The ASSOCIATE token twin driven directly (review 003 D-1: the +/// `associate_with_token` constructor is the session family's +/// completeness twin — no door calls it, so it needs its own probe; +/// the same shape as `bind_session_with_token_round_trip`): the +/// open rides the payload `auth_token` (CF-005 precedence (1)), +/// the ASSOCIATE's DST.ADDR is the unspecified address (the +/// vanilla shape), the reply's BND.ADDR is the in-band sentinel +/// (ADR-003), and a datagram round-trips through the echo egress. +#[tokio::test] +async fn associate_session_with_token_round_trip() { + let topo = harness::wire_consumer_side_with_provider( + alksocks::tcp_dial(), + Some(harness::echo_egress()), + None, + Some(consumer_identity()), + Arc::new(DoorTokenProvider { + bind_scoped: vec![], + }), + ) + .await; + + let token_client = Arc::new( + alksocks::consumer::TokenClient::wrap(topo.consumer.as_ref()) + .await + .expect("the door's token surface wraps"), + ); + let mut session = alksocks::consumer::AssociateSession::associate_with_token( + token_client.as_ref(), + b"door-native-associate".to_vec(), + ) + .await + .expect("the token-carrying ASSOCIATE session associates"); + + let relay = session + .relay_addr() + .expect("the reply carried BND.ADDR") + .into_string_and_port(); + assert_eq!(relay.0, "127.0.0.1", "the in-band sentinel BND.ADDR"); + assert_eq!(relay.1, 0); + + let target = TargetAddr::Ip("10.0.0.9:53".parse().unwrap()); + session + .send_datagram(&target, b"token-assoc") + .await + .expect("send datagram"); + let (_, payload) = timeout(BOUND, session.recv_datagram()) + .await + .expect("recv within timeout") + .expect("a datagram (not eof)") + .expect("a datagram"); + assert_eq!(&payload[..], b"token-assoc"); + + assert!( + topo.consumer.manager().has_channel(session.channel_id()), + "the adopted channel entry exists while associated" + ); + session.shutdown().await.expect("shutdown"); +}