mirror of
https://github.com/kristoferssolo/tls-pq-bench.git
synced 2026-03-21 16:26:22 +00:00
feat(server): implement http1 /bytes/{n} endpoint over TLS
This commit is contained in:
97
Cargo.lock
generated
97
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -5,6 +5,7 @@ authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
clap.workspace = true
|
||||
miette.workspace = true
|
||||
rcgen.workspace = true
|
||||
rustls.workspace = true
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
7
justfile
7
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}}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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::<Vec<_>>().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");
|
||||
}
|
||||
|
||||
|
||||
131
server/src/server/http1.rs
Normal file
131
server/src/server/http1.rs
Normal file
@@ -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<Bytes>;
|
||||
|
||||
pub async fn handle_http1_connection(
|
||||
stream: TcpStream,
|
||||
peer: SocketAddr,
|
||||
tls_config: Arc<ServerConfig>,
|
||||
) {
|
||||
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<Incoming>) -> Response<RespBody> {
|
||||
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<u64, StatusCode> {
|
||||
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::<u64>().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<RespBody> {
|
||||
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
|
||||
}
|
||||
@@ -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<ServerConfig>) -> error::Result<()> {
|
||||
pub async fn run_server(args: &Args, tls_config: Arc<ServerConfig>) -> 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<ServerConfig>) -> 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,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user