diff --git a/Cargo.lock b/Cargo.lock index e46a0808ad..93260f1f49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1718,7 +1718,7 @@ dependencies = [ "tokio", "tracing", "url", - "winreg", + "winreg 0.51.0", ] [[package]] @@ -1847,6 +1847,18 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" +[[package]] +name = "enum-as-inner" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" +dependencies = [ + "heck 0.4.1", + "proc-macro2 1.0.78", + "quote 1.0.35", + "syn 2.0.48", +] + [[package]] name = "enum-iterator" version = "0.7.0" @@ -2750,6 +2762,51 @@ dependencies = [ "rayon", ] +[[package]] +name = "hickory-proto" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091a6fbccf4860009355e3efc52ff4acf37a63489aad7435372d44ceeb6fbbcf" +dependencies = [ + "async-trait", + "cfg-if 1.0.0", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.4.0", + "ipnet", + "once_cell", + "rand 0.8.5", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35b8f021164e6a984c9030023544c57789c51760065cd510572fedcfb04164e8" +dependencies = [ + "cfg-if 1.0.0", + "futures-util", + "hickory-proto", + "ipconfig", + "lru-cache", + "once_cell", + "parking_lot 0.12.1", + "rand 0.8.5", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "tracing", +] + [[package]] name = "home" version = "0.5.9" @@ -2759,6 +2816,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + [[package]] name = "http" version = "0.2.11" @@ -2937,6 +3005,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.5.0" @@ -3070,6 +3148,24 @@ dependencies = [ "mach2", ] +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2", + "widestring", + "windows-sys 0.48.0", + "winreg 0.50.0", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "is-terminal" version = "0.4.10" @@ -3399,6 +3495,15 @@ dependencies = [ "hashbrown 0.14.3", ] +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "lz-fear" version = "0.1.1" @@ -3455,6 +3560,12 @@ dependencies = [ "libc", ] +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + [[package]] name = "matchers" version = "0.1.0" @@ -4695,6 +4806,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quick-xml" version = "0.30.0" @@ -5103,6 +5220,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "216080ab382b992234dda86873c18d4c48358f5cfcb70fd693d7f6f2131b628b" +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + [[package]] name = "ring" version = "0.16.20" @@ -6778,7 +6905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", - "idna", + "idna 0.5.0", "percent-encoding", ] @@ -6852,11 +6979,13 @@ dependencies = [ "byteorder", "clap", "hashbrown 0.13.2", + "hickory-resolver", "image", "num 0.4.1", "quinn", "rayon", "ron", + "rustls", "rustyline", "serde", "specs", @@ -8423,6 +8552,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if 1.0.0", + "windows-sys 0.48.0", +] + [[package]] name = "winreg" version = "0.51.0" diff --git a/client/Cargo.toml b/client/Cargo.toml index 5f4f252810..1a8714eeb3 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -23,7 +23,9 @@ network = { package = "veloren-network", path = "../network", features = ["compr byteorder = "1.3.2" tokio = { workspace = true, features = ["rt-multi-thread"] } -quinn = "0.10" +quinn = { version = "0.10", features = ["rustls"] } +rustls = { version = "0.21.6", features = ["dangerous_configuration"] } +hickory-resolver = { version = "0.24.0", features = ["system-config", "tokio-runtime"] } image = { workspace = true } num = { workspace = true } tracing = { workspace = true } diff --git a/client/src/addr.rs b/client/src/addr.rs index 01c1e0e92b..e3f4281735 100644 --- a/client/src/addr.rs +++ b/client/src/addr.rs @@ -8,12 +8,24 @@ pub enum ConnectionArgs { Quic { hostname: String, prefer_ipv6: bool, + validate_tls: bool, }, ///hostname: (hostname|ip):[] Tcp { hostname: String, prefer_ipv6: bool, }, + /// SRV lookup + /// + /// SRV lookups can not contain a port, but will be able to connect to + /// configured port automatically. If a connection with a port is given, + /// this will gracefully fall back to TCP. + Srv { + hostname: String, + prefer_ipv6: bool, + validate_tls: bool, + use_quic: bool, + }, Mpsc(u64), } @@ -51,6 +63,7 @@ pub(crate) async fn resolve( pub(crate) async fn try_connect( network: &network::Network, address: &str, + override_port: Option, prefer_ipv6: bool, f: F, ) -> Result @@ -59,10 +72,15 @@ where { use crate::error::Error; let mut participant = None; - for addr in resolve(address, prefer_ipv6) + for mut addr in resolve(address, prefer_ipv6) .await .map_err(Error::HostnameLookupFailed)? { + // Override the port if one was passed. Used for SRV lookups which get port info + // out-of-band + if let Some(port) = override_port { + addr.set_port(port); + } match network.connect(f(addr)).await { Ok(p) => { participant = Some(Ok(p)); diff --git a/client/src/lib.rs b/client/src/lib.rs index 17ca930b22..6f7f41731e 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -68,16 +68,22 @@ use common_state::State; use common_systems::add_local_systems; use comp::BuffKind; use hashbrown::{HashMap, HashSet}; +use hickory_resolver::{ + config::{ResolverConfig, ResolverOpts}, + AsyncResolver, +}; use image::DynamicImage; use network::{ConnectAddr, Network, Participant, Pid, Stream}; use num::traits::FloatConst; use rayon::prelude::*; +use rustls::client::ServerCertVerified; use specs::Component; use std::{ collections::{BTreeMap, VecDeque}, + fmt::Debug, mem, sync::Arc, - time::{Duration, Instant}, + time::{Duration, Instant, SystemTime}, }; use tokio::runtime::Runtime; use tracing::{debug, error, trace, warn}; @@ -327,6 +333,50 @@ pub struct CharacterList { pub loading: bool, } +async fn connect_quic( + network: &Network, + hostname: String, + override_port: Option, + prefer_ipv6: bool, + validate_tls: bool, +) -> Result { + let config = if validate_tls { + quinn::ClientConfig::with_native_roots() + } else { + warn!( + "skipping validation of server identity. There is no guarantee that the server you're \ + connected to is the one you expect to be connecting to." + ); + struct Verifier; + impl rustls::client::ServerCertVerifier for Verifier { + fn verify_server_cert( + &self, + _: &rustls::Certificate, + _: &[rustls::Certificate], + _: &rustls::ServerName, + _: &mut dyn Iterator, + _: &[u8], + _: SystemTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + } + + let mut cfg = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_custom_certificate_verifier(Arc::new(Verifier)) + .with_no_client_auth(); + cfg.enable_early_data = true; + + quinn::ClientConfig::new(Arc::new(cfg)) + }; + + addr::try_connect(network, &hostname, override_port, prefer_ipv6, |a| { + ConnectAddr::Quic(a, config.clone(), hostname.clone()) + }) + .await +} + impl Client { pub async fn new( addr: ConnectionArgs, @@ -343,24 +393,132 @@ impl Client { let network = Network::new(Pid::new(), &runtime); init_stage_update(ClientInitStage::ConnectionEstablish); + let mut participant = match addr { + ConnectionArgs::Srv { + hostname, + prefer_ipv6, + validate_tls, + use_quic, + } => { + // Try to create a resolver backed by /etc/resolv.conf or the Windows Registry + // first. If that fails, create a resolver being hard-coded to + // Google's 8.8.8.8 public resolver. + let resolver = AsyncResolver::tokio_from_system_conf().unwrap_or_else(|error| { + error!("Failed to create DNS resolver using system configuration: {error:?}"); + warn!("Falling back to a default configured resolver."); + AsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default()) + }); + + let quic_service_host = format!("_veloren._udp.{hostname}"); + let quic_lookup_future = resolver.srv_lookup(quic_service_host); + let tcp_service_host = format!("_veloren._tcp.{hostname}"); + let tcp_lookup_future = resolver.srv_lookup(tcp_service_host); + let (quic_rr, tcp_rr) = tokio::join!(quic_lookup_future, tcp_lookup_future); + + #[derive(Eq, PartialEq)] + enum ConnMode { + Quic, + Tcp, + } + + // Push the results of both futures into `srv_rr`. This uses map_or_else purely + // for side effects. + let mut srv_rr = Vec::new(); + let () = quic_rr.map_or_else( + |error| { + warn!("QUIC SRV lookup failed: {error:?}"); + }, + |srv_lookup| { + srv_rr.extend(srv_lookup.iter().cloned().map(|srv| (ConnMode::Quic, srv))) + }, + ); + let () = tcp_rr.map_or_else( + |error| { + warn!("TCP SRV lookup failed: {error:?}"); + }, + |srv_lookup| { + srv_rr.extend(srv_lookup.iter().cloned().map(|srv| (ConnMode::Tcp, srv))) + }, + ); + + // SRV records have a priority; lowest priority hosts MUST be contacted first. + let srv_rr_slice = srv_rr.as_mut_slice(); + srv_rr_slice.sort_by_key(|(_, srv)| srv.priority()); + + let mut iter = srv_rr_slice.iter(); + + // This loops exits as soon as the above iter over `srv_rr_slice` is exhausted + loop { + if let Some((conn_mode, srv_rr)) = iter.next() { + let hostname = format!("{}", srv_rr.target()); + let port = Some(srv_rr.port()); + let conn_result = match conn_mode { + ConnMode::Quic => { + connect_quic(&network, hostname, port, prefer_ipv6, validate_tls) + .await + }, + ConnMode::Tcp => { + addr::try_connect( + &network, + &hostname, + port, + prefer_ipv6, + ConnectAddr::Tcp, + ) + .await + }, + }; + match conn_result { + Ok(c) => break c, + Err(error) => { + warn!("Failed to connect to host {}: {error:?}", srv_rr.target()) + }, + } + } else { + warn!( + "No SRV hosts succeeded connection, falling back to direct connection" + ); + // This case is also hit if no SRV host was returned from the query, so we + // check for QUIC/TCP preference. + let c = if use_quic { + connect_quic(&network, hostname, None, prefer_ipv6, validate_tls) + .await? + } else { + match addr::try_connect( + &network, + &hostname, + None, + prefer_ipv6, + ConnectAddr::Tcp, + ) + .await + { + Ok(c) => c, + Err(error) => return Err(error), + } + }; + break c; + } + } + }, ConnectionArgs::Tcp { hostname, prefer_ipv6, - } => addr::try_connect(&network, &hostname, prefer_ipv6, ConnectAddr::Tcp).await?, + } => { + addr::try_connect(&network, &hostname, None, prefer_ipv6, ConnectAddr::Tcp).await? + }, ConnectionArgs::Quic { hostname, prefer_ipv6, + validate_tls, } => { warn!( "QUIC is enabled. This is experimental and you won't be able to connect to \ TCP servers unless deactivated" ); - let config = quinn::ClientConfig::with_native_roots(); - addr::try_connect(&network, &hostname, prefer_ipv6, |a| { - ConnectAddr::Quic(a, config.clone(), hostname.clone()) - }) - .await? + + connect_quic(&network, hostname, None, prefer_ipv6, validate_tls).await? }, ConnectionArgs::Mpsc(id) => network.connect(ConnectAddr::Mpsc(id)).await?, }; diff --git a/voxygen/src/menu/main/mod.rs b/voxygen/src/menu/main/mod.rs index ae728db74d..86cc02230b 100644 --- a/voxygen/src/menu/main/mod.rs +++ b/voxygen/src/menu/main/mod.rs @@ -334,7 +334,9 @@ impl PlayState for MainMenuState { server_address, } => { let net_settings = &mut global_state.settings.networking; + let use_srv = net_settings.use_srv; let use_quic = net_settings.use_quic; + let validate_tls = net_settings.validate_tls; net_settings.username = username.clone(); net_settings.default_server = server_address.clone(); if !net_settings.servers.contains(&server_address) { @@ -344,10 +346,18 @@ impl PlayState for MainMenuState { .settings .save_to_file_warn(&global_state.config_dir); - let connection_args = if use_quic { + let connection_args = if use_srv { + ConnectionArgs::Srv { + hostname: server_address, + prefer_ipv6: false, + validate_tls, + use_quic, + } + } else if use_quic { ConnectionArgs::Quic { hostname: server_address, prefer_ipv6: false, + validate_tls, } } else { ConnectionArgs::Tcp { diff --git a/voxygen/src/settings/networking.rs b/voxygen/src/settings/networking.rs index 6b4af2ef9a..d47219bc4c 100644 --- a/voxygen/src/settings/networking.rs +++ b/voxygen/src/settings/networking.rs @@ -9,7 +9,9 @@ pub struct NetworkingSettings { pub servers: Vec, pub default_server: String, pub trusted_auth_servers: HashSet, + pub use_srv: bool, pub use_quic: bool, + pub validate_tls: bool, pub player_physics_behavior: bool, pub lossy_terrain_compression: bool, pub enable_discord_integration: bool, @@ -25,7 +27,9 @@ impl Default for NetworkingSettings { .iter() .map(|s| s.to_string()) .collect(), + use_srv: true, use_quic: false, + validate_tls: true, player_physics_behavior: false, lossy_terrain_compression: false, enable_discord_integration: true,