diff --git a/Cargo.lock b/Cargo.lock index 965f0343aa..98ccc7c20a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2893,9 +2893,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.107" +version = "0.2.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219" +checksum = "565dbd88872dbe4cc8a46e527f26483c1d1f7afa6b884a3bd6cd893d4f98da74" [[package]] name = "libgit2-sys" @@ -5383,9 +5383,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" dependencies = [ "libc", "winapi 0.3.9", @@ -6374,6 +6374,7 @@ dependencies = [ "rustls 0.20.1", "serde", "shellexpand", + "socket2", "tokio", "tokio-stream", "tracing", diff --git a/network/Cargo.toml b/network/Cargo.toml index fc3bb2f03b..58ae4dd59b 100644 --- a/network/Cargo.toml +++ b/network/Cargo.toml @@ -20,6 +20,8 @@ network-protocol = { package = "veloren-network-protocol", path = "protocol" } #serialisation bincode = "1.3.2" serde = { version = "1.0" } +#ipv4/ipv6 behavior +socket2 = "0.4.4" #sending crossbeam-channel = "0.5" tokio = { version = "1.14", default-features = false, features = ["io-util", "macros", "rt", "net", "time"] } diff --git a/network/src/channel.rs b/network/src/channel.rs index f014b177fa..7457fb0785 100644 --- a/network/src/channel.rs +++ b/network/src/channel.rs @@ -94,7 +94,21 @@ impl Protocols { s2s_stop_listening_r: oneshot::Receiver<()>, c2s_protocol_s: mpsc::UnboundedSender<(Self, Cid)>, ) -> std::io::Result<()> { - let listener = net::TcpListener::bind(addr).await?; + use socket2::{Domain, Socket, Type}; + let domain = Domain::for_address(addr); + let socket2_socket = Socket::new(domain, Type::STREAM, None)?; + if domain == Domain::IPV6 { + socket2_socket.set_only_v6(true)? + } + socket2_socket.set_nonblocking(true)?; // Needed by Tokio + // See https://docs.rs/tokio/latest/tokio/net/struct.TcpSocket.html + #[cfg(not(windows))] + socket2_socket.set_reuse_address(true)?; + let socket2_addr = addr.into(); + socket2_socket.bind(&socket2_addr)?; + socket2_socket.listen(1024)?; + let std_listener: std::net::TcpListener = socket2_socket.into(); + let listener = tokio::net::TcpListener::from_std(std_listener)?; trace!(?addr, "Tcp Listener bound"); let mut end_receiver = s2s_stop_listening_r.fuse(); tokio::spawn(async move { diff --git a/network/tests/helper.rs b/network/tests/helper.rs index b22df211b4..517e8ea3a9 100644 --- a/network/tests/helper.rs +++ b/network/tests/helper.rs @@ -1,6 +1,6 @@ use lazy_static::*; use std::{ - net::SocketAddr, + net::{Ipv4Addr, SocketAddr}, sync::{ atomic::{AtomicU16, AtomicU64, Ordering}, Arc, @@ -89,8 +89,8 @@ pub fn tcp() -> (ListenAddr, ConnectAddr) { } let port = PORTS.fetch_add(1, Ordering::Relaxed); ( - ListenAddr::Tcp(SocketAddr::from(([127, 0, 0, 1], port))), - ConnectAddr::Tcp(SocketAddr::from(([127, 0, 0, 1], port))), + ListenAddr::Tcp(SocketAddr::from((Ipv4Addr::LOCALHOST, port))), + ConnectAddr::Tcp(SocketAddr::from((Ipv4Addr::LOCALHOST, port))), ) } @@ -117,7 +117,7 @@ pub fn quic() -> (ListenAddr, ConnectAddr) { let server_config = quinn::ServerConfig::with_single_cert(vec![cert], key) .expect("Server Config Cert/Key failed"); let client_config = quinn::ClientConfig::with_root_certificates(root_store); - use std::net::{IpAddr, Ipv4Addr}; + use std::net::IpAddr; ( ListenAddr::Quic( SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port), diff --git a/network/tests/integration.rs b/network/tests/integration.rs index d49c7a49ef..84d54d6211 100644 --- a/network/tests/integration.rs +++ b/network/tests/integration.rs @@ -262,3 +262,48 @@ fn multiple_try_recv() { assert_eq!(s1_b.try_recv::(), Err(StreamError::StreamClosed)); drop((_n_a, _n_b, _p_a, _p_b)); //clean teardown } + +/// If we listen on a IPv6 UNSPECIFIED address, on linux it will automatically +/// listen on the respective IPv4 address. This must not be as we should behave +/// similar under windows and linux. +/// +/// As most CI servers don't have IPv6 configured, this would return +/// ConnectFailed(Io(Os { code: 99, kind: AddrNotAvailable, message: "Cannot +/// assign requested address" })) we have to disable this test in CI, but it was +/// manually tested on linux and windows +/// +/// On Windows this test must be executed as root to listen on IPv6::UNSPECIFIED +#[test] +#[ignore] +fn listen_on_ipv6_doesnt_block_ipv4() { + let (_, _) = helper::setup(false, 0); + let tcpv4 = tcp(); + let port = if let ListenAddr::Tcp(x) = tcpv4.0 { + x.port() + } else { + unreachable!() + }; + let tcpv6 = ( + ListenAddr::Tcp(std::net::SocketAddr::from(( + std::net::Ipv6Addr::UNSPECIFIED, + port, + ))), + ConnectAddr::Tcp(std::net::SocketAddr::from(( + std::net::Ipv6Addr::UNSPECIFIED, + port, + ))), + ); + + let (_r, _n_a, _p_a, mut s1_a, _n_b, _p_b, mut s1_b) = network_participant_stream(tcpv6); + std::thread::sleep(SLEEP_EXTERNAL); + let (_r2, _n_a2, _p_a2, mut s1_a2, _n_b2, _p_b2, mut s1_b2) = network_participant_stream(tcpv4); + + s1_a.send(42u32).unwrap(); + s1_a2.send(1337u32).unwrap(); + std::thread::sleep(SLEEP_EXTERNAL); + assert_eq!(s1_b.try_recv::(), Ok(Some(42u32))); + assert_eq!(s1_b2.try_recv::(), Ok(Some(1337u32))); + + drop((s1_a, s1_b, _n_a, _n_b, _p_a, _p_b)); + drop((s1_a2, s1_b2, _n_a2, _n_b2, _p_a2, _p_b2)); //clean teardown +} diff --git a/server-cli/src/main.rs b/server-cli/src/main.rs index c49aebd963..b2d3c043d1 100644 --- a/server-cli/src/main.rs +++ b/server-cli/src/main.rs @@ -18,7 +18,7 @@ use crate::{ use common::{clock::Clock, consts::MIN_RECOMMENDED_TOKIO_THREADS}; use common_base::span; use core::sync::atomic::{AtomicUsize, Ordering}; -use server::{persistence::DatabaseSettings, Event, Input, Server}; +use server::{persistence::DatabaseSettings, settings::Protocol, Event, Input, Server}; use std::{ io, sync::{atomic::AtomicBool, mpsc, Arc}, @@ -154,7 +154,11 @@ fn main() -> io::Result<()> { info!("Starting server..."); - let server_port = &server_settings.gameserver_address.port(); + if no_auth { + server_settings.auth_server_address = None; + } + + let protocols_and_addresses = server_settings.gameserver_protocols.clone(); let metrics_port = &server_settings.metrics_address.port(); // Create server let mut server = Server::new( @@ -166,9 +170,21 @@ fn main() -> io::Result<()> { ) .expect("Failed to create server instance!"); + let mut gameserver_addresses = vec![]; + for protocol in protocols_and_addresses { + gameserver_addresses.push(match protocol { + Protocol::Tcp { address } => ("TCP", address), + Protocol::Quic { + address, + cert_file_path: _, + key_file_path: _, + } => ("QUIC", address), + }) + } + info!( - ?server_port, ?metrics_port, + ?gameserver_addresses, "Server is ready to accept connections." ); diff --git a/server/src/lib.rs b/server/src/lib.rs index c3a083f323..1dd69b605a 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -115,6 +115,8 @@ use crate::{ use hashbrown::HashMap; use std::sync::RwLock; +use crate::settings::Protocol; + #[cfg(feature = "plugins")] use { common::uid::UidAllocator, @@ -463,51 +465,75 @@ impl Server { ) .await }); - runtime.block_on(network.listen(ListenAddr::Tcp(settings.gameserver_address)))?; - runtime.block_on(network.listen(ListenAddr::Mpsc(14004)))?; - if let Some(quic) = &settings.quic_files { - use rustls_pemfile::Item; - use std::fs; - match || -> Result<_, Box> { - let key = fs::read(&quic.key)?; - let key = if quic.key.extension().map_or(false, |x| x == "der") { - rustls::PrivateKey(key) - } else { - debug!("convert pem key to der"); - let key = rustls_pemfile::read_all(&mut key.as_slice())? - .into_iter() - .find_map(|item| match item { - Item::RSAKey(v) | Item::PKCS8Key(v) => Some(v), - Item::X509Certificate(_) => None, - }) - .ok_or("No valid pem key in file")?; - rustls::PrivateKey(key) - }; - let cert_chain = fs::read(&quic.cert)?; - let cert_chain = if quic.cert.extension().map_or(false, |x| x == "der") { - vec![rustls::Certificate(cert_chain)] - } else { - debug!("convert pem cert to der"); - let certs = rustls_pemfile::certs(&mut cert_chain.as_slice())?; - certs.into_iter().map(rustls::Certificate).collect() - }; - let server_config = quinn::ServerConfig::with_single_cert(cert_chain, key)?; - Ok(server_config) - }() { - Ok(server_config) => { - warn!( - "QUIC is enabled. This is experimental and not recommended in production" - ); - runtime.block_on( - network - .listen(ListenAddr::Quic(settings.gameserver_address, server_config)), - )?; + + let mut printed_quic_warning = false; + for protocol in &settings.gameserver_protocols { + match protocol { + Protocol::Tcp { address } => { + runtime.block_on(network.listen(ListenAddr::Tcp(*address)))?; }, - Err(e) => { - error!(?e, ?settings.quic_files, "Failed to load Quic Certificate, run without Quic") + Protocol::Quic { + address, + cert_file_path, + key_file_path, + } => { + use rustls_pemfile::Item; + use std::fs; + + match || -> Result<_, Box> { + let key = fs::read(&key_file_path)?; + let key = if key_file_path.extension().map_or(false, |x| x == "der") { + rustls::PrivateKey(key) + } else { + debug!("convert pem key to der"); + let key = rustls_pemfile::read_all(&mut key.as_slice())? + .into_iter() + .find_map(|item| match item { + Item::RSAKey(v) | Item::PKCS8Key(v) => Some(v), + Item::X509Certificate(_) => None, + }) + .ok_or("No valid pem key in file")?; + rustls::PrivateKey(key) + }; + let cert_chain = fs::read(&cert_file_path)?; + let cert_chain = if cert_file_path.extension().map_or(false, |x| x == "der") + { + vec![rustls::Certificate(cert_chain)] + } else { + debug!("convert pem cert to der"); + let certs = rustls_pemfile::certs(&mut cert_chain.as_slice())?; + certs.into_iter().map(rustls::Certificate).collect() + }; + let server_config = quinn::ServerConfig::with_single_cert(cert_chain, key)?; + Ok(server_config) + }() { + Ok(server_config) => { + runtime.block_on( + network.listen(ListenAddr::Quic(*address, server_config.clone())), + )?; + + if !printed_quic_warning { + warn!( + "QUIC is enabled. This is experimental and not recommended in \ + production" + ); + printed_quic_warning = true; + } + }, + Err(e) => { + error!( + ?e, + "Failed to load the TLS certificate, running without QUIC {}", + *address + ); + }, + } }, } } + + runtime.block_on(network.listen(ListenAddr::Mpsc(14004)))?; + let connection_handler = ConnectionHandler::new(network, &runtime); // Initiate real-time world simulation diff --git a/server/src/settings.rs b/server/src/settings.rs index d23fd7698d..ad17a09837 100644 --- a/server/src/settings.rs +++ b/server/src/settings.rs @@ -23,7 +23,7 @@ use portpicker::pick_unused_port; use serde::{Deserialize, Serialize}; use std::{ fs, - net::SocketAddr, + net::{Ipv4Addr, Ipv6Addr, SocketAddr}, path::{Path, PathBuf}, }; use tracing::{error, warn}; @@ -37,12 +37,6 @@ const BANLIST_FILENAME: &str = "banlist.ron"; const SERVER_DESCRIPTION_FILENAME: &str = "description.ron"; const ADMINS_FILENAME: &str = "admins.ron"; -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct X509FilePair { - pub cert: PathBuf, - pub key: PathBuf, -} - #[derive(Copy, Clone, Debug, Deserialize, Serialize)] pub enum ServerBattleMode { Global(BattleMode), @@ -65,6 +59,18 @@ impl ServerBattleMode { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Protocol { + Quic { + address: SocketAddr, + cert_file_path: PathBuf, + key_file_path: PathBuf, + }, + Tcp { + address: SocketAddr, + }, +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub enum CalendarMode { None, @@ -91,10 +97,9 @@ impl CalendarMode { #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default)] pub struct Settings { - pub gameserver_address: SocketAddr, + pub gameserver_protocols: Vec, pub metrics_address: SocketAddr, pub auth_server_address: Option, - pub quic_files: Option, pub max_players: usize, pub world_seed: u32, pub battle_mode: ServerBattleMode, @@ -121,10 +126,16 @@ pub struct Settings { impl Default for Settings { fn default() -> Self { Self { - gameserver_address: SocketAddr::from(([0; 4], 14004)), - metrics_address: SocketAddr::from(([0; 4], 14005)), + gameserver_protocols: vec![ + Protocol::Tcp { + address: SocketAddr::from((Ipv6Addr::UNSPECIFIED, 14004)), + }, + Protocol::Tcp { + address: SocketAddr::from((Ipv4Addr::UNSPECIFIED, 14004)), + }, + ], + metrics_address: SocketAddr::from((Ipv4Addr::LOCALHOST, 14005)), auth_server_address: Some("https://auth.veloren.net".into()), - quic_files: None, world_seed: DEFAULT_WORLD_SEED, server_name: "Veloren Alpha".into(), max_players: 100, @@ -195,18 +206,19 @@ impl Settings { pub fn singleplayer(path: &Path) -> Self { let load = Self::load(path); Self { - //BUG: theoretically another process can grab the port between here and server - // creation, however the timewindow is quite small - gameserver_address: SocketAddr::from(( - [127, 0, 0, 1], - pick_unused_port().expect("Failed to find unused port!"), - )), + // BUG: theoretically another process can grab the port between here and server + // creation, however the time window is quite small. + gameserver_protocols: vec![Protocol::Tcp { + address: SocketAddr::from(( + Ipv4Addr::LOCALHOST, + pick_unused_port().expect("Failed to find unused port!"), + )), + }], metrics_address: SocketAddr::from(( - [127, 0, 0, 1], + Ipv4Addr::LOCALHOST, pick_unused_port().expect("Failed to find unused port!"), )), auth_server_address: None, - quic_files: None, // If loading the default map file, make sure the seed is also default. world_seed: if load.map_file.is_some() { load.world_seed