Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 13 additions & 35 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions iroh-relay/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,7 @@ async fn build_relay_config(cfg: Config) -> Result<relay::ServerConfig<std::io::
quic_config = Some(QuicConfig {
server_config: tls.server_config.clone(),
bind_addr: tls.quic_bind_addr,
alternate_port: Default::default(),
});
} else {
whatever!(
Expand Down
132 changes: 84 additions & 48 deletions iroh-relay/src/quic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub const QUIC_ADDR_DISC_CLOSE_REASON: &[u8] = b"finished";

#[cfg(feature = "server")]
pub(crate) mod server {

use quinn::{
ApplicationClose, ConnectionError,
crypto::rustls::{NoInitialCipherSuite, QuicServerConfig},
Expand Down Expand Up @@ -103,7 +104,7 @@ pub(crate) mod server {
pub(crate) fn spawn(mut quic_config: QuicConfig) -> Result<Self, QuicSpawnError> {
quic_config.server_config.alpn_protocols =
vec![crate::quic::ALPN_QUIC_ADDR_DISC.to_vec()];
let server_config = QuicServerConfig::try_from(quic_config.server_config)?;
let server_config = QuicServerConfig::try_from(quic_config.server_config.clone())?;
let mut server_config = quinn::ServerConfig::with_crypto(Arc::new(server_config));
let transport_config =
Arc::get_mut(&mut server_config.transport).expect("not used yet");
Expand All @@ -113,64 +114,45 @@ pub(crate) mod server {
// enable sending quic address discovery frames
.send_observed_address_reports(true);

let endpoint = quinn::Endpoint::server(server_config, quic_config.bind_addr)
// Spawn main QUIC server
let endpoint = quinn::Endpoint::server(server_config.clone(), quic_config.bind_addr)
.context(EndpointServerSnafu)?;
let bind_addr = endpoint.local_addr().context(LocalAddrSnafu)?;

// Spawn port variation server for NAT testing based on configuration
let port_variation_addr = quic_config
.alternate_port
.get_bind_addr(quic_config.bind_addr);
let port_variation_endpoint = match port_variation_addr {
None => None,
Some(addr) => {
let endpoint = quinn::Endpoint::server(server_config, addr)
.context(EndpointServerSnafu)?;
let actual_addr = endpoint.local_addr().context(LocalAddrSnafu)?;
info!(?actual_addr, "QUIC port variation server listening on");
Some(endpoint)
}
};

info!(?bind_addr, "QUIC server listening on");

let cancel = CancellationToken::new();
let cancel_accept_loop = cancel.clone();

let task = tokio::task::spawn(
let task = tokio::task::spawn({
let cancel = cancel.child_token();
async move {
let mut set = JoinSet::new();
debug!("waiting for connections...");
loop {
tokio::select! {
biased;
_ = cancel_accept_loop.cancelled() => {
break;
}
Some(res) = set.join_next() => {
if let Err(err) = res {
if err.is_panic() {
panic!("task panicked: {err:#?}");
} else {
debug!("error accepting incoming connection: {err:#?}");
}
}
}
res = endpoint.accept() => match res {
Some(conn) => {
debug!("accepting connection");
let remote_addr = conn.remote_address();
set.spawn(
handle_connection(conn).instrument(info_span!("qad-conn", %remote_addr))
); }
None => {
debug!("endpoint closed");
break;
}
}
let main_fut = accept_loop(endpoint, cancel.clone())
.instrument(info_span!("quic-endpoint"));
let port_variation_fut = async move {
if let Some(endpoint) = port_variation_endpoint {
accept_loop(endpoint, cancel).await
}
}
// close all connections and wait until they have all grace
// fully closed.
endpoint.close(QUIC_ADDR_DISC_CLOSE_CODE, QUIC_ADDR_DISC_CLOSE_REASON);
endpoint.wait_idle().await;

// all tasks should be closed, since the endpoint has shutdown
// all connections, but await to ensure they are finished.
set.abort_all();
while !set.is_empty() {
_ = set.join_next().await;
}

debug!("quic endpoint has been shutdown.");
.instrument(info_span!("quic-endpoint-alt-port"));
let _ = tokio::join!(main_fut, port_variation_fut);
}
.instrument(info_span!("quic-endpoint")),
);
});

Ok(Self {
bind_addr,
cancel,
Expand All @@ -190,6 +172,59 @@ pub(crate) mod server {
}
}

async fn accept_loop(endpoint: quinn::Endpoint, cancel_accept_loop: CancellationToken) {
let mut set = JoinSet::new();
let local_addr = endpoint
.local_addr()
.unwrap_or_else(|_| "unknown".parse().unwrap());
debug!("waiting for connections on {local_addr}...");
loop {
tokio::select! {
biased;
_ = cancel_accept_loop.cancelled() => {
break;
}
Some(res) = set.join_next() => {
if let Err(err) = res {
if err.is_panic() {
panic!("task panicked: {err:#?}");
} else {
debug!("error accepting incoming connection: {err:#?}");
}
}
}
res = endpoint.accept() => match res {
Some(conn) => {
debug!("accepting connection on {local_addr}");
let remote_addr = conn.remote_address();
set.spawn(async move {
if let Err(err) = handle_connection(conn).await {
debug!("connection error: {err:#?}");
}
}.instrument(info_span!("qad-conn", %remote_addr)));
}
None => {
debug!("endpoint closed");
break;
}
}
}
}
// close all connections and wait until they have all grace
// fully closed.
endpoint.close(QUIC_ADDR_DISC_CLOSE_CODE, QUIC_ADDR_DISC_CLOSE_REASON);
endpoint.wait_idle().await;

// all tasks should be closed, since the endpoint has shutdown
// all connections, but await to ensure they are finished.
set.abort_all();
while !set.is_empty() {
set.join_next().await;
}

debug!("quic endpoint has been shutdown.");
}

/// A handle for the Server side of QUIC address discovery.
///
/// This does not allow access to the task but can communicate with it.
Expand Down Expand Up @@ -383,6 +418,7 @@ mod tests {
let quic_server = QuicServer::spawn(QuicConfig {
server_config,
bind_addr,
alternate_port: crate::server::AlternatePortConfig::Disabled,
})?;

// create a client-side endpoint
Expand Down
28 changes: 28 additions & 0 deletions iroh-relay/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,40 @@ pub struct QuicConfig {
/// The socket address on which the QUIC server should bind.
///
/// Normally you'd chose port `7842`, see [`crate::defaults::DEFAULT_RELAY_QUIC_PORT`].
/// A second server will automatically be spawned on port + 1 for NAT port mapping variation testing.
pub bind_addr: SocketAddr,
/// The TLS server configuration for the QUIC server.
///
/// If this [`rustls::ServerConfig`] does not support TLS 1.3, the QUIC server will fail
/// to spawn.
pub server_config: rustls::ServerConfig,

/// Select an additional port to listen on.
///
/// This may be used by nodes to check for NAT port binding variation.
pub alternate_port: AlternatePortConfig,
}

/// Configuration for the alternate listening port of the QUIC server.
#[derive(Debug, Default, Copy, Clone)]
pub enum AlternatePortConfig {
/// Don't listen on an additional port.
Disabled,
/// Use the port of the bind address + 1.
#[default]
Enabled,
}

impl AlternatePortConfig {
/// Returns the bind address for the alternate endpoint, or None if disabled.
pub(crate) fn get_bind_addr(self, main_addr: SocketAddr) -> Option<SocketAddr> {
match self {
AlternatePortConfig::Disabled => None,
AlternatePortConfig::Enabled => {
Some(SocketAddr::from((main_addr.ip(), main_addr.port() + 1)))
}
}
}
}

/// TLS configuration for Relay server.
Expand Down
21 changes: 19 additions & 2 deletions iroh-relay/src/server/testing.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
//! Exposes functions to quickly configure a server suitable for testing.
use std::net::Ipv4Addr;

use super::{AccessConfig, CertConfig, QuicConfig, RelayConfig, ServerConfig, TlsConfig};
use super::{
AccessConfig, AlternatePortConfig, CertConfig, QuicConfig, RelayConfig, ServerConfig, TlsConfig,
};

/// Creates a [`rustls::ServerConfig`] and certificates suitable for testing.
///
Expand Down Expand Up @@ -65,19 +67,34 @@ pub fn relay_config() -> RelayConfig<()> {
/// Creates a [`QuicConfig`] suitable for testing.
///
/// - Binds to an OS assigned port on ipv4
/// - Disables alternate port to avoid conflicts in parallel tests
/// - Uses [`self_signed_tls_certs_and_config`] to create tls certificates
pub fn quic_config() -> QuicConfig {
let (_, server_config) = self_signed_tls_certs_and_config();
QuicConfig {
bind_addr: (Ipv4Addr::UNSPECIFIED, 0).into(),
server_config,
alternate_port: AlternatePortConfig::Disabled, // Explicitly disable for tests
}
}

/// Creates a [`QuicConfig`] with port variation enabled for testing port detection logic.
///
/// - Same as regular quic_config but with alternate port enabled
/// - Uses OS-assigned port (0) with alternate port enabled (main_port + 1)
pub fn quic_config_with_port_variation() -> QuicConfig {
let (_, server_config) = self_signed_tls_certs_and_config();
QuicConfig {
bind_addr: (Ipv4Addr::UNSPECIFIED, 0).into(), // Let OS assign port
server_config,
alternate_port: AlternatePortConfig::Enabled, // Enable alternate port (main + 1)
}
}

/// Creates a [`ServerConfig`] suitable for testing.
///
/// - Relaying is enabled using [`relay_config`]
/// - QUIC addr discovery is disabled.
/// - QUIC addr discovery is enabled with disabled alternate port to avoid conflicts.
/// - Metrics are not enabled.
pub fn server_config() -> ServerConfig<()> {
ServerConfig {
Expand Down
Loading
Loading