use super::*;
use std::fs;
use std::io::Write;
use std::net::TcpListener;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use rustls::pki_types::pem::PemObject;
use rustls::pki_types::{CertificateDer, ServerName};
use rustls::{ClientConfig, ClientConnection, RootCertStore, StreamOwned};
const ACK_LINE: &[u8] = b"NETBAT/2 SUB_ACK orders.open.v1 1 aabb\n";
const CANCEL_LINE: &[u8] = b"NETBAT/2 SUB_CANCEL orders.open.v1 client.cancel\n";
fn token() -> SubscriptionToken {
SubscriptionToken::new("orders.open.v1", &Limits::default()).expect("token")
}
#[test]
fn forwards_a_complete_ack_without_stopping() {
let (tx, rx) = flume::bounded(16);
let mut acc = ControlAccumulator::new();
acc.extend(ACK_LINE);
assert!(matches!(
acc.forward_complete_lines(&tx, &Limits::default(), &token()),
LineFlow::NeedMore
));
assert!(matches!(rx.try_recv(), Ok(SessionControl::Ack { .. })));
assert!(acc.buffer.is_empty(), "the consumed line is drained");
}
#[test]
fn cancel_is_terminal_and_stops() {
let (tx, rx) = flume::bounded(16);
let mut acc = ControlAccumulator::new();
acc.extend(CANCEL_LINE);
assert!(matches!(
acc.forward_complete_lines(&tx, &Limits::default(), &token()),
LineFlow::Stopped
));
assert!(matches!(rx.try_recv(), Ok(SessionControl::Cancel)));
}
#[test]
fn reassembles_a_frame_split_across_extends() {
let (tx, rx) = flume::bounded(16);
let mut acc = ControlAccumulator::new();
let split = CANCEL_LINE.len() / 2;
acc.extend(&CANCEL_LINE[..split]);
assert!(matches!(
acc.forward_complete_lines(&tx, &Limits::default(), &token()),
LineFlow::NeedMore
));
assert!(rx.try_recv().is_err(), "no frame before the line completes");
acc.extend(&CANCEL_LINE[split..]);
assert!(matches!(
acc.forward_complete_lines(&tx, &Limits::default(), &token()),
LineFlow::Stopped
));
assert!(matches!(rx.try_recv(), Ok(SessionControl::Cancel)));
}
#[test]
fn oversize_unterminated_line_is_malformed_terminal() {
let limits = Limits::default().with_max_line_bytes(8);
let (tx, rx) = flume::bounded(16);
let mut acc = ControlAccumulator::new();
acc.extend(b"NETBAT/2 SUB_ACK no-newline-here-yet");
assert!(matches!(
acc.forward_complete_lines(&tx, &limits, &token()),
LineFlow::Stopped
));
assert!(matches!(rx.try_recv(), Ok(SessionControl::Malformed)));
assert!(acc.buffer.is_empty());
}
#[test]
fn full_lane_reports_backpressure_and_keeps_the_line() {
let (tx, rx) = flume::bounded(1);
tx.try_send(SessionControl::Cancel)
.expect("prefill the lane");
let mut acc = ControlAccumulator::new();
acc.extend(ACK_LINE);
assert!(matches!(
acc.forward_complete_lines(&tx, &Limits::default(), &token()),
LineFlow::Backpressure
));
assert_eq!(
acc.buffer, ACK_LINE,
"the unsent line is retained for retry"
);
assert!(matches!(rx.try_recv(), Ok(SessionControl::Cancel)));
assert!(matches!(
acc.forward_complete_lines(&tx, &Limits::default(), &token()),
LineFlow::NeedMore
));
assert!(matches!(rx.try_recv(), Ok(SessionControl::Ack { .. })));
}
#[test]
fn unterminated_line_at_the_exact_cap_is_not_yet_malformed() {
let limits = Limits::default().with_max_line_bytes(8);
let (tx, rx) = flume::bounded(16);
let mut acc = ControlAccumulator::new();
acc.extend(b"12345678"); assert!(matches!(
acc.forward_complete_lines(&tx, &limits, &token()),
LineFlow::NeedMore
));
assert!(
rx.try_recv().is_err(),
"an at-cap partial line is not malformed"
);
assert_eq!(acc.buffer, b"12345678", "the at-cap partial stays buffered");
acc.extend(b"9"); assert!(matches!(
acc.forward_complete_lines(&tx, &limits, &token()),
LineFlow::Stopped
));
assert!(matches!(rx.try_recv(), Ok(SessionControl::Malformed)));
assert!(acc.buffer.is_empty());
}
fn fixture_bytes(name: &str) -> Vec<u8> {
let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"));
fs::read(&path).expect("read committed TLS test fixture")
}
fn tls_pair() -> (TlsStream, StreamOwned<ClientConnection, TcpStream>) {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind localhost listener");
let addr = listener.local_addr().expect("listener addr");
let client_thread = thread::Builder::new()
.name("netbat-tls-unit-client".to_owned())
.spawn(move || {
let mut roots = RootCertStore::empty();
for cert in CertificateDer::pem_slice_iter(&fixture_bytes("tls_test_ca_cert.pem")) {
roots
.add(cert.expect("parse fixture CA cert"))
.expect("add fixture CA cert");
}
let config = ClientConfig::builder_with_provider(Arc::new(
rustls::crypto::ring::default_provider(),
))
.with_safe_default_protocol_versions()
.expect("client protocol versions")
.with_root_certificates(roots)
.with_no_client_auth();
let server_name = ServerName::try_from("localhost").expect("server name");
let mut conn =
ClientConnection::new(Arc::new(config), server_name).expect("client connection");
let mut sock = TcpStream::connect(addr).expect("connect tls client");
sock.set_read_timeout(Some(Duration::from_secs(5)))
.expect("client read timeout");
sock.set_write_timeout(Some(Duration::from_secs(5)))
.expect("client write timeout");
conn.complete_io(&mut sock).expect("client handshake");
StreamOwned::new(conn, sock)
})
.expect("spawn tls unit client");
let (server_sock, _peer) = listener.accept().expect("accept tls client");
server_sock
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("server read timeout");
server_sock
.set_write_timeout(Some(Duration::from_secs(5)))
.expect("server write timeout");
let tls = TlsServerConfig::from_pem(
&fixture_bytes("tls_test_cert.pem"),
&fixture_bytes("tls_test_key.pem"),
)
.expect("build server TLS config from fixtures");
let server = tls.handshake(server_sock).expect("server handshake");
let client = client_thread.join().expect("client handshake thread");
(server, client)
}
fn drain_once(
server: &mut TlsStream,
accumulator: &mut ControlAccumulator,
control_tx: &flume::Sender<SessionControl>,
limits: &Limits,
) -> ControlDrain {
server
.sock
.set_nonblocking(true)
.expect("flip server socket non-blocking");
let outcome = drain_control_frames(server, accumulator, control_tx, limits, &token());
server
.sock
.set_nonblocking(false)
.expect("restore blocking server socket");
outcome
}
#[test]
fn quiet_connection_drain_is_idle() {
let (mut server, client) = tls_pair();
let limits = Limits::default();
let (control_tx, control_rx) = flume::bounded(16);
let mut accumulator = ControlAccumulator::new();
let outcome = drain_once(&mut server, &mut accumulator, &control_tx, &limits);
assert!(
matches!(outcome, ControlDrain::Idle),
"a quiet healthy connection must drain to Idle"
);
assert!(
control_rx.try_recv().is_err(),
"no control frame is forwarded for a quiet connection"
);
drop(client);
}
#[test]
fn eof_without_close_notify_drains_to_peer_gone() {
let (mut server, client) = tls_pair();
let consumed = server
.conn
.read_tls(&mut std::io::empty())
.expect("record-layer EOF injection");
assert_eq!(consumed, 0, "io::empty() must register EOF");
let limits = Limits::default();
let (control_tx, _control_rx) = flume::bounded(16);
let mut accumulator = ControlAccumulator::new();
let outcome = drain_once(&mut server, &mut accumulator, &control_tx, &limits);
assert!(
matches!(outcome, ControlDrain::PeerGone),
"an unclean EOF must drain to PeerGone"
);
drop(client);
}
#[test]
fn reset_peer_drains_to_peer_gone() {
let mut outcome = ControlDrain::Idle;
for _attempt in 0_u8..10 {
let (mut server, client) = tls_pair();
server
.sock
.write_all(b"unread-seed")
.expect("seed unread client bytes");
drop(client);
let limits = Limits::default();
let (control_tx, _control_rx) = flume::bounded(16);
let mut accumulator = ControlAccumulator::new();
outcome = drain_once(&mut server, &mut accumulator, &control_tx, &limits);
if !matches!(outcome, ControlDrain::Idle) {
break;
}
}
assert!(
matches!(outcome, ControlDrain::PeerGone),
"a reset peer must drain to PeerGone"
);
}
fn flooded_tls_pair() -> (TlsStream, StreamOwned<ClientConnection, TcpStream>, usize) {
let (server, mut client) = tls_pair();
client
.sock
.set_nonblocking(true)
.expect("non-blocking client socket for the flood");
let payload = vec![b'x'; 16 * 1024]; let mut wire_bytes = 0_usize;
'flood: loop {
let _buffered = client
.conn
.writer()
.write(&payload)
.expect("buffer flood plaintext");
while client.conn.wants_write() {
match client.conn.write_tls(&mut client.sock) {
Ok(count) => wire_bytes += count,
Err(error) => {
assert_eq!(
error.kind(),
io::ErrorKind::WouldBlock,
"the flood must only stop on a full socket"
);
break 'flood;
}
}
}
}
(server, client, wire_bytes)
}
#[test]
fn drain_budget_bounds_one_flood_pass() {
const PER_READ_CEILING: usize = 16_384 + 2_048 + 5;
const BUDGET_PASS_FLOOR: usize = 200_000;
let upper = MAX_TLS_READS_PER_DRAIN * PER_READ_CEILING;
let mut observed: Vec<usize> = Vec::new();
for _attempt in 0_u8..5 {
let (mut server, _client, wire_bytes) = flooded_tls_pair();
assert!(
wire_bytes >= upper + 256 * 1024,
"kernel socket buffers hold too little flood to exercise the budget: {wire_bytes}"
);
let limits = Limits::default().with_max_line_bytes(64 * 1024 * 1024);
let (control_tx, control_rx) = flume::bounded(16);
let mut accumulator = ControlAccumulator::new();
server
.sock
.set_nonblocking(true)
.expect("non-blocking server socket");
let outcome = drain_control_frames(
&mut server,
&mut accumulator,
&control_tx,
&limits,
&token(),
);
server
.sock
.set_nonblocking(false)
.expect("restore blocking server socket");
assert!(
matches!(outcome, ControlDrain::Idle),
"a budget-bounded drain pass yields Idle so deliveries keep flowing"
);
assert!(
control_rx.try_recv().is_err(),
"a newline-free flood forwards no control frame"
);
let drained = accumulator.buffer.len();
assert!(
drained < upper,
"one drain pass must accumulate strictly less than cap * MAX_WIRE_SIZE \
plaintext bytes; drained {drained} of a {wire_bytes}-byte flood"
);
let mut probe = [0_u8; 1];
let peeked = server
.sock
.peek(&mut probe)
.expect("a budget-capped pass leaves flood bytes unread on the socket");
assert_eq!(peeked, 1, "the unread flood remainder is peekable");
observed.push(drained);
if drained > BUDGET_PASS_FLOOR {
break;
}
}
let last = observed
.last()
.copied()
.expect("at least one drain pass ran");
assert!(
last > BUDGET_PASS_FLOOR,
"the pass must be ended by the read budget, not by supply; observed \
plaintext per attempt: {observed:?}"
);
}