Automatic port forwarding using UPnP and invitation link system #27

Merged
CoCo_Sol merged 6 commits from upnp into main 2024-02-07 16:55:09 +00:00
3 changed files with 460 additions and 22 deletions

384
Cargo.lock generated
View file

@ -37,17 +37,50 @@ dependencies = [
"subtle", "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]] [[package]]
name = "bevnet" name = "bevnet"
version = "0.2.0" version = "0.2.0"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"base64",
"igd",
"local-ip-address",
] ]
[[package]] [[package]]
name = "border-wars" name = "border-wars"
version = "0.1.0" 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]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@ -93,6 +126,27 @@ dependencies = [
"cipher", "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]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.7" version = "0.14.7"
@ -124,6 +178,40 @@ dependencies = [
"polyval", "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]] [[package]]
name = "inout" name = "inout"
version = "0.1.3" version = "0.1.3"
@ -133,18 +221,73 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "itoa"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.153" version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 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]] [[package]]
name = "opaque-debug" name = "opaque-debug"
version = "0.3.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]] [[package]]
name = "polyval" name = "polyval"
version = "0.6.1" version = "0.6.1"
@ -157,6 +300,51 @@ dependencies = [
"universal-hash", "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]] [[package]]
name = "rand_core" name = "rand_core"
version = "0.6.4" version = "0.6.4"
@ -166,18 +354,116 @@ dependencies = [
"getrandom", "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]] [[package]]
name = "subtle" name = "subtle"
version = "2.5.0" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" 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]] [[package]]
name = "typenum" name = "typenum"
version = "1.17.0" version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 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]] [[package]]
name = "universal-hash" name = "universal-hash"
version = "0.5.1" version = "0.5.1"
@ -188,6 +474,17 @@ dependencies = [
"subtle", "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]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"
@ -199,3 +496,90 @@ name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 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",
]

View file

@ -12,4 +12,7 @@ categories = ["network-programming", "game-development"]
workspace = true workspace = true
[dependencies] [dependencies]
local-ip-address = "0.5.7"
aes-gcm = "0.10.3" aes-gcm = "0.10.3"
base64 = "0.21.7"
igd = "0.12.1"

View file

@ -2,24 +2,26 @@
use std::collections::LinkedList; use std::collections::LinkedList;
use std::io::{self, Read, Write}; 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::aead::{Aead, AeadCore, KeyInit, OsRng};
use aes_gcm::{Aes128Gcm, Key, Nonce}; use aes_gcm::{Aes128Gcm, Key, Nonce};
use base64::prelude::*;
use igd::{Gateway, PortMappingProtocol};
use local_ip_address::local_ip;
/// A non-blocking tcp connection. /// A non-blocking tcp connection.
/// ///
/// # Example /// # Example
/// ///
/// ```rust /// ```no_run
/// use std::io; /// use std::io;
/// ///
/// use bevnet::{Connection, Listener}; /// use bevnet::{Connection, Listener};
/// ///
/// # fn main() -> io::Result<()> { /// # fn main() -> io::Result<()> {
/// let secret_key = Connection::generate_key(); /// let listener = Listener::new()?;
/// let listener = Listener::bind("127.0.0.1:23732", &secret_key)?; /// let mut connection = Connection::connect(&listener.connection_string())?;
/// let mut connection = Connection::connect("127.0.0.1:23732", &secret_key)?;
/// ///
/// // The accept operation is not blocking. So we need to loop here. /// // The accept operation is not blocking. So we need to loop here.
/// let mut server_connection; /// let mut server_connection;
@ -77,11 +79,6 @@ pub struct Connection {
} }
impl 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]. /// Creates a new [Connection] from a [TcpStream].
fn new(stream: TcpStream, secret_key: &Key<Aes128Gcm>) -> io::Result<Self> { fn new(stream: TcpStream, secret_key: &Key<Aes128Gcm>) -> io::Result<Self> {
stream.set_nonblocking(true)?; 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. /// This function is blocking.
pub fn connect(address: impl ToSocketAddrs, secret_key: &[u8; 16]) -> io::Result<Self> { pub fn connect(connection_string: &str) -> io::Result<Self> {
Self::new(TcpStream::connect(address)?, secret_key.into()) 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::<Aes128Gcm>::from_slice(&data[6..]),
)
} }
/// Sends a message over the connection. /// Sends a message over the connection.
@ -288,15 +306,14 @@ impl Connection {
/// A non-blocking tcp listener. /// A non-blocking tcp listener.
/// ///
/// ```rust /// ```no_run
/// use std::io; /// use std::io;
/// ///
/// use bevnet::{Connection, Listener}; /// use bevnet::{Connection, Listener};
/// ///
/// # fn main() -> io::Result<()> { /// # fn main() -> io::Result<()> {
/// let secret_key = Connection::generate_key(); /// let listener = Listener::new()?;
/// let listener = Listener::bind("127.0.0.1:23732", &secret_key)?; /// let mut connection = Connection::connect(&listener.connection_string())?;
/// let mut connection = Connection::connect("127.0.0.1:23732", &secret_key)?;
/// ///
/// // The accept operation is not blocking. So we need to loop here. /// // The accept operation is not blocking. So we need to loop here.
/// let mut server_connection; /// let mut server_connection;
@ -309,16 +326,32 @@ impl Connection {
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
pub struct Listener(TcpListener, Key<Aes128Gcm>); pub struct Listener(TcpListener, Gateway, SocketAddrV4, Key<Aes128Gcm>);
impl Listener { impl Listener {
/// Creates a new listener. /// Creates a new listener.
pub fn bind(addr: impl ToSocketAddrs, secret_key: &[u8; 16]) -> io::Result<Self> { pub fn new() -> io::Result<Self> {
let listener = TcpListener::bind(addr)?; 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)?; listener.set_nonblocking(true)?;
Ok(Self( Ok(Self(
listener, listener,
Key::<Aes128Gcm>::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. /// This function is not blocking.
pub fn accept(&self) -> io::Result<Option<Connection>> { pub fn accept(&self) -> io::Result<Option<Connection>> {
match self.0.accept() { 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::WouldBlock => Ok(None),
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => Ok(None), Err(ref e) if e.kind() == io::ErrorKind::Interrupted => Ok(None),
Err(e) => Err(e), 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();
}
} }