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:
2026-05-21 18:24:47 +02:00
parent 93f0a17f79
commit 34ba2f2375
3 changed files with 95 additions and 10 deletions
+52 -6
View File
@@ -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 bytes::Bytes;
@@ -59,7 +59,10 @@ struct PeerSession {
impl RelayServer {
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())
.context("failed to bind QUIC relay endpoint")?;
@@ -440,10 +443,22 @@ async fn leave_peer(
Ok(())
}
fn development_server_config() -> Result<ServerConfig> {
let (server_config, _) = development_server_config_with_certificate()?;
fn write_development_certificate(path: &Path, certificate: &CertificateDer<'_>) -> Result<()> {
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>)> {
@@ -477,7 +492,7 @@ fn development_server_config_with_certificate() -> Result<(ServerConfig, Certifi
mod tests {
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
time::Duration,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use bytes::Bytes;
@@ -558,6 +573,25 @@ mod tests {
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]
async fn forwards_ethernet_datagrams_between_joined_peers() {
let (server, certificate) = bind_test_server(DEFAULT_MAX_CLIENTS_PER_ROOM);
@@ -709,4 +743,16 @@ mod tests {
frame.extend_from_slice(b"payload");
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()
))
}
}