review-003 D-1/D-3: the associate_with_token probe + the io-kind mapping tables table-tested
This commit is contained in:
1 parent
87afe706de
commit
59e8fb1649
2 files changed
+209
No files matched your search
@@ -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:?}");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
Reference in new issue
Block a user