mirror of
https://github.com/NLnetLabs/domain.git
synced 2026-09-23 18:24:59 +02:00
* Add regression test for immediate EOF on stream * Fix dropped responses when DNS stream encounters EOF The `stream::Transport` loop did not account for responses that had been added to the reply channel prior to `reader_fut` returning. If a server sent a response and immediately closed the stream afterwards, that response was lost. I fixed the bug by adding an explicit check for any remaining items from `reply_receiver` in the `reader_fut` select branch. Additionally, the `Transport::error` function did not await the futures returned by `ReplySender::send`. This meant the errors were never sent and instead the channel closed without any message. I fixed that by making `Transort::error` an async fn. The PR additionally contains a regression test. Running it without the fix fails, but with the fixes it passes. I did not use stelline for this test as it requires a very specific order of IO events on the connection.
212 lines
6.8 KiB
Rust
212 lines
6.8 KiB
Rust
#![cfg(feature = "net")]
|
|
|
|
use domain::stelline::client::do_client_simple;
|
|
use domain::stelline::client::CurrStepValue;
|
|
use domain::stelline::connect::Connect;
|
|
use domain::stelline::connection::Connection;
|
|
use domain::stelline::dgram::Dgram;
|
|
use domain::stelline::parse_stelline::parse_file;
|
|
// use domain::net::client::clock::{Clock, FakeClock};
|
|
use domain::net::client::dgram;
|
|
use domain::net::client::dgram_stream;
|
|
use domain::net::client::multi_stream;
|
|
use domain::net::client::redundant;
|
|
use domain::net::client::request::{
|
|
GetResponse as _, RequestMessage, RequestMessageMulti,
|
|
};
|
|
use domain::net::client::stream;
|
|
use std::fs::File;
|
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
|
use std::str::FromStr;
|
|
use std::sync::Arc;
|
|
use tokio::net::TcpStream;
|
|
|
|
const TEST_FILE: &str = "test-data/client/basic.rpl";
|
|
|
|
#[test]
|
|
fn dgram() {
|
|
tokio_test::block_on(async {
|
|
let file = File::open(TEST_FILE).unwrap();
|
|
let stelline = parse_file(file, TEST_FILE);
|
|
let step_value = Arc::new(CurrStepValue::new());
|
|
|
|
let conn = Dgram::new(stelline.clone(), step_value.clone());
|
|
let dgram = dgram::Connection::new(conn);
|
|
|
|
do_client_simple(&stelline, &step_value, dgram).await;
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn single() {
|
|
tokio_test::block_on(async {
|
|
let file = File::open(TEST_FILE).unwrap();
|
|
let stelline = parse_file(file, TEST_FILE);
|
|
let step_value = Arc::new(CurrStepValue::new());
|
|
|
|
let conn = Connection::new(stelline.clone(), step_value.clone());
|
|
let (octstr, transport) =
|
|
stream::Connection::<_, RequestMessageMulti<Vec<u8>>>::new(conn);
|
|
tokio::spawn(async move {
|
|
transport.run().await;
|
|
});
|
|
|
|
do_client_simple(&stelline, &step_value, octstr).await;
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn multi() {
|
|
tokio_test::block_on(async {
|
|
let file = File::open(TEST_FILE).unwrap();
|
|
let stelline = parse_file(file, TEST_FILE);
|
|
let step_value = Arc::new(CurrStepValue::new());
|
|
|
|
let multi_conn = Connect::new(stelline.clone(), step_value.clone());
|
|
let (ms, ms_tran) = multi_stream::Connection::new(multi_conn);
|
|
tokio::spawn(async move {
|
|
ms_tran.run().await;
|
|
println!("multi conn run terminated");
|
|
});
|
|
|
|
do_client_simple(&stelline, &step_value, ms).await;
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn dgram_stream() {
|
|
tokio_test::block_on(async {
|
|
let file = File::open(TEST_FILE).unwrap();
|
|
let stelline = parse_file(file, TEST_FILE);
|
|
let step_value = Arc::new(CurrStepValue::new());
|
|
|
|
let conn = Dgram::new(stelline.clone(), step_value.clone());
|
|
let multi_conn = Connect::new(stelline.clone(), step_value.clone());
|
|
let (ds, tran) = dgram_stream::Connection::new(conn, multi_conn);
|
|
tokio::spawn(async move {
|
|
tran.run().await;
|
|
println!("dgram_stream conn run terminated");
|
|
});
|
|
|
|
do_client_simple(&stelline, &step_value, ds).await;
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn redundant() {
|
|
tokio_test::block_on(async {
|
|
let file = File::open(TEST_FILE).unwrap();
|
|
let stelline = parse_file(file, TEST_FILE);
|
|
let step_value = Arc::new(CurrStepValue::new());
|
|
|
|
let multi_conn = Connect::new(stelline.clone(), step_value.clone());
|
|
let (ms, ms_tran) = multi_stream::Connection::new(multi_conn);
|
|
tokio::spawn(async move {
|
|
ms_tran.run().await;
|
|
println!("multi conn run terminated");
|
|
});
|
|
|
|
// Redundant add previous connection.
|
|
let (redun, transp) = redundant::Connection::new();
|
|
let run_fut = transp.run();
|
|
tokio::spawn(async move {
|
|
run_fut.await;
|
|
println!("redundant conn run terminated");
|
|
});
|
|
|
|
redun.add(Box::new(ms.clone())).await.unwrap();
|
|
|
|
do_client_simple(&stelline, &step_value, redun).await;
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
// Connect directly to the internet. Disabled by default.
|
|
fn tcp() {
|
|
tokio_test::block_on(async {
|
|
let file = File::open(TEST_FILE).unwrap();
|
|
let stelline = parse_file(file, TEST_FILE);
|
|
let step_value = Arc::new(CurrStepValue::new());
|
|
|
|
let server_addr =
|
|
SocketAddr::new(IpAddr::from_str("9.9.9.9").unwrap(), 53);
|
|
|
|
let tcp_conn = match TcpStream::connect(server_addr).await {
|
|
Ok(conn) => conn,
|
|
Err(err) => {
|
|
panic!(
|
|
"TCP Connection to {server_addr} failed: {err}, exiting"
|
|
);
|
|
}
|
|
};
|
|
|
|
let (tcp, transport) = stream::Connection::<
|
|
_,
|
|
RequestMessageMulti<Vec<u8>>,
|
|
>::new(tcp_conn);
|
|
tokio::spawn(async move {
|
|
transport.run().await;
|
|
println!("single TCP run terminated");
|
|
});
|
|
|
|
do_client_simple(&stelline, &step_value, tcp).await;
|
|
});
|
|
}
|
|
|
|
/// Regression test: Ensure responses are not lost when a stream closes.
|
|
#[test]
|
|
fn stream_immediate_eof() {
|
|
use domain::base::{iana::Rcode, MessageBuilder, Name, Rtype};
|
|
use domain::rdata::A;
|
|
use futures_util::FutureExt as _;
|
|
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
|
|
|
|
// Query and response for `a.b.c. IN A`
|
|
let domain_name: Name<Vec<u8>> = "a.b.c.".parse().unwrap();
|
|
let mut query = MessageBuilder::new_vec().question();
|
|
query.push((&domain_name, Rtype::A)).unwrap();
|
|
|
|
let mut response = MessageBuilder::new_stream_vec()
|
|
.start_answer(&query.as_message(), Rcode::NOERROR)
|
|
.unwrap();
|
|
response
|
|
.push((&domain_name, 3600, A::new(Ipv4Addr::LOCALHOST)))
|
|
.unwrap();
|
|
|
|
let response = response.finish().into_target();
|
|
let query = RequestMessage::new(query).unwrap();
|
|
|
|
tokio_test::block_on(async move {
|
|
let (client, mut server) = tokio::io::duplex(response.len());
|
|
tokio::spawn(async move {
|
|
// Read the entire query to avoid breaking the client's write half.
|
|
let len = server.read_u16().await.unwrap();
|
|
let _ = server.read_exact(&mut vec![0; len as usize]).await;
|
|
|
|
// Write the entire response in one shot. This is important so that
|
|
// we can be sure the client sees the response and EOF at the same time.
|
|
server
|
|
.write_all(&response)
|
|
.now_or_never()
|
|
.expect("write should not return pending")
|
|
.expect("client dead?");
|
|
drop(server);
|
|
});
|
|
|
|
let (conn, transport) = stream::Connection::<
|
|
_,
|
|
RequestMessageMulti<Vec<u8>>,
|
|
>::new(client);
|
|
|
|
tokio::spawn(async move {
|
|
transport.run().await;
|
|
println!("single stream run terminated");
|
|
});
|
|
|
|
let resp = conn.get_request(query).get_response().await;
|
|
let answer = resp.expect("request should yield a response");
|
|
assert!(answer.no_error());
|
|
});
|
|
}
|