diff --git a/Cargo.lock b/Cargo.lock index 7a5bf43..da18ce5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,6 +115,12 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -282,6 +288,7 @@ version = "0.1.0" dependencies = [ "cargo-husky", "claims", + "clap", "miette", "rcgen", "rustls", @@ -496,6 +503,86 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", +] + [[package]] name = "indexmap" version = "2.13.0" @@ -776,6 +863,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "powerfmt" version = "0.2.0" @@ -1012,9 +1105,13 @@ name = "server" version = "0.1.0" dependencies = [ "base64", + "bytes", "claims", "clap", "common", + "http-body-util", + "hyper", + "hyper-util", "miette", "rustls", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 3649aad..ddf205c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,12 @@ common = { path = "common" } aws-lc-rs = "1" base64 = "0.22" +bytes = "1.11" clap = { version = "4.5", features = ["derive"] } futures = "0.3" +http-body-util = "0.1" +hyper = { version = "1.8", features = ["http1"] } +hyper-util = { version = "0.1", features = ["tokio"] } miette = { version = "7", features = ["fancy"] } rcgen = "0.14" rustls = { version = "0.23", default-features = false, features = [ diff --git a/common/Cargo.toml b/common/Cargo.toml index 35a3641..dad3abd 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -5,6 +5,7 @@ authors.workspace = true edition.workspace = true [dependencies] +clap.workspace = true miette.workspace = true rcgen.workspace = true rustls.workspace = true diff --git a/common/src/lib.rs b/common/src/lib.rs index 85ae3a1..0861211 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -3,18 +3,20 @@ pub mod error; pub mod prelude; pub mod protocol; +use clap::ValueEnum; pub use error::Error; use serde::{Deserialize, Serialize}; use std::fmt; -use strum::{Display, EnumString}; +use strum::Display; /// TLS 1.3 key exchange mode used for benchmark runs -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumString, Display)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, ValueEnum)] #[strum(serialize_all = "lowercase")] #[serde(rename_all = "lowercase")] pub enum KeyExchangeMode { /// Classical X25519 ECDH. X25519, + #[value(name = "x25519mlkem768")] /// Hybrid post-quantum: X25519 + ML-KEM-768. X25519Mlkem768, } @@ -26,7 +28,7 @@ pub enum KeyExchangeMode { /// /// `Http1` is an HTTP/1.1 request/response mode (`GET /bytes/{n}`) used for realism-oriented /// comparisons where HTTP parsing and headers are part of measured overhead. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumString, Display)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, ValueEnum)] #[strum(serialize_all = "lowercase")] #[serde(rename_all = "lowercase")] pub enum ProtocolMode { @@ -77,7 +79,6 @@ mod tests { use super::*; use claims::{assert_err, assert_ok}; use serde_json::Value; - use std::str::FromStr; #[test] fn bench_record_serializes_to_ndjson() { @@ -117,18 +118,18 @@ mod tests { #[test] fn key_exchange_mode_from_str() { - let mode = assert_ok!(KeyExchangeMode::from_str("x25519")); + let mode = assert_ok!(KeyExchangeMode::from_str("x25519", true)); assert_eq!(mode, KeyExchangeMode::X25519); - let mode = assert_ok!(KeyExchangeMode::from_str("x25519mlkem768")); + let mode = assert_ok!(KeyExchangeMode::from_str("x25519mlkem768", true)); assert_eq!(mode, KeyExchangeMode::X25519Mlkem768); } #[test] fn key_exchange_mode_parse_error() { - assert_err!(KeyExchangeMode::from_str("invalid")); - assert_err!(KeyExchangeMode::from_str("x25519invalid")); - assert_err!(KeyExchangeMode::from_str("")); + assert_err!(KeyExchangeMode::from_str("invalid", true)); + assert_err!(KeyExchangeMode::from_str("x25519invalid", true)); + assert_err!(KeyExchangeMode::from_str("", true)); } #[test] diff --git a/common/src/prelude.rs b/common/src/prelude.rs index d82f6e1..6224d5c 100644 --- a/common/src/prelude.rs +++ b/common/src/prelude.rs @@ -1,4 +1,7 @@ pub use crate::{ BenchRecord, KeyExchangeMode, ProtocolMode, - protocol::{read_payload, read_request, write_payload, write_request}, + protocol::{ + MAX_PAYLOAD_SIZE, generate_payload, read_payload, read_request, write_payload, + write_request, + }, }; diff --git a/justfile b/justfile index 0d4edcb..e71e337 100644 --- a/justfile +++ b/justfile @@ -61,16 +61,15 @@ setup: # Run server (default: x25519 on localhost:4433) [group("run")] -server mode="x25519" listen="127.0.0.1:4433": - cargo run --release --bin server -- --mode {{mode}} --listen {{listen}} +server mode="x25519" proto="raw" listen="127.0.0.1:4433": + cargo run --release --bin server -- --mode {{mode}} --proto {{proto}} --listen {{listen}} # Run benchmark runner [group("run")] -runner server mode="x25519" proto="raw" payload="1024" iters="100" warmup="10": +runner server mode="x25519" payload="1024" iters="100" warmup="10": cargo run --release --bin runner -- \ --server {{server}} \ --mode {{mode}} \ - --proto {{proto}} \ --payload-bytes {{payload}} \ --iters {{iters}} \ --warmup {{warmup}} diff --git a/server/Cargo.toml b/server/Cargo.toml index e74dd20..6a5167f 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -6,8 +6,12 @@ edition.workspace = true [dependencies] base64.workspace = true +bytes.workspace = true clap.workspace = true common.workspace = true +http-body-util.workspace = true +hyper-util = { workspace = true, features = ["server"] } +hyper = { workspace = true, features = ["server"] } miette.workspace = true rustls.workspace = true thiserror.workspace = true diff --git a/server/src/main.rs b/server/src/main.rs index 3f9bde3..272157d 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -26,7 +26,7 @@ struct Args { /// Protocol carrier #[arg(long, default_value = "raw")] - pub proto: ProtocolMode, + proto: ProtocolMode, /// Address to listen on #[arg(long, default_value = "127.0.0.1:4433")] @@ -49,6 +49,7 @@ async fn main() -> miette::Result<()> { command = env::args().collect::>().join(" "), listen = %args.listen, mode = %args.mode, + proto = %args.proto, "server started" ); @@ -82,6 +83,7 @@ mod tests { fn default_args() { let args = Args::parse_from(["server"]); assert_eq!(args.mode, KeyExchangeMode::X25519); + assert_eq!(args.proto, ProtocolMode::Raw); assert_eq!(args.listen.to_string(), "127.0.0.1:4433"); } diff --git a/server/src/server/http1.rs b/server/src/server/http1.rs new file mode 100644 index 0000000..8313ecd --- /dev/null +++ b/server/src/server/http1.rs @@ -0,0 +1,131 @@ +use bytes::Bytes; +use common::prelude::*; +use http_body_util::Full; +use hyper::{ + Method, Request, Response, StatusCode, + body::Incoming, + header::{ALLOW, CONNECTION, CONTENT_LENGTH, CONTENT_TYPE, HeaderValue}, + server::conn::http1::Builder, + service::service_fn, +}; +use hyper_util::rt::TokioIo; +use rustls::{ServerConfig, server::Acceptor}; +use std::{convert::Infallible, net::SocketAddr, sync::Arc}; +use tokio::net::TcpStream; +use tokio_rustls::LazyConfigAcceptor; +use tracing::{info, warn}; + +type RespBody = Full; + +pub async fn handle_http1_connection( + stream: TcpStream, + peer: SocketAddr, + tls_config: Arc, +) { + let acceptor = LazyConfigAcceptor::new(Acceptor::default(), stream); + let start_handshake = match acceptor.await { + Ok(sh) => sh, + Err(e) => { + return warn!(peer = %peer, error = %e, "TLS accept error"); + } + }; + + let tls_stream = match start_handshake.into_stream(tls_config).await { + Ok(s) => s, + Err(e) => { + return warn!(peer = %peer, error = %e, "TLS handshake error"); + } + }; + + let (_, conn) = tls_stream.get_ref(); + info!( + cipher = ?conn.negotiated_cipher_suite(), + version = ?conn.protocol_version(), + "connection established" + ); + + let service = service_fn(move |req| async move { Ok::<_, Infallible>(handle_request(&req)) }); + + let io = TokioIo::new(tls_stream); + + if let Err(e) = Builder::new() + .keep_alive(false) + .serve_connection(io, service) + .await + { + warn!(peer = %peer, error = %e, "http1 serve error"); + } +} + +fn handle_request(req: &Request) -> Response { + if req.method() != Method::GET { + let mut response = text_response(StatusCode::METHOD_NOT_ALLOWED, "method not allowed"); + response + .headers_mut() + .insert(ALLOW, HeaderValue::from_static("GET")); + return response; + } + + let n = match parse_bytes_path(req.uri().path()) { + Ok(n) => n, + Err(status) => { + let msg = match status { + StatusCode::NOT_FOUND => "not found", + StatusCode::PAYLOAD_TOO_LARGE => "payload too large", + _ => "bad request", + }; + return text_response(status, msg); + } + }; + + let payload = generate_payload(n); + let mut response = Response::new(Full::new(Bytes::from(payload))); + *response.status_mut() = StatusCode::OK; + + let headers = response.headers_mut(); + headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/octet-stream"), + ); + headers.insert(CONNECTION, HeaderValue::from_static("close")); + + #[allow(clippy::option_if_let_else)] + match HeaderValue::from_str(&n.to_string()) { + Ok(v) => { + headers.insert(CONTENT_LENGTH, v); + response + } + Err(_) => text_response(StatusCode::INTERNAL_SERVER_ERROR, "internal server error"), + } +} + +fn parse_bytes_path(path: &str) -> Result { + let Some(rest) = path.strip_prefix("/bytes/") else { + return Err(StatusCode::NOT_FOUND); + }; + + if rest.is_empty() || rest.contains('/') { + return Err(StatusCode::BAD_REQUEST); + } + + let n = rest.parse::().map_err(|_| StatusCode::BAD_REQUEST)?; + + if n > MAX_PAYLOAD_SIZE { + return Err(StatusCode::PAYLOAD_TOO_LARGE); + } + + Ok(n) +} + +fn text_response(status: StatusCode, msg: &'static str) -> Response { + let mut response = Response::new(Full::new(Bytes::from_static(msg.as_bytes()))); + *response.status_mut() = status; + response.headers_mut().insert( + CONTENT_TYPE, + HeaderValue::from_static("text/plain; charset=utf-8"), + ); + response + .headers_mut() + .insert(CONNECTION, HeaderValue::from_static("close")); + response +} diff --git a/server/src/server/mod.rs b/server/src/server/mod.rs index 0b29456..b051fe2 100644 --- a/server/src/server/mod.rs +++ b/server/src/server/mod.rs @@ -1,18 +1,28 @@ +mod http1; mod raw; -use crate::{Args, error, server::raw::handle_raw_connection}; +use crate::{ + Args, + error::{Error as ServerError, Result as ServerResult}, + server::{http1::handle_http1_connection, raw::handle_raw_connection}, +}; use common::prelude::*; use rustls::ServerConfig; use std::sync::Arc; use tokio::net::TcpListener; -use tracing::info; +use tracing::{error, info}; -pub async fn run_server(args: &Args, tls_config: Arc) -> error::Result<()> { +pub async fn run_server(args: &Args, tls_config: Arc) -> ServerResult<()> { let listener = TcpListener::bind(args.listen) .await - .map_err(|e| error::Error::network(format!("failed to bind to {}: {e}", args.listen)))?; + .map_err(|e| ServerError::network(format!("failed to bind to {}: {e}", args.listen)))?; - info!(listen = %args.listen, mode = %args.mode, "listening"); + info!( + listen = %args.listen, + mode = %args.mode, + proto = %args.proto, + "listening" + ); loop { let (stream, peer) = match listener.accept().await { @@ -24,9 +34,12 @@ pub async fn run_server(args: &Args, tls_config: Arc) -> error::Re }; let config = tls_config.clone(); - tokio::spawn(match args.proto { - ProtocolMode::Raw => handle_raw_connection(stream, peer, config), - ProtocolMode::Http1 => todo!(), + let proto = args.proto; + tokio::spawn(async move { + match proto { + ProtocolMode::Raw => handle_raw_connection(stream, peer, config).await, + ProtocolMode::Http1 => handle_http1_connection(stream, peer, config).await, + } }); } }