From 6c9424ab009527c23061f052583a457615b7bc9a Mon Sep 17 00:00:00 2001 From: Marcin Anforowicz Date: Sat, 8 Jun 2024 21:46:47 -0700 Subject: [PATCH] Commenting code within functions --- gday/src/dialog.rs | 40 ++++--- gday/src/main.rs | 5 +- gday_contact_exchange_protocol/src/lib.rs | 8 +- gday_encryption/benches/benchmark.rs | 13 +- gday_encryption/src/test.rs | 43 +++++-- gday_file_transfer/src/offer.rs | 6 +- gday_hole_punch/src/contact_sharer.rs | 30 ++++- gday_hole_punch/src/hole_puncher.rs | 139 +++++++++++++--------- gday_hole_punch/src/lib.rs | 51 +++----- gday_hole_punch/src/peer_code.rs | 19 +-- gday_hole_punch/src/server_connector.rs | 28 +++-- 11 files changed, 237 insertions(+), 145 deletions(-) diff --git a/gday/src/dialog.rs b/gday/src/dialog.rs index 4c56059..df2c42f 100644 --- a/gday/src/dialog.rs +++ b/gday/src/dialog.rs @@ -6,10 +6,11 @@ use owo_colors::OwoColorize; use std::io::Write; /// Confirms that the user wants to send these files. -/// If not, exits the program. -pub fn ask_send(files: &[FileMetaLocal]) -> std::io::Result<()> { +/// +/// If not, returns false. +pub fn confirm_send(files: &[FileMetaLocal]) -> std::io::Result { // print all the file names and sizes - println!("{} {}", files.len().bold(), "files to send:".bold()); + println!("{}", "Files to send:".bold()); for file in files { println!("{} ({})", file.short_path.display(), HumanBytes(file.len)); } @@ -18,7 +19,8 @@ pub fn ask_send(files: &[FileMetaLocal]) -> std::io::Result<()> { // print their total size let total_size: u64 = files.iter().map(|file| file.len).sum(); print!( - "Would you like to send these ({})? (y/n): ", + "Would you like to send these {} files ({})? (y/n): ", + files.len(), HumanBytes(total_size).bold() ); std::io::stdout().flush()?; @@ -26,10 +28,10 @@ pub fn ask_send(files: &[FileMetaLocal]) -> std::io::Result<()> { // act on user choice if "yes".starts_with(&input) { - Ok(()) + Ok(true) } else { println!("Cancelled."); - std::process::exit(0); + Ok(false) } } @@ -40,17 +42,16 @@ pub fn ask_send(files: &[FileMetaLocal]) -> std::io::Result<()> { /// - `Some(0)` represents fully accepting the file at this index, /// - `Some(x)` represents resuming with the first `x` bytes skipped. pub fn ask_receive(files: &[FileMeta]) -> Result>, gday_file_transfer::Error> { - println!( - "{} {} {}", - "Your mate wants to send you".bold(), - files.len().bold(), - "files:".bold() - ); + println!("{}", "Your mate wants to send you:".bold(),); // A response accepting files that are not already fully saved let mut new_files = Vec::>::with_capacity(files.len()); // The total size that `new_files` would download. let mut new_size = 0; + // Number of new files + let mut num_new_files = 0; + // Number of interrupted files + let mut num_interrupted = 0; // Print all the offered files. for file in files { @@ -74,11 +75,13 @@ pub fn ask_receive(files: &[FileMeta]) -> Result>, gday_file_tra ); new_size += remaining_len; new_files.push(Some(local_len)); + num_interrupted += 1; // this file does not exist } else { new_size += file.len; new_files.push(Some(0)); + num_new_files += 1; } println!(); } @@ -91,7 +94,11 @@ pub fn ask_receive(files: &[FileMeta]) -> Result>, gday_file_tra // If there are no existing/interrupted files, // send or quit. if new_size == total_size { - print!("Download all ({})? (y/n): ", HumanBytes(total_size).bold()); + print!( + "Download all {} files ({})? (y/n): ", + files.len(), + HumanBytes(total_size).bold() + ); std::io::stdout().flush()?; let input = get_lowercase_input()?; @@ -104,11 +111,14 @@ pub fn ask_receive(files: &[FileMeta]) -> Result>, gday_file_tra } println!( - "1. Fully download all files ({}).", + "1. Fully download all {} files ({}).", + files.len(), HumanBytes(total_size).bold() ); println!( - "2. Download only new files, resuming any interrupted downloads ({}).", + "2. Download only the {} new files, and resume {} interrupted downloads ({}).", + num_new_files, + num_interrupted, HumanBytes(new_size).bold() ); println!("3. Cancel."); diff --git a/gday/src/main.rs b/gday/src/main.rs index 080b498..63d999e 100644 --- a/gday/src/main.rs +++ b/gday/src/main.rs @@ -134,7 +134,10 @@ fn run(args: crate::Args) -> Result<(), Box> { let local_files = gday_file_transfer::get_file_metas(&paths)?; // confirm the user wants to send these files - dialog::ask_send(&local_files)?; + if !dialog::confirm_send(&local_files)? { + // Send aborted + return Ok(()); + } // create a room in the server let (contact_sharer, my_contact) = diff --git a/gday_contact_exchange_protocol/src/lib.rs b/gday_contact_exchange_protocol/src/lib.rs index 5710d5d..d756d94 100644 --- a/gday_contact_exchange_protocol/src/lib.rs +++ b/gday_contact_exchange_protocol/src/lib.rs @@ -228,7 +228,7 @@ impl std::fmt::Display for FullContact { } } -/// Write `msg` to `writer` using [`serde_json`]. +/// Write `msg` to `writer` using [`serde_json`], and flush. /// /// Prefixes the message with 4 big-endian bytes that hold its length. pub fn write_to(msg: impl Serialize, writer: &mut impl Write) -> Result<(), Error> { @@ -240,7 +240,7 @@ pub fn write_to(msg: impl Serialize, writer: &mut impl Write) -> Result<(), Erro Ok(()) } -/// Asynchronously write `msg` to `writer` using [`serde_json`]. +/// Asynchronously write `msg` to `writer` using [`serde_json`], and flush. /// /// Prefixes the message with a 4 big-endian bytes that hold its length. pub async fn write_to_async( @@ -255,7 +255,7 @@ pub async fn write_to_async( Ok(()) } -/// Read `msg` from `reader` using [`serde_json`]. +/// Read a message from `reader` using [`serde_json`]. /// /// Assumes the message is prefixed with 4 big-endian bytes that holds its length. pub fn read_from(reader: &mut impl Read) -> Result { @@ -268,7 +268,7 @@ pub fn read_from(reader: &mut impl Read) -> Result( diff --git a/gday_encryption/benches/benchmark.rs b/gday_encryption/benches/benchmark.rs index 83b84d8..3700e9a 100644 --- a/gday_encryption/benches/benchmark.rs +++ b/gday_encryption/benches/benchmark.rs @@ -22,8 +22,11 @@ pub fn encryption_bench(c: &mut Criterion) { c.bench_function("EncryptedStream write 200,000 bytes", |b| { b.iter(|| { - let mut encryptor: EncryptedStream<&mut [u8]> = - EncryptedStream::new(&mut encrypted_data[..], &key, &nonce); + let mut encryptor: EncryptedStream<&mut [u8]> = EncryptedStream::new( + black_box(&mut encrypted_data[..]), + black_box(&key), + black_box(&nonce), + ); EncryptedStream::write_all(black_box(&mut encryptor), black_box(&random_data)).unwrap(); EncryptedStream::flush(black_box(&mut encryptor)).unwrap(); }) @@ -53,7 +56,11 @@ pub fn decryption_bench(c: &mut Criterion) { c.bench_function("EncryptedStream read 200,000 bytes", |b| { b.iter(|| { - let mut decryptor = EncryptedStream::new(&encrypted_data[..], &key, &nonce); + let mut decryptor = EncryptedStream::new( + black_box(&encrypted_data[..]), + black_box(&key), + black_box(&nonce), + ); EncryptedStream::read_exact(black_box(&mut decryptor), black_box(&mut read_data)) .unwrap() }) diff --git a/gday_encryption/src/test.rs b/gday_encryption/src/test.rs index c8b0cc6..b0744b7 100644 --- a/gday_encryption/src/test.rs +++ b/gday_encryption/src/test.rs @@ -3,6 +3,8 @@ use std::{ io::{Read, Write}, }; +use rand::{rngs::StdRng, RngCore, SeedableRng}; + const TEST_DATA: &[&[u8]] = &[ b"abc5423gsgdds43", b"def432gfd2354", @@ -25,16 +27,27 @@ const TEST_DATA: &[&[u8]] = &[ /// Test sending and receiving many small messages. #[test] fn test_small_messages() { - let nonce: [u8; 7] = [42; 7]; - let key: [u8; 32] = [123; 32]; + // generate pseudorandom data from a seed + let mut rng = StdRng::seed_from_u64(5); + + // set up a pipe + let mut nonce: [u8; 7] = [0; 7]; + let mut key: [u8; 32] = [0; 32]; + rng.fill_bytes(&mut nonce); + rng.fill_bytes(&mut key); let mut pipe = VecDeque::new(); let mut stream = crate::EncryptedStream::new(&mut pipe, &key, &nonce); for &msg in TEST_DATA { + // write the message stream.write_all(msg).unwrap(); stream.flush().unwrap(); + + // receive the message let mut buf = vec![0; msg.len()]; stream.read_exact(&mut buf).unwrap(); + + // verify the message is correct assert_eq!(buf, msg); } } @@ -43,18 +56,32 @@ fn test_small_messages() { /// several large messages. #[test] fn test_large_messages() { - let nonce: [u8; 7] = [75; 7]; - let key: [u8; 32] = [22; 32]; - let pipe = VecDeque::new(); - let mut stream = crate::EncryptedStream::new(pipe, &key, &nonce); + // generate pseudorandom data from a seed + let mut rng = StdRng::seed_from_u64(0); - let msg = vec![123; 200_000]; + // set up a pipe + let mut nonce: [u8; 7] = [0; 7]; + let mut key: [u8; 32] = [0; 32]; + rng.fill_bytes(&mut nonce); + rng.fill_bytes(&mut key); + let mut pipe = VecDeque::new(); + let mut stream = crate::EncryptedStream::new(&mut pipe, &key, &nonce); + + let mut msg = vec![123; 200_000]; for _ in 0..5 { + // prepare a pseudorandom message to write + rng.fill_bytes(&mut msg); + + // write the message stream.write_all(&msg).unwrap(); stream.flush().unwrap(); + + // receive the message let mut received = vec![0; msg.len()]; stream.read_exact(&mut received).unwrap(); + + // verify the message is correct assert_eq!(msg, received); } } @@ -67,7 +94,7 @@ fn test_unexpected_eof() { let mut pipe = VecDeque::new(); let mut writer = crate::EncryptedStream::new(&mut pipe, &key, &nonce); - let msg = TEST_DATA[0]; + let msg = b"fjsdka;8u39fsdkaf"; writer.write_all(msg).unwrap(); writer.flush().unwrap(); diff --git a/gday_file_transfer/src/offer.rs b/gday_file_transfer/src/offer.rs index 6ecdade..3b85e61 100644 --- a/gday_file_transfer/src/offer.rs +++ b/gday_file_transfer/src/offer.rs @@ -189,7 +189,8 @@ impl FileResponseMsg { } } -/// Write `msg` to `writer` using [`serde_json`]. +/// Write `msg` to `writer` using [`serde_json`], and flush. +/// /// Prefixes the message with 4 big-endian bytes that hold its length. pub fn write_to(msg: impl Serialize, writer: &mut impl Write) -> Result<(), Error> { let vec = serde_json::to_vec(&msg)?; @@ -202,7 +203,8 @@ pub fn write_to(msg: impl Serialize, writer: &mut impl Write) -> Result<(), Erro Ok(()) } -/// Read `msg` from `reader` using [`serde_json`]. +/// Read a message from `reader` using [`serde_json`]. +/// /// Assumes the message is prefixed with 4 big-endian bytes that hold its length. pub fn read_from(reader: &mut impl Read) -> Result { let mut len = [0_u8; 4]; diff --git a/gday_hole_punch/src/contact_sharer.rs b/gday_hole_punch/src/contact_sharer.rs index ce78cf0..1db4e18 100644 --- a/gday_hole_punch/src/contact_sharer.rs +++ b/gday_hole_punch/src/contact_sharer.rs @@ -12,23 +12,28 @@ impl<'a> ContactSharer<'a> { /// Creates a new room with `room_code` in the Gday server /// that `server_connection` connects to. /// - /// Sends local socket addresses to the server + /// Sends local socket addresses to the server. + /// + /// Panics if both `v4` and `v6` in `server_connection` are `None`. /// /// Returns - /// - The [`ContactSharer`] + /// - The [`ContactSharer`]. /// - The [`FullContact`] of this endpoint, as /// determined by the server pub fn create_room( room_code: u64, server_connection: &'a mut ServerConnection, ) -> Result<(Self, FullContact), Error> { + // set reuse addr and reuse port, so that these sockets + // can be later reused for hole punching server_connection.configure()?; + // choose a stream to talk to the server with let messenger = &mut server_connection.streams()[0]; + // try creating a room in the server write_to(ClientMsg::CreateRoom { room_code }, messenger)?; let response: ServerMsg = read_from(messenger)?; - if response != ServerMsg::RoomCreated { return Err(Error::UnexpectedServerReply(response)); } @@ -39,6 +44,7 @@ impl<'a> ContactSharer<'a> { connection: server_connection, }; + // send personal socket addresses to the server let contact = this.share_contact()?; Ok((this, contact)) @@ -47,7 +53,9 @@ impl<'a> ContactSharer<'a> { /// Joins a room with `room_code` in the Gday server /// that `server_connection` connects to. /// - /// Sends local socket addresses to the server + /// Sends local socket addresses to the server. + /// + /// Panics if both `v4` and `v6` in `server_connection` are `None`. /// /// Returns /// - The [`ContactSharer`] @@ -57,6 +65,8 @@ impl<'a> ContactSharer<'a> { room_code: u64, server_connection: &'a mut ServerConnection, ) -> Result<(Self, FullContact), Error> { + // set reuse addr and reuse port, so that these sockets + // can be later reused for hole punching server_connection.configure()?; let mut this = Self { @@ -65,6 +75,7 @@ impl<'a> ContactSharer<'a> { connection: server_connection, }; + // send personal socket addresses to the server let contact = this.share_contact()?; Ok((this, contact)) @@ -74,8 +85,11 @@ impl<'a> ContactSharer<'a> { /// Sends personal contact information the the server, and /// returns it's response. fn share_contact(&mut self) -> Result { + // Get all connections to the server let mut streams = self.connection.streams(); + // For each connection, send its private socket address + // to the server for stream in &mut streams { let private_addr = Some(stream.local_addr()?); let msg = ClientMsg::SendAddr { @@ -90,15 +104,16 @@ impl<'a> ContactSharer<'a> { } } + // tell the server that we're done + // sending socket addresses let msg = ClientMsg::DoneSending { room_code: self.room_code, is_creator: self.is_creator, }; - write_to(msg, streams[0])?; + // Get our local contact info from the server let reply: ServerMsg = read_from(streams[0])?; - let ServerMsg::ClientContact(my_contact) = reply else { return Err(Error::UnexpectedServerReply(reply)); }; @@ -110,6 +125,9 @@ impl<'a> ContactSharer<'a> { /// other peer submitted. Returns the peer's [`FullContact`], as /// determined by the server pub fn get_peer_contact(self) -> Result { + // This is the same stream we used to send DoneSending, + // so the server should respond on it, + // once the other peer is also done. let stream = &mut self.connection.streams()[0]; let reply: ServerMsg = read_from(stream)?; let ServerMsg::PeerContact(peer) = reply else { diff --git a/gday_hole_punch/src/hole_puncher.rs b/gday_hole_punch/src/hole_puncher.rs index 5eacc64..ef6be48 100644 --- a/gday_hole_punch/src/hole_puncher.rs +++ b/gday_hole_punch/src/hole_puncher.rs @@ -9,95 +9,126 @@ use tokio::{ net::TcpSocket, }; +/// Alias to the return type of [`try_connect_to_peer()`]. type PeerConnection = (std::net::TcpStream, [u8; 32]); +/// How often a connection attempt is made during hole punching. const RETRY_INTERVAL: Duration = Duration::from_millis(200); -// TODO: Update all comments here! - -// TODO: ADD BETTER ERROR REPORTING. -// add a timeout. -// if fails, specify if it failed on connecting to peer, or verifying peer. - /// Tries to establish a TCP connection with the other peer by using /// [TCP hole punching](https://en.wikipedia.org/wiki/TCP_hole_punching). /// /// - `local_contact` should be the `private` field of your [`FullContact`] -/// that the [`crate::ContactSharer`] returned when you created or joined a room. +/// that [`crate::ContactSharer`] returned when you created or joined a room. +/// Panics if both `v4` and `v6` are `None`. /// - `peer_contact` should be the [`FullContact`] returned by [`crate::ContactSharer::get_peer_contact()`]. -/// - `shared_secret` should be a secret that both peers know. It will be used to verify -/// the peer's identity, and derive a stronger shared key using [SPAKE2](https://docs.rs/spake2/latest/spake2/). +/// - `shared_secret` should be a randomized secret that both peers know. +/// It will be used to verify the peer's identity, and derive a stronger shared key +/// using [SPAKE2](https://docs.rs/spake2/). +/// - Gives up after `timeout` time, and returns [`Error::HolePunchTimeout`]. /// /// Returns: -/// - A [`std::net::TcpStream`] to the other peer. +/// - An authenticated [`std::net::TcpStream`] connected to the other peer. /// - A `[u8; 32]` shared key that was derived using -/// [SPAKE2](https://docs.rs/spake2/latest/spake2/) and the weaker `shared_secret`. +/// [SPAKE2](https://docs.rs/spake2/) and the weaker `shared_secret`. pub fn try_connect_to_peer( local_contact: Contact, peer_contact: FullContact, shared_secret: &[u8], timeout: std::time::Duration, ) -> Result { + // Instantiate an asynchronous runtime let runtime = tokio::runtime::Builder::new_current_thread() .enable_io() .enable_time() .build() .expect("Tokio async runtime error."); - // hole punch asynchronously - match runtime.block_on(async { - tokio::time::timeout( - timeout, - hole_punch(local_contact, peer_contact, shared_secret), - ) - .await - }) { + // Run the asynchronous hole-punch function. + // It is asynchronous to simplify the process + // of trying multiple connections concurrently. + let result = runtime.block_on(tokio::time::timeout( + timeout, + hole_punch(local_contact, peer_contact, shared_secret), + )); + + match result { + // function succeeded, or ended + // early with error Ok(result) => result, + + // function timed out Err(..) => Err(Error::HolePunchTimeout), } } -/// TODO: Comment +/// Asynchronous hole-punching function. async fn hole_punch( local_contact: Contact, peer_contact: FullContact, shared_secret: &[u8], ) -> Result { - // shorten the variable name for conciseness + // shorten the variable name for brevity let p = shared_secret; - let mut futs = tokio::task::JoinSet::new(); + // A set of tasks that will run concurrently, + // trying to establish a connection to the peer. + let mut tasks = tokio::task::JoinSet::new(); + + // If we have an IPv4 socket address if let Some(local) = local_contact.v4 { - futs.spawn(try_accept(local, p.to_vec())); + // listen to connections from the peer + tasks.spawn(try_accept(local, p.to_vec())); + // try connecting to the peer's private socket address if let Some(peer) = peer_contact.private.v4 { - futs.spawn(try_connect(local, peer, p.to_vec())); + tasks.spawn(try_connect(local, peer, p.to_vec())); } + // try connecting to the peer's public socket address if let Some(peer) = peer_contact.public.v4 { - futs.spawn(try_connect(local, peer, p.to_vec())); + tasks.spawn(try_connect(local, peer, p.to_vec())); } } + // If we have an IPv6 socket address if let Some(local) = local_contact.v6 { - futs.spawn(try_accept(local, p.to_vec())); + // listen to connections from the peer + tasks.spawn(try_accept(local, p.to_vec())); + // try connecting to the peer's private socket address if let Some(peer) = peer_contact.private.v6 { - futs.spawn(try_connect(local, peer, p.to_vec())); + tasks.spawn(try_connect(local, peer, p.to_vec())); } + + // try connecting to the peer's public socket address if let Some(peer) = peer_contact.public.v6 { - futs.spawn(try_connect(local, peer, p.to_vec())); + tasks.spawn(try_connect(local, peer, p.to_vec())); } } - match futs.join_next().await { + + // Wait for the first hole-punch attempt to complete. + // Return its outcome. + // Note: the try_connect() and try_accept() functions + // will only return error, when something critical goes + // wrong. Otherwise they'll keep trying. + match tasks.join_next().await { + // A task finished Some(Ok(result)) => result, + + // Couldn't join the task Some(Err(..)) => panic!("Tokio join error."), - None => Err(Error::ContactEmpty), + + // No tasks were spawned + None => panic!( + "local_contact passed to try_connect_to_peer() \ + had None for both v4 and v6" + ), } } -/// Tries to TCP connect to `peer` from `local`. -/// Returns the most recent error if not successful by `end_time`. +/// Tries to TCP connect to `local` to `peer`, +/// and authenticate using `shared_secret`. async fn try_connect>( local: T, peer: T, @@ -113,24 +144,26 @@ async fn try_connect>( if let Ok(stream) = local_socket.connect(peer).await { break stream; } + // wait some time to avoid flooding the network interval.tick().await; }; - debug!("Connected to {peer} from {local}. Will try to authenticate."); + debug!("Connected from {local} to {peer}. Will try to authenticate."); verify_peer(&shared_secret, stream).await } -/// Tries to accept a peer TCP connection on `local`. -/// Returns the most recent error if not successful by `end_time`. +/// Tries to accept a peer TCP connection on `local`, +/// and authenticate using `shared_secret`. async fn try_accept( local: impl Into, shared_secret: Vec, ) -> Result { let local = local.into(); + let mut interval = tokio::time::interval(RETRY_INTERVAL); trace!("Waiting to accept connections on {local}."); + let local_socket = get_local_socket(local)?; let listener = local_socket.listen(1024)?; - let mut interval = tokio::time::interval(RETRY_INTERVAL); let (stream, addr) = loop { if let Ok(ok) = listener.accept().await { @@ -140,27 +173,22 @@ async fn try_accept( interval.tick().await; }; - debug!( - "Connected from {} to {}. Will try to authenticate.", - addr, - stream.local_addr()? - ); - + debug!("Received connection on {local} from {addr}. Will try to authenticate."); verify_peer(&shared_secret, stream).await } /// Uses [SPAKE 2](https://docs.rs/spake2/latest/spake2/) /// to derive a cryptographically secure secret from -/// a potentially weak `shared_secret`. +/// a `weak_secret`. /// Verifies that the other peer derived the same secret. /// If successful, returns a [`PeerConnection`]. async fn verify_peer( - shared_secret: &[u8], + weak_secret: &[u8], mut stream: tokio::net::TcpStream, ) -> Result { //// Password authenticated key exchange //// let (spake, outbound_msg) = Spake2::::start_symmetric( - &Password::new(shared_secret), + &Password::new(weak_secret), &Identity::new(b"gday mates"), ); @@ -206,13 +234,16 @@ async fn verify_peer( hasher.update(&my_challenge); let expected = hasher.finalize(); - if expected == peer_hash { - let stream = stream.into_std()?; - stream.set_nonblocking(false)?; - Ok((stream, shared_key)) - } else { - Err(Error::PeerAuthenticationFailed) + // Peer authentication failed + if expected != peer_hash { + return Err(Error::PeerAuthenticationFailed); } + + // Convert the authenticated stream into + // an std TCP stream. + let stream = stream.into_std()?; + stream.set_nonblocking(false)?; + Ok((stream, shared_key)) } /// Makes a new socket with this address. @@ -227,16 +258,16 @@ fn get_local_socket(local_addr: SocketAddr) -> std::io::Result { let sock = SockRef::from(&socket); - let _ = sock.set_reuse_address(true); + sock.set_reuse_address(true)?; // socket2 only supports this method on these systems #[cfg(all(unix, not(any(target_os = "solaris", target_os = "illumos"))))] - let _ = sock.set_reuse_port(true); + sock.set_reuse_port(true)?; let keepalive = TcpKeepalive::new() .with_time(Duration::from_secs(60)) .with_interval(Duration::from_secs(10)); - let _ = sock.set_tcp_keepalive(&keepalive); + sock.set_tcp_keepalive(&keepalive)?; socket.bind(local_addr)?; Ok(socket) diff --git a/gday_hole_punch/src/lib.rs b/gday_hole_punch/src/lib.rs index 043a65c..9fc58b2 100644 --- a/gday_hole_punch/src/lib.rs +++ b/gday_hole_punch/src/lib.rs @@ -47,50 +47,35 @@ use gday_contact_exchange_protocol::ServerMsg; #[derive(thiserror::Error, Debug)] #[non_exhaustive] pub enum Error { - /// The given ServerConnection contains no streams - #[error("The given ServerConnection contains no streams.")] - NoStreamsProvided, - - /// Expected IPv4 address, but received an IPv6 address - #[error("Expected IPv4 address, but received an IPv6 address.")] - ExpectedIPv4, - - /// Expected IPv6 address, but received an IPv4 address - #[error("Expected IPv6 address, but received an IPv4 address.")] - ExpectedIPv6, - - /// Local contact or peer contact were empty, so couldn't try connecting - #[error("Local contact or peer contact were empty, so couldn't try connecting.")] - ContactEmpty, - /// IO Error #[error("IO Error: {0}")] IO(#[from] std::io::Error), /// Error talking with contact exchange server #[error("Error talking with contact exchange server: {0}")] - MessengerError(#[from] gday_contact_exchange_protocol::Error), + ServerProtocolError(#[from] gday_contact_exchange_protocol::Error), /// Unexpected reply from server - #[error("Server unexpectedly replied: {0}")] + #[error("Unexpected reply from server: {0}")] UnexpectedServerReply(ServerMsg), /// Connected to peer, but key exchange failed - #[error("Connected to peer, but key exchange failed: {0}. Check the peer shared secret.")] + #[error( + "Connected to peer, but key exchange failed: {0}. \ + Ensure your peer has the same shared secret." + )] SpakeFailed(#[from] spake2::Error), /// Connected to peer, but couldn't verify their shared secret. - /// This could be due to a man-in-the-middle attack or a mismatched shared secret. #[error( "Connected to peer, but couldn't verify their shared secret. \ - This could be due to a man-in-the-middle attack or a mismatched shared secret. - Re-check your shared secret." + Ensure your peer has the same shared secret." )] PeerAuthenticationFailed, - /// Couldn't resolve any IP addresses for this contact exchange server - #[error("Couldn't resolve any IP addresses for contact exchange server '{0}'")] - CouldntResolveAddress(String), + /// Couldn't resolve contact exchange server domain name + #[error("Couldn't resolve contact exchange server domain name '{0}'")] + CouldntResolveServer(String), /// TLS error with contact exchange server #[error("TLS error with contact exchange server: {0}")] @@ -100,11 +85,11 @@ pub enum Error { #[error("No contact exchange server with ID '{0}' exists in this server list.")] ServerIDNotFound(u64), - /// Couldn't connect to any of these contact exchange servers - #[error("Couldn't connect to any of these contact exchange servers.")] + /// Couldn't connect to any of the contact exchange servers listed + #[error("Couldn't connect to any of the contact exchange servers listed.")] CouldntConnectToServers, - /// Invalid server DNS name for TLS + /// Invalid server DNS name for TLS #[error("Invalid server DNS name for TLS: {0}")] InvalidDNSName(#[from] rustls::pki_types::InvalidDnsNameError), @@ -113,24 +98,24 @@ pub enum Error { #[error( "Timed out while trying to connect to peer, likely due to an uncooperative \ NAT (network address translator). \ - Try from a different network, enable IPv6, or use a tool that transfers \ + Try from a different network, enable IPv6, or switch to a tool that transfers \ files over a relay to circumvent NATs, such as magic-wormhole." )] HolePunchTimeout, /// Couldn't parse [`PeerCode`] #[error("Couldn't parse your code: {0}. Check it for typos!")] - CouldntParse(#[from] std::num::ParseIntError), + CouldntParsePeerCode(#[from] std::num::ParseIntError), /// Incorrect checksum when parsing [`PeerCode`] #[error("Your code's checksum (last digit) is incorrect. Check it for typos!")] - IncorrectChecksum, + IncorrectChecksumPeerCode, /// Couldn't parse [`PeerCode`] #[error("Wrong number of segments in your code. Check it for typos!")] - WrongNumberOfSegments, + WrongNumberOfSegmentsPeerCode, /// Missing required checksum in [`PeerCode`] #[error("Your code is missing the required checksum digit. Check it for typos!")] - MissingChecksum, + MissingChecksumPeerCode, } diff --git a/gday_hole_punch/src/peer_code.rs b/gday_hole_punch/src/peer_code.rs index 54254c4..ca40f45 100644 --- a/gday_hole_punch/src/peer_code.rs +++ b/gday_hole_punch/src/peer_code.rs @@ -34,7 +34,7 @@ impl PeerCode { for segment in &mut segments { let Some(substring) = substrings.next() else { // return error if less than 4 substrings - return Err(Error::WrongNumberOfSegments); + return Err(Error::WrongNumberOfSegmentsPeerCode); }; *segment = u64::from_str_radix(substring, 16)?; } @@ -51,15 +51,15 @@ impl PeerCode { let checksum = u64::from_str_radix(substring, 16)?; // verify checksum if checksum != peer_code.get_checksum() { - return Err(Error::IncorrectChecksum); + return Err(Error::IncorrectChecksumPeerCode); } } else if require_checksum { - return Err(Error::MissingChecksum); + return Err(Error::MissingChecksumPeerCode); } // return error if there are too many substrings if substrings.next().is_some() { - return Err(Error::WrongNumberOfSegments); + return Err(Error::WrongNumberOfSegmentsPeerCode); } Ok(peer_code) @@ -121,7 +121,7 @@ mod tests { let message = " 1b.13A.f "; let received = PeerCode::parse(message, true); - assert!(matches!(received, Err(Error::MissingChecksum))); + assert!(matches!(received, Err(Error::MissingChecksumPeerCode))); let received = PeerCode::parse(message, false).unwrap(); let expected = PeerCode { @@ -133,7 +133,7 @@ mod tests { let message = " 1c.13A.f.3 "; let received = PeerCode::parse(message, true); - assert!(matches!(received, Err(Error::IncorrectChecksum))); + assert!(matches!(received, Err(Error::IncorrectChecksumPeerCode))); } #[test] @@ -141,12 +141,15 @@ mod tests { let message = " 21.q.3 "; let received = PeerCode::parse(message, false); - assert!(matches!(received, Err(Error::CouldntParse(..)))); + assert!(matches!(received, Err(Error::CouldntParsePeerCode(..)))); let message = " 1b.13A.f.3.4 "; let received = PeerCode::parse(message, false); - assert!(matches!(received, Err(Error::WrongNumberOfSegments))); + assert!(matches!( + received, + Err(Error::WrongNumberOfSegmentsPeerCode) + )); } #[test] diff --git a/gday_hole_punch/src/server_connector.rs b/gday_hole_punch/src/server_connector.rs index 8063885..a660bdc 100644 --- a/gday_hole_punch/src/server_connector.rs +++ b/gday_hole_punch/src/server_connector.rs @@ -3,7 +3,7 @@ use crate::Error; use gday_contact_exchange_protocol::DEFAULT_TLS_PORT; -use log::{debug, error}; +use log::{debug, warn}; use rand::seq::SliceRandom; use socket2::SockRef; use std::io::{Read, Write}; @@ -35,7 +35,7 @@ pub struct ServerInfo { pub domain_name: &'static str, /// The unique ID of the server. /// - /// Helpful when telling the other peer whichserver to connect to. + /// Helpful when telling the other peer which server to connect to. /// Should NOT be zero, since peers can use that value to represent /// a custom server. pub id: u64, @@ -95,7 +95,7 @@ impl ServerStream { } /// Enables SO_REUSEADDR and SO_REUSEPORT - /// So that this socket can be reused for + /// so that this socket can be reused for /// hole punching. fn enable_reuse(&self) { let stream: &TcpStream = match self { @@ -113,27 +113,28 @@ impl ServerStream { } /// Can hold both an IPv4 and IPv6 [`ServerStream`] to a Gday server. +#[derive(Debug)] pub struct ServerConnection { pub v4: Option, pub v6: Option, } -/// Some private helper functions used by [`ContactSharer`] +// some private helper functions used by ContactSharer impl ServerConnection { /// Enables `SO_REUSEADDR` and `SO_REUSEPORT` so that the ports of - /// these streams can be reused for hole punching. + /// these sockets can be reused for hole punching. /// /// Returns an error if both streams are `None`. /// Returns an error if a `v4` is passed where `v6` should, or vice versa. pub(super) fn configure(&self) -> Result<(), Error> { if self.v4.is_none() && self.v6.is_none() { - return Err(Error::NoStreamsProvided); + panic!("ServerConnection had None for both v4 and v6 streams."); } if let Some(stream) = &self.v4 { let addr = stream.local_addr()?; if !matches!(addr, V4(_)) { - return Err(Error::ExpectedIPv4); + panic!("ServerConnection had IPv6 stream where IPv4 stream was expected."); }; stream.enable_reuse(); } @@ -141,7 +142,7 @@ impl ServerConnection { if let Some(stream) = &self.v6 { let addr = stream.local_addr()?; if !matches!(addr, V6(_)) { - return Err(Error::ExpectedIPv6); + panic!("ServerConnection had IPv4 stream where IPv6 stream was expected."); }; stream.enable_reuse(); } @@ -149,7 +150,7 @@ impl ServerConnection { } /// Returns a [`Vec`] of all the [`ServerStream`]s in this connection. - /// Will return IPV6 followed by IPV4 + /// Will return `v6` followed by `v4` pub(super) fn streams(&mut self) -> Vec<&mut ServerStream> { let mut streams = Vec::new(); @@ -227,7 +228,7 @@ pub fn connect_to_random_domain_name( Ok(streams) => streams, Err(err) => { recent_error = err; - error!("Couldn't connect to \"{server}:{DEFAULT_TLS_PORT}\": {recent_error}"); + warn!("Couldn't connect to \"{server}:{DEFAULT_TLS_PORT}\": {recent_error}"); continue; } }; @@ -251,11 +252,16 @@ pub fn connect_to_domain_name( ) -> Result { let address = format!("{domain_name}:{port}"); debug!("Connecting to server '{address}`"); + + // do a DNS lookup to get socket addresses for + // this domain name let addrs = address.to_socket_addrs()?; + // try connecting to the first IPv4 address let addr_v4: Option = addrs.clone().find(|a| a.is_ipv4()); let tcp_v4 = addr_v4.map(|addr| TcpStream::connect_timeout(&addr, timeout)); + // try connecting to the first IPv6 addresss let addr_v6: Option = addrs.clone().find(|a| a.is_ipv6()); let tcp_v6 = addr_v6.map(|addr| TcpStream::connect_timeout(&addr, timeout)); @@ -266,7 +272,7 @@ pub fn connect_to_domain_name( } else if let Some(Err(err_v6)) = tcp_v6 { return Err(Error::IO(err_v6)); } else { - return Err(Error::CouldntResolveAddress(domain_name.to_string())); + return Err(Error::CouldntResolveServer(domain_name.to_string())); } }