Skip to content
Merged
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
1 change: 1 addition & 0 deletions apps/skit/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@ mod tests {
api_max_ttl_secs: 86400,
moq_default_ttl_secs: 3600,
moq_max_ttl_secs: 86400,
moq_public_paths: Vec::new(),
};

let state = AuthState::new(&config, true).await.unwrap();
Expand Down
24 changes: 23 additions & 1 deletion apps/skit/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,16 @@ pub struct ServerConfig {
pub cors: CorsConfig,
#[cfg(feature = "moq")]
pub moq_address: Option<String>,
/// TLS certificate for the MoQ WebTransport listener.
/// When set, the MoQ QUIC server uses these certs independently of `[server].tls`.
/// When unset, falls back to `cert_path`/`key_path` (if `tls = true`) or self-signed.
#[cfg(feature = "moq")]
#[serde(default)]
pub moq_cert_path: Option<String>,
/// TLS private key for the MoQ WebTransport listener (see `moq_cert_path`).
#[cfg(feature = "moq")]
#[serde(default)]
pub moq_key_path: Option<String>,
/// MoQ Gateway URL to use in the frontend (can be overridden via SK_SERVER__MOQ_GATEWAY_URL)
#[cfg(feature = "moq")]
pub moq_gateway_url: Option<String>,
Expand All @@ -329,7 +339,11 @@ impl Default for ServerConfig {
base_path: None,
cors: CorsConfig::default(),
#[cfg(feature = "moq")]
moq_address: Some("127.0.0.1:4545".to_string()),
moq_address: None,
#[cfg(feature = "moq")]
moq_cert_path: None,
#[cfg(feature = "moq")]
moq_key_path: None,
#[cfg(feature = "moq")]
moq_gateway_url: None,
}
Expand Down Expand Up @@ -747,6 +761,13 @@ pub struct AuthConfig {
/// Maximum TTL for MoQ tokens in seconds. Default: 86400 (1 day)
#[serde(default = "default_moq_max_ttl")]
pub moq_max_ttl_secs: u64,

/// Gateway paths that allow unauthenticated MoQ WebTransport connections.
/// Connections to listed path prefixes skip JWT validation; the HTTP API remains protected.
/// Example: `["/moq"]` makes all `/moq/**` paths public; `["/moq/abc123"]` for a single path.
/// Empty list (default) = all MoQ connections require auth.
#[serde(default)]
pub moq_public_paths: Vec<String>,
}

impl Default for AuthConfig {
Expand All @@ -759,6 +780,7 @@ impl Default for AuthConfig {
api_max_ttl_secs: default_api_max_ttl(),
moq_default_ttl_secs: default_moq_default_ttl(),
moq_max_ttl_secs: default_moq_max_ttl(),
moq_public_paths: Vec::new(),
}
}
}
Expand Down
59 changes: 50 additions & 9 deletions apps/skit/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3382,18 +3382,37 @@ fn start_moq_webtransport_acceptor(

let auth_state = Arc::clone(&app_state.auth);

// Parse address for WebTransport (UDP will use the same port as HTTP/HTTPS)
let addr: SocketAddr = config.server.address.parse()?;
// Parse address for WebTransport — use moq_address when set, otherwise fall back
// to the main server address (same port for HTTP and QUIC).
let addr: SocketAddr =
config.server.moq_address.as_deref().unwrap_or(&config.server.address).parse()?;

// Configure TLS for MoQ WebTransport.
// Priority: moq_cert_path/moq_key_path → server cert_path/key_path (when tls=true) → self-signed.
let moq_cert = config.server.moq_cert_path.as_deref().filter(|s| !s.is_empty());
let moq_key = config.server.moq_key_path.as_deref().filter(|s| !s.is_empty());

if moq_cert.is_some() != moq_key.is_some() {
return Err(format!(
"Invalid MoQ TLS config: both moq_cert_path and moq_key_path must be set (got cert={:?}, key={:?})",
config.server.moq_cert_path, config.server.moq_key_path
).into());
}

// Configure TLS - use provided certificates if available, otherwise auto-generate
let tls = if config.server.tls
let tls = if let (Some(cert), Some(key)) = (moq_cert, moq_key) {
info!(cert_path = %cert, key_path = %key, "Using MoQ-specific TLS certificates for WebTransport");
let mut tls = ServerTlsConfig::default();
tls.cert = vec![std::path::PathBuf::from(cert)];
tls.key = vec![std::path::PathBuf::from(key)];
tls
} else if config.server.tls
&& !config.server.cert_path.is_empty()
&& !config.server.key_path.is_empty()
{
info!(
cert_path = %config.server.cert_path,
key_path = %config.server.key_path,
"Using provided TLS certificates for MoQ WebTransport"
"Using server TLS certificates for MoQ WebTransport"
);
let mut tls = ServerTlsConfig::default();
tls.cert = vec![std::path::PathBuf::from(&config.server.cert_path)];
Expand All @@ -3410,9 +3429,26 @@ fn start_moq_webtransport_acceptor(
moq_config.bind = Some(addr);
moq_config.tls = tls;

let moq_public_paths: Arc<[String]> = config
.auth
.moq_public_paths
.iter()
.filter(|p| {
if p.is_empty() {
warn!("Ignoring empty string in moq_public_paths (would bypass all MoQ auth)");
false
} else {
true
}
})
.cloned()
.collect::<Vec<_>>()
.into();

info!(
address = %addr,
"Starting MoQ WebTransport acceptor on UDP (same port as HTTP server)"
moq_public_paths = ?moq_public_paths,
"Starting MoQ WebTransport acceptor on UDP"
);

tokio::spawn(async move {
Expand All @@ -3430,14 +3466,15 @@ fn start_moq_webtransport_acceptor(
for (i, fp) in fingerprints.iter().enumerate() {
info!("🔐 MoQ WebTransport certificate fingerprint #{}: {}", i + 1, fp);
}
info!("💡 Access fingerprints at: http://{}/api/v1/moq/fingerprints", addr);
info!("💡 Access fingerprints at: /api/v1/moq/fingerprints (served by the HTTP server)");

info!("MoQ WebTransport server listening for connections");

// Accept connections in a loop
while let Some(request) = server.accept().await {
let gateway = Arc::clone(&gateway);
let auth_state = Arc::clone(&auth_state);
let moq_public_paths = Arc::clone(&moq_public_paths);

tokio::spawn(async move {
// Extract URL data before consuming the request.
Expand All @@ -3458,8 +3495,12 @@ fn start_moq_webtransport_acceptor(
// SECURITY: Never log the full URL (may contain jwt)
debug!(path = %path, "Received MoQ connection request");

// Validate MoQ auth if enabled
let moq_auth = if auth_state.is_enabled() {
// Validate MoQ auth if enabled (skipped for paths matching moq_public_paths).
// Segment-based: "/moq" matches "/moq" and "/moq/foo" but NOT "/moq2".
let is_public = moq_public_paths.iter().any(|prefix| {
path == prefix.as_str() || path.starts_with(&format!("{prefix}/"))
});
let moq_auth = if auth_state.is_enabled() && !is_public {
match validate_moq_auth(&auth_state, &path, jwt_param).await {
Ok(ctx) => Some(ctx),
Err(status) => {
Expand Down
7 changes: 4 additions & 3 deletions samples/skit.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ samples_dir = "./samples/pipelines"
# Default: 100MB
max_body_size = 104857600

# MoQ WebTransport address (optional, requires 'moq' feature)
# Note: currently unused; the MoQ acceptor binds to `server.address`.
# moq_address = "127.0.0.1:4545"
# MoQ WebTransport bind address (optional, requires 'moq' feature).
# When unset, MoQ QUIC binds to the same address as the HTTP server.
# Set to a different address/port to separate HTTP and MoQ traffic.
# moq_address = "0.0.0.0:4445"

# MoQ Gateway URL to expose to the UI (optional, requires 'moq' feature)
# moq_gateway_url = "https://example.com/moq"
Expand Down
Loading