feat(relay): write development certificate
The gateway and Windows client now pin a relay certificate, but local relay runs generated an ephemeral self-signed certificate only in memory. That made the development trust flow awkward because there was no stable DER artifact to feed into the new CLIs. Add `--dev-cert-der-out` to write the generated development certificate before the relay binds its endpoint. The file is DER-encoded and parent directories are created when needed. This keeps the production certificate/key path explicit future work while making the current pinned-trust flow usable. Test Plan: - cargo fmt --check - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check Refs: PLAN.md relay/client trust bootstrap
This commit is contained in:
@@ -68,15 +68,16 @@ cargo check --workspace
|
|||||||
## Relay
|
## Relay
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo run -p lanparty-relay -- --listen 443/udp
|
cargo run -p lanparty-relay -- --listen 443/udp --dev-cert-der-out relay-cert.der
|
||||||
```
|
```
|
||||||
|
|
||||||
`--listen` accepts either a socket address or a UDP port shorthand such as
|
`--listen` accepts either a socket address or a UDP port shorthand such as
|
||||||
`443/udp`. The relay binds a QUIC endpoint, accepts a control-stream `hello`,
|
`443/udp`. The relay binds a QUIC endpoint, accepts a control-stream `hello`,
|
||||||
replies with `welcome` or `reject`, and forwards live Ethernet QUIC datagrams
|
replies with `welcome` or `reject`, and forwards live Ethernet QUIC datagrams
|
||||||
between accepted peers in the same room. It currently uses a generated
|
between accepted peers in the same room. It currently uses a generated
|
||||||
self-signed development certificate; production certificate and client trust
|
self-signed development certificate; `--dev-cert-der-out` writes that
|
||||||
handling remain future work.
|
certificate so the gateway and client can pin it in development. Production
|
||||||
|
certificate handling remains future work.
|
||||||
|
|
||||||
## Gateway
|
## Gateway
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fmt,
|
fmt,
|
||||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||||
|
path::{Path, PathBuf},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,11 +24,18 @@ pub struct RelayArgs {
|
|||||||
/// Maximum number of remote clients allowed in one room.
|
/// Maximum number of remote clients allowed in one room.
|
||||||
#[arg(long = "max-clients-per-room", default_value_t = DEFAULT_MAX_CLIENTS_PER_ROOM)]
|
#[arg(long = "max-clients-per-room", default_value_t = DEFAULT_MAX_CLIENTS_PER_ROOM)]
|
||||||
max_clients_per_room: usize,
|
max_clients_per_room: usize,
|
||||||
|
/// Write the generated development relay certificate in DER format.
|
||||||
|
#[arg(long = "dev-cert-der-out", value_name = "PATH")]
|
||||||
|
dev_cert_der_out: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RelayArgs {
|
impl RelayArgs {
|
||||||
pub fn into_config(self) -> Result<RelayConfig, ConfigError> {
|
pub fn into_config(self) -> Result<RelayConfig, ConfigError> {
|
||||||
RelayConfig::new(self.listen, self.max_clients_per_room)
|
RelayConfig::with_dev_cert_der_out(
|
||||||
|
self.listen,
|
||||||
|
self.max_clients_per_room,
|
||||||
|
self.dev_cert_der_out,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,10 +43,19 @@ impl RelayArgs {
|
|||||||
pub struct RelayConfig {
|
pub struct RelayConfig {
|
||||||
listen: ListenEndpoint,
|
listen: ListenEndpoint,
|
||||||
max_clients_per_room: usize,
|
max_clients_per_room: usize,
|
||||||
|
dev_cert_der_out: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RelayConfig {
|
impl RelayConfig {
|
||||||
pub fn new(listen: ListenEndpoint, max_clients_per_room: usize) -> Result<Self, ConfigError> {
|
pub fn new(listen: ListenEndpoint, max_clients_per_room: usize) -> Result<Self, ConfigError> {
|
||||||
|
Self::with_dev_cert_der_out(listen, max_clients_per_room, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_dev_cert_der_out(
|
||||||
|
listen: ListenEndpoint,
|
||||||
|
max_clients_per_room: usize,
|
||||||
|
dev_cert_der_out: Option<PathBuf>,
|
||||||
|
) -> Result<Self, ConfigError> {
|
||||||
if max_clients_per_room == 0 {
|
if max_clients_per_room == 0 {
|
||||||
return Err(ConfigError::ZeroMaxClientsPerRoom);
|
return Err(ConfigError::ZeroMaxClientsPerRoom);
|
||||||
}
|
}
|
||||||
@@ -46,6 +63,7 @@ impl RelayConfig {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
listen,
|
listen,
|
||||||
max_clients_per_room,
|
max_clients_per_room,
|
||||||
|
dev_cert_der_out,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +76,11 @@ impl RelayConfig {
|
|||||||
pub const fn max_clients_per_room(&self) -> usize {
|
pub const fn max_clients_per_room(&self) -> usize {
|
||||||
self.max_clients_per_room
|
self.max_clients_per_room
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn dev_cert_der_out(&self) -> Option<&Path> {
|
||||||
|
self.dev_cert_der_out.as_deref()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
@@ -196,4 +219,19 @@ mod tests {
|
|||||||
ConfigError::ZeroMaxClientsPerRoom
|
ConfigError::ZeroMaxClientsPerRoom
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_development_certificate_output_path() {
|
||||||
|
let args = RelayArgs::parse_from([
|
||||||
|
"lanparty-relay",
|
||||||
|
"--dev-cert-der-out",
|
||||||
|
"target/relay-cert.der",
|
||||||
|
]);
|
||||||
|
let config = args.into_config().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config.dev_cert_der_out(),
|
||||||
|
Some(Path::new("target/relay-cert.der"))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::{net::SocketAddr, sync::Arc};
|
use std::{fs, net::SocketAddr, path::Path, sync::Arc};
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
@@ -59,7 +59,10 @@ struct PeerSession {
|
|||||||
|
|
||||||
impl RelayServer {
|
impl RelayServer {
|
||||||
pub fn bind(config: &RelayConfig) -> Result<Self> {
|
pub fn bind(config: &RelayConfig) -> Result<Self> {
|
||||||
let server_config = development_server_config()?;
|
let (server_config, certificate) = development_server_config_with_certificate()?;
|
||||||
|
if let Some(path) = config.dev_cert_der_out() {
|
||||||
|
write_development_certificate(path, &certificate)?;
|
||||||
|
}
|
||||||
let endpoint = Endpoint::server(server_config, config.listen().socket_addr())
|
let endpoint = Endpoint::server(server_config, config.listen().socket_addr())
|
||||||
.context("failed to bind QUIC relay endpoint")?;
|
.context("failed to bind QUIC relay endpoint")?;
|
||||||
|
|
||||||
@@ -440,10 +443,22 @@ async fn leave_peer(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn development_server_config() -> Result<ServerConfig> {
|
fn write_development_certificate(path: &Path, certificate: &CertificateDer<'_>) -> Result<()> {
|
||||||
let (server_config, _) = development_server_config_with_certificate()?;
|
if let Some(parent) = path
|
||||||
|
.parent()
|
||||||
|
.filter(|parent| !parent.as_os_str().is_empty())
|
||||||
|
{
|
||||||
|
fs::create_dir_all(parent).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to create certificate directory {}",
|
||||||
|
parent.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
fs::write(path, certificate.as_ref())
|
||||||
|
.with_context(|| format!("failed to write development certificate {}", path.display()))?;
|
||||||
|
|
||||||
Ok(server_config)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn development_server_config_with_certificate() -> Result<(ServerConfig, CertificateDer<'static>)> {
|
fn development_server_config_with_certificate() -> Result<(ServerConfig, CertificateDer<'static>)> {
|
||||||
@@ -477,7 +492,7 @@ fn development_server_config_with_certificate() -> Result<(ServerConfig, Certifi
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::{
|
use std::{
|
||||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||||
time::Duration,
|
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
@@ -558,6 +573,25 @@ mod tests {
|
|||||||
assert_eq!(rooms.lock().await.room_count(), 0);
|
assert_eq!(rooms.lock().await.room_count(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn writes_development_certificate_when_configured() {
|
||||||
|
let cert_path = unique_temp_cert_path();
|
||||||
|
let config = RelayConfig::with_dev_cert_der_out(
|
||||||
|
ListenEndpoint::new(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)),
|
||||||
|
DEFAULT_MAX_CLIENTS_PER_ROOM,
|
||||||
|
Some(cert_path.clone()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let server = RelayServer::bind(&config).unwrap();
|
||||||
|
let cert = std::fs::read(&cert_path).unwrap();
|
||||||
|
|
||||||
|
assert!(!cert.is_empty());
|
||||||
|
|
||||||
|
server.shutdown("test complete").await;
|
||||||
|
std::fs::remove_file(cert_path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn forwards_ethernet_datagrams_between_joined_peers() {
|
async fn forwards_ethernet_datagrams_between_joined_peers() {
|
||||||
let (server, certificate) = bind_test_server(DEFAULT_MAX_CLIENTS_PER_ROOM);
|
let (server, certificate) = bind_test_server(DEFAULT_MAX_CLIENTS_PER_ROOM);
|
||||||
@@ -709,4 +743,16 @@ mod tests {
|
|||||||
frame.extend_from_slice(b"payload");
|
frame.extend_from_slice(b"payload");
|
||||||
frame
|
frame
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn unique_temp_cert_path() -> std::path::PathBuf {
|
||||||
|
let nanos = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
|
||||||
|
std::env::temp_dir().join(format!(
|
||||||
|
"lanparty-relay-dev-cert-{}-{nanos}.der",
|
||||||
|
std::process::id()
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user