feat: add protocol comparison docs, P2P crate, production audit, and design fixes

Add comprehensive documentation comparing quicnprotochat against classical
chat protocols (IRC+SSL, XMPP, Telegram) with diagrams and attack scenarios.
Promote comparison pages to top-level sidebar section. Include P2P transport
crate (iroh), production readiness audit, CI workflows, dependency policy,
and continued architecture improvements across all crates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 12:15:44 +01:00
parent 0bdc222724
commit 00b0aa92a1
28 changed files with 1566 additions and 340 deletions

View File

@@ -0,0 +1,12 @@
[package]
name = "quicnprotochat-p2p"
version = "0.1.0"
edition = "2021"
description = "P2P transport layer for quicnprotochat using iroh."
license = "MIT"
[dependencies]
iroh = "0.96"
tokio = { workspace = true }
tracing = { workspace = true }
anyhow = { workspace = true }

View File

@@ -0,0 +1,189 @@
//! P2P transport layer for quicnprotochat using iroh.
//!
//! Provides direct peer-to-peer QUIC connections with NAT traversal via iroh
//! relay servers. When both peers are online, messages bypass the central
//! server entirely.
//!
//! # Architecture
//!
//! ```text
//! Client A ── iroh direct (QUIC) ── Client B (preferred: low latency)
//! │ │
//! └── QUIC/TLS ── Server ── QUIC/TLS ┘ (fallback: store-and-forward)
//! ```
use iroh::{Endpoint, EndpointAddr, PublicKey, SecretKey};
/// ALPN protocol identifier for quicnprotochat P2P messaging.
const P2P_ALPN: &[u8] = b"quicnprotochat/p2p/1";
/// A P2P node backed by an iroh endpoint.
///
/// Manages direct QUIC connections to peers with automatic NAT traversal.
pub struct P2pNode {
endpoint: Endpoint,
}
/// Received P2P message with sender information.
pub struct P2pMessage {
pub sender: PublicKey,
pub payload: Vec<u8>,
}
impl P2pNode {
/// Start a new P2P node.
///
/// Generates a fresh identity or reuses a provided secret key.
pub async fn start(secret_key: Option<SecretKey>) -> anyhow::Result<Self> {
let mut builder = Endpoint::builder();
if let Some(sk) = secret_key {
builder = builder.secret_key(sk);
}
builder = builder.alpns(vec![P2P_ALPN.to_vec()]);
let endpoint = builder.bind().await?;
tracing::info!(
node_id = %endpoint.id().fmt_short(),
"P2P node started"
);
Ok(Self { endpoint })
}
/// This node's public key (used as node ID for peer discovery).
pub fn node_id(&self) -> PublicKey {
self.endpoint.id()
}
/// This node's secret key (for persistence across restarts).
pub fn secret_key(&self) -> SecretKey {
self.endpoint.secret_key().clone()
}
/// Get the node's network address information for publishing to discovery.
pub fn endpoint_addr(&self) -> EndpointAddr {
self.endpoint.addr()
}
/// Send a payload directly to a peer via P2P QUIC.
pub async fn send(&self, peer: impl Into<EndpointAddr>, payload: &[u8]) -> anyhow::Result<()> {
let peer = peer.into();
let conn = self.endpoint.connect(peer, P2P_ALPN).await?;
let mut send = conn.open_uni().await.map_err(|e| anyhow::anyhow!("{e}"))?;
// Simple framing: 4-byte length prefix + payload.
let len = (payload.len() as u32).to_be_bytes();
send.write_all(&len)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
send.write_all(payload)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
send.finish().map_err(|e| anyhow::anyhow!("{e}"))?;
// Wait until the peer has consumed the stream before dropping.
send.stopped().await.map_err(|e| anyhow::anyhow!("{e}"))?;
tracing::debug!(
peer = %conn.remote_id().fmt_short(),
bytes = payload.len(),
"P2P message sent"
);
Ok(())
}
/// Accept a single incoming P2P message.
///
/// Blocks until a peer connects and sends data.
pub async fn recv(&self) -> anyhow::Result<P2pMessage> {
let incoming = self
.endpoint
.accept()
.await
.ok_or_else(|| anyhow::anyhow!("no more incoming connections"))?;
let conn = incoming.await.map_err(|e| anyhow::anyhow!("{e}"))?;
let sender = conn.remote_id();
let mut recv = conn
.accept_uni()
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
// Read length-prefixed payload.
let mut len_buf = [0u8; 4];
recv.read_exact(&mut len_buf)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
let len = u32::from_be_bytes(len_buf) as usize;
if len > 5 * 1024 * 1024 {
anyhow::bail!("P2P payload too large: {len} bytes");
}
let mut payload = vec![0u8; len];
recv.read_exact(&mut payload)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
tracing::debug!(
peer = %sender.fmt_short(),
bytes = len,
"P2P message received"
);
Ok(P2pMessage { sender, payload })
}
/// Gracefully shut down the P2P node.
pub async fn close(self) {
self.endpoint.close().await;
}
}
#[cfg(test)]
mod tests {
use super::*;
use iroh::RelayMode;
/// Create a local-only P2P node with relays disabled (for testing).
async fn local_node() -> P2pNode {
let endpoint = Endpoint::builder()
.alpns(vec![P2P_ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind()
.await
.unwrap();
P2pNode { endpoint }
}
#[tokio::test]
async fn p2p_round_trip() {
let alice = local_node().await;
let bob = local_node().await;
let bob_addr = bob.endpoint_addr();
let alice_id = alice.node_id();
let payload = b"hello from alice via P2P";
// Spawn Bob's receiver.
let bob_handle = tokio::spawn(async move {
let msg = bob.recv().await.unwrap();
assert_eq!(msg.payload, payload.to_vec());
assert_eq!(msg.sender, alice_id);
});
// Give Bob a moment to start accepting.
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
alice.send(bob_addr, payload).await.unwrap();
// Wait for Bob to finish reading before closing.
bob_handle.await.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
alice.close().await;
}
}