use std::{
collections::HashMap,
future::Future,
io,
net::{IpAddr, SocketAddr},
};
use socket2::{Domain, Protocol, SockAddr, Socket, Type};
use thiserror::Error;
use tokio::{
net::UdpSocket,
sync::watch,
task::{Id, JoinError, JoinSet},
};
use crate::{Server, ServerConfig, ServerRuntimeError};
#[derive(Debug)]
pub struct ServerSet {
servers: Vec<Server>,
local_addrs: Vec<SocketAddr>,
}
impl ServerSet {
pub async fn bind<I>(addrs: I, config: ServerConfig) -> Result<Self, ServerSetError>
where
I: IntoIterator<Item = SocketAddr>,
{
let requested: Vec<SocketAddr> = addrs.into_iter().collect();
if requested.is_empty() {
return Err(ServerSetError::NoListeners);
}
let mut servers = Vec::with_capacity(requested.len());
let mut local_addrs = Vec::with_capacity(requested.len());
for &addr in &requested {
let server = bind_listener(addr, config.clone(), needs_ipv6_only(addr, &requested))
.await
.map_err(|source| ServerSetError::ListenerSetup { addr, source })?;
let local_addr = server
.local_addr()
.map_err(|source| ServerSetError::ListenerSetup { addr, source })?;
servers.push(server);
local_addrs.push(local_addr);
}
Ok(Self {
servers,
local_addrs,
})
}
#[must_use]
pub fn local_addrs(&self) -> &[SocketAddr] {
&self.local_addrs
}
pub async fn run<F>(self, shutdown: F) -> Result<(), ServerSetError>
where
F: Future<Output = ()>,
{
let (shutdown_tx, _) = watch::channel(false);
let mut listeners = JoinSet::new();
let mut listener_addrs: HashMap<Id, SocketAddr> = HashMap::new();
for (mut server, addr) in self.servers.into_iter().zip(self.local_addrs) {
let mut shutdown_rx = shutdown_tx.subscribe();
let handle = listeners.spawn(async move {
let outcome = server
.run(async move {
let _ = shutdown_rx.wait_for(|requested| *requested).await;
})
.await;
(addr, outcome)
});
listener_addrs.insert(handle.id(), addr);
}
tokio::pin!(shutdown);
let mut shutting_down = false;
let mut failure: Option<ServerSetError> = None;
loop {
tokio::select! {
() = &mut shutdown, if !shutting_down => {
shutting_down = true;
let _ = shutdown_tx.send(true);
}
joined = listeners.join_next() => {
let Some(joined) = joined else { break };
let listener_failure = match joined {
Ok((_, Ok(()))) if shutting_down => None,
Ok((addr, Ok(()))) => Some(ServerSetError::ListenerStopped { addr }),
Ok((addr, Err(source))) => {
Some(ServerSetError::ListenerRun { addr, source })
}
Err(source) => Some(ServerSetError::ListenerTask {
addr: listener_addrs.get(&source.id()).copied(),
source,
}),
};
if let Some(listener_failure) = listener_failure {
failure.get_or_insert(listener_failure);
if !shutting_down {
shutting_down = true;
let _ = shutdown_tx.send(true);
}
}
}
}
}
failure.map_or(Ok(()), Err)
}
}
#[must_use]
pub fn address_family_available(addr: SocketAddr) -> bool {
let wildcard = if addr.is_ipv6() {
SocketAddr::from((std::net::Ipv6Addr::UNSPECIFIED, 0))
} else {
SocketAddr::from((std::net::Ipv4Addr::UNSPECIFIED, 0))
};
let domain = if addr.is_ipv6() {
Domain::IPV6
} else {
Domain::IPV4
};
let Ok(socket) = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP)) else {
return false;
};
socket.bind(&SockAddr::from(wildcard)).is_ok()
}
async fn bind_listener(
addr: SocketAddr,
config: ServerConfig,
ipv6_only: bool,
) -> Result<Server, ServerRuntimeError> {
if !ipv6_only {
return Server::bind(addr, config).await;
}
let socket =
bind_ipv6_only(addr).map_err(|source| ServerRuntimeError::Bind { addr, source })?;
Server::from_socket(socket, config)
}
fn bind_ipv6_only(addr: SocketAddr) -> io::Result<UdpSocket> {
let socket = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP))?;
socket.set_only_v6(true)?;
socket.set_nonblocking(true)?;
socket.bind(&SockAddr::from(addr))?;
UdpSocket::from_std(socket.into())
}
fn needs_ipv6_only(addr: SocketAddr, requested: &[SocketAddr]) -> bool {
is_genuine_ipv6(addr)
&& addr.port() != 0
&& requested
.iter()
.any(|other| other.port() == addr.port() && serves_ipv4(*other))
}
fn is_genuine_ipv6(addr: SocketAddr) -> bool {
addr.is_ipv6() && !serves_ipv4(addr)
}
fn serves_ipv4(addr: SocketAddr) -> bool {
match addr.ip() {
IpAddr::V4(_) => true,
IpAddr::V6(address) => address.to_ipv4_mapped().is_some(),
}
}
#[derive(Debug, Error)]
pub enum ServerSetError {
#[error("a server set needs at least one listener address")]
NoListeners,
#[error("could not set up listener {addr}: {source}")]
ListenerSetup {
addr: SocketAddr,
#[source]
source: ServerRuntimeError,
},
#[error("listener {addr} failed: {source}")]
ListenerRun {
addr: SocketAddr,
#[source]
source: ServerRuntimeError,
},
#[error("listener {addr} stopped before the server set was asked to shut down")]
ListenerStopped { addr: SocketAddr },
#[error(
"listener task{} did not complete: {source}",
.addr.map(|addr| format!(" for {addr}")).unwrap_or_default()
)]
ListenerTask {
addr: Option<SocketAddr>,
#[source]
source: JoinError,
},
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::{token::TokenSource, ServerCore, ServerError};
#[test]
fn only_a_genuine_ipv6_listener_sharing_an_explicit_port_is_bound_ipv6_only() {
for (addr, requested, ipv6_only, why) in [
(
"[::]:2112",
vec!["0.0.0.0:2112", "[::]:2112"],
true,
"the wildcard pair is the reason this exists",
),
(
"[::1]:2112",
vec!["127.0.0.1:2112", "[::1]:2112"],
true,
"and so is the explicit loopback pair",
),
(
"0.0.0.0:2112",
vec!["0.0.0.0:2112", "[::]:2112"],
false,
"the IPv4 half is bound exactly as it always was",
),
(
"[::]:2112",
vec!["[::]:2112"],
false,
"an IPv6 listener alone keeps ordinary bind semantics",
),
(
"[::]:2112",
vec!["0.0.0.0:2113", "[::]:2112"],
false,
"different ports cannot collide",
),
(
"[::]:0",
vec!["0.0.0.0:0", "[::]:0"],
false,
"two ephemeral requests resolve independently",
),
(
"[::ffff:0.0.0.0]:2112",
vec!["0.0.0.0:2112", "[::ffff:0.0.0.0]:2112"],
false,
"a mapped wildcard is an IPv4 listener, not an IPv6 one",
),
(
"[::]:2112",
vec!["[::ffff:0.0.0.0]:2112", "[::]:2112"],
true,
"and it is the IPv4 half a genuine IPv6 listener must avoid",
),
(
"[::ffff:127.0.0.1]:2112",
vec!["[::ffff:127.0.0.1]:2112", "[::1]:2112"],
false,
"a mapped host address is explicit IPv4 too",
),
] {
let requested: Vec<SocketAddr> = requested
.iter()
.map(|addr| addr.parse().expect("a test address must parse"))
.collect();
assert_eq!(
needs_ipv6_only(addr.parse().unwrap(), &requested),
ipv6_only,
"{addr} among {requested:?}: {why}"
);
}
}
#[tokio::test(flavor = "current_thread")]
async fn a_set_serving_nothing_is_refused() {
assert!(matches!(
ServerSet::bind([], ServerConfig::default()).await,
Err(ServerSetError::NoListeners)
));
}
#[derive(Debug)]
struct FailAfterFirstToken(Option<u64>);
impl TokenSource for FailAfterFirstToken {
fn next_token(&mut self) -> Result<u64, ServerError> {
self.0.take().ok_or_else(|| ServerError::RandomSource {
reason: "test fault injection: exhausted after the first token".to_owned(),
})
}
}
const FIRST_TOKEN: u64 = 0xdead_beef_dead_beef;
const TEST_TIMEOUT: Duration = Duration::from_secs(5);
const REPLY_TIMEOUT: Duration = Duration::from_secs(1);
fn loopback(port: u16) -> SocketAddr {
SocketAddr::from((std::net::Ipv4Addr::LOCALHOST, port))
}
fn requested_params() -> irtt_proto::Params {
irtt_proto::Params {
protocol_version: 1,
duration_ns: 10_000_000_000,
interval_ns: 100_000_000,
received_stats: irtt_proto::ReceivedStats::Both,
stamp_at: irtt_proto::StampAt::Both,
clock: irtt_proto::Clock::Both,
..irtt_proto::Params::default()
}
}
async fn send_open(client: &UdpSocket, addr: SocketAddr) {
let request = irtt_proto::encode_request(
irtt_proto::RequestToEncode::Open {
params: &requested_params(),
no_test: false,
},
None,
)
.unwrap();
client.send_to(&request, addr).await.unwrap();
}
async fn open(client: &UdpSocket, addr: SocketAddr) -> Option<irtt_proto::OpenReply> {
send_open(client, addr).await;
let mut buffer = [0u8; 4096];
match tokio::time::timeout(REPLY_TIMEOUT, client.recv_from(&mut buffer)).await {
Ok(received) => {
let (len, from) = received.unwrap();
assert_eq!(from, addr, "a reply came from the wrong listener");
Some(irtt_proto::decode_open_reply(&buffer[..len], None).unwrap())
}
Err(_) => None,
}
}
#[tokio::test(flavor = "current_thread")]
async fn a_mid_run_listener_failure_shuts_down_its_siblings_and_fails_the_set() {
let config = ServerConfig::default().with_min_send_interval(Duration::ZERO);
let failing_socket = UdpSocket::bind(loopback(0)).await.unwrap();
let failing_core = ServerCore::with_token_source(
config.clone(),
Box::new(FailAfterFirstToken(Some(FIRST_TOKEN))),
);
let failing = Server::from_socket_with_core(failing_socket, failing_core).unwrap();
let failing_addr = failing.local_addr().unwrap();
let healthy_socket = UdpSocket::bind(loopback(0)).await.unwrap();
let healthy = Server::from_socket(healthy_socket, config).unwrap();
let healthy_addr = healthy.local_addr().unwrap();
let set = ServerSet {
servers: vec![failing, healthy],
local_addrs: vec![failing_addr, healthy_addr],
};
let run = tokio::spawn(set.run(std::future::pending::<()>()));
let client = UdpSocket::bind(loopback(0)).await.unwrap();
let opened = open(&client, failing_addr)
.await
.expect("the failing listener is bound and serving before it fails");
assert_eq!(opened.token, FIRST_TOKEN);
assert!(
open(&client, healthy_addr).await.is_some(),
"the healthy listener is bound and serving too"
);
send_open(&client, failing_addr).await;
let outcome = tokio::time::timeout(TEST_TIMEOUT, run)
.await
.expect("the set stopped on its own, with no external shutdown sent")
.expect("the supervising task did not panic");
match outcome {
Err(ServerSetError::ListenerRun { addr, source }) => {
assert_eq!(
addr, failing_addr,
"the failure names the listener that actually failed"
);
assert!(
matches!(
source,
ServerRuntimeError::Core(ServerError::RandomSource { .. })
),
"unexpected failure source: {source}"
);
}
other => panic!("expected the injected listener failure, got {other:?}"),
}
for addr in [healthy_addr, failing_addr] {
std::net::UdpSocket::bind(addr)
.unwrap_or_else(|error| panic!("{addr} was not released: {error}"));
}
}
}