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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/mqp2p/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mqp2p"
version = "0.2.0"
version = "0.3.0"
edition = "2024"
rust-version = "1.88"
license = "Apache-2.0"
Expand Down
12 changes: 10 additions & 2 deletions crates/mqp2p/src/peer.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::error::{Error, Result};
use crate::quic::{self, CertIdentity, QuicEndpoint};
use crate::quic::{self, CertIdentity, QuicEndpoint, SegmentationOffload};
use crate::signaling::{
Candidate, CandidateKind, ConnectionOffer, ConnectionSync, MIN_LEASE_SECS, PEER_LEASE_SECS,
PeerInfo, SignalingClient, heartbeat_interval_secs,
Expand All @@ -20,6 +20,7 @@ pub struct PeerConfig {
pub stun_server: Option<String>,
pub credentials: Option<(String, String)>,
pub lease_secs: u64,
pub segmentation_offload: SegmentationOffload,
}

impl PeerConfig {
Expand All @@ -31,6 +32,7 @@ impl PeerConfig {
stun_server: Some(stun::DEFAULT_STUN_SERVER.into()),
credentials: None,
lease_secs: PEER_LEASE_SECS,
segmentation_offload: SegmentationOffload::Enabled,
}
}

Expand All @@ -54,6 +56,11 @@ impl PeerConfig {
self
}

pub fn without_segmentation_offload(mut self) -> Self {
self.segmentation_offload = SegmentationOffload::Disabled;
self
}

pub fn with_credentials(
mut self,
username: impl Into<String>,
Expand Down Expand Up @@ -118,13 +125,14 @@ impl Peer {
};

let quic_socket = std_socket.try_clone()?;
let quic = QuicEndpoint::bind(
let quic = QuicEndpoint::bind_with_offload(
quic_socket,
CertIdentity {
cert_der: identity.cert_der.clone(),
key_der: identity.key_der.clone(),
fingerprint: identity.fingerprint.clone(),
},
config.segmentation_offload,
)?;

let client_id = format!("mqp2p-{}", config.name);
Expand Down
47 changes: 40 additions & 7 deletions crates/mqp2p/src/quic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,22 @@ pub fn compute_fingerprint(cert_der: &[u8]) -> String {
format!("sha256:{hex}")
}

fn build_server_config(identity: &CertIdentity) -> Result<ServerConfig> {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SegmentationOffload {
Enabled,
Disabled,
}

fn transport_config(offload: SegmentationOffload) -> Arc<quinn::TransportConfig> {
let mut transport = quinn::TransportConfig::default();
transport.enable_segmentation_offload(offload == SegmentationOffload::Enabled);
Arc::new(transport)
}

fn build_server_config(
identity: &CertIdentity,
offload: SegmentationOffload,
) -> Result<ServerConfig> {
let cert = CertificateDer::from(identity.cert_der.clone());
let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(identity.key_der.clone()));

Expand All @@ -66,12 +81,15 @@ fn build_server_config(identity: &CertIdentity) -> Result<ServerConfig> {
let quic_server_config = quinn::crypto::rustls::QuicServerConfig::try_from(server_crypto)
.map_err(|e| Error::Quic(format!("QUIC server config failed: {e}")))?;

Ok(ServerConfig::with_crypto(Arc::new(quic_server_config)))
let mut server_config = ServerConfig::with_crypto(Arc::new(quic_server_config));
server_config.transport_config(transport_config(offload));
Ok(server_config)
}

fn build_client_config(
identity: &CertIdentity,
expected_fingerprint: &str,
offload: SegmentationOffload,
) -> Result<quinn::ClientConfig> {
let cert = CertificateDer::from(identity.cert_der.clone());
let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(identity.key_der.clone()));
Expand All @@ -89,7 +107,9 @@ fn build_client_config(
let quic_client_config = quinn::crypto::rustls::QuicClientConfig::try_from(client_crypto)
.map_err(|e| Error::Quic(format!("QUIC client config failed: {e}")))?;

Ok(quinn::ClientConfig::new(Arc::new(quic_client_config)))
let mut client_config = quinn::ClientConfig::new(Arc::new(quic_client_config));
client_config.transport_config(transport_config(offload));
Ok(client_config)
}

#[derive(Debug)]
Expand Down Expand Up @@ -215,11 +235,20 @@ impl rustls::server::danger::ClientCertVerifier for CollectClientCert {
pub struct QuicEndpoint {
endpoint: Endpoint,
identity: CertIdentity,
offload: SegmentationOffload,
}

impl QuicEndpoint {
pub fn bind(socket: std::net::UdpSocket, identity: CertIdentity) -> Result<Self> {
let server_config = build_server_config(&identity)?;
Self::bind_with_offload(socket, identity, SegmentationOffload::Enabled)
}

pub fn bind_with_offload(
socket: std::net::UdpSocket,
identity: CertIdentity,
offload: SegmentationOffload,
) -> Result<Self> {
let server_config = build_server_config(&identity, offload)?;
let runtime = quinn::default_runtime()
.ok_or_else(|| Error::Quic("no async runtime available".into()))?;

Expand All @@ -234,9 +263,13 @@ impl QuicEndpoint {
let local = endpoint
.local_addr()
.map_err(|e| Error::Quic(format!("{e}")))?;
info!(addr = %local, "QUIC endpoint bound");
info!(addr = %local, ?offload, "QUIC endpoint bound");

Ok(Self { endpoint, identity })
Ok(Self {
endpoint,
identity,
offload,
})
}

pub fn local_addr(&self) -> Result<SocketAddr> {
Expand All @@ -250,7 +283,7 @@ impl QuicEndpoint {
}

pub async fn connect(&self, addr: SocketAddr, remote_fingerprint: &str) -> Result<Connection> {
let client_config = build_client_config(&self.identity, remote_fingerprint)?;
let client_config = build_client_config(&self.identity, remote_fingerprint, self.offload)?;

let connection = self
.endpoint
Expand Down
33 changes: 33 additions & 0 deletions crates/mqp2p/tests/quic_offload.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use mqp2p::quic::{QuicEndpoint, SegmentationOffload, generate_self_signed_cert};
use std::net::UdpSocket;

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn endpoints_without_segmentation_offload_exchange_data() {
let id_a = generate_self_signed_cert().expect("cert a");
let id_b = generate_self_signed_cert().expect("cert b");
let fp_a = id_a.fingerprint.clone();
let fp_b = id_b.fingerprint.clone();
let socket = || UdpSocket::bind("127.0.0.1:0").expect("socket");
let ep_a = QuicEndpoint::bind_with_offload(socket(), id_a, SegmentationOffload::Disabled)
.expect("bind a");
let ep_b = QuicEndpoint::bind_with_offload(socket(), id_b, SegmentationOffload::Disabled)
.expect("bind b");
let addr_b = ep_b.local_addr().expect("addr b");
let payload = vec![0x5a_u8; 64 * 1024];
let expected = payload.clone();

let receiver = tokio::spawn(async move {
let conn = ep_b.accept_with_fingerprint(&fp_a).await.expect("accept");
let mut recv = conn.accept_uni().await.expect("stream");
let received = recv.read_to_end(1024 * 1024).await.expect("read");
conn.close(0u32.into(), b"done");
received
});

let conn = ep_a.connect(addr_b, &fp_b).await.expect("connect");
let mut send = conn.open_uni().await.expect("open");
send.write_all(&payload).await.expect("write");
send.finish().expect("finish");

assert_eq!(receiver.await.expect("receiver"), expected);
}
2 changes: 1 addition & 1 deletion crates/stitch-p2p/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ readme = "README.md"
[dependencies]
ed25519-dalek = { version = "3", default-features = false, features = ["fast"], optional = true }
fjall = { version = "3.0", optional = true }
mqp2p = { version = "0.2.0", path = "../mqp2p", optional = true }
mqp2p = { version = "0.3.0", path = "../mqp2p", optional = true }
serde_json = { version = "1.0.150", optional = true }
thiserror = "2.0.18"
tokio = { version = "1.52.3", features = ["sync", "io-util", "macros", "rt", "rt-multi-thread", "time", "net"] }
Expand Down
2 changes: 1 addition & 1 deletion crates/stitch-tasks/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ license = "Apache-2.0"
publish = false

[dependencies]
mqp2p = { version = "0.2.0", path = "../mqp2p" }
mqp2p = { version = "0.3.0", path = "../mqp2p" }
rand = "0.10.1"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.150"
Expand Down
Loading