From e8b48992d581572d25db13a8ea937deafb2b88f7 Mon Sep 17 00:00:00 2001 From: Ross Younger Date: Fri, 25 Oct 2024 21:19:19 +1300 Subject: [PATCH] Initial docs setup * enable docs lints, add to CI; add cargo doc task * Add readme and top level docs * tidy module structure and rustdocs --- .github/FUNDING.yml | 4 ++ .github/workflows/ci.yml | 1 + .vscode/tasks.json | 17 ++++++ Cargo.toml | 12 +++- README.md | 110 +++++++++++++++++++++++++++++++++++++ schema/session.capnp | 3 +- src/cli/args.rs | 2 +- src/cli/cli_main.rs | 2 + src/client/args.rs | 8 +-- src/client/control.rs | 20 +++---- src/client/job.rs | 6 +- src/client/main_loop.rs | 17 +++--- src/client/mod.rs | 15 +++-- src/doc/mod.rs | 4 ++ src/doc/performance.rs | 56 +++++++++++++++++++ src/doc/troubleshooting.rs | 55 +++++++++++++++++++ src/lib.rs | 58 +++++++++++++++++-- src/os/mod.rs | 8 +-- src/os/unix.rs | 2 +- src/protocol/control.rs | 78 +++++++++----------------- src/protocol/mod.rs | 110 ++++++++++++++++++++++++++++++++++++- src/protocol/session.rs | 50 +++++++++++++---- src/server.rs | 6 +- src/util/cert.rs | 2 +- src/util/dns.rs | 1 + src/util/mod.rs | 4 +- src/util/stats.rs | 2 +- src/util/tracing.rs | 5 +- 28 files changed, 544 insertions(+), 114 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 .vscode/tasks.json create mode 100644 README.md create mode 100644 src/doc/mod.rs create mode 100644 src/doc/performance.rs create mode 100644 src/doc/troubleshooting.rs diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..e1868cc --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +github: crazyscot +ko_fi: rossyounger +buy_me_a_coffee: rossyounger +# custom: [] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84bfbef..e6b1228 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,3 +52,4 @@ jobs: # We care that the benchmarks build and run, not about their numeric output. # To keep the CI a bit leaner, do this in the dev profile. - run: cargo build --locked --all-targets + - run: cargo doc --document-private-items diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..2049b01 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "cargo", + "command": "doc", + "problemMatcher": [ + "$rustc" + ], + "group": { + "kind": "build", + "isDefault": false + }, + "label": "rust: cargo doc" + } + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 809c939..ef9aaeb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "qcp" -description = "A remote file copy utility like scp, which uses the QUIC protocol over UDP" +description = "Secure remote file copy utility which uses the QUIC protocol over UDP" rust-version = "1.81.0" resolver = "2" version = "0.1.0" @@ -67,3 +67,13 @@ variant_size_differences = "deny" [lints.clippy] pedantic = { level = "deny", priority = -1 } missing_errors_doc = "allow" + +[lints.rustdoc] +bare_urls = "deny" +broken_intra_doc_links = "deny" +invalid_codeblock_attributes = "deny" +invalid_html_tags = "deny" +invalid_rust_codeblocks = "deny" +missing_crate_level_docs = "deny" +private_intra_doc_links = "deny" +unescaped_backticks = "deny" diff --git a/README.md b/README.md new file mode 100644 index 0000000..311b0f3 --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ +The Quic Copier (`qcp`) is an experimental +high-performance remote file copy utility for long-distance internet connections. + +[//]: # (TODO: Badges, after first publication. Crates, docs, ci, license.) + +## 📋 Features + +- 🔧 Drop-in replacement for `scp` or `rcp` +- 🛡️ Similar security to `scp`, using existing, well-known mechanisms +- 🚀 Better throughput on congested networks + +#### Platform support status + +- Well tested: Debian and Ubuntu using OpenSSH +- Tested: Ubuntu on WSL +- Untested: OSX/BSD family +- Not currently supported: Windows + +## 🧰 Getting Started + +* You must have ssh access to the target machine. +* Install the `qcp` binary on both machines. It needs to be in your `PATH` on the remote machine. +* Run `qcp --help-buffers` and follow its instructions. + +Install it from crates.io using `cargo`: + +```bash +cargo install qcp +``` + +Or, clone the repo and build it manually: + +```bash +git clone https://github.com/crazyscot/qcp +cd qcp +cargo build --release --locked +``` + +#### If you are new to Rust and don't have the tools installed + +* Install the `rustup` tool via your package manager, or see [Rust installation](https://www.rust-lang.org/tools/install) +* `rustup toolchain install stable` +* Proceed as above + +## ⚙️ Usage + +The basic syntax is the same as scp or rcp. + +You can run the program like this: + +```bash +$ qcp my-server:/tmp/testfile /tmp/ +⠂ Transferring data, instant rate: 2.1MB/s +testfile ████████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░ 1s @ 6.71 MB/s [60%/10.49 MB] +``` + +The program uses ssh to connect to the target machine and run `qcp --server`. ssh will check the remote host key and prompt you for a password or passphrase in the usual way. + +The default options are for a 100Mbit connection, with 300ms round-trip time to the target server. + +You may care to set the options for your internet connection. For example, if you have 300Mbit/s (37.5MB/s) download and 100Mbit/s (12.5MB/s) upload: + +```bash +qcp my-server:/tmp/testfile /tmp/ --tx 12M --rx 37M +``` + +[//]: # (TODO: link to crates.rs: Performance is a tricky subject, more fully discussed in ...) + +## ⚖️ License + +The initial release is made under the [GNU Affero General Public License](LICENSE). + +## 🧑‍🏭 Contributing + +Feel free to report bugs via the [bug tracker]. + +I'd particularly welcome performance reports from BSD/OSX users as that's not a platform I use regularly. + +While suggestions and feature requests are welcome, please be aware that I mostly work on this project in my own time. + +## 💸 Supporting the project + +If you find this software useful and would like to support me, please consider [buying me a coffee] (or via [ko-fi]). + +If you're a business and need a formal invoice for your accountant, my freelancing company can issue the paperwork. For this, and any other commercial enquiries (alternative licensing, support, sponsoring features) please get in touch. + +Please also consider supporting the galaxy of projects this work builds upon. +Most notably, [Quinn] is a pure-Rust implementation of the [QUIC] protocol, without which qcp simply wouldn't exist in its current form. + +### 💡 Roadmap + +Some ideas for the future, in no particular order: + +* A local config mechanism, so you don't have to type out the network parameters every time +* Support for copying multiple files (e.g. shell globs or `scp -r`) +* Windows native support, at least for client mode +* Firewall/NAT traversal +* Interactive file transfer (akin to `ftp`) +* Smart file copy using the `rsync` protocol or similar (send only the sections you need to) +* Graphical interface for ftp mode +* Review the protocol and perhaps pivot to using capnp RPC +* Bind a daemon to a fixed port, for better firewall/NAT traversal properties but at the cost of having to implement user authentication. +* _The same thing we do every night, Pinky. We try to take over the world!_ + +[bug tracker]: https://github.com/crazyscot/qcp/issues +[quic]: https://quicwg.github.io/ +[Quinn]: https://opencollective.com/quinn-rs +[rfc9000]: https://www.rfc-editor.org/rfc/rfc9000.html +[buying me a coffee]: https://buymeacoffee.com/rossyounger +[ko-fi]: https://ko-fi.com/rossyounger diff --git a/schema/session.capnp b/schema/session.capnp index 4f2baeb..32b6a2d 100644 --- a/schema/session.capnp +++ b/schema/session.capnp @@ -21,8 +21,7 @@ struct Command { # C->S: FileHeader, file data, FileTrailer # S->C: Response (showing transfer status) # Then close the stream. - # If the server needs to abort the transfer: - # S->C: Status (explaining why), then close the stream. + # If the server needs to abort the transfer, it may send a Response explaining why, then close the stream. } struct GetCmdArgs { diff --git a/src/cli/args.rs b/src/cli/args.rs index 4bc359e..1ae6b1e 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -44,7 +44,7 @@ pub(crate) struct CliArgs { // CLIENT-ONLY OPTIONS ================================================================= #[command(flatten)] - pub client: crate::client::args::ClientOptions, + pub client: crate::client::Options, // NETWORK OPTIONS ===================================================================== #[command(flatten)] diff --git a/src/cli/cli_main.rs b/src/cli/cli_main.rs index 0914889..56dda72 100644 --- a/src/cli/cli_main.rs +++ b/src/cli/cli_main.rs @@ -18,6 +18,8 @@ use tracing::error_span; /// Main CLI entrypoint /// /// Call this from `main`. It reads argv. +/// # Exit status +/// 0 indicates success; non-zero indicates failure. #[tokio::main(flavor = "current_thread")] #[allow(clippy::missing_panics_doc)] pub async fn cli() -> anyhow::Result { diff --git a/src/client/args.rs b/src/client/args.rs index 7cdabe6..6b14ccd 100644 --- a/src/client/args.rs +++ b/src/client/args.rs @@ -1,4 +1,4 @@ -//! qcp Client parameters +//! Options specific to qcp client-mode // (c) 2024 Ross Younger use clap::Parser; @@ -7,10 +7,10 @@ use crate::{protocol::control::ConnectionType, util::PortRange}; use super::job::FileSpec; -/// Options for client mode +/// Options specific to qcp client mode #[derive(Debug, Parser, Clone)] #[allow(clippy::struct_excessive_bools)] -pub struct ClientOptions { +pub struct Options { /// Quiet mode /// /// Switches off progress display and statistics; reports only errors @@ -89,7 +89,7 @@ pub struct ClientOptions { pub destination: Option, } -impl ClientOptions { +impl Options { pub(crate) fn address_family(&self) -> Option { if self.ipv4 { Some(ConnectionType::Ipv4) diff --git a/src/client/control.rs b/src/client/control.rs index 25f1a11..4081051 100644 --- a/src/client/control.rs +++ b/src/client/control.rs @@ -14,19 +14,18 @@ use tracing::{debug, trace, warn}; use crate::{ protocol::control::{ClientMessage, ClosedownReport, ConnectionType, ServerMessage, BANNER}, transport::{BandwidthParams, QuicParams}, - util::cert::Credentials, + util::Credentials, }; -use super::args::ClientOptions; +use super::args::Options; /// Control channel abstraction #[derive(Debug)] -#[allow(clippy::module_name_repetitions)] -pub struct ControlChannel { +pub struct Channel { process: tokio::process::Child, } -impl ControlChannel { +impl Channel { /// A reasonably controlled shutdown. /// (If you want to be rough, simply drop the `ControlChannel`.) pub async fn close(&mut self) -> Result<()> { @@ -36,14 +35,14 @@ impl ControlChannel { } /// Opens the control channel, checks the banner, sends the Client Message, reads the Server Message. - pub(crate) async fn transact( + pub async fn transact( credentials: &Credentials, server_address: IpAddr, display: &MultiProgress, - client: &ClientOptions, + client: &Options, bandwidth: BandwidthParams, quic: QuicParams, - ) -> Result<(ControlChannel, ServerMessage)> { + ) -> Result<(Channel, ServerMessage)> { trace!("opening control channel"); let mut new1 = Self::launch(display, client, bandwidth, quic)?; new1.wait_for_banner().await?; @@ -79,7 +78,7 @@ impl ControlChannel { /// This is effectively a constructor. At present, it launches a subprocess. fn launch( display: &MultiProgress, - client: &ClientOptions, + client: &Options, bandwidth: BandwidthParams, quic: QuicParams, ) -> Result { @@ -181,7 +180,8 @@ impl ControlChannel { Ok(()) } - pub(crate) async fn read_closedown_report(&mut self) -> Result { + /// Retrieves the closedown report + pub async fn read_closedown_report(&mut self) -> Result { let pipe = self .process .stdout diff --git a/src/client/job.rs b/src/client/job.rs index e1d1459..089be1b 100644 --- a/src/client/job.rs +++ b/src/client/job.rs @@ -5,7 +5,7 @@ use std::str::FromStr; use crate::transport::ThroughputMode; -use super::args::ClientOptions; +use super::args::Options; /// A file source or destination specified by the user #[derive(Debug, Clone, Default)] @@ -72,10 +72,10 @@ impl CopyJobSpec { } } -impl TryFrom<&ClientOptions> for CopyJobSpec { +impl TryFrom<&Options> for CopyJobSpec { type Error = anyhow::Error; - fn try_from(args: &ClientOptions) -> Result { + fn try_from(args: &Options) -> Result { let source = args .source .as_ref() diff --git a/src/client/main_loop.rs b/src/client/main_loop.rs index 694713c..0e94b6f 100644 --- a/src/client/main_loop.rs +++ b/src/client/main_loop.rs @@ -1,15 +1,14 @@ // qcp client event loop // (c) 2024 Ross Younger -use crate::client::control::ControlChannel; +use crate::client::control::Channel; use crate::protocol::session::session_capnp::Status; use crate::protocol::session::{FileHeader, FileTrailer, Response}; use crate::protocol::{RawStreamPair, StreamPair}; use crate::transport::{BandwidthParams, QuicParams, ThroughputMode}; -use crate::util::cert::Credentials; -use crate::util::time::Stopwatch; -use crate::util::PortRange; -use crate::util::{self, lookup_host_by_family, time::StopwatchChain}; +use crate::util::{ + self, lookup_host_by_family, time::Stopwatch, time::StopwatchChain, Credentials, PortRange, +}; use anyhow::{Context, Result}; use futures_util::TryFutureExt as _; @@ -26,16 +25,16 @@ use tokio::time::Instant; use tokio::{self, io::AsyncReadExt, time::timeout, time::Duration}; use tracing::{debug, error, info, span, trace, trace_span, warn, Instrument as _, Level}; -use super::args::ClientOptions; +use super::args::Options; use super::job::CopyJobSpec; const SHOW_TIME: &str = "file transfer"; -/// Main CLI entrypoint +/// Main client mode event loop // Caution: As we are using ProgressBar, anything to be printed to console should use progress.println() ! #[allow(clippy::module_name_repetitions)] pub async fn client_main( - options: ClientOptions, + options: Options, bandwidth: BandwidthParams, quic: QuicParams, display: MultiProgress, @@ -52,7 +51,7 @@ pub async fn client_main( // Control channel --------------- timers.next("control channel"); - let (mut control, server_message) = ControlChannel::transact( + let (mut control, server_message) = Channel::transact( &credentials, server_address, &display, diff --git a/src/client/mod.rs b/src/client/mod.rs index 0368d8b..577e3b9 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,8 +1,15 @@ -//! qcp client main loop +//! client-side (_initiator_) main loop and supporting structures + +mod args; +pub use args::Options; + +mod control; +pub use control::Channel; + +mod job; +pub use job::CopyJobSpec; +pub use job::FileSpec; -pub mod args; -pub mod control; -pub mod job; mod main_loop; mod meter; mod progress; diff --git a/src/doc/mod.rs b/src/doc/mod.rs new file mode 100644 index 0000000..9294813 --- /dev/null +++ b/src/doc/mod.rs @@ -0,0 +1,4 @@ +//! 📖 Additional documentation + +pub mod performance; +pub mod troubleshooting; diff --git a/src/doc/performance.rs b/src/doc/performance.rs new file mode 100644 index 0000000..2171a52 --- /dev/null +++ b/src/doc/performance.rs @@ -0,0 +1,56 @@ +// (c) 2024 Ross Younger + +//! # 🚀 Performance tuning +//! +//! There's probably a whole book to be written about this. +//! +//! I've spent some time tuning this for my use case and leave some hooks so you can experiment. +//! +//! **It is critical to understand that the Internet is a strange place with many variables, which will likely confound any experiment you may try.** +//! +//! In my experience, long-distance traffic flows vary wildly from second to second. +//! This is why I added a near-instant (last 1s) bandwidth readout, as well the average. +//! +//! I found that the throughput from my build server (data flow from Europe to NZ) is sometimes very +//! fast, able to saturate my 300Mbit last-mile downlink, and sometimes falls back to hundreds of +//! kilobits or even worse. But the way QUIC applies congestion control worked around this really well. +//! Throughput accelerates rapidly when congestion clears; I _think_ this is subjectively much faster than scp does, but I've not yet gathered the data to do a proper statistical analysis. +//! +//! Yes, it's inefficient to do an ssh handshake and then a QUIC/TLS handshake. +//! But the cost of doing so isn't much in absolute terms (sometimes a few seconds), +//! and this amortises nicely over a large file transfer. +//! +//! ### Tips +//! +//! * When qcp tells you to set up the kernel UDP buffers, do so; they really make a difference. **You need to do this on both machines.** +//! * Run `qcp -h` and study the network tuning options available to you. +//! * With bandwidth and RTT - at least on my network conditions - I've found that perfect accuracy of configuration isn't so important, as long as it's in the right ballpark. +//! * In many cases your _last-mile_ bandwidth, i.e. whatever you're paying your ISP for, +//! is a good setting to use. +//! But there's a trap here: ISPs often describe their packages in bits per second, +//! but qcp expects a configuration in bytes! +//! * Try out `--congestion bbr` if you like. Sometimes it helps. +//! But understand that it remains experimental, and it does send out a lot more packets. +//! _If you have a metered connection, this may be an issue!_ +//! [More about BBR](https://github.com/google/bbr/blob/master/Documentation/bbr-faq.md). +//! * Mess with the initial congestion window if you like, but I didn't find it reliably useful. +//! * Watch out for either end becoming CPU bound. One of my test machines on my local LAN was unable to move more than 7MB/s. It turned out that its CPU was so old it didn't have a useful crypto accelerator. If that applies to you, unfortunately you're not going to be able to move data any faster without a hardware upgrade. +//! * If you want to copy multiple files to/from the same remote machine, ssh connection multiplexing will save you a few seconds for each. (You can visualise the difference with the `--profile` option.) +//! * The `--debug` option will report additional information that might help you diagnose configuration issues. +//! +//! qcp will report the number of congestion events it detected, unless you run in `-q` mode. +//! +//! You might find it useful to run in `--stats` mode for additional insights; here's the output from a typical run: +//! +//! ```log +//! 2024-10-14T09:20:52.543540Z INFO Transferred 104.9MB in 12.75s; average 8.2MB/s +//! 2024-10-14T09:20:52.543782Z INFO Total packets sent: 3,279 by us; 75,861 by remote +//! 2024-10-14T09:20:52.543955Z WARN Congestion events detected: 2 +//! 2024-10-14T09:20:52.544138Z WARN Remote lost packets: 112/75.9k (0.15%, for 157kB) +//! 2024-10-14T09:20:52.544320Z INFO Path MTU 1452, round-trip time 303.1ms, final congestion window 15,537,114 +//! 2024-10-14T09:20:52.544530Z INFO 3.3k datagrams sent, 75.7k received, 0 black holes detected +//! 2024-10-14T09:20:52.544715Z INFO 107,526,015 total bytes sent for 104,857,600 bytes payload (2.54% overhead/loss) +//! 2024-10-14T09:20:52.544903Z WARN Measured path RTT 303.128843ms was greater than configuration 300; for better performance, next time try --rtt 304 +//! ``` +//! +//! (This was with a 100MB test file, which isn't always enough for the protocol to get fully up to speed.) diff --git a/src/doc/troubleshooting.rs b/src/doc/troubleshooting.rs new file mode 100644 index 0000000..49f50af --- /dev/null +++ b/src/doc/troubleshooting.rs @@ -0,0 +1,55 @@ +// (c) 2024 Ross Younger + +//! 🕵️ Troubleshooting +//! +//! ## General +//! +//! The `--debug` and `--remote-debug` options report information that may help you diagnose issues. +//! +//! This program also understands the `RUST_LOG` environment variable which might let you probe deeper. +//! Some possible settings for this variable are: +//! +//! * `qcp=trace` outputs tracing-level output from this crate +//! * `trace` sets all the Rust components to trace mode, which includes an _awful lot_ of output from quinn (the QUIC implementation). +//! +//! Note that this variable setting applies to the local machine, not the remote. If you arrange to set it on the remote, the output will come back over the ssh channel; **this may impact performance**. +//! +//! ### You can't ssh to the remote machine +//! +//! Sorry, that's a prerequisite. Get that working first, then come back to qcp. +//! +//! qcp calls ssh directly; ssh will prompt you for a password and may invite you to verify the remote host key. +//! +//! ### The QUIC connection times out +//! +//! * Does the remote host firewall inbound UDP connections? +//! If so, you will need to allocate and open up a small range of inbound ports for use by qcp. +//! Use the `--remote-port` option to tell it which. +//! * Is the remote host behind NAT? Sorry, NAT traversal is not currently supported. +//! At best, you might be able to open up a small range of UDP ports on the NAT gateway which are directly forwarded to the target machine. +//! Use the `--remote-port` option to tell it which. +//! * Are outbound UDP packets from the initiator firewalled? +//! You will need to open up some outbound ports; use the `--port` option to tell qcp which. +//! +//! ### Performance is poor? +//! +//! (This became a separate doc. See [performance](super::performance).) +//! +//! ### Excess bandwidth usage +//! +//! This utility is designed to soak up all the bandwidth it can. +//! +//! When there is little packet loss, the overhead is minimal (2-3%). However when packets do go astray, the retransmits can add up. If you use the BBR congestion controller, this will add up much faster as it tries to keep the pipe fuller; I've seen it report over 20% packet loss. +//! +//! If you're on 95th percentile billing, you may need to take this into account. But if you are on that sort of deal, you are hopefully already spending time to understand and optimise your traffic profile. +//! +//! ### Using qcp interferes with video calls / Netflix / VOIP / etc +//! +//! This utility is designed to soak up all the bandwidth it can. +//! +//! QUIC packets are UDP over IP, the same underlying protocol used for streaming video, calls and so forth. +//! They are quite literally competing with any A/V data you may be running. +//! +//! If this bothers you, you might want to look into setting up QoS on your router. +//! +//! There might be some mileage in having qcp try to limit its bandwidth use or tune it to be less aggressive in the face of congestion, but that's not implemented at the moment. diff --git a/src/lib.rs b/src/lib.rs index 426ac5b..22fa413 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,53 @@ -//! QCP client & server library // (c) 2024 Ross Younger +#![allow(clippy::doc_markdown)] +//! The Quic Copier is an experimental high-performance remote file copy utility, +//! intended for long-distance internet connections. +//! +//! ## Overview +//! - 🔧 Drop-in replacement for `scp` or `rcp` +//! - 🛡️ Similar security to `scp`, using well-known and trustworthy mechanisms +//! - User authentication uses `ssh` to establish a control channel and exchange TLS certificates. No PKI is necessary. +//! - Data in transit is protected by TLS, with strict certificate checks in both directions +//! - 🚀 Better throughput on congested networks +//! - Data is transported using the [QUIC](https://quicwg.github.io/) protocol over UDP +//! - Tunable network properties +//! +//! ### Use case +//! +//! I was inspired to write this when I needed to copy a load of multi-GB files from a server on the other side of the planet. +//! +//! If you want to copy **large** files (tens of MB or more), +//! from _point to point_ over a _long, fat, congested pipe_, +//! then you may find it useful too. +//! +//! #### Limitations +//! - You must be able to ssh directly to the remote machine, and exchange UDP packets with it on a given port. (If the local machine is behind connection-tracking NAT, things work just fine. This is the case for the vast majority of home and business network connections.) +//! - Network security systems can't readily identify QUIC traffic as such. It's opaque, and high bandwidth. Some security systems might flag it as a potential threat. +//! +//! #### What qcp is not +//! +//! * A way to serve files to the public (Use http3.) +//! * A way to speed up downloads from sites you do not control (It's up to whoever runs those sites to install http3 or set up a [CDN].) +//! * Peer to peer file transfer (Use [BitTorrent]?) +//! * An improvement for interactive shells (Use [mosh].) +//! * Delta-based copying (Use [rsync].) +//! +//! ## How it works 📖 +//! +//! See [protocol]. +//! +//! ## Getting the best out of qcp +//! +//! See [performance](doc::performance) and [troubleshooting](doc::troubleshooting). +//! +//! [QUIC]: https://quicwg.github.io/ +//! [ssh]: https://en.wikipedia.org/wiki/Secure_Shell +//! [CDN]: https://en.wikipedia.org/wiki/Content_delivery_network +//! [BitTorrent]: https://en.wikipedia.org/wiki/BitTorrent +//! [rsync]: https://en.wikipedia.org/wiki/Rsync +//! [mosh]: https://mosh.org/ + mod cli; pub use cli::cli; // needs to be re-exported for the binary crate @@ -10,9 +57,12 @@ pub mod server; pub mod transport; pub mod util; -mod os; +pub mod doc; + +pub mod os; -/// Build-time info (from `built`) -pub mod build_info { +/// Build-time info (autogenerated by `built`) +#[allow(unreachable_pub)] +mod build_info { include!(concat!(env!("OUT_DIR"), "/built.rs")); } diff --git a/src/os/mod.rs b/src/os/mod.rs index 1fe360c..97b93ee 100644 --- a/src/os/mod.rs +++ b/src/os/mod.rs @@ -1,10 +1,10 @@ -// OS abstraction layer for qcp +//! OS abstraction layer // (c) 2024 Ross Younger use anyhow::Result; /// OS abstraction trait providing access to socket options -pub(crate) trait SocketOptions { +pub trait SocketOptions { /// Wrapper for getsockopt `SO_SNDBUF`. /// On Linux, this call halves the number returned from the kernel. /// This takes account of kernel behaviour: the internal buffer @@ -28,10 +28,10 @@ pub(crate) trait SocketOptions { fn force_recvbuf(&mut self, size: usize) -> Result<()>; } -#[cfg(unix)] +#[cfg(any(unix, doc))] mod unix; -#[cfg(unix)] +#[cfg(any(unix, doc))] pub(crate) use unix::*; static_assertions::assert_cfg!(unix, "This OS is not yet supported"); diff --git a/src/os/unix.rs b/src/os/unix.rs index 702d629..85ac7a3 100644 --- a/src/os/unix.rs +++ b/src/os/unix.rs @@ -58,7 +58,7 @@ pub(crate) fn print_udp_buffer_size_help_message(rmem: u64, wmem: u64) { println!( r#"For best performance, it is necessary to set the kernel UDP buffer size limits. This program attempts to automatically set buffer sizes for itself, -but this requires elevated privileges."# +but doing so requires elevated privileges."# ); if bsdish() { diff --git a/src/protocol/control.rs b/src/protocol/control.rs index 940c1ac..088d3b8 100644 --- a/src/protocol/control.rs +++ b/src/protocol/control.rs @@ -1,26 +1,26 @@ -//! QCP control protocol definitions and helper types +//! Control protocol definitions and helper types // (c) 2024 Ross Younger - -/* - * The control protocol is data passed between the local qcp client process and the remote qcp server process - * before establishing the QUIC connection. - * The two processes are usually connected by stdio, via ssh. - * - * The protocol looks like this: - * (Client creates remote process, which we call the Server) - * Server -> Client: Banner - * C -> S: `ClientMessage` - * S -> C: `ServerMessage` - * The client then establishes a QUIC connection to the server, on the port given in the `ServerMessage`. - * The client then opens one or more bidirectional QUIC streams ('sessions') on that connection. - * See the session protocol for what happens there. - * When all streams are finished: - * C -> S: `Closedown` - * S -> C: `ClosedownReport` - * C -> S: (closes control channel; server process exits) - * - * On the wire these messages are sent using standard capnproto framing. - */ +//! +//! The control protocol consists data passed between the local qcp client process and the remote qcp server process +//! before establishing the [QUIC] connection. +//! The two processes are connected via ssh. +//! +//! The control protocol looks like this: +//! * Server ➡️ Client: Banner +//! * C ➡️ S: [`ClientMessage`] +//! * S ➡️ C: [`ServerMessage`] +//! * Client establishes a QUIC connection to the server, on the port given in the [`ServerMessage`]. +//! * Client then opens one or more bidirectional QUIC streams ('sessions') on that connection. +//! (See the session protocol for what happens there.) +//! +//! When transfer is complete and all QUIC streams are closed: +//! * S ➡️ C: [`ClosedownReport`] +//! * C ➡️ S: (closes control channel; server takes this as a cue to exit) +//! +//! On the wire these are [CapnProto] messages, sent using standard framing. +//! +//! [quic]: https://quicwg.github.io/ +//! [capnproto]: https://capnproto.org/ use anyhow::Result; use capnp::message::ReaderOptions; @@ -28,7 +28,7 @@ pub use control_capnp::client_message::ConnectionType; use quinn::ConnectionStats; use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _}; -/// Low-level protocol structures and serialisation, autogenerated from session.capnp +/// Low-level protocol structures and serialisation, autogenerated from `session.capnp.` #[allow( missing_debug_implementations, single_use_lifetimes, @@ -62,7 +62,7 @@ pub mod control_capnp { /// Server banner message, sent on stdout and checked by the client pub const BANNER: &str = "qcp-server-1\n"; -/// Rust type analogue to the capnproto struct +/// Helper type for [`control_capnp::client_message`] #[derive(Debug)] #[allow(missing_docs)] pub struct ClientMessage { @@ -104,7 +104,7 @@ impl ClientMessage { } } -/// Rust type analogue to the capnproto struct +/// Helper type for [`control_capnp::server_message`] pub struct ServerMessage { /// Port the server is bound to pub port: u16, @@ -185,33 +185,7 @@ impl ServerMessage { } } -/// Helper struct (currently empty, but with methods) for capnp `Closedown` -#[derive(Clone, Copy, Debug)] -pub struct Closedown {} - -impl Closedown { - /// Serializer - pub async fn write(write: &mut W) -> Result<()> - where - W: tokio::io::AsyncWrite + Unpin, - { - let msg = ::capnp::message::Builder::new_default(); - capnp_futures::serialize::write_message(write.compat_write(), &msg).await?; - Ok(()) - } - - /// Deserializer - pub async fn read(read: &mut R) -> anyhow::Result - where - R: tokio::io::AsyncRead + Unpin, - { - let _reader = - capnp_futures::serialize::read_message(read.compat(), ReaderOptions::new()).await?; - Ok(Self {}) - } -} - -/// Helper struct for capnp `ClosedownReport` +/// Helper type for [`control_capnp::closedown_report`] #[derive(Clone, Copy, Debug)] pub struct ClosedownReport { /// Final congestion window diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index b241b42..8e91ce7 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -1,4 +1,112 @@ -//! Protocol defininitions owned by qcp +// (c) 2024 Ross Younger + +//! Protocol defininitions +//! +#![allow(clippy::doc_markdown)] +//! # The QCP protocol +//! `qcp` is a **hybrid protocol**. +//! The binary contains the complete protocol implementation, +//! but not the ssh binary used to establish the control channel itself. +//! +//! The protocol flow looks like this: +//! +//! 1. The user runs `qcp` from the a machine we will call the _initiator_ or _client_. +//! * qcp uses ssh to connect to the _remote_ machine and start a `qcp --server` process there. +//! * We call this link between the two processes the _control channel_. +//! * The _remote_ machine is also known as the _server_, in keeping with other communication protocols. +//! 1. Both sides generate ephemeral self-signed TLS certificates. +//! 1. The remote machine binds to a UDP port and sets up a [QUIC] _endpoint_. +//! 1. The two machines exchange messages over the [control] channel containing: +//! * cryptographic identities +//! * server UDP port +//! * bandwidth configuration and any resulting warning +//! 1. The initiator opens up a QUIC connection to the remote. +//! * N.B. While UDP is a connectionless protocol, QUIC provides connection semantics, with multiple bidirectional _streams_ possible on top of a connection between two endpoints.) +//! 1. For each file to be transferred in either direction, the initiator opens a QUIC _stream_ over the existing connection. +//! * We call this a _session_. +//! * The two endpoints use the [session] protocol to move data to where it needs to be. +//! 1. When all is said and done, the initiator closes the control channel. This leads to everything being torn down. +//! +//! ## Motivation +//! +//! This protocol exists because I needed to copy multiple large (3+ GB) files from +//! a server in Europe to my home in New Zealand. +//! +//! I've got nothing against `ssh` or `scp`. They're brilliant. I've been using them since the 1990s. +//! However they run on top of [TCP], which does not perform very well when the network is congested. +//! With a fast fibre internet connection, a long round-trip time and noticeable packet +//! loss, I was right in the sour spot. +//! TCP did its thing and slowed down, but when the congestion cleared it was very slow to +//! get back up to speed. +//! +//! If you've ever been frustrated by download performance from distant websites, +//! you might have been experiencing this same issue. +//! Friends with satellite (pre-Starlink) internet connections seem to be particularly badly affected. +//! +//! ## Security design 🛡️ +//! +//! The security goals for this project are fairly straightforward: +//! +//! - Only authenticated users can transfer files to/from a system +//! - Data in transit should be kept confidential, with its authenticity and integrity protected; all of this by well-known, reputable cryptographic algorithms +//! - **Security of data at rest at either end is out of scope**, save for the obvious requirement that the copied file be put where the user wanted us to put it +//! - _I do not want to write my own cryptography or user authentication_ +//! - _I do not want to rely on PKI if I can help it_ +//! +//! [ssh] includes a perfectly serviceable, well understood and battle-tested user authentication system. +//! Sysadmins can set their own policies regarding password, cryptographic or other authentication methods. +//! +//! +//! [QUIC] traffic is protected by [TLS]. In many cases, a QUIC server would have a TLS certificate +//! signed by a [CA] in the same way as a website. +//! +//! However, I wanted bidirectional endpoint authentication. I also didn't want the hassle of setting +//! up and maintaining certificates at both ends. ([LetsEncrypt] is great for many things, +//! but not so useful in this case; I don't want to run a web server on my home net connection.) +//! +//! After some thought I realised that the solution lay in a hybrid, bootstrapping protocol. +//! * Each endpoint generates a fresh, ephemeral TLS key every time. +//! * With ssh connecting the two endpoints, we have an easy way to ensure that TLS +//! credentials genuinely belong to the other end. +//! +//! ### Results +//! +//! The endpoints will only establish a connection: +//! +//! * to one specific TLS instance; +//! * identified by a self-signed certificate that it just received over the control channel, which is assumed secure; +//! * confirmed by use of a private key that only the other endpoint knows (having just generated it). +//! +//! Therefore, data remains secure in transit provided: +//! +//! * the ssh and TLS protocols themselves have not been compromised +//! * your credentials to log in to the remote machine have not been compromised +//! * the random number generators on both endpoints are of sufficient quality +//! * nobody has perpetrated a software supply chain attack on qcp, ssh, or any of the myriad components they depend on +//! +//! ## Prior Art +//! +//! * [FASP](https://en.wikipedia.org/wiki/Fast_and_Secure_Protocol) is a high-speed data transfer protocol that runs on UDP. +//! It is proprietary and patented; the patents are held by [Aspera](http://ibm.com/aspera/) which was acquired by IBM. +//! * [QUIC] was invented by a team at Google in 2012, and adopted as a standard by the IETF in 2016. +//! The idea is simple: your data travels over UDP instead of TCP. +//! * Obviously, you lose the benefits of TCP (reliability, packet sequencing, flow control), so you have to reimplement those. +//! While TCP is somewhat ossified, the team behind QUIC picked and chose the best bits and changed its shape. +//! * [quinn](https://docs.rs/quinn/latest/quinn/), a Rust implementation of QUIC +//! * [quicfiletransfer](https://github.com/sirgallo/quicfiletransfer) uses [QUIC] to transfer files, but without an automated control channel. +//! +//! ## See Also +//! * [RFC 9000 "QUIC: A UDP-Based Multiplexed and Secure Transport"](https://www.rfc-editor.org/rfc/rfc9000.html) +//! * [RFC 9001 "Using TLS to Secure QUIC"](https://www.rfc-editor.org/rfc/rfc9001.html) +//! * [RFC 9002 "QUIC Loss Detection and Congestion Control"](https://www.rfc-editor.org/rfc/rfc9002.html) +//! * [quinn comparison of TCP, UDP and QUIC](https://quinn-rs.github.io/quinn/) +//! +//! [QUIC]: +//! [ssh]: +//! [TCP]: +//! [TLS]: +//! [CA]: +//! [LetsEncrypt]: pub mod control; pub mod session; diff --git a/src/protocol/session.rs b/src/protocol/session.rs index 8bdcf41..395d965 100644 --- a/src/protocol/session.rs +++ b/src/protocol/session.rs @@ -1,16 +1,42 @@ //! QCP session protocol definitions and helper types // (c) 2024 Ross Younger - -/* - * The session protocol frames a QUIC (Quinn) bidirectional stream. - * The protocol consists of Command and Response packets defined in schema/session.capnp. - * Packets are sent using the standard capnp framing. - * - * Client -> Server: - * C -> S : Command packet - * S -> C : Response packet - * (Then they do whatever is appropriate for the command. See the notes in session.capnp.) - */ +//! +//! The session protocol operates over a QUIC bidirectional stream. +//! +//! The protocol consists of [Command] and [Response] packets and helper structs. +//! Packets are sent using the standard CapnProto framing. +//! +//! * Client ➡️ Server: (initiates QUIC stream) +//! * C ➡️ S : [Command] packet. This is an enum containing arguments needed by the selected command. +//! * S ➡️ C : [Response] packet +//! * Then they do whatever is appropriate for the command. +//! +//! The following commands are defined: +//! ### Get +//! +//! Retrieves a file from the remote. +//! * C ➡️ S: [GetArgs] _(within [Command])_ +//! * S ➡️ C: [Response] . If the status within was not OK, the command does not proceed. +//! * S ➡️ C: [FileHeader], file data, [FileTrailer]. +//! +//! After transfer, close the stream. +//! +//! Either side may close the stream mid-flow if it needs to abort the transfer. +//! +//! ### Put +//! +//! Sends a file to the remote. +//! * C ➡️ S: [PutArgs] _(within [Command])_ +//! * S ➡️ C: [Response] to the command +//! * C ➡️ S: [FileHeader], file data, [FileTrailer]. +//! * S ➡️ C: [Response] indicating transfer status +//! +//! After transfer, close the stream. +//! +//! If the server needs to abort the transfer mid-flow, it may send a Response explaining why, then close the stream. +//! +//! [quic]: https://quicwg.github.io/ +//! [capnproto]: https://capnproto.org/ /// Low-level protocol structures and serialisation, autogenerated from session.capnp #[allow( @@ -46,11 +72,13 @@ pub enum Command { Put(PutArgs), } #[derive(Debug)] +/// Arguments for [Command::Get] #[allow(missing_docs)] pub struct GetArgs { pub filename: String, } #[derive(Debug)] +/// Arguments for [Command::Put] #[allow(missing_docs)] pub struct PutArgs { pub filename: String, diff --git a/src/server.rs b/src/server.rs index 9578c48..92cdd16 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,4 +1,4 @@ -//! qcp server event loop +//! server-side _(remote)_ event loop // (c) 2024 Ross Younger use std::path::PathBuf; @@ -8,8 +8,8 @@ use crate::protocol::control::{ClientMessage, ClosedownReport, ServerMessage}; use crate::protocol::session::{session_capnp::Status, Command, FileHeader, FileTrailer, Response}; use crate::protocol::{self, StreamPair}; use crate::transport::BandwidthParams; -use crate::util::cert::Credentials; use crate::util::socket::bind_range_for_family; +use crate::util::Credentials; use crate::util::PortRange; use crate::{transport, util}; @@ -25,7 +25,7 @@ use tokio::task::JoinSet; use tokio::time::timeout; use tracing::{debug, error, info, trace, trace_span, warn, Instrument}; -/// Server main loop +/// Server event loop #[allow(clippy::module_name_repetitions)] pub async fn server_main( bandwidth: crate::transport::BandwidthParams, diff --git a/src/util/cert.rs b/src/util/cert.rs index 545e7bd..49c9d73 100644 --- a/src/util/cert.rs +++ b/src/util/cert.rs @@ -4,7 +4,7 @@ use anyhow::Result; use rustls_pki_types::{CertificateDer, PrivateKeyDer}; -/// In-memory represenatation of X509 credentials (for TLS) +/// In-memory representation of X509 credentials (for TLS) #[derive(Debug)] pub struct Credentials { /// X509 certificate diff --git a/src/util/dns.rs b/src/util/dns.rs index 9887ff2..e78545d 100644 --- a/src/util/dns.rs +++ b/src/util/dns.rs @@ -7,6 +7,7 @@ use crate::protocol::control::ConnectionType; use anyhow::Context as _; /// DNS lookup helper +/// /// Results can be restricted to a given address family. /// Only the first matching result is returned. /// If there are no matching records of the required type, returns an error. diff --git a/src/util/mod.rs b/src/util/mod.rs index 40877ff..f972f28 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -4,7 +4,9 @@ mod dns; pub use dns::lookup_host_by_family; -pub mod cert; +mod cert; +pub use cert::Credentials; + pub mod io; pub mod socket; pub mod stats; diff --git a/src/util/stats.rs b/src/util/stats.rs index f9b97ff..850035b 100644 --- a/src/util/stats.rs +++ b/src/util/stats.rs @@ -51,7 +51,7 @@ impl Display for DataRate { } /// Output the end-of-game statistics -pub(crate) fn process_statistics( +pub fn process_statistics( stats: &ConnectionStats, payload_bytes: u64, transport_time: Option, diff --git a/src/util/tracing.rs b/src/util/tracing.rs index 2218b84..403d5c5 100644 --- a/src/util/tracing.rs +++ b/src/util/tracing.rs @@ -42,10 +42,13 @@ fn filter_for(trace_level: &str, key: &str) -> anyhow::Result { } /// Set up rust tracing, to console (via an optional `MultiProgress`) and optionally to file. +/// /// By default we log only our events (qcp), at a given trace level. /// This can be overridden by setting `RUST_LOG`. +/// /// For examples, see -/// CAUTION: If this function fails, tracing won't be set up; callers must take extra care to report the error. +/// +/// **CAUTION:** If this function fails, tracing won't be set up; callers must take extra care to report the error. pub fn setup( trace_level: &str, display: Option<&MultiProgress>,