diff --git a/Cargo.lock b/Cargo.lock index 50e3df519a..5995e4cc28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5477,6 +5477,7 @@ dependencies = [ "hashbrown", "image", "num 0.4.0", + "quinn", "rayon", "ron", "rustyline", @@ -5745,6 +5746,7 @@ dependencies = [ "portpicker", "prometheus", "prometheus-hyper", + "quinn", "rand 0.8.3", "rand_distr", "rayon", diff --git a/client/Cargo.toml b/client/Cargo.toml index e0113e12f2..02861fba46 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -18,11 +18,12 @@ common-base = { package = "veloren-common-base", path = "../common/base" } common-state = { package = "veloren-common-state", path = "../common/state", default-features = false } common-systems = { package = "veloren-common-systems", path = "../common/systems", default-features = false } common-net = { package = "veloren-common-net", path = "../common/net" } -network = { package = "veloren-network", path = "../network", features = ["compression"], default-features = false } +network = { package = "veloren-network", path = "../network", features = ["compression","quic"], default-features = false } byteorder = "1.3.2" futures-util = "0.3.7" tokio = { version = "1", default-features = false, features = ["rt-multi-thread"] } +quinn = "0.7.2" image = { version = "0.23.12", default-features = false, features = ["png"] } num = "0.4" tracing = { version = "0.1", default-features = false } diff --git a/client/src/addr.rs b/client/src/addr.rs index fa8c871aea..6002ce4a4c 100644 --- a/client/src/addr.rs +++ b/client/src/addr.rs @@ -4,47 +4,80 @@ use tracing::trace; #[derive(Clone, Debug)] pub enum ConnectionArgs { - IpAndPort(Vec<SocketAddr>), + ///hostname: (hostname|ip):[<port>] + Quic { + hostname: String, + prefer_ipv6: bool, + }, + ///hostname: (hostname|ip):[<port>] + Tcp { + hostname: String, + prefer_ipv6: bool, + }, Mpsc(u64), } impl ConnectionArgs { const DEFAULT_PORT: u16 = 14004; +} - /// Parse ip address or resolves hostname. - /// Note: If you use an ipv6 address, the number after the last - /// colon will be used as the port unless you use [] around the address. - pub async fn resolve( - /* <hostname/ip>:[<port>] */ server_address: &str, - prefer_ipv6: bool, - ) -> Result<Self, std::io::Error> { - // `lookup_host` will internally try to parse it as a SocketAddr - // 1. Assume it's a hostname + port - match lookup_host(server_address).await { - Ok(s) => { - trace!("Host lookup succeeded"); - Ok(Self::sort_ipv6(s, prefer_ipv6)) - }, - Err(e) => { - // 2. Assume its a hostname without port - match lookup_host((server_address, Self::DEFAULT_PORT)).await { - Ok(s) => { - trace!("Host lookup without ports succeeded"); - Ok(Self::sort_ipv6(s, prefer_ipv6)) - }, - Err(_) => Err(e), // Todo: evaluate returning both errors - } +/// Parse ip address or resolves hostname. +/// Note: If you use an ipv6 address, the number after the last +/// colon will be used as the port unless you use [] around the address. +pub(crate) async fn resolve( + address: &str, + prefer_ipv6: bool, +) -> Result<Vec<SocketAddr>, std::io::Error> { + // `lookup_host` will internally try to parse it as a SocketAddr + // 1. Assume it's a hostname + port + match lookup_host(address).await { + Ok(s) => { + trace!("Host lookup succeeded"); + Ok(sort_ipv6(s, prefer_ipv6)) + }, + Err(e) => { + // 2. Assume its a hostname without port + match lookup_host((address, ConnectionArgs::DEFAULT_PORT)).await { + Ok(s) => { + trace!("Host lookup without ports succeeded"); + Ok(sort_ipv6(s, prefer_ipv6)) + }, + Err(_) => Err(e), // Todo: evaluate returning both errors + } + }, + } +} + +pub(crate) async fn try_connect<F>( + network: &network::Network, + address: &str, + prefer_ipv6: bool, + f: F, +) -> Result<network::Participant, crate::error::Error> +where + F: Fn(std::net::SocketAddr) -> network::ConnectAddr, +{ + use crate::error::Error; + let mut participant = None; + for addr in resolve(&address, prefer_ipv6) + .await + .map_err(|e| Error::HostnameLookupFailed(e))? + { + match network.connect(f(addr)).await { + Ok(p) => { + participant = Some(Ok(p)); + break; }, + Err(e) => participant = Some(Err(Error::NetworkErr(e))), } } + participant.unwrap_or_else(|| Err(Error::Other("No Ip Addr provided".to_string()))) +} - fn sort_ipv6(s: impl Iterator<Item = SocketAddr>, prefer_ipv6: bool) -> Self { - let (mut first_addrs, mut second_addrs) = - s.partition::<Vec<_>, _>(|a| a.is_ipv6() == prefer_ipv6); - let addr = std::iter::Iterator::chain(first_addrs.drain(..), second_addrs.drain(..)) - .collect::<Vec<_>>(); - ConnectionArgs::IpAndPort(addr) - } +fn sort_ipv6(s: impl Iterator<Item = SocketAddr>, prefer_ipv6: bool) -> Vec<SocketAddr> { + let (mut first_addrs, mut second_addrs) = + s.partition::<Vec<_>, _>(|a| a.is_ipv6() == prefer_ipv6); + std::iter::Iterator::chain(first_addrs.drain(..), second_addrs.drain(..)).collect::<Vec<_>>() } #[cfg(test)] diff --git a/client/src/error.rs b/client/src/error.rs index a9079d77d6..c5021ec2e5 100644 --- a/client/src/error.rs +++ b/client/src/error.rs @@ -18,6 +18,7 @@ pub enum Error { AuthClientError(AuthClientError), AuthServerUrlInvalid(String), AuthServerNotTrusted, + HostnameLookupFailed(std::io::Error), Banned(String), /// Persisted character data is invalid or missing InvalidCharacter, diff --git a/client/src/lib.rs b/client/src/lib.rs index 0d04801bb5..aaf0a342b9 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -203,7 +203,6 @@ pub struct CharacterList { } impl Client { - /// Create a new `Client`. pub async fn new( addr: ConnectionArgs, view_distance: Option<u32>, @@ -214,20 +213,23 @@ impl Client { let network = Network::new(Pid::new(), &runtime); let participant = match addr { - ConnectionArgs::IpAndPort(addrs) => { - // Try to connect to all IP's and return the first that works - let mut participant = None; - for addr in addrs { - match network.connect(ConnectAddr::Tcp(addr)).await { - Ok(p) => { - participant = Some(Ok(p)); - break; - }, - Err(e) => participant = Some(Err(Error::NetworkErr(e))), - } - } - participant - .unwrap_or_else(|| Err(Error::Other("No Ip Addr provided".to_string())))? + ConnectionArgs::Tcp { + hostname, + prefer_ipv6, + } => { + addr::try_connect(&network, &hostname, prefer_ipv6, |a| ConnectAddr::Tcp(a)).await? + }, + ConnectionArgs::Quic { + hostname, + prefer_ipv6, + } => { + let mut config = quinn::ClientConfigBuilder::default(); + config.protocols(&[b"VELOREN"]); + let config = config.build(); + addr::try_connect(&network, &hostname, prefer_ipv6, |a| { + ConnectAddr::Quic(a, config.clone(), hostname.clone()) + }) + .await? }, ConnectionArgs::Mpsc(id) => network.connect(ConnectAddr::Mpsc(id)).await?, }; diff --git a/server/Cargo.toml b/server/Cargo.toml index e6c61d8516..4f24502bd5 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -20,7 +20,7 @@ common-state = { package = "veloren-common-state", path = "../common/state" } common-systems = { package = "veloren-common-systems", path = "../common/systems" } common-net = { package = "veloren-common-net", path = "../common/net" } world = { package = "veloren-world", path = "../world" } -network = { package = "veloren-network", path = "../network", features = ["metrics", "compression"], default-features = false } +network = { package = "veloren-network", path = "../network", features = ["metrics", "compression", "quic"], default-features = false } # inline_tweak = "1.0.8" @@ -33,6 +33,7 @@ vek = { version = "0.14.1", features = ["serde"] } futures-util = "0.3.7" tokio = { version = "1", default-features = false, features = ["rt"] } prometheus-hyper = "0.1.2" +quinn = "0.7.2" atomicwrites = "0.3.0" chrono = { version = "0.4.9", features = ["serde"] } humantime = "2.1.0" diff --git a/server/src/lib.rs b/server/src/lib.rs index eba8a14299..c773a4e14c 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -398,6 +398,38 @@ impl Server { }); 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 std::fs; + match || -> Result<_, Box<dyn std::error::Error>> { + let mut server_config = + quinn::ServerConfigBuilder::new(quinn::ServerConfig::default()); + server_config.protocols(&[b"VELOREN"]); + let key = fs::read(&quic.key)?; + let key = if quic.key.extension().map_or(false, |x| x == "der") { + quinn::PrivateKey::from_der(&key)? + } else { + quinn::PrivateKey::from_pem(&key)? + }; + let cert_chain = fs::read(&quic.cert)?; + let cert_chain = if quic.cert.extension().map_or(false, |x| x == "der") { + quinn::CertificateChain::from_certs(Some( + quinn::Certificate::from_der(&cert_chain).unwrap(), + )) + } else { + quinn::CertificateChain::from_pem(&cert_chain)? + }; + server_config.certificate(cert_chain, key)?; + Ok(server_config.build()) + }() { + Ok(server_config) => { + runtime.block_on( + network + .listen(ListenAddr::Quic(settings.gameserver_address, server_config)), + )?; + }, + Err(e) => error!(?e, "Failed to load Quic Certificate, run without Quic"), + } + } 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 1424af9fe5..5c819381d1 100644 --- a/server/src/settings.rs +++ b/server/src/settings.rs @@ -33,12 +33,19 @@ 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(Clone, Debug, Serialize, Deserialize)] #[serde(default)] pub struct Settings { pub gameserver_address: SocketAddr, pub metrics_address: SocketAddr, pub auth_server_address: Option<String>, + pub quic_files: Option<X509FilePair>, pub max_players: usize, pub world_seed: u32, //pub pvp_enabled: bool, @@ -62,6 +69,7 @@ impl Default for Settings { gameserver_address: SocketAddr::from(([0; 4], 14004)), metrics_address: SocketAddr::from(([0; 4], 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, @@ -140,6 +148,7 @@ impl Settings { 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 diff --git a/voxygen/src/menu/main/client_init.rs b/voxygen/src/menu/main/client_init.rs index de69abc5a5..5decad8a2d 100644 --- a/voxygen/src/menu/main/client_init.rs +++ b/voxygen/src/menu/main/client_init.rs @@ -31,12 +31,6 @@ pub enum Msg { Done(Result<Client, Error>), } -pub enum ClientConnArgs { - Host(String), - #[allow(dead_code)] //singleplayer - Resolved(ConnectionArgs), -} - pub struct AuthTrust(String, bool); // Used to asynchronously parse the server address, resolve host names, @@ -51,7 +45,7 @@ impl ClientInit { #[allow(clippy::op_ref)] // TODO: Pending review in #587 #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 pub fn new( - connection_args: ClientConnArgs, + connection_args: ConnectionArgs, username: String, view_distance: Option<u32>, password: String, @@ -89,18 +83,6 @@ impl ClientInit { .unwrap_or(false) }; - let connection_args = match connection_args { - ClientConnArgs::Host(host) => match ConnectionArgs::resolve(&host, false).await { - Ok(r) => r, - Err(_) => { - let _ = tx.send(Msg::Done(Err(Error::NoAddress))); - tokio::task::block_in_place(move || drop(runtime2)); - return; - }, - }, - ClientConnArgs::Resolved(r) => r, - }; - let mut last_err = None; const FOUR_MINUTES_RETRIES: u64 = 48; diff --git a/voxygen/src/menu/main/mod.rs b/voxygen/src/menu/main/mod.rs index dac729c725..b92a2d9171 100644 --- a/voxygen/src/menu/main/mod.rs +++ b/voxygen/src/menu/main/mod.rs @@ -5,22 +5,18 @@ use super::char_selection::CharSelectionState; #[cfg(feature = "singleplayer")] use crate::singleplayer::Singleplayer; use crate::{ - i18n::{Localization, LocalizationHandle}, - render::Renderer, - settings::Settings, - window::Event, - Direction, GlobalState, PlayState, PlayStateResult, + i18n::LocalizationHandle, render::Renderer, settings::Settings, window::Event, Direction, + GlobalState, PlayState, PlayStateResult, }; -#[cfg(feature = "singleplayer")] -use client::addr::ConnectionArgs; use client::{ + addr::ConnectionArgs, error::{InitProtocolError, NetworkConnectError, NetworkError}, ServerInfo, }; -use client_init::{ClientConnArgs, ClientInit, Error as InitError, Msg as InitMsg}; +use client_init::{ClientInit, Error as InitError, Msg as InitMsg}; use common::comp; use common_base::span; -use std::{fmt::Debug, sync::Arc}; +use std::sync::Arc; use tokio::runtime; use tracing::error; use ui::{Event as MainMenuEvent, MainMenuUi}; @@ -78,7 +74,7 @@ impl PlayState for MainMenuState { &mut global_state.info_message, "singleplayer".to_owned(), "".to_owned(), - ClientConnArgs::Resolved(ConnectionArgs::Mpsc(14004)), + ConnectionArgs::Mpsc(14004), &mut self.client_init, Some(runtime), ); @@ -120,117 +116,14 @@ impl PlayState for MainMenuState { std::rc::Rc::new(std::cell::RefCell::new(client)), ))); }, - Some(InitMsg::Done(Err(err))) => { - let localized_strings = global_state.i18n.read(); + Some(InitMsg::Done(Err(e))) => { self.client_init = None; - global_state.info_message = Some({ - let err = match err { - InitError::NoAddress => { - localized_strings.get("main.login.server_not_found").into() - }, - InitError::ClientError { - error, - mismatched_server_info, - } => match error { - client::Error::SpecsErr(e) => format!( - "{}: {}", - localized_strings.get("main.login.internal_error"), - e - ), - client::Error::AuthErr(e) => format!( - "{}: {}", - localized_strings.get("main.login.authentication_error"), - e - ), - client::Error::Kicked(e) => e, - client::Error::TooManyPlayers => { - localized_strings.get("main.login.server_full").into() - }, - client::Error::AuthServerNotTrusted => localized_strings - .get("main.login.untrusted_auth_server") - .into(), - client::Error::ServerWentMad => localized_strings - .get("main.login.outdated_client_or_server") - .into(), - client::Error::ServerTimeout => { - localized_strings.get("main.login.timeout").into() - }, - client::Error::ServerShutdown => { - localized_strings.get("main.login.server_shut_down").into() - }, - client::Error::NotOnWhitelist => { - localized_strings.get("main.login.not_on_whitelist").into() - }, - client::Error::Banned(reason) => format!( - "{}: {}", - localized_strings.get("main.login.banned"), - reason - ), - client::Error::InvalidCharacter => { - localized_strings.get("main.login.invalid_character").into() - }, - client::Error::NetworkErr(NetworkError::ConnectFailed( - NetworkConnectError::Handshake(InitProtocolError::WrongVersion(_)), - )) => get_network_error_text( - &localized_strings, - localized_strings.get("main.login.network_wrong_version"), - mismatched_server_info, - ), - client::Error::NetworkErr(e) => get_network_error_text( - &localized_strings, - e, - mismatched_server_info, - ), - client::Error::ParticipantErr(e) => get_network_error_text( - &localized_strings, - e, - mismatched_server_info, - ), - client::Error::StreamErr(e) => get_network_error_text( - &localized_strings, - e, - mismatched_server_info, - ), - client::Error::Other(e) => { - format!("{}: {}", localized_strings.get("common.error"), e) - }, - client::Error::AuthClientError(e) => match e { - // TODO: remove parentheses - client::AuthClientError::RequestError(e) => format!( - "{}: {}", - localized_strings.get("main.login.failed_sending_request"), - e - ), - client::AuthClientError::JsonError(e) => format!( - "{}: {}", - localized_strings.get("main.login.failed_sending_request"), - e - ), - client::AuthClientError::InsecureSchema => localized_strings - .get("main.login.insecure_auth_scheme") - .into(), - client::AuthClientError::ServerError(_, e) => { - String::from_utf8_lossy(&e).to_string() - }, - }, - client::Error::AuthServerUrlInvalid(e) => { - format!( - "{}: https://{}", - localized_strings - .get("main.login.failed_auth_server_url_invalid"), - e - ) - }, - }, - InitError::ClientCrashed => { - localized_strings.get("main.login.client_crashed").into() - }, - }; - // Log error for possible additional use later or incase that the error - // displayed is cut of. - error!("{}", err); - err - }); + tracing::trace!(?e, "raw Client Init error"); + let e = get_client_msg_error(e, &global_state.i18n); + // Log error for possible additional use later or incase that the error + // displayed is cut of. + error!(?e, "Client Init failed"); + global_state.info_message = Some(e); }, Some(InitMsg::IsAuthTrusted(auth_server)) => { if global_state @@ -264,6 +157,7 @@ impl PlayState for MainMenuState { server_address, } => { let mut net_settings = &mut global_state.settings.networking; + let use_quic = net_settings.use_quic; net_settings.username = username.clone(); net_settings.default_server = server_address.clone(); if !net_settings.servers.contains(&server_address) { @@ -271,12 +165,23 @@ impl PlayState for MainMenuState { } global_state.settings.save_to_file_warn(); + let connection_args = if use_quic { + ConnectionArgs::Quic { + hostname: server_address, + prefer_ipv6: false, + } + } else { + ConnectionArgs::Tcp { + hostname: server_address, + prefer_ipv6: false, + } + }; attempt_login( &mut global_state.settings, &mut global_state.info_message, username, password, - ClientConnArgs::Host(server_address), + connection_args, &mut self.client_init, None, ); @@ -347,37 +252,115 @@ impl PlayState for MainMenuState { } } -/// When a network error is received and there is a mismatch between the client -/// and server version it is almost definitely due to this mismatch rather than -/// a true networking error. -fn get_network_error_text( - localization: &Localization, - error: impl Debug, - mismatched_server_info: Option<ServerInfo>, -) -> String { - if let Some(server_info) = mismatched_server_info { - format!( - "{} {}: {} {}: {}", - localization.get("main.login.network_wrong_version"), - localization.get("main.login.client_version"), - common::util::GIT_HASH.to_string(), - localization.get("main.login.server_version"), - server_info.git_hash - ) - } else { - format!( - "{}: {:?}", - localization.get("main.login.network_error"), - error - ) +fn get_client_msg_error(e: client_init::Error, localized_strings: &LocalizationHandle) -> String { + let localization = localized_strings.read(); + + // When a network error is received and there is a mismatch between the client + // and server version it is almost definitely due to this mismatch rather than + // a true networking error. + let net_e = |error: String, mismatched_server_info: Option<ServerInfo>| -> String { + if let Some(server_info) = mismatched_server_info { + format!( + "{} {}: {} {}: {}", + localization.get("main.login.network_wrong_version"), + localization.get("main.login.client_version"), + common::util::GIT_HASH.to_string(), + localization.get("main.login.server_version"), + server_info.git_hash + ) + } else { + format!( + "{}: {}", + localization.get("main.login.network_error"), + error + ) + } + }; + + use client::Error; + match e { + InitError::NoAddress => localization.get("main.login.server_not_found").into(), + InitError::ClientError { + error, + mismatched_server_info, + } => match error { + Error::SpecsErr(e) => { + format!("{}: {}", localization.get("main.login.internal_error"), e) + }, + Error::AuthErr(e) => format!( + "{}: {}", + localization.get("main.login.authentication_error"), + e + ), + Error::Kicked(e) => e, + Error::TooManyPlayers => localization.get("main.login.server_full").into(), + Error::AuthServerNotTrusted => { + localization.get("main.login.untrusted_auth_server").into() + }, + Error::ServerWentMad => localization + .get("main.login.outdated_client_or_server") + .into(), + Error::ServerTimeout => localization.get("main.login.timeout").into(), + Error::ServerShutdown => localization.get("main.login.server_shut_down").into(), + Error::NotOnWhitelist => localization.get("main.login.not_on_whitelist").into(), + Error::Banned(reason) => { + format!("{}: {}", localization.get("main.login.banned"), reason) + }, + Error::InvalidCharacter => localization.get("main.login.invalid_character").into(), + Error::NetworkErr(NetworkError::ConnectFailed(NetworkConnectError::Handshake( + InitProtocolError::WrongVersion(_), + ))) => net_e( + localization + .get("main.login.network_wrong_version") + .to_owned(), + mismatched_server_info, + ), + Error::NetworkErr(e) => net_e(e.to_string(), mismatched_server_info), + Error::ParticipantErr(e) => net_e(e.to_string(), mismatched_server_info), + Error::StreamErr(e) => net_e(e.to_string(), mismatched_server_info), + Error::HostnameLookupFailed(e) => { + format!("{}: {}", localization.get("main.login.server_not_found"), e) + }, + Error::Other(e) => { + format!("{}: {}", localization.get("common.error"), e) + }, + Error::AuthClientError(e) => match e { + // TODO: remove parentheses + client::AuthClientError::RequestError(e) => format!( + "{}: {}", + localization.get("main.login.failed_sending_request"), + e + ), + client::AuthClientError::JsonError(e) => format!( + "{}: {}", + localization.get("main.login.failed_sending_request"), + e + ), + client::AuthClientError::InsecureSchema => { + localization.get("main.login.insecure_auth_scheme").into() + }, + client::AuthClientError::ServerError(_, e) => { + String::from_utf8_lossy(&e).to_string() + }, + }, + Error::AuthServerUrlInvalid(e) => { + format!( + "{}: https://{}", + localization.get("main.login.failed_auth_server_url_invalid"), + e + ) + }, + }, + InitError::ClientCrashed => localization.get("main.login.client_crashed").into(), } } + fn attempt_login( settings: &mut Settings, info_message: &mut Option<String>, username: String, password: String, - connection_args: ClientConnArgs, + connection_args: ConnectionArgs, client_init: &mut Option<ClientInit>, runtime: Option<Arc<runtime::Runtime>>, ) { diff --git a/voxygen/src/settings/networking.rs b/voxygen/src/settings/networking.rs index 4732b5dc7e..402fb5f4e4 100644 --- a/voxygen/src/settings/networking.rs +++ b/voxygen/src/settings/networking.rs @@ -9,6 +9,7 @@ pub struct NetworkingSettings { pub servers: Vec<String>, pub default_server: String, pub trusted_auth_servers: HashSet<String>, + pub use_quic: bool, } impl Default for NetworkingSettings { @@ -21,6 +22,7 @@ impl Default for NetworkingSettings { .iter() .map(|s| s.to_string()) .collect(), + use_quic: false, } } }