diff --git a/Cargo.lock b/Cargo.lock index e6f0799..1896218 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -226,6 +226,17 @@ dependencies = [ "slab", ] +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock 3.3.0", + "blocking", + "futures-lite 2.3.0", +] + [[package]] name = "async-global-executor" version = "2.4.1" @@ -301,6 +312,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io 2.3.2", + "blocking", + "futures-lite 2.3.0", +] + [[package]] name = "async-process" version = "1.8.1" @@ -318,6 +340,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "async-process" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7eda79bbd84e29c2b308d1dc099d7de8dcc7035e48f4bf5dc4a531a44ff5e2a" +dependencies = [ + "async-channel 2.3.1", + "async-io 2.3.2", + "async-lock 3.3.0", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.3.1", + "futures-lite 2.3.0", + "rustix 0.38.34", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "async-signal" version = "0.2.7" @@ -346,7 +388,7 @@ dependencies = [ "async-global-executor", "async-io 1.13.0", "async-lock 2.8.0", - "async-process", + "async-process 1.8.1", "crossbeam-utils", "futures-channel", "futures-core", @@ -599,6 +641,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bencher" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfdb4953a096c551ce9ace855a604d702e6e62d77fac690575ae347571717f5" + [[package]] name = "bincode" version = "1.3.3" @@ -746,6 +794,12 @@ dependencies = [ "serde", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cexpr" version = "0.6.0" @@ -857,6 +911,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -966,6 +1030,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.4.2" @@ -1214,6 +1293,16 @@ dependencies = [ "dirs-sys 0.3.7", ] +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs" version = "5.0.1" @@ -1246,6 +1335,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + [[package]] name = "dlv-list" version = "0.5.2" @@ -1271,17 +1371,24 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" name = "easy_example" version = "0.4.0" dependencies = [ + "anyhow", + "async-io 2.3.2", + "async-std", "async-stream", "axum 0.7.5", "axum-extra", "base64 0.22.1", + "bencher", "bytes", "clap", "config", + "crc", "csv", + "directories-next", "env_logger", "flume", "futures", + "futures-io", "futures-util", "git-version", "headers", @@ -1294,16 +1401,26 @@ dependencies = [ "log4rs", "ndarray", "once_cell", + "pin-project-lite", "plotly", "prost", "prost-types", + "quinn 0.11.1", + "quinn-proto 0.11.2", + "quinn-udp 0.5.1", "rand", "rand_distr", + "rcgen", "reqwest", "rumqttc", + "rustc-hash", + "rustls 0.23.9", + "rustls-pemfile 2.1.2", "serde", "serde_derive", "serde_yaml", + "smol", + "thiserror", "tmq", "tokio", "tokio-serial", @@ -1315,7 +1432,9 @@ dependencies = [ "tower", "tower-http", "tracing", + "tracing-futures", "tracing-subscriber", + "url", "zenoh", "zenoh-ext", "zenoh-util", @@ -2421,6 +2540,26 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.31" @@ -2842,6 +2981,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "num-bigint" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -2914,6 +3063,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "numtoa" version = "0.1.0" @@ -3090,6 +3248,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -3498,8 +3666,8 @@ checksum = "8cc2c5017e4b43d5995dcea317bc46c1e09404c0a9664d2908f7f02dfe943d75" dependencies = [ "bytes", "pin-project-lite", - "quinn-proto", - "quinn-udp", + "quinn-proto 0.10.6", + "quinn-udp 0.4.1", "rustc-hash", "rustls 0.21.12", "thiserror", @@ -3507,6 +3675,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "quinn" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904e3d3ba178131798c6d9375db2b13b34337d489b089fc5ba0825a2ff1bee73" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto 0.11.2", + "quinn-udp 0.5.1", + "rustc-hash", + "rustls 0.23.9", + "thiserror", + "tokio", + "tracing", +] + [[package]] name = "quinn-proto" version = "0.10.6" @@ -3525,6 +3710,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "quinn-proto" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e974563a4b1c2206bbc61191ca4da9c22e4308b4c455e8906751cc7828393f08" +dependencies = [ + "bytes", + "rand", + "ring 0.17.8", + "rustc-hash", + "rustls 0.23.9", + "rustls-platform-verifier", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + [[package]] name = "quinn-udp" version = "0.4.1" @@ -3538,6 +3741,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "quinn-udp" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4f0def2590301f4f667db5a77f9694fb004f82796dc1a8b1508fafa3d0e8b72" +dependencies = [ + "libc", + "once_cell", + "socket2 0.5.7", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.36" @@ -3613,6 +3829,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54077e1872c46788540de1ea3d7f4ccb1983d12f9aa909b234468676c1a36779" +dependencies = [ + "pem", + "ring 0.17.8", + "rustls-pki-types", + "time 0.3.36", + "yasna", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -3908,6 +4137,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls" +version = "0.23.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a218f0f6d05669de4eabfb24f31ce802035c952429d037507b4a4a39f0e60c5b" +dependencies = [ + "once_cell", + "ring 0.17.8", + "rustls-pki-types", + "rustls-webpki 0.102.4", + "subtle", + "zeroize", +] + [[package]] name = "rustls-native-certs" version = "0.6.3" @@ -3958,6 +4201,33 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +[[package]] +name = "rustls-platform-verifier" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f0d26fa1ce3c790f9590868f0109289a044acb954525f933e2aa3b871c157d" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.9", + "rustls-native-certs 0.7.0", + "rustls-platform-verifier-android", + "rustls-webpki 0.102.4", + "security-framework", + "security-framework-sys", + "webpki-roots", + "winapi 0.3.9", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84e217e7fdc8466b5b35d30f8c0a30febd29173df4a3a0c2115d306b9c4117ad" + [[package]] name = "rustls-webpki" version = "0.101.7" @@ -4069,6 +4339,7 @@ dependencies = [ "core-foundation", "core-foundation-sys", "libc", + "num-bigint", "security-framework-sys", ] @@ -4343,6 +4614,23 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "smol" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e635339259e51ef85ac7aa29a1cd991b957047507288697a690e80ab97d07cad" +dependencies = [ + "async-channel 2.3.1", + "async-executor", + "async-fs", + "async-io 2.3.2", + "async-lock 3.3.0", + "async-net", + "async-process 2.2.3", + "blocking", + "futures-lite 2.3.0", +] + [[package]] name = "socket2" version = "0.4.10" @@ -4605,7 +4893,9 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde", "time-core", @@ -4993,6 +5283,16 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + [[package]] name = "tracing-log" version = "0.2.0" @@ -5029,6 +5329,7 @@ dependencies = [ "sharded-slab", "smallvec", "thread_local", + "time 0.3.36", "tracing", "tracing-core", "tracing-log", @@ -5663,6 +5964,15 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time 0.3.36", +] + [[package]] name = "zenoh" version = "0.11.0-rc.3" @@ -5880,7 +6190,7 @@ dependencies = [ "async-trait", "base64 0.21.7", "futures", - "quinn", + "quinn 0.10.2", "rustls 0.21.12", "rustls-native-certs 0.7.0", "rustls-pemfile 1.0.4", diff --git a/Cargo.toml b/Cargo.toml index c2bcf0f..0d22e44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,22 @@ version = "0.4.0" authors = ["youngday"] edition = "2021" +[features] +default = ["log", "platform-verifier", "ring", "runtime-tokio", "rustls"] +# Records how long locks are held, and warns if they are held >= 1ms +lock_tracking = [] +# Provides `ClientConfig::with_platform_verifier()` convenience method +platform-verifier = ["proto/platform-verifier"] +rustls = ["dep:rustls", "proto/rustls", "proto/ring"] +# Enables `Endpoint::client` and `Endpoint::server` conveniences +ring = ["proto/ring"] +runtime-tokio = ["tokio/time", "tokio/rt", "tokio/net"] +runtime-async-std = ["async-io", "async-std"] +runtime-smol = ["async-io", "smol"] + +# Write logs via the `log` crate when no `tracing` subscriber exists +log = ["tracing/log", "proto/log", "udp/log"] + [dependencies] log = "0.4" log4rs = { version = "1.3.0", features = ["toml", "json_encoder"] } @@ -66,6 +82,37 @@ tower-http = { version = "0.5.0", features = ["fs", "trace"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } #webrtc axum tokio-tungstenite ----------------- + + +# quiche++++++++++++++++++++++++++++++++++++++++++++ +async-io = { version = "2.0", optional = true } +async-std = { version = "1.11", optional = true } +# Enables futures::io::{AsyncRead, AsyncWrite} support for streams +futures-io = { version = "0.3.19", optional = true } +rustc-hash = "1.1" +pin-project-lite = "0.2" +quinn="0.11.1" +proto = { package = "quinn-proto",version = "0.11.2", default-features = false } +rustls = { version = "0.23", default-features = false, features = ["ring", "std"], optional = true } +smol = { version = "2", optional = true } +thiserror = "1.0.21" +udp = { package = "quinn-udp", version = "0.5", default-features = false } +#quiche---------------------------------------------- +[dev-dependencies] +# quiche++++++++++++++++++++++++++++++++++++++++++++ +anyhow = "1.0.22" +crc = "3" +bencher = "0.1.5" +directories-next = "2" +rand = "0.8" +rcgen = "0.13" +rustls-pemfile = "2" +clap = { version = "4", features = ["derive"] } +tokio = { version = "1.28.1", features = ["rt", "rt-multi-thread", "time", "macros", "sync"] } +tracing-subscriber = { version = "0.3.0", default-features = false, features = ["env-filter", "fmt", "ansi", "time", "local-time"] } +tracing-futures = { version = "0.2.0", default-features = false, features = ["std-future"] } +url = "2" +#quiche---------------------------------------------- [build-dependencies] tonic-build = { version = "0.11", features = ["prost"] } @@ -150,3 +197,11 @@ path = "examples/websocket_axum/server.rs" [[example]] name = "ws_client" path = "examples/websocket_axum/client.rs" + +[[example]] +name = "quic_client" +path = "examples/quic/client.rs" + +[[example]] +name = "quic_server" +path = "examples/quic/server.rs" \ No newline at end of file diff --git a/README.md b/README.md index 223101a..b8e1192 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ |iceoryx2|dds|pubsub dds ipc for ros |new realtime(10us) ipc | |tonic| |grpc |⚠️ proto genarate .rs files | |axum websocket| |websocket | axum example ,tokio-tungstenite | +|quinn quic| |quic http3 | quic http3 client server | ## examples |name|fun|note| @@ -39,6 +40,7 @@ |discovery|iceoryx2 discovery| | |grpc_client,grpc_server| |⚠️ proto genarate .rs files | |ws_client,ws_server| | websocket | +|quic_server,quic_client|quic,http3|quic,http3| ## vscode build https://code.visualstudio.com/docs/languages/rust @@ -67,4 +69,12 @@ pub and sub ,test ok , but not c++ and python app,now are planning,waiting for release -⚠️ \ No newline at end of file +⚠️ + +### quiche + +```sh +cargo run --example quic_server ./ + +cargo run --example quic_client https://localhost:4433/Cargo.toml +``` \ No newline at end of file diff --git a/examples/quic/README.md b/examples/quic/README.md new file mode 100644 index 0000000..4baf933 --- /dev/null +++ b/examples/quic/README.md @@ -0,0 +1,92 @@ +## HTTP/0.9 File Serving Example + +The `server` and `client` examples demonstrate fetching files using a HTTP-like toy protocol. + +1. Server (`server.rs`) + +The server listens for any client requesting a file. +If the file path is valid and allowed, it returns the contents. + +Open up a terminal and execute: + +```text +$ cargo run --example server ./ +``` + +2. Client (`client.rs`) + +The client requests a file and prints it to the console. +If the file is on the server, it will receive the response. + +In a new terminal execute: + +```test +$ cargo run --example client https://localhost:4433/Cargo.toml +``` + +where `Cargo.toml` is any file in the directory passed to the server. + +**Result:** + +The output will be the contents of this README. + +**Troubleshooting:** + +If the client times out with no activity on the server, try forcing the server to run on IPv4 by +running it with `cargo run --example server -- ./ --listen 127.0.0.1:4433`. The server listens on +IPv6 by default, `localhost` tends to resolve to IPv4, and support for accepting IPv4 packets on +IPv6 sockets varies between platforms. + +If the client prints `failed to process request: failed reading file`, the request was processed +successfully but the path segment of the URL did not correspond to a file in the directory being +served. + +## Minimal Example +The `connection.rs` example intends to use the smallest amount of code to make a simple QUIC connection. +The server issues it's own certificate and passes it to the client to trust. + +```text +$ cargo run --example connection +``` + +This example will make a QUIC connection on localhost, and you should see output like: + +```text +[client] connected: addr=127.0.0.1:5000 +[server] connection accepted: addr=127.0.0.1:53712 +``` + +## Insecure Connection Example + +The `insecure_connection.rs` example demonstrates how to make a QUIC connection that ignores the server certificate. + +```text +$ cargo run --example insecure_connection --features="rustls/dangerous_configuration" +``` + +## Single Socket Example + +You can have multiple QUIC connections over a single UDP socket. This is especially +useful, if you are building a peer-to-peer system where you potentially need to communicate with +thousands of peers or if you have a +[hole punched](https://en.wikipedia.org/wiki/UDP_hole_punching) UDP socket. +Additionally, QUIC servers and clients can both operate on the same UDP socket. +This example demonstrates how to make multiple outgoing connections on a single UDP socket. + +```text +$ cargo run --example single_socket +``` + +The expected output should be something like: + +```text +[client] connected: addr=127.0.0.1:5000 +[server] incoming connection: addr=127.0.0.1:48930 +[client] connected: addr=127.0.0.1:5001 +[client] connected: addr=127.0.0.1:5002 +[server] incoming connection: addr=127.0.0.1:48930 +[server] incoming connection: addr=127.0.0.1:48930 +``` + +Notice how the server sees multiple incoming connections with different IDs coming from the same +endpoint. diff --git a/examples/quic/client.rs b/examples/quic/client.rs new file mode 100644 index 0000000..c037833 --- /dev/null +++ b/examples/quic/client.rs @@ -0,0 +1,169 @@ +//! This example demonstrates an HTTP client that requests files from a server. +//! +//! Checkout the `README.md` for guidance. + +use std::{ + fs, + io::{self, Write}, + net::{SocketAddr, ToSocketAddrs}, + path::PathBuf, + sync::Arc, + time::{Duration, Instant}, +}; + +use anyhow::{anyhow, Result}; +use clap::Parser; +use proto::crypto::rustls::QuicClientConfig; +use rustls::pki_types::CertificateDer; +use tracing::{error, info}; +use url::Url; + +mod common; + +/// HTTP/0.9 over QUIC client +#[derive(Parser, Debug)] +#[clap(name = "client")] +struct Opt { + /// Perform NSS-compatible TLS key logging to the file specified in `SSLKEYLOGFILE`. + #[clap(long = "keylog")] + keylog: bool, + + url: Url, + + /// Override hostname used for certificate verification + #[clap(long = "host")] + host: Option, + + /// Custom certificate authority to trust, in DER format + #[clap(long = "ca")] + ca: Option, + + /// Simulate NAT rebinding after connecting + #[clap(long = "rebind")] + rebind: bool, + + /// Address to bind on + #[clap(long = "bind", default_value = "[::]:0")] + bind: SocketAddr, +} + +fn main() { + tracing::subscriber::set_global_default( + tracing_subscriber::FmtSubscriber::builder() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .finish(), + ) + .unwrap(); + let opt = Opt::parse(); + let code = { + if let Err(e) = run(opt) { + eprintln!("ERROR: {e}"); + 1 + } else { + 0 + } + }; + ::std::process::exit(code); +} + +#[tokio::main] +async fn run(options: Opt) -> Result<()> { + let url = options.url; + let url_host = strip_ipv6_brackets(url.host_str().unwrap()); + let remote = (url_host, url.port().unwrap_or(4433)) + .to_socket_addrs()? + .next() + .ok_or_else(|| anyhow!("couldn't resolve to an address"))?; + + let mut roots = rustls::RootCertStore::empty(); + if let Some(ca_path) = options.ca { + roots.add(CertificateDer::from(fs::read(ca_path)?))?; + } else { + let dirs = directories_next::ProjectDirs::from("org", "quinn", "quinn-examples").unwrap(); + match fs::read(dirs.data_local_dir().join("cert.der")) { + Ok(cert) => { + roots.add(CertificateDer::from(cert))?; + } + Err(ref e) if e.kind() == io::ErrorKind::NotFound => { + info!("local server certificate not found"); + } + Err(e) => { + error!("failed to open local server certificate: {}", e); + } + } + } + let mut client_crypto = rustls::ClientConfig::builder() + .with_root_certificates(roots) + .with_no_client_auth(); + + client_crypto.alpn_protocols = common::ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect(); + if options.keylog { + client_crypto.key_log = Arc::new(rustls::KeyLogFile::new()); + } + + let client_config = + quinn::ClientConfig::new(Arc::new(QuicClientConfig::try_from(client_crypto)?)); + let mut endpoint = quinn::Endpoint::client(options.bind)?; + endpoint.set_default_client_config(client_config); + + let request = format!("GET {}\r\n", url.path()); + let start = Instant::now(); + let rebind = options.rebind; + let host = options.host.as_deref().unwrap_or(url_host); + + eprintln!("connecting to {host} at {remote}"); + let conn = endpoint + .connect(remote, host)? + .await + .map_err(|e| anyhow!("failed to connect: {}", e))?; + eprintln!("connected at {:?}", start.elapsed()); + let (mut send, mut recv) = conn + .open_bi() + .await + .map_err(|e| anyhow!("failed to open stream: {}", e))?; + if rebind { + let socket = std::net::UdpSocket::bind("[::]:0").unwrap(); + let addr = socket.local_addr().unwrap(); + eprintln!("rebinding to {addr}"); + endpoint.rebind(socket).expect("rebind failed"); + } + + send.write_all(request.as_bytes()) + .await + .map_err(|e| anyhow!("failed to send request: {}", e))?; + send.finish().unwrap(); + let response_start = Instant::now(); + eprintln!("request sent at {:?}", response_start - start); + let resp = recv + .read_to_end(usize::max_value()) + .await + .map_err(|e| anyhow!("failed to read response: {}", e))?; + let duration = response_start.elapsed(); + eprintln!( + "response received in {:?} - {} KiB/s", + duration, + resp.len() as f32 / (duration_secs(&duration) * 1024.0) + ); + io::stdout().write_all(&resp).unwrap(); + io::stdout().flush().unwrap(); + conn.close(0u32.into(), b"done"); + + // Give the server a fair chance to receive the close packet + endpoint.wait_idle().await; + + Ok(()) +} + +fn strip_ipv6_brackets(host: &str) -> &str { + // An ipv6 url looks like eg https://[::1]:4433/Cargo.toml, wherein the host [::1] is the + // ipv6 address ::1 wrapped in brackets, per RFC 2732. This strips those. + if host.starts_with('[') && host.ends_with(']') { + &host[1..host.len() - 1] + } else { + host + } +} + +fn duration_secs(x: &Duration) -> f32 { + x.as_secs() as f32 + x.subsec_nanos() as f32 * 1e-9 +} diff --git a/examples/quic/common/mod.rs b/examples/quic/common/mod.rs new file mode 100644 index 0000000..05e9f37 --- /dev/null +++ b/examples/quic/common/mod.rs @@ -0,0 +1,73 @@ +#![cfg(feature = "rustls")] +//! Commonly used code in most examples. + +use quinn::{ClientConfig, Endpoint, ServerConfig}; +use rustls::pki_types::{CertificateDer, PrivatePkcs8KeyDer}; + +use std::{error::Error, net::SocketAddr, sync::Arc}; + +/// Constructs a QUIC endpoint configured for use a client only. +/// +/// ## Args +/// +/// - server_certs: list of trusted certificates. +#[allow(unused)] +pub fn make_client_endpoint( + bind_addr: SocketAddr, + server_certs: &[&[u8]], +) -> Result> { + let client_cfg = configure_client(server_certs)?; + let mut endpoint = Endpoint::client(bind_addr)?; + endpoint.set_default_client_config(client_cfg); + Ok(endpoint) +} + +/// Constructs a QUIC endpoint configured to listen for incoming connections on a certain address +/// and port. +/// +/// ## Returns +/// +/// - a stream of incoming QUIC connections +/// - server certificate serialized into DER format +#[allow(unused)] +pub fn make_server_endpoint( + bind_addr: SocketAddr, +) -> Result<(Endpoint, CertificateDer<'static>), Box> { + let (server_config, server_cert) = configure_server()?; + let endpoint = Endpoint::server(server_config, bind_addr)?; + Ok((endpoint, server_cert)) +} + +/// Builds default quinn client config and trusts given certificates. +/// +/// ## Args +/// +/// - server_certs: a list of trusted certificates in DER format. +fn configure_client( + server_certs: &[&[u8]], +) -> Result> { + let mut certs = rustls::RootCertStore::empty(); + for cert in server_certs { + certs.add(CertificateDer::from(*cert))?; + } + + Ok(ClientConfig::with_root_certificates(Arc::new(certs))?) +} + +/// Returns default server configuration along with its certificate. +fn configure_server( +) -> Result<(ServerConfig, CertificateDer<'static>), Box> { + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let cert_der = CertificateDer::from(cert.cert); + let priv_key = PrivatePkcs8KeyDer::from(cert.key_pair.serialize_der()); + + let mut server_config = + ServerConfig::with_single_cert(vec![cert_der.clone()], priv_key.into())?; + let transport_config = Arc::get_mut(&mut server_config.transport).unwrap(); + transport_config.max_concurrent_uni_streams(0_u8.into()); + + Ok((server_config, cert_der)) +} + +#[allow(unused)] +pub const ALPN_QUIC_HTTP: &[&[u8]] = &[b"hq-29"]; diff --git a/examples/quic/connection.rs b/examples/quic/connection.rs new file mode 100644 index 0000000..3326d69 --- /dev/null +++ b/examples/quic/connection.rs @@ -0,0 +1,42 @@ +//! This example intends to use the smallest amount of code to make a simple QUIC connection. +//! +//! Checkout the `README.md` for guidance. + +use std::error::Error; + +mod common; +use common::{make_client_endpoint, make_server_endpoint}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let server_addr = "127.0.0.1:5000".parse().unwrap(); + let (endpoint, server_cert) = make_server_endpoint(server_addr)?; + // accept a single connection + let endpoint2 = endpoint.clone(); + tokio::spawn(async move { + let incoming_conn = endpoint2.accept().await.unwrap(); + let conn = incoming_conn.await.unwrap(); + println!( + "[server] connection accepted: addr={}", + conn.remote_address() + ); + // Dropping all handles associated with a connection implicitly closes it + }); + + let endpoint = make_client_endpoint("0.0.0.0:0".parse().unwrap(), &[&server_cert])?; + // connect to server + let connection = endpoint + .connect(server_addr, "localhost") + .unwrap() + .await + .unwrap(); + println!("[client] connected: addr={}", connection.remote_address()); + + // Waiting for a stream will complete with an error when the server closes the connection + let _ = connection.accept_uni().await; + + // Make sure the server has a chance to clean up + endpoint.wait_idle().await; + + Ok(()) +} diff --git a/examples/quic/insecure_connection.rs b/examples/quic/insecure_connection.rs new file mode 100644 index 0000000..54767aa --- /dev/null +++ b/examples/quic/insecure_connection.rs @@ -0,0 +1,115 @@ +//! This example demonstrates how to make a QUIC connection that ignores the server certificate. +//! +//! Checkout the `README.md` for guidance. + +use std::{error::Error, net::SocketAddr, sync::Arc}; + +use proto::crypto::rustls::QuicClientConfig; +use quinn::{ClientConfig, Endpoint}; +use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; + +mod common; +use common::make_server_endpoint; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // server and client are running on the same thread asynchronously + let addr = "127.0.0.1:5000".parse().unwrap(); + tokio::spawn(run_server(addr)); + run_client(addr).await?; + Ok(()) +} + +/// Runs a QUIC server bound to given address. +async fn run_server(addr: SocketAddr) { + let (endpoint, _server_cert) = make_server_endpoint(addr).unwrap(); + // accept a single connection + let incoming_conn = endpoint.accept().await.unwrap(); + let conn = incoming_conn.await.unwrap(); + println!( + "[server] connection accepted: addr={}", + conn.remote_address() + ); +} + +async fn run_client(server_addr: SocketAddr) -> Result<(), Box> { + let provider = rustls::crypto::CryptoProvider::get_default().unwrap(); + let mut endpoint = Endpoint::client("127.0.0.1:0".parse().unwrap())?; + + endpoint.set_default_client_config(ClientConfig::new(Arc::new(QuicClientConfig::try_from( + rustls::ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(SkipServerVerification::new(provider.clone())) + .with_no_client_auth(), + )?))); + + // connect to server + let connection = endpoint + .connect(server_addr, "localhost") + .unwrap() + .await + .unwrap(); + println!("[client] connected: addr={}", connection.remote_address()); + // Dropping handles allows the corresponding objects to automatically shut down + drop(connection); + // Make sure the server has a chance to clean up + endpoint.wait_idle().await; + + Ok(()) +} + +/// Dummy certificate verifier that treats any certificate as valid. +/// NOTE, such verification is vulnerable to MITM attacks, but convenient for testing. +#[derive(Debug)] +struct SkipServerVerification(Arc); + +impl SkipServerVerification { + fn new(provider: Arc) -> Arc { + Arc::new(Self(provider)) + } +} + +impl rustls::client::danger::ServerCertVerifier for SkipServerVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp: &[u8], + _now: UnixTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls12_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls13_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + self.0.signature_verification_algorithms.supported_schemes() + } +} diff --git a/examples/quic/server.rs b/examples/quic/server.rs new file mode 100644 index 0000000..5f9e6f3 --- /dev/null +++ b/examples/quic/server.rs @@ -0,0 +1,271 @@ +//! This example demonstrates an HTTP server that serves files from a directory. +//! +//! Checkout the `README.md` for guidance. + +use std::{ + ascii, fs, io, + net::SocketAddr, + path::{self, Path, PathBuf}, + str, + sync::Arc, +}; + +use anyhow::{anyhow, bail, Context, Result}; +use clap::Parser; +use proto::crypto::rustls::QuicServerConfig; +use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; +use tracing::{error, info, info_span}; +use tracing_futures::Instrument as _; + +mod common; + +#[derive(Parser, Debug)] +#[clap(name = "server")] +struct Opt { + /// file to log TLS keys to for debugging + #[clap(long = "keylog")] + keylog: bool, + /// directory to serve files from + root: PathBuf, + /// TLS private key in PEM format + #[clap(short = 'k', long = "key", requires = "cert")] + key: Option, + /// TLS certificate in PEM format + #[clap(short = 'c', long = "cert", requires = "key")] + cert: Option, + /// Enable stateless retries + #[clap(long = "stateless-retry")] + stateless_retry: bool, + /// Address to listen on + #[clap(long = "listen", default_value = "[::1]:4433")] + listen: SocketAddr, + /// Client address to block + #[clap(long = "block")] + block: Option, + /// Maximum number of concurrent connections to allow + #[clap(long = "connection-limit")] + connection_limit: Option, +} + +fn main() { + tracing::subscriber::set_global_default( + tracing_subscriber::FmtSubscriber::builder() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .finish(), + ) + .unwrap(); + let opt = Opt::parse(); + let code = { + if let Err(e) = run(opt) { + eprintln!("ERROR: {e}"); + 1 + } else { + 0 + } + }; + ::std::process::exit(code); +} + +#[tokio::main] +async fn run(options: Opt) -> Result<()> { + let (certs, key) = if let (Some(key_path), Some(cert_path)) = (&options.key, &options.cert) { + let key = fs::read(key_path).context("failed to read private key")?; + let key = if key_path.extension().map_or(false, |x| x == "der") { + PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key)) + } else { + rustls_pemfile::private_key(&mut &*key) + .context("malformed PKCS #1 private key")? + .ok_or_else(|| anyhow::Error::msg("no private keys found"))? + }; + let cert_chain = fs::read(cert_path).context("failed to read certificate chain")?; + let cert_chain = if cert_path.extension().map_or(false, |x| x == "der") { + vec![CertificateDer::from(cert_chain)] + } else { + rustls_pemfile::certs(&mut &*cert_chain) + .collect::>() + .context("invalid PEM-encoded certificate")? + }; + + (cert_chain, key) + } else { + let dirs = directories_next::ProjectDirs::from("org", "quinn", "quinn-examples").unwrap(); + let path = dirs.data_local_dir(); + let cert_path = path.join("cert.der"); + let key_path = path.join("key.der"); + let (cert, key) = match fs::read(&cert_path).and_then(|x| Ok((x, fs::read(&key_path)?))) { + Ok((cert, key)) => ( + CertificateDer::from(cert), + PrivateKeyDer::try_from(key).map_err(anyhow::Error::msg)?, + ), + Err(ref e) if e.kind() == io::ErrorKind::NotFound => { + info!("generating self-signed certificate"); + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let key = PrivatePkcs8KeyDer::from(cert.key_pair.serialize_der()); + let cert = cert.cert.into(); + fs::create_dir_all(path).context("failed to create certificate directory")?; + fs::write(&cert_path, &cert).context("failed to write certificate")?; + fs::write(&key_path, key.secret_pkcs8_der()) + .context("failed to write private key")?; + (cert, key.into()) + } + Err(e) => { + bail!("failed to read certificate: {}", e); + } + }; + + (vec![cert], key) + }; + + let mut server_crypto = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(certs, key)?; + server_crypto.alpn_protocols = common::ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect(); + if options.keylog { + server_crypto.key_log = Arc::new(rustls::KeyLogFile::new()); + } + + let mut server_config = + quinn::ServerConfig::with_crypto(Arc::new(QuicServerConfig::try_from(server_crypto)?)); + let transport_config = Arc::get_mut(&mut server_config.transport).unwrap(); + transport_config.max_concurrent_uni_streams(0_u8.into()); + + let root = Arc::::from(options.root.clone()); + if !root.exists() { + bail!("root path does not exist"); + } + + let endpoint = quinn::Endpoint::server(server_config, options.listen)?; + eprintln!("listening on {}", endpoint.local_addr()?); + + while let Some(conn) = endpoint.accept().await { + if options + .connection_limit + .map_or(false, |n| endpoint.open_connections() >= n) + { + info!("refusing due to open connection limit"); + conn.refuse(); + } else if Some(conn.remote_address()) == options.block { + info!("refusing blocked client IP address"); + conn.refuse(); + } else if options.stateless_retry && !conn.remote_address_validated() { + info!("requiring connection to validate its address"); + conn.retry().unwrap(); + } else { + info!("accepting connection"); + let fut = handle_connection(root.clone(), conn); + tokio::spawn(async move { + if let Err(e) = fut.await { + error!("connection failed: {reason}", reason = e.to_string()) + } + }); + } + } + + Ok(()) +} + +async fn handle_connection(root: Arc, conn: quinn::Incoming) -> Result<()> { + let connection = conn.await?; + let span = info_span!( + "connection", + remote = %connection.remote_address(), + protocol = %connection + .handshake_data() + .unwrap() + .downcast::().unwrap() + .protocol + .map_or_else(|| "".into(), |x| String::from_utf8_lossy(&x).into_owned()) + ); + async { + info!("established"); + + // Each stream initiated by the client constitutes a new request. + loop { + let stream = connection.accept_bi().await; + let stream = match stream { + Err(quinn::ConnectionError::ApplicationClosed { .. }) => { + info!("connection closed"); + return Ok(()); + } + Err(e) => { + return Err(e); + } + Ok(s) => s, + }; + let fut = handle_request(root.clone(), stream); + tokio::spawn( + async move { + if let Err(e) = fut.await { + error!("failed: {reason}", reason = e.to_string()); + } + } + .instrument(info_span!("request")), + ); + } + } + .instrument(span) + .await?; + Ok(()) +} + +async fn handle_request( + root: Arc, + (mut send, mut recv): (quinn::SendStream, quinn::RecvStream), +) -> Result<()> { + let req = recv + .read_to_end(64 * 1024) + .await + .map_err(|e| anyhow!("failed reading request: {}", e))?; + let mut escaped = String::new(); + for &x in &req[..] { + let part = ascii::escape_default(x).collect::>(); + escaped.push_str(str::from_utf8(&part).unwrap()); + } + info!(content = %escaped); + // Execute the request + let resp = process_get(&root, &req).unwrap_or_else(|e| { + error!("failed: {}", e); + format!("failed to process request: {e}\n").into_bytes() + }); + // Write the response + send.write_all(&resp) + .await + .map_err(|e| anyhow!("failed to send response: {}", e))?; + // Gracefully terminate the stream + send.finish().unwrap(); + info!("complete"); + Ok(()) +} + +fn process_get(root: &Path, x: &[u8]) -> Result> { + if x.len() < 4 || &x[0..4] != b"GET " { + bail!("missing GET"); + } + if x[4..].len() < 2 || &x[x.len() - 2..] != b"\r\n" { + bail!("missing \\r\\n"); + } + let x = &x[4..x.len() - 2]; + let end = x.iter().position(|&c| c == b' ').unwrap_or(x.len()); + let path = str::from_utf8(&x[..end]).context("path is malformed UTF-8")?; + let path = Path::new(&path); + let mut real_path = PathBuf::from(root); + let mut components = path.components(); + match components.next() { + Some(path::Component::RootDir) => {} + _ => { + bail!("path must be absolute"); + } + } + for c in components { + match c { + path::Component::Normal(x) => { + real_path.push(x); + } + x => { + bail!("illegal component in path: {:?}", x); + } + } + } + let data = fs::read(&real_path).context("failed reading file")?; + Ok(data) +} diff --git a/examples/quic/single_socket.rs b/examples/quic/single_socket.rs new file mode 100644 index 0000000..54bb604 --- /dev/null +++ b/examples/quic/single_socket.rs @@ -0,0 +1,62 @@ +//! This example demonstrates how to make multiple outgoing connections on a single UDP socket. +//! +//! Checkout the `README.md` for guidance. + +use std::{error::Error, net::SocketAddr}; + +use quinn::Endpoint; + +mod common; +use common::{make_client_endpoint, make_server_endpoint}; +use rustls::pki_types::CertificateDer; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let addr1 = "127.0.0.1:5000".parse().unwrap(); + let addr2 = "127.0.0.1:5001".parse().unwrap(); + let addr3 = "127.0.0.1:5002".parse().unwrap(); + let server1_cert = run_server(addr1)?; + let server2_cert = run_server(addr2)?; + let server3_cert = run_server(addr3)?; + + let client = make_client_endpoint( + "127.0.0.1:0".parse().unwrap(), + &[&server1_cert, &server2_cert, &server3_cert], + )?; + + // connect to multiple endpoints using the same socket/endpoint + tokio::join!( + run_client(&client, addr1), + run_client(&client, addr2), + run_client(&client, addr3), + ); + + // Make sure the server has a chance to clean up + client.wait_idle().await; + + Ok(()) +} + +/// Runs a QUIC server bound to given address and returns server certificate. +fn run_server( + addr: SocketAddr, +) -> Result, Box> { + let (endpoint, server_cert) = make_server_endpoint(addr)?; + // accept a single connection + tokio::spawn(async move { + let connection = endpoint.accept().await.unwrap().await.unwrap(); + println!( + "[server] incoming connection: addr={}", + connection.remote_address() + ); + }); + + Ok(server_cert) +} + +/// Attempt QUIC connection with the given server address. +async fn run_client(endpoint: &Endpoint, server_addr: SocketAddr) { + let connect = endpoint.connect(server_addr, "localhost").unwrap(); + let connection = connect.await.unwrap(); + println!("[client] connected: addr={}", connection.remote_address()); +}