From 34190a70b1efa0972ef58b88d356f985c46b89ae Mon Sep 17 00:00:00 2001 From: metamuffin Date: Sat, 21 Sep 2024 15:31:18 +0200 Subject: server: mdns and upnp --- Cargo.lock | 169 ++++++++++++++++++++++++++++++++++++++++- server/Cargo.toml | 5 +- server/src/lib.rs | 2 +- server/src/main.rs | 19 ++++- server/src/network/mdns.rs | 61 +++++++++++++++ server/src/network/mod.rs | 20 +++++ server/src/network/register.rs | 132 ++++++++++++++++++++++++++++++++ server/src/network/upnp.rs | 72 ++++++++++++++++++ server/src/register.rs | 132 -------------------------------- 9 files changed, 473 insertions(+), 139 deletions(-) create mode 100644 server/src/network/mdns.rs create mode 100644 server/src/network/mod.rs create mode 100644 server/src/network/register.rs create mode 100644 server/src/network/upnp.rs delete mode 100644 server/src/register.rs diff --git a/Cargo.lock b/Cargo.lock index a3e41432..82342302 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,6 +178,18 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "attohttpc" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb8867f378f33f78a811a8eb9bf108ad99430d7aad43315dd9319c827ef6247" +dependencies = [ + "http 0.2.12", + "log", + "url", + "wildmatch", +] + [[package]] name = "autocfg" version = "1.3.0" @@ -372,6 +384,12 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +[[package]] +name = "c_linked_list" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4964518bd3b4a8190e832886cdc0da9794f12e8e6c1613a9e90ff331c4c8724b" + [[package]] name = "c_vec" version = "2.0.0" @@ -485,6 +503,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "cookie" version = "0.18.1" @@ -750,6 +777,8 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ + "futures-core", + "futures-sink", "spin", ] @@ -782,6 +811,7 @@ checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -804,6 +834,17 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.30" @@ -851,6 +892,12 @@ dependencies = [ "slab", ] +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + [[package]] name = "generator" version = "0.7.5" @@ -874,6 +921,28 @@ dependencies = [ "version_check", ] +[[package]] +name = "get_if_addrs" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abddb55a898d32925f3148bd281174a68eeb68bbfd9a5938a57b18f506ee4ef7" +dependencies = [ + "c_linked_list", + "get_if_addrs-sys", + "libc", + "winapi 0.2.8", +] + +[[package]] +name = "get_if_addrs-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04f9fb746cf36b191c00f3ede8bde9c8e64f9f4b05ae2694a9ccf5e3f5ab48" +dependencies = [ + "gcc", + "libc", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -1146,17 +1215,20 @@ dependencies = [ [[package]] name = "hurrycurry-server" -version = "0.2.0" +version = "2.1.0" dependencies = [ "anyhow", "bincode", "clap", "env_logger", "futures-util", + "get_if_addrs", "hurrycurry-bot", "hurrycurry-client-lib", "hurrycurry-protocol", + "igd", "log", + "mdns-sd", "pollster", "rand 0.9.0-alpha.2", "reqwest", @@ -1261,6 +1333,34 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "if-addrs" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78a89907582615b19f6f0da1af18abf6ff08be259395669b834b057a7ee92d8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "igd" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556b5a75cd4adb7c4ea21c64af1c48cefb2ce7d43dc4352c720a1fe47c21f355" +dependencies = [ + "attohttpc", + "bytes", + "futures", + "http 0.2.12", + "hyper 0.14.30", + "log", + "rand 0.8.5", + "tokio", + "url", + "xmltree", +] + [[package]] name = "image" version = "0.25.2" @@ -1534,6 +1634,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "mdns-sd" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35d1967e64b4ca7bba8af2458d0b9dd50471d541959ca2120cb9cc965946ef61" +dependencies = [ + "flume", + "if-addrs", + "log", + "polling", + "socket2", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1628,7 +1741,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1855,6 +1968,22 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + [[package]] name = "pollster" version = "0.3.0" @@ -3339,6 +3468,18 @@ dependencies = [ "rustix", ] +[[package]] +name = "wildmatch" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f44b95f62d34113cf558c93511ac93027e03e9c29a60dd0fd70e6e025c7270a" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -3400,6 +3541,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -3554,6 +3704,21 @@ version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" +[[package]] +name = "xml-rs" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/server/Cargo.toml b/server/Cargo.toml index 520e3d24..ef14fddd 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hurrycurry-server" -version = "0.2.0" +version = "2.1.0" edition = "2021" default-run = "hurrycurry-server" @@ -26,6 +26,9 @@ reqwest = { version = "0.12.7", default-features = false, features = [ pollster = "0.3.0" bincode = "2.0.0-rc.3" xdg = "2.5.2" +igd = { version = "0.12.1", features = ["aio"] } +get_if_addrs = "0.5.3" +mdns-sd = "0.11.4" hurrycurry-protocol = { path = "protocol" } hurrycurry-client-lib = { path = "client-lib" } diff --git a/server/src/lib.rs b/server/src/lib.rs index ea514d5b..c0ed8edb 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -27,10 +27,10 @@ pub mod commands; pub mod data; pub mod entity; pub mod interaction; -pub mod register; pub mod scoreboard; pub mod server; pub mod state; +pub mod network; use hurrycurry_protocol::{glam::Vec2, Message}; diff --git a/server/src/main.rs b/server/src/main.rs index 4db43c64..d0538126 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -21,7 +21,7 @@ use futures_util::{SinkExt, StreamExt}; use hurrycurry_protocol::{PacketC, PacketS, BINCODE_CONFIG, VERSION}; use hurrycurry_server::{ data::DATA_DIR, - register::Register, + network::{mdns::mdns_loop, register::Register, upnp::upnp_loop}, server::{GameServerExt, Server}, trm, ConnectionID, }; @@ -59,9 +59,12 @@ pub(crate) struct Args { /// Enables submissions to the public server registry #[arg(long)] register: bool, - /// Enables mDNS discoverability + /// Enables the mDNS responder for local network discovery #[arg(long)] - discoverable: bool, + mdns: bool, + // Enables automatic gateway port forwarding using UPnP + #[arg(long)] + upnp: bool, /// Server name #[arg(long, short = 'N', default_value = "A Hurry Curry! Server")] server_name: String, @@ -130,6 +133,16 @@ async fn run(args: Args) -> anyhow::Result<()> { ); tokio::task::spawn(r.register_loop()); } + if args.upnp { + tokio::task::spawn(upnp_loop(args.listen.port())); + } + if args.mdns { + tokio::task::spawn(mdns_loop( + args.server_name.clone(), + args.listen.port(), + state.clone(), + )); + } { let state = state.clone(); diff --git a/server/src/network/mdns.rs b/server/src/network/mdns.rs new file mode 100644 index 00000000..870ae7ec --- /dev/null +++ b/server/src/network/mdns.rs @@ -0,0 +1,61 @@ +use crate::server::Server; +use anyhow::Result; +use get_if_addrs::get_if_addrs; +use hurrycurry_protocol::VERSION; +use log::{info, warn}; +use mdns_sd::{ServiceDaemon, ServiceInfo}; +use rand::random; +use std::{collections::HashMap, sync::Arc, time::Duration}; +use tokio::{sync::RwLock, time::interval}; + +pub async fn mdns_loop(name: String, port: u16, state: Arc>) { + let d = match ServiceDaemon::new() { + Ok(d) => d, + Err(e) => { + warn!("mDNS daemon failed to start: {e}"); + return; + } + }; + let mut interval = interval(Duration::from_secs(60)); + let hostname = format!("hks-{}.local.", random::()); + loop { + interval.tick().await; + if let Err(e) = update_service(&d, &state, &name, &hostname, port).await { + warn!("mDNS service update failed: {e}"); + } + info!("updated mDNS service record"); + } +} + +async fn update_service( + d: &ServiceDaemon, + state: &Arc>, + name: &str, + hostname: &str, + port: u16, +) -> Result<()> { + let addrs = get_if_addrs()? + .into_iter() + .map(|e| e.addr.ip()) + .filter(|a| !a.is_loopback()) + .collect::>(); + + let players = state.read().await.count_chefs(); + + d.register(ServiceInfo::new( + "_hurrycurry._tcp.local.", + name, + hostname, + &*addrs, + port, + HashMap::from_iter([ + ("players".to_string(), players.to_string()), + ("version".to_string(), env!("CARGO_PKG_VERSION").to_string()), + ( + "protocol".to_string(), + format!("{}.{}", VERSION.0, VERSION.1), + ), + ]), + )?)?; + Ok(()) +} diff --git a/server/src/network/mod.rs b/server/src/network/mod.rs new file mode 100644 index 00000000..b7ffc15e --- /dev/null +++ b/server/src/network/mod.rs @@ -0,0 +1,20 @@ +/* + Hurry Curry! - a game about cooking + Copyright 2024 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +*/ +pub mod register; +pub mod upnp; +pub mod mdns; diff --git a/server/src/network/register.rs b/server/src/network/register.rs new file mode 100644 index 00000000..b26768a2 --- /dev/null +++ b/server/src/network/register.rs @@ -0,0 +1,132 @@ +/* + Hurry Curry! - a game about cooking + Copyright 2024 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +*/ +use crate::server::Server; +use anyhow::{bail, Result}; +use hurrycurry_protocol::{registry::Submission, VERSION}; +use log::{info, warn}; +use rand::random; +use reqwest::{header::USER_AGENT, Client, Url}; +use std::{ + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + str::FromStr, + sync::Arc, + time::Duration, +}; +use tokio::{sync::RwLock, time::interval}; + +const REGISTRY_URI: &'static str = "https://hurrycurry-registry.metamuffin.org"; + +pub struct Register { + name: String, + port: u16, + register_uri: Option, + state: Arc>, + inet_client: Client, + ip4_client: Client, + ip6_client: Client, + secret: u128, + players: usize, +} + +impl Register { + pub fn new( + name: String, + port: u16, + register_uri: Option, + state: Arc>, + ) -> Self { + Self { + name, + register_uri, + players: 0, + port, + secret: random(), + state, + inet_client: Client::new(), + ip4_client: Client::builder() + .local_address(IpAddr::V4(Ipv4Addr::UNSPECIFIED)) + .build() + .unwrap(), + ip6_client: Client::builder() + .local_address(IpAddr::V6(Ipv6Addr::UNSPECIFIED)) + .build() + .unwrap(), + } + } + pub async fn register_loop(mut self) { + let mut interval = interval(Duration::from_secs(60)); + loop { + interval.tick().await; + self.players = self.state.read().await.count_chefs(); + if let Err(e) = self.register().await { + warn!("register error: {e}") + } + } + } + + pub async fn register(&self) -> Result<()> { + info!("register update"); + if let Some(uri) = &self.register_uri { + self.register_with("uri", &self.inet_client, uri.to_owned()) + .await?; + } else { + let (v4, v6) = tokio::join!( + self.register_with( + "ip4", + &self.ip4_client, + format!("ws://0.0.0.0:{}", self.port), + ), + self.register_with( + "ip6", + &self.ip6_client, + format!("ws://0.0.0.0:{}", self.port), + ) + ); + info!("v4: {v4:?}"); + info!("v6: {v6:?}"); + } + Ok(()) + } + // TODO ip v6 + pub async fn register_with(&self, label: &str, client: &Client, uri: String) -> Result<()> { + let res = client + .post(Url::from_str(&format!("{REGISTRY_URI}/v1/register")).unwrap()) + .header( + USER_AGENT, + format!("hurrycurry-server {}", env!("CARGO_PKG_VERSION")), + ) + .json(&Submission { + last_game: 0, + name: self.name.clone(), + uri, + players: self.players, + secret: self.secret, + version: VERSION, + }) + .send() + .await?; + + let r = res.text().await?; + if r == "ok" { + info!("register ok ({label})"); + Ok(()) + } else { + bail!("{r}"); + } + } +} diff --git a/server/src/network/upnp.rs b/server/src/network/upnp.rs new file mode 100644 index 00000000..a16e9684 --- /dev/null +++ b/server/src/network/upnp.rs @@ -0,0 +1,72 @@ +/* + Hurry Curry! - a game about cooking + Copyright 2024 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +*/ +use anyhow::{bail, Result}; +use get_if_addrs::get_if_addrs; +use igd::{ + aio::{search_gateway, Gateway}, + PortMappingProtocol, SearchOptions, +}; +use log::{error, info}; +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddrV4}, + time::Duration, +}; +use tokio::time::interval; + +pub async fn upnp_loop(port: u16) { + // TODO multiple gateways + let (gateway, local_addr) = match upnp_setup().await { + Ok(g) => g, + Err(e) => { + error!("UPnP gateway not found: {e}"); + return; + } + }; + let mut interval = interval(Duration::from_secs(120)); + loop { + interval.tick().await; + match gateway + .add_port( + PortMappingProtocol::TCP, + 27032, + SocketAddrV4::new(local_addr, port), + 800, + "Hurry Curry! Game Server", + ) + .await + { + Ok(()) => info!("port mapped successfully"), + Err(e) => error!("upnp failed: {e}"), + } + } +} + +async fn upnp_setup() -> Result<(Gateway, Ipv4Addr)> { + let gateway = search_gateway(SearchOptions::default()).await?; + info!("IGD address is {}", gateway.addr); + for i in get_if_addrs()? { + let a = i.addr.ip(); + if !a.is_loopback() { + if let IpAddr::V4(a) = a { + info!("local v4 address is {a}"); + return Ok((gateway, a)); + } + } + } + bail!("no good local address found") +} diff --git a/server/src/register.rs b/server/src/register.rs deleted file mode 100644 index b26768a2..00000000 --- a/server/src/register.rs +++ /dev/null @@ -1,132 +0,0 @@ -/* - Hurry Curry! - a game about cooking - Copyright 2024 metamuffin - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, version 3 of the License only. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -*/ -use crate::server::Server; -use anyhow::{bail, Result}; -use hurrycurry_protocol::{registry::Submission, VERSION}; -use log::{info, warn}; -use rand::random; -use reqwest::{header::USER_AGENT, Client, Url}; -use std::{ - net::{IpAddr, Ipv4Addr, Ipv6Addr}, - str::FromStr, - sync::Arc, - time::Duration, -}; -use tokio::{sync::RwLock, time::interval}; - -const REGISTRY_URI: &'static str = "https://hurrycurry-registry.metamuffin.org"; - -pub struct Register { - name: String, - port: u16, - register_uri: Option, - state: Arc>, - inet_client: Client, - ip4_client: Client, - ip6_client: Client, - secret: u128, - players: usize, -} - -impl Register { - pub fn new( - name: String, - port: u16, - register_uri: Option, - state: Arc>, - ) -> Self { - Self { - name, - register_uri, - players: 0, - port, - secret: random(), - state, - inet_client: Client::new(), - ip4_client: Client::builder() - .local_address(IpAddr::V4(Ipv4Addr::UNSPECIFIED)) - .build() - .unwrap(), - ip6_client: Client::builder() - .local_address(IpAddr::V6(Ipv6Addr::UNSPECIFIED)) - .build() - .unwrap(), - } - } - pub async fn register_loop(mut self) { - let mut interval = interval(Duration::from_secs(60)); - loop { - interval.tick().await; - self.players = self.state.read().await.count_chefs(); - if let Err(e) = self.register().await { - warn!("register error: {e}") - } - } - } - - pub async fn register(&self) -> Result<()> { - info!("register update"); - if let Some(uri) = &self.register_uri { - self.register_with("uri", &self.inet_client, uri.to_owned()) - .await?; - } else { - let (v4, v6) = tokio::join!( - self.register_with( - "ip4", - &self.ip4_client, - format!("ws://0.0.0.0:{}", self.port), - ), - self.register_with( - "ip6", - &self.ip6_client, - format!("ws://0.0.0.0:{}", self.port), - ) - ); - info!("v4: {v4:?}"); - info!("v6: {v6:?}"); - } - Ok(()) - } - // TODO ip v6 - pub async fn register_with(&self, label: &str, client: &Client, uri: String) -> Result<()> { - let res = client - .post(Url::from_str(&format!("{REGISTRY_URI}/v1/register")).unwrap()) - .header( - USER_AGENT, - format!("hurrycurry-server {}", env!("CARGO_PKG_VERSION")), - ) - .json(&Submission { - last_game: 0, - name: self.name.clone(), - uri, - players: self.players, - secret: self.secret, - version: VERSION, - }) - .send() - .await?; - - let r = res.text().await?; - if r == "ok" { - info!("register ok ({label})"); - Ok(()) - } else { - bail!("{r}"); - } - } -} -- cgit v1.2.3-70-g09d2