From 2d2ffa2b10825bdb3fc98c0e2cadf72b9eb4b7fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nadja=20von=20Reitzenstein=20=C4=8Cerpnjak?= Date: Tue, 6 Feb 2024 14:01:51 +0100 Subject: [PATCH] Add SRV lookup functionality to voxygen This will make voxygen issue a SRV lookup when connecting to a host, allowing server owners to configure non-standard ports for servers and host servers using vanity domains easily. It additionally allows servers to be hosted on both QUIC and TCP at the same time, with the client connecting to the preferred protocol automatically, but gracefully falling back if a connection is not possible. --- Cargo.lock | 143 +++++++++++++++++++++++- client/Cargo.toml | 4 +- client/src/addr.rs | 20 +++- client/src/lib.rs | 172 +++++++++++++++++++++++++++-- voxygen/src/menu/main/mod.rs | 12 +- voxygen/src/settings/networking.rs | 4 + 6 files changed, 343 insertions(+), 12 deletions(-) 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,