diff --git a/Cargo.toml b/Cargo.toml index c4fa2bc4c8..dec317e396 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,9 @@ [workspace] members = [ - "neqo-client", + "neqo-bin", "neqo-common", "neqo-crypto", "neqo-http3", - "neqo-server", "neqo-qpack", "neqo-transport", "test-fixture", diff --git a/neqo-server/Cargo.toml b/neqo-bin/Cargo.toml similarity index 72% rename from neqo-server/Cargo.toml rename to neqo-bin/Cargo.toml index 2f2162fea0..8b7b48ab86 100644 --- a/neqo-server/Cargo.toml +++ b/neqo-bin/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "neqo-server" -description = "A basic HTTP3 server." +name = "neqo-bin" +description = "A basic QUIC HTTP/0.9 and HTTP/3 client and server." authors.workspace = true homepage.workspace = true repository.workspace = true @@ -9,13 +9,22 @@ edition.workspace = true rust-version.workspace = true license.workspace = true +[[bin]] +name = "neqo-client" +path = "src/bin/client.rs" + +[[bin]] +name = "neqo-server" +path = "src/bin/server/main.rs" + [lints] workspace = true [dependencies] -# neqo-server is not used in Firefox, so we can be liberal with dependency versions +# neqo-bin is not used in Firefox, so we can be liberal with dependency versions clap = { version = "4.4", default-features = false, features = ["std", "color", "help", "usage", "error-context", "suggestions", "derive"] } futures = { version = "0.3", default-features = false, features = ["alloc"] } +hex = { version = "0.4", default-features = false, features = ["std"] } log = { version = "0.4", default-features = false } neqo-common = { path = "./../neqo-common", features = ["udp"] } neqo-crypto = { path = "./../neqo-crypto" } @@ -25,3 +34,4 @@ neqo-transport = { path = "./../neqo-transport" } qlog = { version = "0.12", default-features = false } regex = { version = "1.9", default-features = false, features = ["unicode-perl"] } tokio = { version = "1", default-features = false, features = ["net", "time", "macros", "rt", "rt-multi-thread"] } +url = { version = "2.5", default-features = false } diff --git a/neqo-client/src/main.rs b/neqo-bin/src/bin/client.rs similarity index 88% rename from neqo-client/src/main.rs rename to neqo-bin/src/bin/client.rs index 34d0626a05..2f9be1f3d7 100644 --- a/neqo-client/src/main.rs +++ b/neqo-bin/src/bin/client.rs @@ -15,7 +15,7 @@ use std::{ pin::Pin, process::exit, rc::Rc, - time::{Duration, Instant}, + time::Instant, }; use clap::Parser; @@ -34,8 +34,8 @@ use neqo_http3::{ Error, Header, Http3Client, Http3ClientEvent, Http3Parameters, Http3State, Output, Priority, }; use neqo_transport::{ - CongestionControlAlgorithm, Connection, ConnectionId, ConnectionParameters, - EmptyConnectionIdGenerator, Error as TransportError, StreamId, StreamType, Version, + Connection, ConnectionId, EmptyConnectionIdGenerator, Error as TransportError, StreamId, + Version, }; use qlog::{events::EventImportance, streamer::QlogStreamer}; use tokio::time::Sleep; @@ -122,11 +122,8 @@ impl KeyUpdateState { #[command(author, version, about, long_about = None)] #[allow(clippy::struct_excessive_bools)] // Not a good use of that lint. pub struct Args { - #[arg(short = 'a', long, default_value = "h3")] - /// ALPN labels to negotiate. - /// - /// This client still only does HTTP/3 no matter what the ALPN says. - alpn: String, + #[command(flatten)] + shared: neqo_bin::SharedArgs, urls: Vec, @@ -136,22 +133,9 @@ pub struct Args { #[arg(short = 'H', long, number_of_values = 2)] header: Vec, - #[arg(name = "encoder-table-size", long, default_value = "16384")] - max_table_size_encoder: u64, - - #[arg(name = "decoder-table-size", long, default_value = "16384")] - max_table_size_decoder: u64, - - #[arg(name = "max-blocked-streams", short = 'b', long, default_value = "10")] - max_blocked_streams: u16, - #[arg(name = "max-push", short = 'p', long, default_value = "10")] max_concurrent_push_streams: u64, - #[arg(name = "use-old-http", short = 'o', long)] - /// Use http 0.9 instead of HTTP/3 - use_old_http: bool, - #[arg(name = "download-in-series", long)] /// Download resources in series using separate connections. download_in_series: bool, @@ -164,18 +148,10 @@ pub struct Args { /// Output received data to stdout output_read_data: bool, - #[arg(name = "qlog-dir", long)] - /// Enable QLOG logging and QLOG traces to this directory - qlog_dir: Option, - #[arg(name = "output-dir", long)] /// Save contents of fetched URLs to a directory output_dir: Option, - #[arg(name = "qns-test", long)] - /// Enable special behavior for use with QUIC Network Simulator - qns_test: Option, - #[arg(short = 'r', long)] /// Client attempts to resume by making multiple connections to servers. /// Requires that 2 or more URLs are listed for each server. @@ -186,19 +162,11 @@ pub struct Args { /// Attempt to initiate a key update immediately after confirming the connection. key_update: bool, - #[arg(short = 'c', long, number_of_values = 1)] - /// The set of TLS cipher suites to enable. - /// From: `TLS_AES_128_GCM_SHA256`, `TLS_AES_256_GCM_SHA384`, `TLS_CHACHA20_POLY1305_SHA256`. - ciphers: Vec, - #[arg(name = "ech", long, value_parser = |s: &str| hex::decode(s))] /// Enable encrypted client hello (ECH). /// This takes an encoded ECH configuration in hexadecimal format. ech: Option>, - #[command(flatten)] - quic_parameters: QuicParameters, - #[arg(name = "ipv4-only", short = '4', long)] /// Connect only over IPv4 ipv4_only: bool, @@ -218,7 +186,8 @@ pub struct Args { impl Args { fn get_ciphers(&self) -> Vec { - self.ciphers + self.shared + .ciphers .iter() .filter_map(|c| match c.as_str() { "TLS_AES_128_GCM_SHA256" => Some(TLS_AES_128_GCM_SHA256), @@ -230,123 +199,51 @@ impl Args { } fn update_for_tests(&mut self) { - let Some(testcase) = self.qns_test.as_ref() else { + let Some(testcase) = self.shared.qns_test.as_ref() else { return; }; // Only use v1 for most QNS tests. - self.quic_parameters.quic_version = vec![Version::Version1]; + self.shared.quic_parameters.quic_version = vec![Version::Version1]; match testcase.as_str() { // TODO: Add "ecn" when that is ready. "http3" => {} "handshake" | "transfer" | "retry" => { - self.use_old_http = true; + self.shared.use_old_http = true; } "zerortt" | "resumption" => { if self.urls.len() < 2 { eprintln!("Warning: resumption tests won't work without >1 URL"); exit(127); } - self.use_old_http = true; + self.shared.use_old_http = true; self.resume = true; } "multiconnect" => { - self.use_old_http = true; + self.shared.use_old_http = true; self.download_in_series = true; } "chacha20" => { - self.use_old_http = true; - self.ciphers.clear(); - self.ciphers + self.shared.use_old_http = true; + self.shared.ciphers.clear(); + self.shared + .ciphers .extend_from_slice(&[String::from("TLS_CHACHA20_POLY1305_SHA256")]); } "keyupdate" => { - self.use_old_http = true; + self.shared.use_old_http = true; self.key_update = true; } "v2" => { - self.use_old_http = true; + self.shared.use_old_http = true; // Use default version set for this test (which allows compatible vneg.) - self.quic_parameters.quic_version.clear(); + self.shared.quic_parameters.quic_version.clear(); } _ => exit(127), } } } -fn from_str(s: &str) -> Res { - let v = u32::from_str_radix(s, 16) - .map_err(|_| ClientError::ArgumentError("versions need to be specified in hex"))?; - Version::try_from(v).map_err(|_| ClientError::ArgumentError("unknown version")) -} - -#[derive(Debug, Parser)] -struct QuicParameters { - #[arg( - short = 'Q', - long, - num_args = 1.., - value_delimiter = ' ', - number_of_values = 1, - value_parser = from_str)] - /// A list of versions to support, in hex. - /// The first is the version to attempt. - /// Adding multiple values adds versions in order of preference. - /// If the first listed version appears in the list twice, the position - /// of the second entry determines the preference order of that version. - quic_version: Vec, - - #[arg(long, default_value = "16")] - /// Set the `MAX_STREAMS_BIDI` limit. - max_streams_bidi: u64, - - #[arg(long, default_value = "16")] - /// Set the `MAX_STREAMS_UNI` limit. - max_streams_uni: u64, - - #[arg(long = "idle", default_value = "30")] - /// The idle timeout for connections, in seconds. - idle_timeout: u64, - - #[arg(long = "cc", default_value = "newreno")] - /// The congestion controller to use. - congestion_control: CongestionControlAlgorithm, - - #[arg(long = "pacing")] - /// Whether pacing is enabled. - pacing: bool, -} - -impl QuicParameters { - fn get(&self, alpn: &str) -> ConnectionParameters { - let params = ConnectionParameters::default() - .max_streams(StreamType::BiDi, self.max_streams_bidi) - .max_streams(StreamType::UniDi, self.max_streams_uni) - .idle_timeout(Duration::from_secs(self.idle_timeout)) - .cc_algorithm(self.congestion_control) - .pacing(self.pacing); - - if let Some(&first) = self.quic_version.first() { - let all = if self.quic_version[1..].contains(&first) { - &self.quic_version[1..] - } else { - &self.quic_version - }; - params.versions(first, all.to_vec()) - } else { - let version = match alpn { - "h3" | "hq-interop" => Version::Version1, - "h3-29" | "hq-29" => Version::Draft29, - "h3-30" | "hq-30" => Version::Draft30, - "h3-31" | "hq-31" => Version::Draft31, - "h3-32" | "hq-32" => Version::Draft32, - _ => Version::default(), - }; - params.versions(version, Version::all()) - } - } -} - fn get_output_file( url: &Url, output_dir: &Option, @@ -889,11 +786,11 @@ fn create_http3_client( ) -> Res { let mut transport = Connection::new_client( hostname, - &[&args.alpn], + &[&args.shared.alpn], Rc::new(RefCell::new(EmptyConnectionIdGenerator::default())), local_addr, remote_addr, - args.quic_parameters.get(args.alpn.as_str()), + args.shared.quic_parameters.get(args.shared.alpn.as_str()), Instant::now(), )?; let ciphers = args.get_ciphers(); @@ -903,9 +800,9 @@ fn create_http3_client( let mut client = Http3Client::new_with_conn( transport, Http3Parameters::default() - .max_table_size_encoder(args.max_table_size_encoder) - .max_table_size_decoder(args.max_table_size_decoder) - .max_blocked_streams(args.max_blocked_streams) + .max_table_size_encoder(args.shared.max_table_size_encoder) + .max_table_size_decoder(args.shared.max_table_size_decoder) + .max_blocked_streams(args.shared.max_blocked_streams) .max_concurrent_push_streams(args.max_concurrent_push_streams), ); @@ -924,7 +821,7 @@ fn create_http3_client( } fn qlog_new(args: &Args, hostname: &str, cid: &ConnectionId) -> Res { - if let Some(qlog_dir) = &args.qlog_dir { + if let Some(qlog_dir) = &args.shared.qlog_dir { let mut qlog_path = qlog_dir.clone(); let filename = format!("{hostname}-{cid}.sqlog"); qlog_path.push(filename); @@ -1002,7 +899,7 @@ async fn main() -> Res<()> { let real_local = socket.local_addr().unwrap(); println!( "{} Client connecting: {:?} -> {:?}", - if args.use_old_http { "H9" } else { "H3" }, + if args.shared.use_old_http { "H9" } else { "H3" }, real_local, remote_addr, ); @@ -1019,7 +916,7 @@ async fn main() -> Res<()> { first = false; - token = if args.use_old_http { + token = if args.shared.use_old_http { old::ClientRunner::new( &args, &mut socket, @@ -1266,8 +1163,8 @@ mod old { url_queue: VecDeque, token: Option, ) -> Res> { - let alpn = match args.alpn.as_str() { - "hq-29" | "hq-30" | "hq-31" | "hq-32" => args.alpn.as_str(), + let alpn = match args.shared.alpn.as_str() { + "hq-29" | "hq-30" | "hq-31" | "hq-32" => args.shared.alpn.as_str(), _ => "hq-interop", }; @@ -1277,7 +1174,7 @@ mod old { Rc::new(RefCell::new(EmptyConnectionIdGenerator::default())), local_addr, remote_addr, - args.quic_parameters.get(alpn), + args.shared.quic_parameters.get(alpn), Instant::now(), )?; diff --git a/neqo-server/src/main.rs b/neqo-bin/src/bin/server/main.rs similarity index 74% rename from neqo-server/src/main.rs rename to neqo-bin/src/bin/server/main.rs index 819014b331..da8de3831c 100644 --- a/neqo-server/src/main.rs +++ b/neqo-bin/src/bin/server/main.rs @@ -33,9 +33,7 @@ use neqo_http3::{ Error, Http3OrWebTransportStream, Http3Parameters, Http3Server, Http3ServerEvent, StreamId, }; use neqo_transport::{ - server::ValidateAddress, tparams::PreferredAddress, CongestionControlAlgorithm, - ConnectionIdGenerator, ConnectionParameters, Output, RandomConnectionIdGenerator, StreamType, - Version, + server::ValidateAddress, ConnectionIdGenerator, Output, RandomConnectionIdGenerator, Version, }; use tokio::time::Sleep; @@ -90,19 +88,13 @@ impl std::error::Error for ServerError {} #[derive(Debug, Parser)] #[command(author, version, about, long_about = None)] struct Args { + #[command(flatten)] + shared: neqo_bin::SharedArgs, + /// List of IP:port to listen on #[arg(default_value = "[::]:4433")] hosts: Vec, - #[arg(name = "encoder-table-size", long, default_value = "16384")] - max_table_size_encoder: u64, - - #[arg(name = "decoder-table-size", long, default_value = "16384")] - max_table_size_decoder: u64, - - #[arg(short = 'b', long, default_value = "10")] - max_blocked_streams: u16, - #[arg(short = 'd', long, default_value = "./test-fixture/db")] /// NSS database directory. db: PathBuf, @@ -111,36 +103,10 @@ struct Args { /// Name of key from NSS database. key: String, - #[arg(short = 'a', long, default_value = "h3")] - /// ALPN labels to negotiate. - /// - /// This server still only does HTTP3 no matter what the ALPN says. - alpn: String, - - #[arg(name = "qlog-dir", long, value_parser=clap::value_parser!(PathBuf))] - /// Enable QLOG logging and QLOG traces to this directory - qlog_dir: Option, - - #[arg(name = "qns-test", long)] - /// Enable special behavior for use with QUIC Network Simulator - qns_test: Option, - - #[arg(name = "use-old-http", short = 'o', long)] - /// Use http 0.9 instead of HTTP/3 - use_old_http: bool, - - #[command(flatten)] - quic_parameters: QuicParameters, - #[arg(name = "retry", long)] /// Force a retry retry: bool, - #[arg(short = 'c', long, number_of_values = 1)] - /// The set of TLS cipher suites to enable. - /// From: `TLS_AES_128_GCM_SHA256`, `TLS_AES_256_GCM_SHA384`, `TLS_CHACHA20_POLY1305_SHA256`. - ciphers: Vec, - #[arg(name = "ech", long)] /// Enable encrypted client hello (ECH). /// This generates a new set of ECH keys when it is invoked. @@ -150,7 +116,8 @@ struct Args { impl Args { fn get_ciphers(&self) -> Vec { - self.ciphers + self.shared + .ciphers .iter() .filter_map(|c| match c.as_str() { "TLS_AES_128_GCM_SHA256" => Some(TLS_AES_128_GCM_SHA256), @@ -166,13 +133,13 @@ impl Args { .iter() .filter_map(|host| host.to_socket_addrs().ok()) .flatten() - .chain(self.quic_parameters.preferred_address_v4()) - .chain(self.quic_parameters.preferred_address_v6()) + .chain(self.shared.quic_parameters.preferred_address_v4()) + .chain(self.shared.quic_parameters.preferred_address_v6()) .collect() } fn now(&self) -> Instant { - if self.qns_test.is_some() { + if self.shared.qns_test.is_some() { // When NSS starts its anti-replay it blocks any acceptance of 0-RTT for a // single period. This ensures that an attacker that is able to force a // server to reboot is unable to use that to flush the anti-replay buffers @@ -191,117 +158,6 @@ impl Args { } } -fn from_str(s: &str) -> Result { - let v = u32::from_str_radix(s, 16) - .map_err(|_| ServerError::ArgumentError("versions need to be specified in hex"))?; - Version::try_from(v).map_err(|_| ServerError::ArgumentError("unknown version")) -} - -#[derive(Debug, Parser)] -struct QuicParameters { - #[arg( - short = 'Q', - long, - num_args = 1.., - value_delimiter = ' ', - number_of_values = 1, - value_parser = from_str - )] - /// A list of versions to support in order of preference, in hex. - quic_version: Vec, - - #[arg(long, default_value = "16")] - /// Set the `MAX_STREAMS_BIDI` limit. - max_streams_bidi: u64, - - #[arg(long, default_value = "16")] - /// Set the `MAX_STREAMS_UNI` limit. - max_streams_uni: u64, - - #[arg(long = "idle", default_value = "30")] - /// The idle timeout for connections, in seconds. - idle_timeout: u64, - - #[arg(long = "cc", default_value = "newreno")] - /// The congestion controller to use. - congestion_control: CongestionControlAlgorithm, - - #[arg(name = "preferred-address-v4", long)] - /// An IPv4 address for the server preferred address. - preferred_address_v4: Option, - - #[arg(name = "preferred-address-v6", long)] - /// An IPv6 address for the server preferred address. - preferred_address_v6: Option, -} - -impl QuicParameters { - fn get_sock_addr(opt: &Option, v: &str, f: F) -> Option - where - F: FnMut(&SocketAddr) -> bool, - { - let addr = opt - .iter() - .filter_map(|spa| spa.to_socket_addrs().ok()) - .flatten() - .find(f); - assert_eq!( - opt.is_some(), - addr.is_some(), - "unable to resolve '{}' to an {} address", - opt.as_ref().unwrap(), - v, - ); - addr - } - - fn preferred_address_v4(&self) -> Option { - Self::get_sock_addr(&self.preferred_address_v4, "IPv4", SocketAddr::is_ipv4) - } - - fn preferred_address_v6(&self) -> Option { - Self::get_sock_addr(&self.preferred_address_v6, "IPv6", SocketAddr::is_ipv6) - } - - fn preferred_address(&self) -> Option { - let v4 = self.preferred_address_v4(); - let v6 = self.preferred_address_v6(); - if v4.is_none() && v6.is_none() { - None - } else { - let v4 = v4.map(|v4| { - let SocketAddr::V4(v4) = v4 else { - unreachable!(); - }; - v4 - }); - let v6 = v6.map(|v6| { - let SocketAddr::V6(v6) = v6 else { - unreachable!(); - }; - v6 - }); - Some(PreferredAddress::new(v4, v6)) - } - } - - fn get(&self) -> ConnectionParameters { - let mut params = ConnectionParameters::default() - .max_streams(StreamType::BiDi, self.max_streams_bidi) - .max_streams(StreamType::UniDi, self.max_streams_uni) - .idle_timeout(Duration::from_secs(self.idle_timeout)) - .cc_algorithm(self.congestion_control); - if let Some(pa) = self.preferred_address() { - params = params.preferred_address(pa); - } - - if let Some(first) = self.quic_version.first() { - params = params.versions(*first, self.quic_version.clone()); - } - params - } -} - fn qns_read_response(filename: &str) -> Option> { let mut file_path = PathBuf::from("/www"); file_path.push(filename.trim_matches(|p| p == '/')); @@ -417,14 +273,14 @@ impl SimpleServer { let server = Http3Server::new( args.now(), &[args.key.clone()], - &[args.alpn.clone()], + &[args.shared.alpn.clone()], anti_replay, cid_mgr, Http3Parameters::default() - .connection_parameters(args.quic_parameters.get()) - .max_table_size_encoder(args.max_table_size_encoder) - .max_table_size_decoder(args.max_table_size_decoder) - .max_blocked_streams(args.max_blocked_streams), + .connection_parameters(args.shared.quic_parameters.get(&args.shared.alpn)) + .max_table_size_encoder(args.shared.max_table_size_encoder) + .max_table_size_decoder(args.shared.max_table_size_decoder) + .max_blocked_streams(args.shared.max_blocked_streams), None, ) .expect("We cannot make a server!"); @@ -470,7 +326,7 @@ impl HttpServer for SimpleServer { let mut response = if let Some(path) = headers.iter().find(|&h| h.name() == ":path") { - if args.qns_test.is_some() { + if args.shared.qns_test.is_some() { if let Some(data) = qns_read_response(path.value()) { ResponseData::from(data) } else { @@ -600,15 +456,15 @@ impl ServersRunner { .expect("unable to setup anti-replay"); let cid_mgr = Rc::new(RefCell::new(RandomConnectionIdGenerator::new(10))); - let mut svr: Box = if args.use_old_http { + let mut svr: Box = if args.shared.use_old_http { Box::new( Http09Server::new( args.now(), &[args.key.clone()], - &[args.alpn.clone()], + &[args.shared.alpn.clone()], anti_replay, cid_mgr, - args.quic_parameters.get(), + args.shared.quic_parameters.get(&args.shared.alpn), ) .expect("We cannot make a server!"), ) @@ -616,7 +472,7 @@ impl ServersRunner { Box::new(SimpleServer::new(args, anti_replay, cid_mgr)) }; svr.set_ciphers(&args.get_ciphers()); - svr.set_qlog_dir(args.qlog_dir.clone()); + svr.set_qlog_dir(args.shared.qlog_dir.clone()); if args.retry { svr.validate_address(ValidateAddress::Always); } @@ -720,39 +576,41 @@ async fn main() -> Result<(), io::Error> { init_db(args.db.clone()); - if let Some(testcase) = args.qns_test.as_ref() { - if args.quic_parameters.quic_version.is_empty() { + if let Some(testcase) = args.shared.qns_test.as_ref() { + if args.shared.quic_parameters.quic_version.is_empty() { // Quic Interop Runner expects the server to support `Version1` // only. Exceptions are testcases `versionnegotiation` (not yet // implemented) and `v2`. if testcase != "v2" { - args.quic_parameters.quic_version = vec![Version::Version1]; + args.shared.quic_parameters.quic_version = vec![Version::Version1]; } } else { qwarn!("Both -V and --qns-test were set. Ignoring testcase specific versions."); } + // TODO: More options to deduplicate with client? match testcase.as_str() { "http3" => (), "zerortt" => { - args.use_old_http = true; - args.alpn = String::from(HQ_INTEROP); - args.quic_parameters.max_streams_bidi = 100; + args.shared.use_old_http = true; + args.shared.alpn = String::from(HQ_INTEROP); + args.shared.quic_parameters.max_streams_bidi = 100; } "handshake" | "transfer" | "resumption" | "multiconnect" | "v2" => { - args.use_old_http = true; - args.alpn = String::from(HQ_INTEROP); + args.shared.use_old_http = true; + args.shared.alpn = String::from(HQ_INTEROP); } "chacha20" => { - args.use_old_http = true; - args.alpn = String::from(HQ_INTEROP); - args.ciphers.clear(); - args.ciphers + args.shared.use_old_http = true; + args.shared.alpn = String::from(HQ_INTEROP); + args.shared.ciphers.clear(); + args.shared + .ciphers .extend_from_slice(&[String::from("TLS_CHACHA20_POLY1305_SHA256")]); } "retry" => { - args.use_old_http = true; - args.alpn = String::from(HQ_INTEROP); + args.shared.use_old_http = true; + args.shared.alpn = String::from(HQ_INTEROP); args.retry = true; } _ => exit(127), diff --git a/neqo-server/src/old_https.rs b/neqo-bin/src/bin/server/old_https.rs similarity index 98% rename from neqo-server/src/old_https.rs rename to neqo-bin/src/bin/server/old_https.rs index 2417c4790c..f36c99c484 100644 --- a/neqo-server/src/old_https.rs +++ b/neqo-bin/src/bin/server/old_https.rs @@ -136,7 +136,7 @@ impl Http09Server { return; }; - let re = if args.qns_test.is_some() { + let re = if args.shared.qns_test.is_some() { Regex::new(r"GET +/(\S+)(?:\r)?\n").unwrap() } else { Regex::new(r"GET +/(\d+)(?:\r)?\n").unwrap() @@ -150,7 +150,7 @@ impl Http09Server { Some(path) => { let path = path.as_str(); eprintln!("Path = '{path}'"); - if args.qns_test.is_some() { + if args.shared.qns_test.is_some() { qns_read_response(path) } else { let count = path.parse().unwrap(); diff --git a/neqo-bin/src/lib.rs b/neqo-bin/src/lib.rs new file mode 100644 index 0000000000..4fe47d5cbf --- /dev/null +++ b/neqo-bin/src/lib.rs @@ -0,0 +1,204 @@ +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use std::{ + fmt::{self, Display}, + net::{SocketAddr, ToSocketAddrs}, + path::PathBuf, + time::Duration, +}; + +use clap::Parser; +use neqo_transport::{ + tparams::PreferredAddress, CongestionControlAlgorithm, ConnectionParameters, StreamType, + Version, +}; + +#[derive(Debug, Parser)] +pub struct SharedArgs { + #[arg(short = 'a', long, default_value = "h3")] + /// ALPN labels to negotiate. + /// + /// This client still only does HTTP/3 no matter what the ALPN says. + pub alpn: String, + + #[arg(name = "qlog-dir", long, value_parser=clap::value_parser!(PathBuf))] + /// Enable QLOG logging and QLOG traces to this directory + pub qlog_dir: Option, + + #[arg(name = "encoder-table-size", long, default_value = "16384")] + pub max_table_size_encoder: u64, + + #[arg(name = "decoder-table-size", long, default_value = "16384")] + pub max_table_size_decoder: u64, + + #[arg(name = "max-blocked-streams", short = 'b', long, default_value = "10")] + pub max_blocked_streams: u16, + + #[arg(short = 'c', long, number_of_values = 1)] + /// The set of TLS cipher suites to enable. + /// From: `TLS_AES_128_GCM_SHA256`, `TLS_AES_256_GCM_SHA384`, `TLS_CHACHA20_POLY1305_SHA256`. + pub ciphers: Vec, + + #[arg(name = "qns-test", long)] + /// Enable special behavior for use with QUIC Network Simulator + pub qns_test: Option, + + #[arg(name = "use-old-http", short = 'o', long)] + /// Use http 0.9 instead of HTTP/3 + pub use_old_http: bool, + + #[command(flatten)] + pub quic_parameters: QuicParameters, +} + +#[derive(Debug, Parser)] +pub struct QuicParameters { + #[arg( + short = 'Q', + long, + num_args = 1.., + value_delimiter = ' ', + number_of_values = 1, + value_parser = from_str)] + /// A list of versions to support, in hex. + /// The first is the version to attempt. + /// Adding multiple values adds versions in order of preference. + /// If the first listed version appears in the list twice, the position + /// of the second entry determines the preference order of that version. + pub quic_version: Vec, + + #[arg(long, default_value = "16")] + /// Set the `MAX_STREAMS_BIDI` limit. + pub max_streams_bidi: u64, + + #[arg(long, default_value = "16")] + /// Set the `MAX_STREAMS_UNI` limit. + pub max_streams_uni: u64, + + #[arg(long = "idle", default_value = "30")] + /// The idle timeout for connections, in seconds. + pub idle_timeout: u64, + + #[arg(long = "cc", default_value = "newreno")] + /// The congestion controller to use. + pub congestion_control: CongestionControlAlgorithm, + + #[arg(long = "pacing")] + /// Whether pacing is enabled. + pub pacing: bool, + + #[arg(name = "preferred-address-v4", long)] + /// An IPv4 address for the server preferred address. + pub preferred_address_v4: Option, + + #[arg(name = "preferred-address-v6", long)] + /// An IPv6 address for the server preferred address. + pub preferred_address_v6: Option, +} + +impl QuicParameters { + fn get_sock_addr(opt: &Option, v: &str, f: F) -> Option + where + F: FnMut(&SocketAddr) -> bool, + { + let addr = opt + .iter() + .filter_map(|spa| spa.to_socket_addrs().ok()) + .flatten() + .find(f); + assert_eq!( + opt.is_some(), + addr.is_some(), + "unable to resolve '{}' to an {} address", + opt.as_ref().unwrap(), + v, + ); + addr + } + + #[must_use] + pub fn preferred_address_v4(&self) -> Option { + Self::get_sock_addr(&self.preferred_address_v4, "IPv4", SocketAddr::is_ipv4) + } + + #[must_use] + pub fn preferred_address_v6(&self) -> Option { + Self::get_sock_addr(&self.preferred_address_v6, "IPv6", SocketAddr::is_ipv6) + } + + #[must_use] + pub fn preferred_address(&self) -> Option { + let v4 = self.preferred_address_v4(); + let v6 = self.preferred_address_v6(); + if v4.is_none() && v6.is_none() { + None + } else { + let v4 = v4.map(|v4| { + let SocketAddr::V4(v4) = v4 else { + unreachable!(); + }; + v4 + }); + let v6 = v6.map(|v6| { + let SocketAddr::V6(v6) = v6 else { + unreachable!(); + }; + v6 + }); + Some(PreferredAddress::new(v4, v6)) + } + } + + #[must_use] + pub fn get(&self, alpn: &str) -> ConnectionParameters { + let params = ConnectionParameters::default() + .max_streams(StreamType::BiDi, self.max_streams_bidi) + .max_streams(StreamType::UniDi, self.max_streams_uni) + .idle_timeout(Duration::from_secs(self.idle_timeout)) + .cc_algorithm(self.congestion_control) + .pacing(self.pacing); + + if let Some(&first) = self.quic_version.first() { + let all = if self.quic_version[1..].contains(&first) { + &self.quic_version[1..] + } else { + &self.quic_version + }; + params.versions(first, all.to_vec()) + } else { + let version = match alpn { + "h3" | "hq-interop" => Version::Version1, + "h3-29" | "hq-29" => Version::Draft29, + "h3-30" | "hq-30" => Version::Draft30, + "h3-31" | "hq-31" => Version::Draft31, + "h3-32" | "hq-32" => Version::Draft32, + _ => Version::default(), + }; + params.versions(version, Version::all()) + } + } +} + +fn from_str(s: &str) -> Result { + let v = u32::from_str_radix(s, 16) + .map_err(|_| Error::Argument("versions need to be specified in hex"))?; + Version::try_from(v).map_err(|_| Error::Argument("unknown version")) +} + +#[derive(Debug)] +pub enum Error { + Argument(&'static str), +} + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Error: {self:?}")?; + Ok(()) + } +} + +impl std::error::Error for Error {} diff --git a/neqo-client/Cargo.toml b/neqo-client/Cargo.toml deleted file mode 100644 index 6fa361020e..0000000000 --- a/neqo-client/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "neqo-client" -description = "A basic QUIC HTTP/0.9 and HTTP/3 client." -authors.workspace = true -homepage.workspace = true -repository.workspace = true -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true - -[lints] -workspace = true - -[dependencies] -# neqo-client is not used in Firefox, so we can be liberal with dependency versions -clap = { version = "4.4", default-features = false, features = ["std", "color", "help", "usage", "error-context", "suggestions", "derive"] } -futures = { version = "0.3", default-features = false } -hex = { version = "0.4", default-features = false, features = ["std"] } -log = { version = "0.4", default-features = false } -neqo-common = { path = "./../neqo-common", features = ["udp"] } -neqo-crypto = { path = "./../neqo-crypto" } -neqo-http3 = { path = "./../neqo-http3" } -neqo-qpack = { path = "./../neqo-qpack" } -neqo-transport = { path = "./../neqo-transport" } -qlog = { version = "0.12", default-features = false } -tokio = { version = "1", default-features = false, features = ["net", "time", "macros", "rt", "rt-multi-thread"] } -url = { version = "2.5", default-features = false }