feat(relay): bind QUIC endpoint
Make the relay binary bind a real Quinn endpoint instead of only printing its configuration. This is the next runtime step toward the public relay while still keeping connection handling out of this commit. The relay now builds a self-signed development TLS configuration, advertises the lanparty ALPN, enables QUIC datagram buffers, binds the configured UDP address, prints the actual local address, and waits for Ctrl-C before closing the endpoint. The generated certificate is explicitly a development placeholder; production certificate and client trust handling remain future work. The rustls dependency is pinned to the ring provider to match Quinn's selected crypto backend and avoid process-level provider ambiguity at runtime. Test Plan: - cargo fmt --check - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - timeout 2s cargo run -p lanparty-relay -- --listen 127.0.0.1:0 || test $? -eq 124 Refs: PLAN.md public relay QUIC data path Refs: https://docs.rs/quinn/0.11.9
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
//! separate makes the important relay invariants testable without sockets.
|
||||
|
||||
mod config;
|
||||
mod server;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -15,6 +16,7 @@ use lanparty_proto::{EthernetFrame, MacAddr, recommended_tap_mtu};
|
||||
use thiserror::Error;
|
||||
|
||||
pub use config::{ConfigError, DEFAULT_RELAY_PORT, ListenEndpoint, RelayArgs, RelayConfig};
|
||||
pub use server::RelayServer;
|
||||
|
||||
pub const DEFAULT_MAX_CLIENTS_PER_ROOM: usize = 16;
|
||||
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
use clap::Parser;
|
||||
use lanparty_relay::{RelayArgs, RelayConfig};
|
||||
use lanparty_relay::{RelayArgs, RelayConfig, RelayServer};
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let config = RelayArgs::parse().into_config()?;
|
||||
run(config)
|
||||
run(config).await
|
||||
}
|
||||
|
||||
fn run(config: RelayConfig) -> anyhow::Result<()> {
|
||||
async fn run(config: RelayConfig) -> anyhow::Result<()> {
|
||||
println!(
|
||||
"lanparty-relay configured for {} with max {} clients per room",
|
||||
config.listen(),
|
||||
config.max_clients_per_room()
|
||||
);
|
||||
println!("QUIC server loop is not wired yet");
|
||||
Ok(())
|
||||
let server = RelayServer::bind(&config)?;
|
||||
println!("lanparty-relay listening on {}", server.local_addr()?);
|
||||
println!("connection handling is not wired yet; press Ctrl-C to stop");
|
||||
server.wait_for_shutdown().await
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use quinn::{Endpoint, ServerConfig, TransportConfig, crypto::rustls::QuicServerConfig};
|
||||
use rustls::pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer};
|
||||
|
||||
use crate::RelayConfig;
|
||||
|
||||
const RELAY_ALPN: &[u8] = b"lanparty-l2/1";
|
||||
const DATAGRAM_BUFFER_BYTES: usize = 4 * 1024 * 1024;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RelayServer {
|
||||
endpoint: Endpoint,
|
||||
}
|
||||
|
||||
impl RelayServer {
|
||||
pub fn bind(config: &RelayConfig) -> Result<Self> {
|
||||
let server_config = development_server_config()?;
|
||||
let endpoint = Endpoint::server(server_config, config.listen().socket_addr())
|
||||
.context("failed to bind QUIC relay endpoint")?;
|
||||
|
||||
Ok(Self { endpoint })
|
||||
}
|
||||
|
||||
pub fn local_addr(&self) -> Result<SocketAddr> {
|
||||
self.endpoint
|
||||
.local_addr()
|
||||
.context("failed to read relay local address")
|
||||
}
|
||||
|
||||
pub async fn wait_for_shutdown(self) -> Result<()> {
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.context("failed to wait for Ctrl-C")?;
|
||||
self.shutdown("relay shutting down").await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn shutdown(self, reason: &str) {
|
||||
self.endpoint.close(0_u32.into(), reason.as_bytes());
|
||||
self.endpoint.wait_idle().await;
|
||||
}
|
||||
}
|
||||
|
||||
fn development_server_config() -> Result<ServerConfig> {
|
||||
let certified_key = rcgen::generate_simple_self_signed(vec!["lanparty-relay.local".into()])
|
||||
.context("failed to generate development relay certificate")?;
|
||||
let cert_chain = vec![certified_key.cert.der().clone()];
|
||||
let private_key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(
|
||||
certified_key.signing_key.serialize_der(),
|
||||
));
|
||||
|
||||
let mut tls_config = rustls::ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(cert_chain, private_key)
|
||||
.context("failed to build relay TLS config")?;
|
||||
tls_config.alpn_protocols = vec![RELAY_ALPN.to_vec()];
|
||||
|
||||
let mut server_config = ServerConfig::with_crypto(Arc::new(
|
||||
QuicServerConfig::try_from(tls_config).context("failed to build QUIC TLS config")?,
|
||||
));
|
||||
|
||||
let mut transport = TransportConfig::default();
|
||||
transport.datagram_receive_buffer_size(Some(DATAGRAM_BUFFER_BYTES));
|
||||
transport.datagram_send_buffer_size(DATAGRAM_BUFFER_BYTES);
|
||||
server_config.transport_config(Arc::new(transport));
|
||||
|
||||
Ok(server_config)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
|
||||
use crate::{DEFAULT_MAX_CLIENTS_PER_ROOM, ListenEndpoint};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn binds_quic_endpoint_on_configured_address() {
|
||||
let config = RelayConfig::new(
|
||||
ListenEndpoint::new(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)),
|
||||
DEFAULT_MAX_CLIENTS_PER_ROOM,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let server = RelayServer::bind(&config).unwrap();
|
||||
let local_addr = server.local_addr().unwrap();
|
||||
|
||||
assert_eq!(local_addr.ip(), IpAddr::V4(Ipv4Addr::LOCALHOST));
|
||||
assert_ne!(local_addr.port(), 0);
|
||||
|
||||
server.shutdown("test complete").await;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user