From b443e4dd313af99c18ad2d8e685aaf3963fc78b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=A4rtens?= Date: Fri, 7 May 2021 13:46:00 +0200 Subject: [PATCH] Add Quin support, as this is not yet very much tested it needs to be activated in the settings of SERVER and CLIENT. Server: provide a certificate file and key file via the settings. When provided it will then listen on TCP and QUIC, if not provided it will be TCP only. The certificate must be known by the client, so you might get problems with self-signed certificates. ```ron quic_files: Some(( cert: "/home/user/veloren_cert.pem", key: "/home/user/veloren_key.key", )), ``` Client: activate the voxygen settin `use_quic: true` to try to connect to the quic backend of a server. --- Cargo.lock | 2 + client/Cargo.toml | 3 +- client/src/addr.rs | 95 +++++++--- client/src/error.rs | 1 + client/src/lib.rs | 32 ++-- server/Cargo.toml | 3 +- server/src/lib.rs | 32 ++++ server/src/settings.rs | 9 + voxygen/src/menu/main/client_init.rs | 20 +- voxygen/src/menu/main/mod.rs | 273 +++++++++++++-------------- voxygen/src/settings/networking.rs | 2 + 11 files changed, 260 insertions(+), 212 deletions(-) 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), + ///hostname: (hostname|ip):[] + Quic { + hostname: String, + prefer_ipv6: bool, + }, + ///hostname: (hostname|ip):[] + 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( - /* :[] */ server_address: &str, - prefer_ipv6: bool, - ) -> Result { - // `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, 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( + network: &network::Network, + address: &str, + prefer_ipv6: bool, + f: F, +) -> Result +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, prefer_ipv6: bool) -> Self { - let (mut first_addrs, mut second_addrs) = - s.partition::, _>(|a| a.is_ipv6() == prefer_ipv6); - let addr = std::iter::Iterator::chain(first_addrs.drain(..), second_addrs.drain(..)) - .collect::>(); - ConnectionArgs::IpAndPort(addr) - } +fn sort_ipv6(s: impl Iterator, prefer_ipv6: bool) -> Vec { + let (mut first_addrs, mut second_addrs) = + s.partition::, _>(|a| a.is_ipv6() == prefer_ipv6); + std::iter::Iterator::chain(first_addrs.drain(..), second_addrs.drain(..)).collect::>() } #[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, @@ -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> { + 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, + pub quic_files: Option, 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), } -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, 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, -) -> 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| -> 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, username: String, password: String, - connection_args: ClientConnArgs, + connection_args: ConnectionArgs, client_init: &mut Option, runtime: Option>, ) { 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, pub default_server: String, pub trusted_auth_servers: HashSet, + 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, } } }