mirror of
https://github.com/cfal/shoes.git
synced 2026-11-06 08:14:11 +00:00
Clarifies the fork-backed REALITY code layout by naming modules after their responsibilities, colocating client and server handlers, and grouping shared fixtures and handshake tests under src/reality/tests. Keeps the reality_ filename prefixes and preserves protocol behavior while reusing existing helpers and simplifying imports.
654 lines
25 KiB
Rust
654 lines
25 KiB
Rust
use super::*;
|
|
use crate::crypto::TlsConnection;
|
|
use std::io;
|
|
use std::pin::Pin;
|
|
use std::task::{Context, Poll};
|
|
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
|
|
|
struct TargetReads {
|
|
chunks: std::collections::VecDeque<Bytes>,
|
|
reads: usize,
|
|
}
|
|
|
|
impl AsyncRead for TargetReads {
|
|
fn poll_read(
|
|
mut self: Pin<&mut Self>,
|
|
_cx: &mut Context<'_>,
|
|
buffer: &mut ReadBuf<'_>,
|
|
) -> Poll<io::Result<()>> {
|
|
let Some(chunk) = self.chunks.pop_front() else {
|
|
// An open, idle target must not make collection wait for an unseen seventh record.
|
|
return Poll::Pending;
|
|
};
|
|
assert!(chunk.len() <= buffer.remaining());
|
|
buffer.put_slice(&chunk);
|
|
self.reads += 1;
|
|
Poll::Ready(Ok(()))
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn target_collection_respects_observed_read_boundaries() {
|
|
use crate::reality::CipherSuite;
|
|
use crate::reality::reality_hello::RealityGroup;
|
|
use crate::reality::tests::reality_fixtures::{
|
|
configs, separate_target_records, write_record_header,
|
|
};
|
|
|
|
for case in ["six", "seven", "split-seven", "combined-trailing"] {
|
|
let (client_config, server_config) = configs(CipherSuite::AES_128_GCM_SHA256);
|
|
let mut client = client_config.create_connection().unwrap();
|
|
let mut hello = Vec::new();
|
|
client.write_tls(&mut hello).unwrap();
|
|
let mut records = if case == "combined-trailing" {
|
|
crate::reality::default_target_records(&hello[44..76])
|
|
} else {
|
|
separate_target_records(
|
|
RealityGroup::X25519MlKem768,
|
|
CipherSuite::AES_128_GCM_SHA256,
|
|
&hello[44..76],
|
|
)
|
|
};
|
|
if matches!(case, "seven" | "split-seven") {
|
|
let mut trailing = write_record_header(23, 59);
|
|
trailing.resize(64, 0);
|
|
records.push(trailing.into());
|
|
}
|
|
let mut wire = records.concat();
|
|
let remaining: &[u8] = if case == "combined-trailing" {
|
|
&[23, 3, 3]
|
|
} else {
|
|
&[]
|
|
};
|
|
wire.extend_from_slice(remaining);
|
|
let chunks = if case == "split-seven" {
|
|
let split = wire.len() - 61;
|
|
vec![
|
|
Bytes::copy_from_slice(&wire[..split]),
|
|
Bytes::copy_from_slice(&wire[split..]),
|
|
]
|
|
} else {
|
|
vec![Bytes::copy_from_slice(&wire)]
|
|
};
|
|
let expected_reads = chunks.len();
|
|
let mut target = TargetReads {
|
|
chunks: chunks.into(),
|
|
reads: 0,
|
|
};
|
|
let flight = tokio::time::timeout(
|
|
Duration::from_secs(1),
|
|
read_target_flight(&mut target, &server_config.dest, true),
|
|
)
|
|
.await
|
|
.expect(case)
|
|
.unwrap();
|
|
assert_eq!(target.reads, expected_reads, "{case}");
|
|
assert_eq!(flight.records, records, "{case}");
|
|
assert_eq!(flight.remaining.as_ref(), remaining, "{case}");
|
|
assert_eq!(flight.usable, case != "combined-trailing", "{case}");
|
|
if flight.usable {
|
|
let mut server = server_config
|
|
.authenticate_client_hello(&hello)
|
|
.unwrap()
|
|
.into_connection(&flight.records)
|
|
.unwrap();
|
|
let mut wire = Vec::new();
|
|
server.write_tls(&mut wire).unwrap();
|
|
client.feed(&wire).unwrap();
|
|
wire.clear();
|
|
client.write_tls(&mut wire).unwrap();
|
|
server.feed(&wire).unwrap();
|
|
assert!(
|
|
!client.is_handshaking() && !server.is_handshaking(),
|
|
"{case}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn synthesis_failures_only_forward_explicit_incompatibilities() {
|
|
assert_eq!(forward_unsupported(Ok(())).unwrap(), Some(()));
|
|
assert_eq!(
|
|
forward_unsupported::<()>(Err(io::ErrorKind::Unsupported.into())).unwrap(),
|
|
None
|
|
);
|
|
for kind in [
|
|
io::ErrorKind::InvalidData,
|
|
io::ErrorKind::UnexpectedEof,
|
|
io::ErrorKind::Other,
|
|
] {
|
|
let error = io::Error::new(kind, "injected synthesis failure");
|
|
let propagated = forward_unsupported::<()>(Err(error)).unwrap_err();
|
|
assert_eq!(propagated.kind(), kind);
|
|
assert_eq!(propagated.to_string(), "injected synthesis failure");
|
|
}
|
|
}
|
|
|
|
fn client_key_share_offsets(hello: &[u8]) -> Vec<usize> {
|
|
use crate::buf_reader::BufReader;
|
|
|
|
let mut reader = BufReader::new(hello);
|
|
reader.skip(43).unwrap();
|
|
let session_len = reader.read_u8().unwrap() as usize;
|
|
reader.skip(session_len).unwrap();
|
|
let cipher_len = reader.read_u16_be().unwrap() as usize;
|
|
reader.skip(cipher_len).unwrap();
|
|
let compression_len = reader.read_u8().unwrap() as usize;
|
|
reader.skip(compression_len).unwrap();
|
|
let extensions_len = reader.read_u16_be().unwrap() as usize;
|
|
assert_eq!(extensions_len, reader.remaining());
|
|
while !reader.is_consumed() {
|
|
let id = reader.read_u16_be().unwrap();
|
|
let len = reader.read_u16_be().unwrap() as usize;
|
|
let start = reader.position();
|
|
let data = reader.read_slice(len).unwrap();
|
|
if id == 51 {
|
|
let mut shares = BufReader::new(data);
|
|
let len = shares.read_u16_be().unwrap() as usize;
|
|
assert_eq!(len, shares.remaining());
|
|
let mut offsets = Vec::new();
|
|
while !shares.is_consumed() {
|
|
offsets.push(start + shares.position());
|
|
shares.skip(2).unwrap();
|
|
let len = shares.read_u16_be().unwrap() as usize;
|
|
shares.skip(len).unwrap();
|
|
}
|
|
return offsets;
|
|
}
|
|
}
|
|
panic!("missing key_share extension");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn incompatible_hellos_and_small_targets_forward_unchanged() {
|
|
use crate::client_proxy_chain::InitialHopEntry;
|
|
use crate::port_forward_handler::PortForwardServerHandler;
|
|
use crate::reality::reality_hello::RealityGroup;
|
|
use crate::reality::tests::reality_fixtures::{
|
|
configs, separate_target_records, target_with_unknown_extension, write_record_header,
|
|
};
|
|
use crate::reality::{CipherSuite, mldsa65_signing_key};
|
|
use crate::tcp::socket_connector_impl::SocketConnectorImpl;
|
|
|
|
for case in [
|
|
"small",
|
|
"small-separate",
|
|
"retry",
|
|
"unsupported",
|
|
"client-unknown-shares",
|
|
"client-truncated-share",
|
|
"client-bad-auth",
|
|
"bad-late-record",
|
|
"bad-ccs",
|
|
"wrong-session",
|
|
"unknown-extension",
|
|
"combined-trailing",
|
|
] {
|
|
let capture = crate::logging::test_capture::Capture::new();
|
|
let (cc, mut sc) = configs(CipherSuite::AES_128_GCM_SHA256);
|
|
let (seed, _) = crate::reality::generate_mldsa65_keypair().unwrap();
|
|
sc.mldsa65_key = Some(mldsa65_signing_key(&seed).unwrap());
|
|
let mut client = cc.create_connection().unwrap();
|
|
let mut hello = Vec::new();
|
|
client.write_tls(&mut hello).unwrap();
|
|
let auth_failure = matches!(
|
|
case,
|
|
"client-unknown-shares" | "client-truncated-share" | "client-bad-auth"
|
|
);
|
|
let mut response = match case {
|
|
"client-unknown-shares" | "client-truncated-share" | "client-bad-auth" => {
|
|
let offsets = client_key_share_offsets(&hello);
|
|
assert_eq!(offsets.len(), 2);
|
|
let expected_error = match case {
|
|
"client-unknown-shares" => {
|
|
for (offset, group) in offsets.into_iter().zip([0xfe30u16, 0xfe31]) {
|
|
hello[offset..offset + 2].copy_from_slice(&group.to_be_bytes());
|
|
}
|
|
io::ErrorKind::InvalidData
|
|
}
|
|
"client-truncated-share" => {
|
|
// Keeps the TLS framing valid while overstating the last share's length.
|
|
let offset = offsets[1] + 2;
|
|
assert_eq!(&hello[offset..offset + 2], &[0, 32]);
|
|
hello[offset..offset + 2].copy_from_slice(&33u16.to_be_bytes());
|
|
io::ErrorKind::UnexpectedEof
|
|
}
|
|
_ => {
|
|
hello[44] ^= 1;
|
|
io::ErrorKind::PermissionDenied
|
|
}
|
|
};
|
|
let check = sc.clone();
|
|
assert_eq!(
|
|
check.authenticate_client_hello(&hello).unwrap_err().kind(),
|
|
expected_error
|
|
);
|
|
// A valid target flight reaches the admission-error branch, not hello fallback.
|
|
crate::reality::default_target_records(&hello[44..76]).concat()
|
|
}
|
|
"small" | "small-separate" => {
|
|
let (mut records, small_record) = if case == "small" {
|
|
(crate::reality::default_target_records(&hello[44..76]), 2)
|
|
} else {
|
|
(
|
|
separate_target_records(
|
|
RealityGroup::X25519MlKem768,
|
|
CipherSuite::AES_128_GCM_SHA256,
|
|
&hello[44..76],
|
|
),
|
|
3,
|
|
)
|
|
};
|
|
let mut encrypted = write_record_header(23, 600);
|
|
encrypted.resize(605, 0);
|
|
records[small_record] = encrypted.into();
|
|
let check = sc.clone();
|
|
let authenticated = check.authenticate_client_hello(&hello).unwrap();
|
|
assert_eq!(
|
|
authenticated
|
|
.into_connection(&records)
|
|
.unwrap_err()
|
|
.to_string(),
|
|
"Unsupported REALITY target or insufficient capacity"
|
|
);
|
|
records.concat()
|
|
}
|
|
"bad-late-record" | "bad-ccs" | "wrong-session" | "unknown-extension"
|
|
| "combined-trailing" => {
|
|
let mut records = crate::reality::default_target_records(&hello[44..76]);
|
|
match case {
|
|
"bad-ccs" => records[1] = Bytes::from_static(&[20, 3, 3, 0, 1, 0]),
|
|
"wrong-session" => {
|
|
let mut sh = records[0].to_vec();
|
|
sh[44] ^= 1;
|
|
records[0] = sh.into();
|
|
}
|
|
"unknown-extension" => records[0] = target_with_unknown_extension(&records[0]),
|
|
"combined-trailing" => {}
|
|
"bad-late-record" => records.push(Bytes::from_static(&[255, 3, 3, 0, 0])),
|
|
_ => unreachable!(),
|
|
}
|
|
records.concat()
|
|
}
|
|
_ => {
|
|
let mut message = vec![2, 0, 0, 0, 3, 3];
|
|
if case == "retry" {
|
|
let retry_random =
|
|
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, b"HelloRetryRequest");
|
|
message.extend_from_slice(retry_random.as_ref());
|
|
} else {
|
|
message.extend_from_slice(&[3; 32]);
|
|
}
|
|
message.push(32);
|
|
message.extend_from_slice(&hello[44..76]);
|
|
message.extend_from_slice(&[0x13, 1, 0]);
|
|
let mut extensions = vec![0, 43, 0, 2, 3, 4];
|
|
if case == "retry" {
|
|
extensions.extend_from_slice(&[0, 51, 0, 2, 0, 23]);
|
|
} else {
|
|
extensions.extend_from_slice(&[0, 51, 0, 5, 0xfe, 0x30, 0, 1, 1]);
|
|
}
|
|
message.extend_from_slice(&(extensions.len() as u16).to_be_bytes());
|
|
message.extend_from_slice(&extensions);
|
|
let len = (message.len() - 4) as u32;
|
|
message[1..4].copy_from_slice(&len.to_be_bytes()[1..]);
|
|
let mut record = write_record_header(22, message.len() as u16);
|
|
record.extend_from_slice(&message);
|
|
record
|
|
}
|
|
};
|
|
// A partial trailing record exercises the deframer's retained bytes.
|
|
if matches!(case, "retry" | "unsupported" | "combined-trailing") {
|
|
response.extend_from_slice(&[23, 3, 3]);
|
|
}
|
|
|
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:0").await.unwrap();
|
|
let dest = NetLocation::new(
|
|
crate::address::Address::Hostname("localhost".into()),
|
|
listener.local_addr().unwrap().port(),
|
|
);
|
|
let selector = Arc::new(ClientProxySelector::new(Vec::new()));
|
|
let target = RealityServerTarget {
|
|
mldsa65_key: sc.mldsa65_key,
|
|
private_key: sc.private_key,
|
|
short_ids: sc.short_ids,
|
|
dest: dest.clone(),
|
|
max_time_diff: None,
|
|
min_client_version: None,
|
|
max_client_version: None,
|
|
cipher_suites: sc.cipher_suites,
|
|
effective_selector: selector.clone(),
|
|
inner_protocol: InnerProtocol::Normal(Box::new(PortForwardServerHandler::new(
|
|
vec![dest],
|
|
selector,
|
|
))),
|
|
dest_client_chain: ClientProxyChain::new(
|
|
vec![InitialHopEntry::Direct(Arc::new(
|
|
SocketConnectorImpl::from_config(&crate::config::ClientConfig::default(), None)
|
|
.unwrap(),
|
|
))],
|
|
Vec::new(),
|
|
),
|
|
};
|
|
let resolver: Arc<dyn Resolver> = Arc::new(crate::resolver::NativeResolver::new());
|
|
let (io, mut peer) = tokio::io::duplex(65536);
|
|
let mut flight = hello.clone();
|
|
flight.extend_from_slice(b"pending");
|
|
peer.write_all(&flight).await.unwrap();
|
|
let mut io: Box<dyn AsyncStream> = Box::new(io);
|
|
let parsed = crate::shadow_tls::read_client_hello(&mut io).await.unwrap();
|
|
assert!(parsed.supports_tls13);
|
|
assert_eq!(parsed.client_reader.unparsed_data(), b"pending");
|
|
let decoy_io = async {
|
|
let (mut socket, _) = listener.accept().await.unwrap();
|
|
let mut forwarded = vec![0; hello.len()];
|
|
socket.read_exact(&mut forwarded).await.unwrap();
|
|
assert_eq!(forwarded, hello);
|
|
socket.write_all(&response).await.unwrap();
|
|
let mut upstream = Vec::new();
|
|
socket.read_to_end(&mut upstream).await.unwrap();
|
|
assert_eq!(upstream, b"pending-live");
|
|
socket.write_all(b"-reply").await.unwrap();
|
|
socket.shutdown().await.unwrap();
|
|
};
|
|
let handler = async {
|
|
let result = setup_reality_server_stream(io, &target, parsed, &resolver)
|
|
.await
|
|
.unwrap();
|
|
let TcpServerSetupResult::Session(session) = result else {
|
|
panic!("expected transparent fallback for {case}");
|
|
};
|
|
session.await;
|
|
};
|
|
let peer_io = async {
|
|
let mut actual = vec![0; response.len()];
|
|
peer.read_exact(&mut actual).await.unwrap();
|
|
assert_eq!(actual, response, "{case}");
|
|
peer.write_all(b"-live").await.unwrap();
|
|
peer.shutdown().await.unwrap();
|
|
let mut reply = Vec::new();
|
|
peer.read_to_end(&mut reply).await.unwrap();
|
|
assert_eq!(reply, b"-reply");
|
|
};
|
|
tokio::time::timeout(Duration::from_secs(2), async {
|
|
tokio::join!(handler, decoy_io, peer_io);
|
|
})
|
|
.await
|
|
.unwrap_or_else(|_| panic!("transparent forwarding stalled for {case}"));
|
|
let logs = capture.finish();
|
|
if auth_failure {
|
|
assert!(logs.contains("REALITY: Auth failed"), "{case}: {logs}");
|
|
}
|
|
if matches!(case, "small" | "small-separate" | "unknown-extension") {
|
|
assert!(
|
|
logs.contains("Auth succeeded, building response"),
|
|
"{case}: {logs}"
|
|
);
|
|
assert!(
|
|
logs.contains("REALITY: Cannot synthesize"),
|
|
"{case}: {logs}"
|
|
);
|
|
}
|
|
if case == "combined-trailing" {
|
|
assert!(
|
|
logs.contains("REALITY: Dest handshake failed"),
|
|
"{case}: {logs}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn authenticated_non_vision_oversized_preread_returns_error_without_panicking() {
|
|
use crate::client_proxy_chain::InitialHopEntry;
|
|
use crate::port_forward_handler::PortForwardServerHandler;
|
|
use crate::reality::RealityClientConfig;
|
|
use crate::tcp::socket_connector_impl::SocketConnectorImpl;
|
|
use aws_lc_rs::agreement;
|
|
|
|
let private_key = [1; 32];
|
|
let public_key = agreement::PrivateKey::from_private_key(&agreement::X25519, &private_key)
|
|
.unwrap()
|
|
.compute_public_key()
|
|
.unwrap();
|
|
let mut client = RealityClientConfig {
|
|
mldsa65_verify: None,
|
|
public_key: public_key.as_ref().try_into().unwrap(),
|
|
short_id: [0; 8],
|
|
server_name: "localhost".try_into().unwrap(),
|
|
cipher_suites: Vec::new(),
|
|
}
|
|
.create_connection()
|
|
.unwrap();
|
|
let mut hello = Vec::new();
|
|
client.write_tls(&mut hello).unwrap();
|
|
|
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let address = listener.local_addr().unwrap();
|
|
let dest = NetLocation::new(
|
|
crate::address::Address::Hostname("localhost".into()),
|
|
address.port(),
|
|
);
|
|
let cert = rcgen::generate_simple_self_signed(
|
|
(0..100)
|
|
.map(|index| format!("host-{index}.example.test"))
|
|
.collect::<Vec<_>>(),
|
|
)
|
|
.unwrap();
|
|
let decoy_config =
|
|
rustls::ServerConfig::builder_with_protocol_versions(&[&rustls::version::TLS13])
|
|
.with_no_client_auth()
|
|
.with_single_cert(
|
|
vec![cert.cert.der().clone()],
|
|
rustls::pki_types::PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der())
|
|
.into(),
|
|
)
|
|
.unwrap();
|
|
let mut decoy =
|
|
TlsConnection::new(rustls::ServerConnection::new(Arc::new(decoy_config)).unwrap());
|
|
decoy.feed(&hello).unwrap();
|
|
let mut response = Vec::new();
|
|
decoy.write_tls(&mut response).unwrap();
|
|
let selector = Arc::new(ClientProxySelector::new(Vec::new()));
|
|
let target = RealityServerTarget {
|
|
mldsa65_key: None,
|
|
private_key,
|
|
short_ids: vec![[0; 8]],
|
|
dest: dest.clone(),
|
|
max_time_diff: None,
|
|
min_client_version: None,
|
|
max_client_version: None,
|
|
cipher_suites: Vec::new(),
|
|
effective_selector: selector.clone(),
|
|
inner_protocol: InnerProtocol::Normal(Box::new(PortForwardServerHandler::new(
|
|
vec![dest],
|
|
selector,
|
|
))),
|
|
dest_client_chain: ClientProxyChain::new(
|
|
vec![InitialHopEntry::Direct(Arc::new(
|
|
SocketConnectorImpl::from_config(&crate::config::ClientConfig::default(), None)
|
|
.unwrap(),
|
|
))],
|
|
Vec::new(),
|
|
),
|
|
};
|
|
let resolver: Arc<dyn Resolver> = Arc::new(crate::resolver::NativeResolver::new());
|
|
let (io, mut peer) = tokio::io::duplex(65540);
|
|
let mut flight = hello.clone();
|
|
flight.extend_from_slice(&[0x17, 0x03, 0x03, 0, 1, 0].repeat(6000));
|
|
peer.write_all(&flight).await.unwrap();
|
|
let mut io: Box<dyn AsyncStream> = Box::new(io);
|
|
let parsed = crate::shadow_tls::read_client_hello(&mut io).await.unwrap();
|
|
assert!(parsed.client_reader.unparsed_data().len() > 33290);
|
|
|
|
let decoy_io = async {
|
|
let (mut socket, _) = listener.accept().await.unwrap();
|
|
let mut forwarded_hello = vec![0; hello.len()];
|
|
socket.read_exact(&mut forwarded_hello).await.unwrap();
|
|
assert_eq!(forwarded_hello, hello);
|
|
socket.write_all(&response).await.unwrap();
|
|
};
|
|
let (result, ()) = tokio::time::timeout(Duration::from_secs(2), async {
|
|
tokio::join!(
|
|
setup_reality_server_stream(io, &target, parsed, &resolver),
|
|
decoy_io,
|
|
)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
let error = result.err().expect("invalid encrypted records must fail");
|
|
assert_eq!(error.kind(), io::ErrorKind::InvalidData, "{error}");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn fallback_preread_backpressure_does_not_block_server_response() {
|
|
let (client, mut client_peer) = tokio::io::duplex(64);
|
|
let (dest, mut dest_peer) = tokio::io::duplex(64);
|
|
let pending = vec![42; 128];
|
|
let TcpServerSetupResult::Session(session) = start_forward_to_dest(
|
|
Box::new(client),
|
|
Box::new(dest),
|
|
vec![Bytes::from_static(b"server")],
|
|
Bytes::new(),
|
|
Bytes::copy_from_slice(&pending),
|
|
) else {
|
|
panic!("expected fallback session");
|
|
};
|
|
tokio::time::timeout(Duration::from_secs(1), async {
|
|
tokio::join!(session, async {
|
|
let mut response = [0; 6];
|
|
client_peer.read_exact(&mut response).await.unwrap();
|
|
assert_eq!(&response, b"server");
|
|
let mut received = vec![0; pending.len()];
|
|
dest_peer.read_exact(&mut received).await.unwrap();
|
|
assert_eq!(received, pending);
|
|
client_peer.shutdown().await.unwrap();
|
|
dest_peer.shutdown().await.unwrap();
|
|
});
|
|
})
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn fallback_preserves_preread_client_bytes() {
|
|
let (client, mut client_peer) = tokio::io::duplex(64);
|
|
let (dest, mut dest_peer) = tokio::io::duplex(64);
|
|
let TcpServerSetupResult::Session(session) = start_forward_to_dest(
|
|
Box::new(client),
|
|
Box::new(dest),
|
|
vec![Bytes::from_static(b"server")],
|
|
Bytes::from_static(b"-tail"),
|
|
Bytes::from_static(b"client"),
|
|
) else {
|
|
panic!("expected fallback session");
|
|
};
|
|
tokio::time::timeout(Duration::from_secs(1), async {
|
|
tokio::join!(session, async {
|
|
client_peer.write_all(b"-live").await.unwrap();
|
|
client_peer.shutdown().await.unwrap();
|
|
let mut received = [0; 11];
|
|
dest_peer.read_exact(&mut received).await.unwrap();
|
|
assert_eq!(&received, b"client-live");
|
|
dest_peer.write_all(b"-reply").await.unwrap();
|
|
dest_peer.shutdown().await.unwrap();
|
|
let mut response = Vec::new();
|
|
client_peer.read_to_end(&mut response).await.unwrap();
|
|
assert_eq!(response, b"server-tail-reply");
|
|
});
|
|
})
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
struct StalledShutdown {
|
|
fail_write: bool,
|
|
fail_flush: bool,
|
|
_marker: Arc<()>,
|
|
}
|
|
|
|
impl AsyncRead for StalledShutdown {
|
|
fn poll_read(
|
|
self: Pin<&mut Self>,
|
|
_: &mut Context<'_>,
|
|
_: &mut ReadBuf<'_>,
|
|
) -> Poll<io::Result<()>> {
|
|
Poll::Ready(Ok(()))
|
|
}
|
|
}
|
|
|
|
impl AsyncWrite for StalledShutdown {
|
|
fn poll_write(
|
|
self: Pin<&mut Self>,
|
|
_: &mut Context<'_>,
|
|
bytes: &[u8],
|
|
) -> Poll<io::Result<usize>> {
|
|
if self.fail_write {
|
|
Poll::Ready(Err(io::ErrorKind::BrokenPipe.into()))
|
|
} else {
|
|
Poll::Ready(Ok(bytes.len()))
|
|
}
|
|
}
|
|
fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<()>> {
|
|
if self.fail_flush {
|
|
Poll::Ready(Err(io::ErrorKind::BrokenPipe.into()))
|
|
} else {
|
|
Poll::Ready(Ok(()))
|
|
}
|
|
}
|
|
fn poll_shutdown(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<()>> {
|
|
Poll::Pending
|
|
}
|
|
}
|
|
|
|
impl crate::async_stream::AsyncPing for StalledShutdown {
|
|
fn supports_ping(&self) -> bool {
|
|
false
|
|
}
|
|
fn poll_write_ping(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<bool>> {
|
|
Poll::Ready(Ok(false))
|
|
}
|
|
}
|
|
|
|
impl AsyncStream for StalledShutdown {}
|
|
|
|
#[tokio::test(start_paused = true)]
|
|
async fn fallback_cleanup_is_bounded_after_copy_and_early_errors() {
|
|
let bytes = Bytes::from_static(b"handshake");
|
|
let cases = [
|
|
(false, false, vec![], Bytes::new(), 10),
|
|
(true, false, vec![bytes.clone()], Bytes::new(), 5),
|
|
(false, true, vec![bytes.clone()], Bytes::new(), 5),
|
|
(true, false, vec![], bytes, 5),
|
|
];
|
|
for (fail_write, fail_flush, records, remaining, seconds) in cases {
|
|
let marker = Arc::new(());
|
|
let client = StalledShutdown {
|
|
fail_write,
|
|
fail_flush,
|
|
_marker: marker.clone(),
|
|
};
|
|
let dest = StalledShutdown {
|
|
fail_write: false,
|
|
fail_flush: false,
|
|
_marker: marker.clone(),
|
|
};
|
|
let TcpServerSetupResult::Session(session) = start_forward_to_dest(
|
|
Box::new(client),
|
|
Box::new(dest),
|
|
records,
|
|
remaining,
|
|
Bytes::new(),
|
|
) else {
|
|
panic!("fallback must remain owned by its caller")
|
|
};
|
|
let started = tokio::time::Instant::now();
|
|
tokio::time::timeout(std::time::Duration::from_secs(11), session)
|
|
.await
|
|
.expect("fallback cleanup retained the streams indefinitely");
|
|
assert_eq!(started.elapsed(), std::time::Duration::from_secs(seconds));
|
|
assert_eq!(Arc::strong_count(&marker), 1);
|
|
}
|
|
}
|