From db43a5be1391e71fd80f7b1712be2e0a278a97bf Mon Sep 17 00:00:00 2001 From: Tipragot Date: Wed, 7 Feb 2024 16:55:07 +0000 Subject: [PATCH] Automatic port forwarding using UPnP and invitation link system (#27) Closes #19 Closes #23 Reviewed-on: https://git.tipragot.fr/corentin/border-wars/pulls/27 Reviewed-by: Corentin Co-authored-by: Tipragot Co-committed-by: Tipragot --- Cargo.lock | 384 +++++++++++++++++++++++++++++++++++++++ crates/bevnet/Cargo.toml | 3 + crates/bevnet/src/lib.rs | 95 +++++++--- 3 files changed, 460 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 175c70f..cff54fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,17 +37,50 @@ dependencies = [ "subtle", ] +[[package]] +name = "attohttpc" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb8867f378f33f78a811a8eb9bf108ad99430d7aad43315dd9319c827ef6247" +dependencies = [ + "http", + "log", + "url", + "wildmatch", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "bevnet" version = "0.2.0" dependencies = [ "aes-gcm", + "base64", + "igd", + "local-ip-address", ] [[package]] name = "border-wars" version = "0.1.0" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + [[package]] name = "cfg-if" version = "1.0.0" @@ -93,6 +126,27 @@ dependencies = [ "cipher", ] +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -124,6 +178,40 @@ dependencies = [ "polyval", ] +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "igd" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556b5a75cd4adb7c4ea21c64af1c48cefb2ce7d43dc4352c720a1fe47c21f355" +dependencies = [ + "attohttpc", + "log", + "rand", + "url", + "xmltree", +] + [[package]] name = "inout" version = "0.1.3" @@ -133,18 +221,73 @@ dependencies = [ "generic-array", ] +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "local-ip-address" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612ed4ea9ce5acfb5d26339302528a5e1e59dfed95e9e11af3c083236ff1d15d" +dependencies = [ + "libc", + "neli", + "thiserror", + "windows-sys", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "neli" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1100229e06604150b3becd61a4965d5c70f3be1759544ea7274166f4be41ef43" +dependencies = [ + "byteorder", + "libc", + "log", + "neli-proc-macros", +] + +[[package]] +name = "neli-proc-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c168194d373b1e134786274020dae7fc5513d565ea2ebb9bc9ff17ffb69106d4" +dependencies = [ + "either", + "proc-macro2", + "quote", + "serde", + "syn 1.0.109", +] + [[package]] name = "opaque-debug" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + [[package]] name = "polyval" version = "0.6.1" @@ -157,6 +300,51 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -166,18 +354,116 @@ dependencies = [ "getrandom", ] +[[package]] +name = "serde" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "subtle" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + [[package]] name = "universal-hash" version = "0.5.1" @@ -188,6 +474,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "version_check" version = "0.9.4" @@ -199,3 +496,90 @@ name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wildmatch" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f44b95f62d34113cf558c93511ac93027e03e9c29a60dd0fd70e6e025c7270a" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "xml-rs" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] diff --git a/crates/bevnet/Cargo.toml b/crates/bevnet/Cargo.toml index 0811872..0399afe 100644 --- a/crates/bevnet/Cargo.toml +++ b/crates/bevnet/Cargo.toml @@ -12,4 +12,7 @@ categories = ["network-programming", "game-development"] workspace = true [dependencies] +local-ip-address = "0.5.7" aes-gcm = "0.10.3" +base64 = "0.21.7" +igd = "0.12.1" diff --git a/crates/bevnet/src/lib.rs b/crates/bevnet/src/lib.rs index 4b897cb..a3d253b 100644 --- a/crates/bevnet/src/lib.rs +++ b/crates/bevnet/src/lib.rs @@ -2,24 +2,26 @@ use std::collections::LinkedList; use std::io::{self, Read, Write}; -use std::net::{TcpListener, TcpStream, ToSocketAddrs}; +use std::net::{IpAddr, Ipv4Addr, SocketAddrV4, TcpListener, TcpStream}; use aes_gcm::aead::{Aead, AeadCore, KeyInit, OsRng}; use aes_gcm::{Aes128Gcm, Key, Nonce}; +use base64::prelude::*; +use igd::{Gateway, PortMappingProtocol}; +use local_ip_address::local_ip; /// A non-blocking tcp connection. /// /// # Example /// -/// ```rust +/// ```no_run /// use std::io; /// /// use bevnet::{Connection, Listener}; /// /// # fn main() -> io::Result<()> { -/// let secret_key = Connection::generate_key(); -/// let listener = Listener::bind("127.0.0.1:23732", &secret_key)?; -/// let mut connection = Connection::connect("127.0.0.1:23732", &secret_key)?; +/// let listener = Listener::new()?; +/// let mut connection = Connection::connect(&listener.connection_string())?; /// /// // The accept operation is not blocking. So we need to loop here. /// let mut server_connection; @@ -77,11 +79,6 @@ pub struct Connection { } impl Connection { - /// Generates a new secret key. - pub fn generate_key() -> [u8; 16] { - Aes128Gcm::generate_key(OsRng).into() - } - /// Creates a new [Connection] from a [TcpStream]. fn new(stream: TcpStream, secret_key: &Key) -> io::Result { stream.set_nonblocking(true)?; @@ -96,11 +93,32 @@ impl Connection { }) } - /// Creates a new [Connection] that connects to the given address. + /// Creates a new [Connection] that connects to the given connection string. /// /// This function is blocking. - pub fn connect(address: impl ToSocketAddrs, secret_key: &[u8; 16]) -> io::Result { - Self::new(TcpStream::connect(address)?, secret_key.into()) + pub fn connect(connection_string: &str) -> io::Result { + let data = BASE64_URL_SAFE_NO_PAD + .decode(connection_string) + .map_err(io::Error::other)?; + if data.len() != 22 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("invalid connection string: {}", connection_string), + )); + } + let address = SocketAddrV4::new( + Ipv4Addr::new(data[0], data[1], data[2], data[3]), + u16::from_ne_bytes(data[4..=5].try_into().map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("invalid connection string: {}", connection_string), + ) + })?), + ); + Self::new( + TcpStream::connect(address)?, + Key::::from_slice(&data[6..]), + ) } /// Sends a message over the connection. @@ -288,15 +306,14 @@ impl Connection { /// A non-blocking tcp listener. /// -/// ```rust +/// ```no_run /// use std::io; /// /// use bevnet::{Connection, Listener}; /// /// # fn main() -> io::Result<()> { -/// let secret_key = Connection::generate_key(); -/// let listener = Listener::bind("127.0.0.1:23732", &secret_key)?; -/// let mut connection = Connection::connect("127.0.0.1:23732", &secret_key)?; +/// let listener = Listener::new()?; +/// let mut connection = Connection::connect(&listener.connection_string())?; /// /// // The accept operation is not blocking. So we need to loop here. /// let mut server_connection; @@ -309,16 +326,32 @@ impl Connection { /// # Ok(()) /// # } /// ``` -pub struct Listener(TcpListener, Key); +pub struct Listener(TcpListener, Gateway, SocketAddrV4, Key); impl Listener { /// Creates a new listener. - pub fn bind(addr: impl ToSocketAddrs, secret_key: &[u8; 16]) -> io::Result { - let listener = TcpListener::bind(addr)?; + pub fn new() -> io::Result { + let local_address = match local_ip().map_err(io::Error::other)? { + IpAddr::V4(address) => address, + IpAddr::V6(_) => unreachable!(), + }; + let listener = TcpListener::bind(SocketAddrV4::new(local_address, 0))?; + let gateway = igd::search_gateway(Default::default()).map_err(io::Error::other)?; + let opened_port = gateway + .add_any_port( + PortMappingProtocol::TCP, + SocketAddrV4::new(local_address, listener.local_addr()?.port()), + 3600 * 24, + "bevnet", + ) + .map_err(io::Error::other)?; + let external_address = gateway.get_external_ip().map_err(io::Error::other)?; listener.set_nonblocking(true)?; Ok(Self( listener, - Key::::from_slice(secret_key).to_owned(), + gateway, + SocketAddrV4::new(external_address, opened_port), + Aes128Gcm::generate_key(OsRng), )) } @@ -327,10 +360,28 @@ impl Listener { /// This function is not blocking. pub fn accept(&self) -> io::Result> { match self.0.accept() { - Ok((stream, _)) => Connection::new(stream, &self.1).map(Some), + Ok((stream, _)) => Connection::new(stream, &self.3).map(Some), Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => Ok(None), Err(ref e) if e.kind() == io::ErrorKind::Interrupted => Ok(None), Err(e) => Err(e), } } + + /// Returns the connection string that can be used to connect to th + /// listener. + pub fn connection_string(&self) -> String { + let mut data = Vec::with_capacity(22); + data.extend(self.2.ip().octets()); + data.extend(self.2.port().to_ne_bytes()); + data.extend(self.3); + BASE64_URL_SAFE_NO_PAD.encode(&data) + } +} + +impl Drop for Listener { + fn drop(&mut self) { + self.1 + .remove_port(PortMappingProtocol::TCP, self.2.port()) + .ok(); + } }