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
3 changes: 3 additions & 0 deletions web-transport-quiche/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ categories = ["network-programming", "web-programming"]
all-features = true

[dependencies]
boring = "4"
bytes = "1"
flume = "0.11"
futures = "0.3"
http = "1"
rustls-pki-types = "1"

thiserror = "2"

Expand All @@ -38,5 +40,6 @@ web-transport-trait = { workspace = true }
[dev-dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }
rustls-pemfile = "2"
tokio = { version = "1", features = ["full"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
24 changes: 12 additions & 12 deletions web-transport-quiche/examples/echo-server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,21 @@ async fn main() -> anyhow::Result<()> {

let args = Args::parse();

let tls = web_transport_quiche::ez::CertificatePath {
cert: args
.tls_cert
.to_str()
.context("failed to convert path to str")?,
private_key: args
.tls_key
.to_str()
.context("failed to convert path to str")?,
kind: web_transport_quiche::ez::CertificateKind::X509,
};
// Load the certificate chain from PEM.
let cert_pem = std::fs::read(&args.tls_cert).context("failed to read certificate file")?;
let chain: Vec<_> = rustls_pemfile::certs(&mut cert_pem.as_slice())
.collect::<Result<_, _>>()
.context("failed to parse certificate PEM")?;

// Load the private key from PEM.
let key_pem = std::fs::read(&args.tls_key).context("failed to read private key file")?;
let key = rustls_pemfile::private_key(&mut key_pem.as_slice())
.context("failed to parse private key PEM")?
.context("no private key found in PEM file")?;

let mut server = web_transport_quiche::ServerBuilder::default()
.with_bind(args.bind)?
.with_cert(tls)?;
.with_single_cert(chain, key)?;

tracing::info!("listening on {}", args.bind);

Expand Down
12 changes: 8 additions & 4 deletions web-transport-quiche/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::sync::Arc;
use url::Url;

use crate::{
ez::{self, CertificatePath, DefaultMetrics, Metrics},
ez::{self, DefaultMetrics, Metrics},
h3, Connection, Settings,
};

Expand Down Expand Up @@ -71,9 +71,13 @@ impl<M: Metrics> ClientBuilder<M> {
Self(self.0.with_settings(settings))
}

/// Optional: Use a client certificate for TLS.
pub fn with_cert(self, tls: CertificatePath<'_>) -> Result<Self, ClientError> {
Ok(Self(self.0.with_cert(tls)?))
/// Optional: Use a client certificate for mTLS.
pub fn with_single_cert(
self,
chain: Vec<ez::CertificateDer<'static>>,
key: ez::PrivateKeyDer<'static>,
) -> Self {
Self(self.0.with_single_cert(chain, key))
}

/// Connect to the WebTransport server at the given URL.
Expand Down
59 changes: 38 additions & 21 deletions web-transport-quiche/src/ez/client.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
use std::io;
use std::sync::Arc;
use tokio_quiche::settings::{Hooks, TlsCertificatePaths};
use tokio_quiche::settings::{CertificateKind, Hooks, TlsCertificatePaths};

use rustls_pki_types::{CertificateDer, PrivateKeyDer};

use crate::ez::tls::StaticCertHook;
use crate::ez::DriverState;

use super::{
CertificateKind, CertificatePath, Connection, DefaultMetrics, Driver, Lock, Metrics, Settings,
};
use super::{Connection, DefaultMetrics, Driver, Lock, Metrics, Settings};

/// Construct a QUIC client using sane defaults.
pub struct ClientBuilder<M: Metrics = DefaultMetrics> {
settings: Settings,
socket: Option<tokio::net::UdpSocket>,
tls: Option<(String, String, CertificateKind)>,
tls: Option<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)>,
metrics: M,
}

Expand Down Expand Up @@ -77,14 +78,18 @@ impl<M: Metrics> ClientBuilder<M> {
self
}

/// Optional: Use a client certificate for TLS.
pub fn with_cert(self, tls: CertificatePath<'_>) -> io::Result<Self> {
Ok(Self {
tls: Some((tls.cert.to_owned(), tls.private_key.to_owned(), tls.kind)),
/// Optional: Use a client certificate for mTLS.
pub fn with_single_cert(
self,
chain: Vec<CertificateDer<'static>>,
key: PrivateKeyDer<'static>,
) -> Self {
Self {
tls: Some((chain, key)),
settings: self.settings,
metrics: self.metrics,
socket: self.socket,
})
}
}

/// Connect to the QUIC server at the given host and port.
Expand Down Expand Up @@ -126,21 +131,33 @@ impl<M: Metrics> ClientBuilder<M> {
Arc<tokio::net::UdpSocket>,
>::from_udp(socket)?;

let tls = self
.tls
.as_ref()
.map(|(cert, private_key, kind)| TlsCertificatePaths {
cert: cert.as_str(),
private_key: private_key.as_str(),
kind: *kind,
});

if !self.settings.verify_peer {
tracing::warn!("TLS certificate verification is disabled, a MITM attack is possible");
}

let params =
tokio_quiche::ConnectionParams::new_client(self.settings, tls, Hooks::default());
let (tls_cert, hooks) = match self.tls {
Some((chain, key)) => {
// ALPN is left empty; tokio-quiche sets h3 at the config level after the hook.
let hook = StaticCertHook {
chain,
key,
alpn: Vec::new(),
};
// ConnectionHook is only invoked when tls_cert is set, so we provide a dummy.
let dummy_tls = TlsCertificatePaths {
cert: "",
private_key: "",
kind: CertificateKind::X509,
};
let hooks = Hooks {
connection_hook: Some(Arc::new(hook)),
};
(Some(dummy_tls), hooks)
}
None => (None, Hooks::default()),
};

let params = tokio_quiche::ConnectionParams::new_client(self.settings, tls_cert, hooks);

let accept_bi = flume::unbounded();
let accept_uni = flume::unbounded();
Expand Down
5 changes: 5 additions & 0 deletions web-transport-quiche/src/ez/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@ impl Connection {
pub fn is_closed(&self) -> bool {
self.close.is_closed()
}

/// Returns the negotiated ALPN protocol, if the handshake has completed.
pub fn alpn(&self) -> Option<Vec<u8>> {
self.driver.lock().alpn().map(|a| a.to_vec())
}
}

impl Deref for Connection {
Expand Down
17 changes: 17 additions & 0 deletions web-transport-quiche/src/ez/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ pub(super) struct DriverState {

local: ConnectionClosed,
remote: ConnectionClosed,

/// The negotiated ALPN protocol, set after the handshake completes.
alpn: Option<Vec<u8>>,
}

impl DriverState {
Expand All @@ -54,6 +57,7 @@ impl DriverState {
remote: ConnectionClosed::default(),
bi: DriverOpen::new(next_bi),
uni: DriverOpen::new(next_uni),
alpn: None,
}
}

Expand All @@ -69,6 +73,11 @@ impl DriverState {
self.local.is_closed() || self.remote.is_closed()
}

/// Returns the negotiated ALPN protocol, if the handshake has completed.
pub fn alpn(&self) -> Option<&[u8]> {
self.alpn.as_deref()
}

#[must_use = "wake the driver"]
pub fn send(&mut self, stream_id: StreamId) -> Option<Waker> {
if !self.send.insert(stream_id) {
Expand Down Expand Up @@ -168,6 +177,14 @@ impl Driver {
qconn: &mut QuicheConnection,
_handshake_info: &HandshakeInfo,
) -> Result<(), ConnectionError> {
// Capture the negotiated ALPN protocol.
let alpn = qconn.application_proto();
self.state.lock().alpn = if alpn.is_empty() {
None
} else {
Some(alpn.to_vec())
};

// Run poll once to advance any pending operations.
match self.poll(Waker::noop(), qconn) {
Poll::Ready(Err(e)) => Err(e),
Expand Down
7 changes: 4 additions & 3 deletions web-transport-quiche/src/ez/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod recv;
mod send;
mod server;
mod stream;
pub mod tls;

pub use client::*;
pub use connection::*;
Expand All @@ -23,7 +24,7 @@ pub use stream::*;
use driver::*;
use lock::*;

pub use rustls_pki_types::{CertificateDer, PrivateKeyDer};
pub use tls::{CertResolver, CertifiedKey};
pub use tokio_quiche::metrics::{DefaultMetrics, Metrics};
pub use tokio_quiche::settings::{
CertificateKind, QuicSettings as Settings, TlsCertificatePaths as CertificatePath,
};
pub use tokio_quiche::settings::QuicSettings as Settings;
Loading