diff options
| author | Markus Pettersson <markus.pettersson@mullvad.net> | 2025-07-09 15:52:35 +0200 |
|---|---|---|
| committer | Markus Pettersson <markus.pettersson@mullvad.net> | 2025-07-09 15:52:35 +0200 |
| commit | ab2cf79c4b36e7ecf0485e57c89b50738b4fc482 (patch) | |
| tree | 9efb23c08d38004266e2459e7e8876dfa6f7620b | |
| parent | 1a3c9c16b64eb8c30db150a2719a1ab028de9cab (diff) | |
| parent | 1d874a65d41ff343f7308da8ad4e3c20f5b67264 (diff) | |
| download | mullvadvpn-ab2cf79c4b36e7ecf0485e57c89b50738b4fc482.tar.xz mullvadvpn-ab2cf79c4b36e7ecf0485e57c89b50738b4fc482.zip | |
Merge branch 'add-quic-to-relay-selector-des-2265'
| -rw-r--r-- | Cargo.lock | 1 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | docs/relay-selector.md | 7 | ||||
| -rw-r--r-- | mullvad-api/src/bin/relay_list.rs | 6 | ||||
| -rw-r--r-- | mullvad-api/src/relay_list.rs | 23 | ||||
| -rw-r--r-- | mullvad-ios/src/tunnel_obfuscator_proxy/mod.rs | 10 | ||||
| -rw-r--r-- | mullvad-management-interface/src/types/conversions/relay_list.rs | 11 | ||||
| -rw-r--r-- | mullvad-masque-proxy/src/client/mod.rs | 49 | ||||
| -rw-r--r-- | mullvad-relay-selector/src/relay_selector/helpers.rs | 22 | ||||
| -rw-r--r-- | mullvad-relay-selector/src/relay_selector/matcher.rs | 7 | ||||
| -rw-r--r-- | mullvad-relay-selector/src/relay_selector/mod.rs | 22 | ||||
| -rw-r--r-- | mullvad-relay-selector/src/relay_selector/query.rs | 22 | ||||
| -rw-r--r-- | mullvad-relay-selector/tests/relay_selector.rs | 60 | ||||
| -rw-r--r-- | mullvad-types/src/relay_list.rs | 117 | ||||
| -rw-r--r-- | talpid-types/src/net/obfuscation.rs | 6 | ||||
| -rw-r--r-- | talpid-wireguard/src/obfuscation.rs | 53 | ||||
| -rw-r--r-- | tunnel-obfuscation/Cargo.toml | 1 | ||||
| -rw-r--r-- | tunnel-obfuscation/src/quic.rs | 173 |
18 files changed, 483 insertions, 108 deletions
diff --git a/Cargo.lock b/Cargo.lock index 482657c481..741615496b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6139,6 +6139,7 @@ dependencies = [ "shadowsocks", "thiserror 2.0.9", "tokio", + "tokio-util 0.7.10", "udp-over-tcp", ] diff --git a/Cargo.toml b/Cargo.toml index e6c6d1eb04..41e1089912 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,6 +84,7 @@ implicit_clone = "warn" [workspace.dependencies] tokio = { version = "1.44" } +tokio-util = "0.7" parity-tokio-ipc = "0.9" futures = "0.3.15" vec1 = "1.12" diff --git a/docs/relay-selector.md b/docs/relay-selector.md index 3f74ffd6ae..f400a632a0 100644 --- a/docs/relay-selector.md +++ b/docs/relay-selector.md @@ -56,7 +56,7 @@ constraints the following default ones will take effect - The first attempt will connect to a Wireguard relay on a random port - The second attempt will connect to a Wireguard relay over IPv6 (if IPv6 is configured on the host) on a random port - The third attempt will connect to a Wireguard relay on a random port using Shadowsocks for obfuscation -- The fourth attempt will connect to a Wireguard relay using QUIC for obfuscation (if QUIC is implemented) +- The fourth attempt will connect to a Wireguard relay using QUIC for obfuscation - The fifth attempt will connect to a Wireguard relay on a random port using [UDP2TCP obfuscation](https://github.com/mullvad/udp-over-tcp) - The sixth attempt will connect to a Wireguard relay over IPv6 on a random port using UDP2TCP obfuscation (if IPv6 is configured on the host) @@ -82,6 +82,9 @@ As such, the above algorithm is simplified to the following version: - The UDP2TCP random port is **either** 80 **or** 5001 - The Shadowsocks port is random within a certain range of ports defined by the relay list +### Ports for QUIC +QUIC will use port 443. + If no tunnel has been established after exhausting this list of attempts, the relay selector will loop back to the first default constraint and continue its search from there. @@ -136,5 +139,5 @@ will indirectly change the bridge state to _Auto_ if it was previously set to _O ### Obfuscator caveats -There are two type of obfuscators - _udp2tcp_, and _shadowsocks_. +There are three types of obfuscators - _udp2tcp_, _shadowsocks_ and _quic_. They are used if the obfuscation mode is set _Auto_ and the user has selected WireGuard to be the only tunnel protocol to be used. diff --git a/mullvad-api/src/bin/relay_list.rs b/mullvad-api/src/bin/relay_list.rs index fbf7e8af42..80e3650b08 100644 --- a/mullvad-api/src/bin/relay_list.rs +++ b/mullvad-api/src/bin/relay_list.rs @@ -11,10 +11,8 @@ mod imp { use talpid_types::ErrorExt; pub async fn main() { - let runtime = mullvad_api::Runtime::new( - tokio::runtime::Handle::current(), - &ApiEndpoint::from_env_vars(), - ); + let api_endpoint = ApiEndpoint::from_env_vars(); + let runtime = mullvad_api::Runtime::new(tokio::runtime::Handle::current(), &api_endpoint); let relay_list_request = RelayListProxy::new( runtime.mullvad_rest_handle(ApiConnectionMode::Direct.into_provider()), diff --git a/mullvad-api/src/relay_list.rs b/mullvad-api/src/relay_list.rs index 9f4100408f..e01d61f9f5 100644 --- a/mullvad-api/src/relay_list.rs +++ b/mullvad-api/src/relay_list.rs @@ -167,6 +167,7 @@ fn into_mullvad_relay( relay: Relay, location: location::Location, endpoint_data: relay_list::RelayEndpointData, + features: relay_list::Features, ) -> relay_list::Relay { relay_list::Relay { hostname: relay.hostname, @@ -181,6 +182,7 @@ fn into_mullvad_relay( weight: relay.weight, endpoint_data, location, + features, } } @@ -247,11 +249,21 @@ struct Relay { impl Relay { fn into_openvpn_mullvad_relay(self, location: location::Location) -> relay_list::Relay { - into_mullvad_relay(self, location, relay_list::RelayEndpointData::Openvpn) + into_mullvad_relay( + self, + location, + relay_list::RelayEndpointData::Openvpn, + relay_list::Features::empty(), + ) } fn into_bridge_mullvad_relay(self, location: location::Location) -> relay_list::Relay { - into_mullvad_relay(self, location, relay_list::RelayEndpointData::Bridge) + into_mullvad_relay( + self, + location, + relay_list::RelayEndpointData::Bridge, + relay_list::Features::empty(), + ) } fn convert_to_lowercase(&mut self) { @@ -345,10 +357,16 @@ struct WireGuardRelay { daita: bool, #[serde(default)] shadowsocks_extra_addr_in: Vec<IpAddr>, + #[serde(default)] + features: relay_list::Features, } impl WireGuardRelay { fn into_mullvad_relay(self, location: location::Location) -> relay_list::Relay { + // Sanity check that new 'features' key is in sync with the old Relay keys. + if self.features.daita() { + debug_assert!(self.daita) + } into_mullvad_relay( self.relay, location, @@ -357,6 +375,7 @@ impl WireGuardRelay { daita: self.daita, shadowsocks_extra_addr_in: self.shadowsocks_extra_addr_in, }), + self.features, ) } } diff --git a/mullvad-ios/src/tunnel_obfuscator_proxy/mod.rs b/mullvad-ios/src/tunnel_obfuscator_proxy/mod.rs index 5faeab7162..604042f919 100644 --- a/mullvad-ios/src/tunnel_obfuscator_proxy/mod.rs +++ b/mullvad-ios/src/tunnel_obfuscator_proxy/mod.rs @@ -30,12 +30,10 @@ impl TunnelObfuscatorRuntime { } pub fn new_quic(peer: SocketAddr, hostname: String, token: String) -> Self { - let settings = ObfuscationSettings::Quic(quic::Settings { - quic_endpoint: peer, - wireguard_endpoint: SocketAddr::from((Ipv4Addr::LOCALHOST, 51820)), - hostname, - token, - }); + let wireguard_endpoint = SocketAddr::from((Ipv4Addr::LOCALHOST, 51820)); + let token: quic::AuthToken = token.parse().unwrap(); + let quic = quic::Settings::new(peer, hostname, token, wireguard_endpoint); + let settings = ObfuscationSettings::Quic(quic); Self { settings } } diff --git a/mullvad-management-interface/src/types/conversions/relay_list.rs b/mullvad-management-interface/src/types/conversions/relay_list.rs index 11718973c1..a089f8e6fd 100644 --- a/mullvad-management-interface/src/types/conversions/relay_list.rs +++ b/mullvad-management-interface/src/types/conversions/relay_list.rs @@ -285,7 +285,11 @@ impl TryFrom<proto::Relay> for mullvad_types::relay_list::Relay { }) .transpose()?; - Ok(MullvadRelay { + // TODO: Eventually, we will need to decide how to represent extra relay features in the + // protobuf message. + let features = mullvad_types::relay_list::Features::default(); + + let relay = MullvadRelay { hostname: relay.hostname, ipv4_addr_in: relay.ipv4_addr_in.parse().map_err(|_err| { FromProtobufTypeError::InvalidArgument("invalid relay IPv4 address") @@ -311,7 +315,10 @@ impl TryFrom<proto::Relay> for mullvad_types::relay_list::Relay { }) .ok_or("missing relay location") .map_err(FromProtobufTypeError::InvalidArgument)?, - }) + features, + }; + + Ok(relay) } } diff --git a/mullvad-masque-proxy/src/client/mod.rs b/mullvad-masque-proxy/src/client/mod.rs index b5aaf81cec..b49fc7a142 100644 --- a/mullvad-masque-proxy/src/client/mod.rs +++ b/mullvad-masque-proxy/src/client/mod.rs @@ -67,6 +67,8 @@ pub type Result<T> = std::result::Result<T, Error>; pub enum Error { #[error("Failed to bind local socket")] Bind(#[source] io::Error), + #[error("Failed to setup a QUIC endpoint")] + Endpoint(#[source] io::Error), #[cfg(target_os = "linux")] #[error("Failed to set fwmark on remote socket")] Fwmark(#[source] io::Error), @@ -112,7 +114,7 @@ pub enum Error { InvalidHttpRedirect(#[source] anyhow::Error), } -#[derive(TypedBuilder)] +#[derive(TypedBuilder, Debug)] pub struct ClientConfig { /// Socket that accepts proxy clients pub client_socket: UdpSocket, @@ -226,32 +228,39 @@ impl Client { max_udp_payload_size: u16, #[cfg(target_os = "linux")] fwmark: Option<u32>, ) -> Result<Endpoint> { - let local_socket = socket2::Socket::new( - socket2::Domain::IPV4, - socket2::Type::DGRAM, - Some(socket2::Protocol::UDP), - ) - .map_err(Error::Bind)?; - - #[cfg(target_os = "linux")] - if let Some(fwmark) = fwmark { - local_socket.set_mark(fwmark).map_err(Error::Fwmark)?; - } - - local_socket.bind(&local_addr.into()).map_err(Error::Bind)?; + // Create a UDP socket which quinn will read/write from/to. + let local_socket = { + // family + let domain = match &local_addr { + SocketAddr::V4(_) => socket2::Domain::IPV4, + SocketAddr::V6(_) => socket2::Domain::IPV6, + }; + let ty = socket2::Type::DGRAM; + let protocol = Some(socket2::Protocol::UDP); + let socket = socket2::Socket::new(domain, ty, protocol).map_err(Error::Bind)?; + #[cfg(target_os = "linux")] + if let Some(fwmark) = fwmark { + socket.set_mark(fwmark).map_err(Error::Fwmark)?; + } + socket.bind(&local_addr.into()).map_err(Error::Bind)?; + socket + }; - let mut endpoint_config = EndpointConfig::default(); - endpoint_config - .max_udp_payload_size(max_udp_payload_size) - .map_err(Error::InvalidMaxUdpPayload)?; + let endpoint_config = { + let mut endpoint_config = EndpointConfig::default(); + endpoint_config + .max_udp_payload_size(max_udp_payload_size) + .map_err(Error::InvalidMaxUdpPayload)?; + endpoint_config + }; Endpoint::new( endpoint_config, None, - local_socket.into(), + std::net::UdpSocket::from(local_socket), Arc::new(TokioRuntime), ) - .map_err(Error::Bind) + .map_err(Error::Endpoint) } /// Returns an h3 connection that is ready to be used for sending UDP datagrams. diff --git a/mullvad-relay-selector/src/relay_selector/helpers.rs b/mullvad-relay-selector/src/relay_selector/helpers.rs index df524501ce..4f340c10f7 100644 --- a/mullvad-relay-selector/src/relay_selector/helpers.rs +++ b/mullvad-relay-selector/src/relay_selector/helpers.rs @@ -15,7 +15,7 @@ use rand::{ seq::{IteratorRandom, SliceRandom}, thread_rng, Rng, }; -use talpid_types::net::obfuscation::ObfuscatorConfig; +use talpid_types::net::{obfuscation::ObfuscatorConfig, IpVersion}; use crate::SelectedObfuscator; @@ -141,6 +141,26 @@ pub fn get_shadowsocks_obfuscator( }) } +pub fn get_quic_obfuscator(relay: Relay, ip_version: IpVersion) -> Option<SelectedObfuscator> { + let quic = relay.features.quic()?; + let config = { + let hostname = quic.hostname().to_string(); + let endpoint = match ip_version { + IpVersion::V4 => SocketAddr::from((quic.in_ipv4()?, quic.port())), + IpVersion::V6 => SocketAddr::from((quic.in_ipv6()?, quic.port())), + }; + let auth_token = quic.auth_token().to_string(); + ObfuscatorConfig::Quic { + hostname, + endpoint, + auth_token, + } + }; + + let obfuscator = SelectedObfuscator { config, relay }; + Some(obfuscator) +} + /// Return an obfuscation config for the wireguard server at `wg_in_addr` or one of `extra_in_addrs` /// (unless empty). `wg_in_addr_port_ranges` contains all valid ports for `wg_in_addr`, and /// `SHADOWSOCKS_EXTRA_PORT_RANGES` contains valid ports for `extra_in_addrs`. diff --git a/mullvad-relay-selector/src/relay_selector/matcher.rs b/mullvad-relay-selector/src/relay_selector/matcher.rs index e14885f2c4..8133677b87 100644 --- a/mullvad-relay-selector/src/relay_selector/matcher.rs +++ b/mullvad-relay-selector/src/relay_selector/matcher.rs @@ -144,9 +144,10 @@ fn filter_on_obfuscation( relay, ) } - - // If Shadowsocks is not a requirement, then there are no relay-specific constraints - _ => true, + // QUIC is only enabled on some relays + ObfuscationQuery::Quic => relay.features.quic().is_some(), + // Other relays are compatible with this query + ObfuscationQuery::Off | ObfuscationQuery::Auto | ObfuscationQuery::Udp2tcp(_) => true, } } diff --git a/mullvad-relay-selector/src/relay_selector/mod.rs b/mullvad-relay-selector/src/relay_selector/mod.rs index 0d5f2f4796..7085ad1ba4 100644 --- a/mullvad-relay-selector/src/relay_selector/mod.rs +++ b/mullvad-relay-selector/src/relay_selector/mod.rs @@ -7,6 +7,7 @@ mod parsed_relays; pub mod query; pub mod relays; +use detailer::resolve_ip_version; use matcher::{filter_matching_bridges, filter_matching_relay_list}; use parsed_relays::ParsedRelays; use relays::{Multihop, Singlehop, WireguardConfig}; @@ -62,13 +63,13 @@ pub static WIREGUARD_RETRY_ORDER: LazyLock<Vec<RelayQuery>> = LazyLock::new(|| { // 1 This works with any wireguard relay RelayQueryBuilder::wireguard().build(), // 2 - RelayQueryBuilder::wireguard().port(443).build(), - // 3 RelayQueryBuilder::wireguard() .ip_version(IpVersion::V6) .build(), - // 4 + // 3 RelayQueryBuilder::wireguard().shadowsocks().build(), + // 4 + RelayQueryBuilder::wireguard().quic().build(), // 5 RelayQueryBuilder::wireguard().udp2tcp().build(), // 6 @@ -215,6 +216,9 @@ struct NormalSelectorConfig<'a> { } /// The return type of [`RelaySelector::get_relay`]. +// There won't ever be many instances of GetRelay floating around, so the 'large' difference in +// size between its variants is negligible. +#[allow(clippy::large_enum_variant)] #[derive(Clone, Debug)] pub enum GetRelay { Wireguard { @@ -915,16 +919,8 @@ impl RelaySelector { Ok(Some(obfuscation)) } ObfuscationQuery::Quic => { - let obfuscator = SelectedObfuscator { - config: ObfuscatorConfig::Quic { - // TODO: do not hardcode port - endpoint: std::net::SocketAddr::from((endpoint.peer.endpoint.ip(), 443)), - // TODO: do not hardcode - hostname: "test.mullvad.net".to_owned(), - }, - relay: obfuscator_relay, - }; - Ok(Some(obfuscator)) + let ip_version = resolve_ip_version(query.wireguard_constraints().ip_version); + Ok(helpers::get_quic_obfuscator(obfuscator_relay, ip_version)) } } } diff --git a/mullvad-relay-selector/src/relay_selector/query.rs b/mullvad-relay-selector/src/relay_selector/query.rs index 4a6d9e70a0..7a2ae3cbd6 100644 --- a/mullvad-relay-selector/src/relay_selector/query.rs +++ b/mullvad-relay-selector/src/relay_selector/query.rs @@ -635,6 +635,12 @@ pub mod builder { quantum_resistant: QuantumResistant, } + /// Quic obfuscation. + /// + /// Quic does not have any user-configurable parameters, so there is no type defined + /// in the mullvad-types crate. + pub struct Quic; + // This impl-block is quantified over all configurations impl<Multihop, Obfuscation, Daita, QuantumResistant> RelayQueryBuilder<Wireguard<Multihop, Obfuscation, Daita, QuantumResistant>> @@ -792,6 +798,22 @@ pub mod builder { protocol, } } + + /// Enable QUIC obufscation. + pub fn quic( + mut self, + ) -> RelayQueryBuilder<Wireguard<Multihop, Quic, Daita, QuantumResistant>> { + self.query.wireguard_constraints.obfuscation = ObfuscationQuery::Quic; + RelayQueryBuilder { + query: self.query, + protocol: Wireguard { + multihop: self.protocol.multihop, + obfuscation: Quic, + daita: self.protocol.daita, + quantum_resistant: self.protocol.quantum_resistant, + }, + } + } } impl<Multihop, Daita, QuantumResistant> diff --git a/mullvad-relay-selector/tests/relay_selector.rs b/mullvad-relay-selector/tests/relay_selector.rs index c104d84015..680f4eba3d 100644 --- a/mullvad-relay-selector/tests/relay_selector.rs +++ b/mullvad-relay-selector/tests/relay_selector.rs @@ -27,9 +27,9 @@ use mullvad_types::{ RelayConstraints, RelayOverride, RelaySettings, TransportPort, }, relay_list::{ - BridgeEndpointData, OpenVpnEndpoint, OpenVpnEndpointData, Relay, RelayEndpointData, - RelayList, RelayListCity, RelayListCountry, ShadowsocksEndpointData, WireguardEndpointData, - WireguardRelayEndpointData, + BridgeEndpointData, Features, OpenVpnEndpoint, OpenVpnEndpointData, Quic, Relay, + RelayEndpointData, RelayList, RelayListCity, RelayListCountry, ShadowsocksEndpointData, + WireguardEndpointData, WireguardRelayEndpointData, }, }; @@ -73,6 +73,16 @@ static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList { shadowsocks_extra_addr_in: vec![], }), location: DUMMY_LOCATION.clone(), + features: Features::default() + .configure_daita() + .configure_quic(Quic::new( + vec![ + "185.213.154.68".parse().unwrap(), + "2a03:1b20:5:f011::a09f".parse().unwrap(), + ], + "Bearer test".to_owned(), + "se9-wireguard.blockerad.eu".to_owned(), + )), }, Relay { hostname: "se10-wireguard".to_string(), @@ -94,6 +104,7 @@ static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList { shadowsocks_extra_addr_in: vec![], }), location: DUMMY_LOCATION.clone(), + features: Features::default(), }, Relay { hostname: "se11-wireguard".to_string(), @@ -115,6 +126,7 @@ static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList { shadowsocks_extra_addr_in: vec![], }), location: DUMMY_LOCATION.clone(), + features: Features::default().configure_daita(), }, Relay { hostname: "se-got-001".to_string(), @@ -129,6 +141,7 @@ static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList { weight: 1, endpoint_data: RelayEndpointData::Openvpn, location: DUMMY_LOCATION.clone(), + features: Features::default(), }, Relay { hostname: "se-got-002".to_string(), @@ -143,6 +156,7 @@ static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList { weight: 1, endpoint_data: RelayEndpointData::Openvpn, location: DUMMY_LOCATION.clone(), + features: Features::default(), }, Relay { hostname: "se-got-br-001".to_string(), @@ -157,6 +171,7 @@ static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList { weight: 1, endpoint_data: RelayEndpointData::Bridge, location: DUMMY_LOCATION.clone(), + features: Features::default(), }, SHADOWSOCKS_RELAY.clone(), ], @@ -241,6 +256,7 @@ static SHADOWSOCKS_RELAY: LazyLock<Relay> = LazyLock::new(|| Relay { shadowsocks_extra_addr_in: SHADOWSOCKS_RELAY_EXTRA_ADDRS.to_vec(), }), location: DUMMY_LOCATION.clone(), + features: Features::default(), }); const SHADOWSOCKS_RELAY_IPV4: Ipv4Addr = Ipv4Addr::new(123, 123, 123, 1); const SHADOWSOCKS_RELAY_IPV6: Ipv6Addr = Ipv6Addr::new(0x123, 0, 0, 0, 0, 0, 0, 2); @@ -319,13 +335,13 @@ fn assert_wireguard_retry_order() { // 1 (wireguard) RelayQueryBuilder::wireguard().build(), // 2 - RelayQueryBuilder::wireguard().port(443).build(), - // 3 RelayQueryBuilder::wireguard() .ip_version(IpVersion::V6) .build(), - // 4 + // 3 RelayQueryBuilder::wireguard().shadowsocks().build(), + // 4 + RelayQueryBuilder::wireguard().quic().build(), // 5 RelayQueryBuilder::wireguard().udp2tcp().build(), // 6 @@ -574,6 +590,7 @@ fn test_wireguard_entry() { shadowsocks_extra_addr_in: vec![], }), location: DUMMY_LOCATION.clone(), + features: Features::default(), }, Relay { hostname: "se10-wireguard".to_string(), @@ -595,6 +612,7 @@ fn test_wireguard_entry() { shadowsocks_extra_addr_in: vec![], }), location: DUMMY_LOCATION.clone(), + features: Features::default(), }, ], }], @@ -872,6 +890,32 @@ fn test_selecting_wireguard_over_shadowsocks_extra_ips() { } } +/// Test whether Quic is always selected as the obfuscation protocol when Quic is selected. +#[test] +fn test_selecting_wireguard_over_quic() { + let relay_selector = RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone()); + + let query = RelayQueryBuilder::wireguard().quic().build(); + assert!(!query.wireguard_constraints().multihop()); + + let relay = relay_selector.get_relay_by_query(query).unwrap(); + match relay { + GetRelay::Wireguard { + obfuscator, + inner: WireguardConfig::Singlehop { .. }, + .. + } => { + assert!(obfuscator.is_some_and(|obfuscator| matches!( + obfuscator.config, + ObfuscatorConfig::Quic { .. }, + ))) + } + wrong_relay => panic!( + "Relay selector should have picked a Wireguard relay with Quic, instead chose {wrong_relay:?}" + ), + } +} + /// Ignore extra IPv4 addresses when overrides are set #[test] fn test_selecting_wireguard_ignore_extra_ips_override_v4() { @@ -1017,7 +1061,7 @@ fn test_selecting_wireguard_endpoint_with_auto_obfuscation() { /// all configurations contain a valid port. #[test] fn test_selected_wireguard_endpoints_use_correct_port_ranges() { - const TCP2UDP_PORTS: [u16; 3] = [80, 443, 5001]; + const TCP2UDP_PORTS: [u16; 2] = [80, 5001]; let relay_selector = default_relay_selector(); // Note that we do *not* specify any port here! let query = RelayQueryBuilder::wireguard().udp2tcp().build(); @@ -1219,6 +1263,7 @@ fn test_include_in_country() { daita: false, }), location: DUMMY_LOCATION.clone(), + features: Features::default(), }, Relay { hostname: "se10-wireguard".to_string(), @@ -1240,6 +1285,7 @@ fn test_include_in_country() { daita: false, }), location: DUMMY_LOCATION.clone(), + features: Features::default(), }, ], }], diff --git a/mullvad-types/src/relay_list.rs b/mullvad-types/src/relay_list.rs index 351c0d0e86..413a23a767 100644 --- a/mullvad-types/src/relay_list.rs +++ b/mullvad-types/src/relay_list.rs @@ -88,6 +88,119 @@ pub struct Relay { pub weight: u64, pub endpoint_data: RelayEndpointData, pub location: Location, + #[serde(default)] + pub features: Features, +} + +/// Extra features enabled on some (Wireguard) relay, such as obfuscation daemons or Daita. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Features { + daita: Option<Daita>, + quic: Option<Quic>, +} + +impl Features { + /// Equivalent to a relay without any additional features. + pub fn empty() -> Features { + Features { + daita: None, + quic: None, + } + } + + /// Whether Daita is enabled + pub fn daita(&self) -> bool { + self.daita.is_some() + } + + /// Whether Quic is enabled and its config + pub fn quic(&self) -> Option<&Quic> { + self.quic.as_ref() + } + + /// Enable Daita for this relay + pub fn configure_daita(self) -> Self { + let daita = Some(Daita {}); + Self { daita, ..self } + } + + /// Configure QUIC for this relay + pub fn configure_quic(self, options: Quic) -> Self { + let quic = Some(options); + Self { quic, ..self } + } +} + +impl Default for Features { + fn default() -> Self { + Features::empty() + } +} + +/// DAITA doesn't have any configuration options (exposed by the API). +/// +/// Note, an empty struct is not the same as an empty tuple struct according to serde_json! +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Daita {} + +/// Parameters for setting up a QUIC obfuscator (connecting to a masque-proxy running on a relay). +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Quic { + /// In-addresses for the QUIC obfuscator. + /// + /// There may be 0, 1 or 2 in IPs, depending on how many masque-proxy daemons running on the + /// relay. Hopefully the API will tell use the correct amount🤞. + addr_in: Vec<IpAddr>, + /// Authorization token + token: String, + /// Hostname where masque proxy is hosted + domain: String, +} + +impl Quic { + pub fn new(addr_in: Vec<IpAddr>, token: String, domain: String) -> Self { + Self { + addr_in, + token, + domain, + } + } + + /// In address as an IPv4 address. + /// + /// Use this if you want to connect to the masque-proxy using IPv4. + pub fn in_ipv4(&self) -> Option<Ipv4Addr> { + let ipv4 = |ipaddr: &IpAddr| match ipaddr { + IpAddr::V4(ipv4_addr) => Some(*ipv4_addr), + IpAddr::V6(_) => None, + }; + self.addr_in.iter().find_map(ipv4) + } + + /// In address as an IPv6 address. + /// + /// Use this if you want to connect to the masque-proxy using IPv6. + pub fn in_ipv6(&self) -> Option<Ipv6Addr> { + let ipv6 = |ipaddr: &IpAddr| match ipaddr { + IpAddr::V4(_) => None, + IpAddr::V6(ipv6_addr) => Some(*ipv6_addr), + }; + self.addr_in.iter().find_map(ipv6) + } + + /// Port of the masque-proxy daemon. + pub const fn port(&self) -> u16 { + // The point of the masque-proxy is to look like a regular web server serving http traffic. + 443 + } + + pub fn hostname(&self) -> &str { + &self.domain + } + + pub fn auth_token(&self) -> &str { + &self.token + } } impl Relay { @@ -117,7 +230,7 @@ impl PartialEq for Relay { /// # Example /// /// ```rust - /// # use mullvad_types::{relay_list::Relay, relay_list::{RelayEndpointData, WireguardRelayEndpointData}}; + /// # use mullvad_types::{relay_list::{Relay, Features}, relay_list::{RelayEndpointData, WireguardRelayEndpointData}}; /// # use talpid_types::net::wireguard::PublicKey; /// /// let relay = Relay { @@ -147,6 +260,7 @@ impl PartialEq for Relay { /// # latitude: 57.71, /// # longitude: 11.97, /// # }, + /// # features: Features::default(), /// }; /// /// let mut different_relay = relay.clone(); @@ -229,6 +343,7 @@ pub struct WireguardRelayEndpointData { /// Public key used by the relay peer pub public_key: wireguard::PublicKey, /// Whether the server supports DAITA + /// FIXME: This has been superceded by [Features] + [Daita]. #[serde(default)] pub daita: bool, /// Optional IP addresses used by Shadowsocks diff --git a/talpid-types/src/net/obfuscation.rs b/talpid-types/src/net/obfuscation.rs index d2735c6a75..161fac92a0 100644 --- a/talpid-types/src/net/obfuscation.rs +++ b/talpid-types/src/net/obfuscation.rs @@ -14,6 +14,7 @@ pub enum ObfuscatorConfig { Quic { hostname: String, endpoint: SocketAddr, + auth_token: String, }, } @@ -28,10 +29,7 @@ impl ObfuscatorConfig { address: *endpoint, protocol: TransportProtocol::Udp, }, - ObfuscatorConfig::Quic { - hostname: _, - endpoint, - } => Endpoint { + ObfuscatorConfig::Quic { endpoint, .. } => Endpoint { address: *endpoint, protocol: TransportProtocol::Udp, }, diff --git a/talpid-wireguard/src/obfuscation.rs b/talpid-wireguard/src/obfuscation.rs index 6677d630cb..f23a534477 100644 --- a/talpid-wireguard/src/obfuscation.rs +++ b/talpid-wireguard/src/obfuscation.rs @@ -10,15 +10,13 @@ use std::{ }; #[cfg(target_os = "android")] use talpid_tunnel::tun_provider::TunProvider; +use talpid_tunnel::WIREGUARD_HEADER_SIZE; use talpid_types::{net::obfuscation::ObfuscatorConfig, ErrorExt}; use tunnel_obfuscation::{ create_obfuscator, quic, shadowsocks, udp2tcp, Settings as ObfuscationSettings, }; -/// Test authentication header to set for the CONNECT request. -const AUTH_HEADER: &str = "test"; - /// Begin running obfuscation machine, if configured. This function will patch `config`'s endpoint /// to point to an endpoint on localhost pub async fn apply_obfuscation_config( @@ -30,11 +28,24 @@ pub async fn apply_obfuscation_config( return Ok(None); }; - let settings = settings_from_config( - obfuscator_config, - #[cfg(target_os = "linux")] - config.fwmark, - ); + let settings = { + let settings = settings_from_config( + obfuscator_config, + #[cfg(target_os = "linux")] + config.fwmark, + ); + + // Adjust MTU for QUIC obfuscator. + match settings { + ObfuscationSettings::Quic(quic) => { + // Account for multihop + // FIXME: Pass proper mtu as an argument / through config? + let quic = quic.mtu(config.mtu - 2 * WIREGUARD_HEADER_SIZE); + ObfuscationSettings::Quic(quic) + } + settings => settings, + } + }; log::trace!("Obfuscation settings: {settings:?}"); @@ -99,15 +110,23 @@ fn settings_from_config( fwmark, }) } - ObfuscatorConfig::Quic { hostname, endpoint } => { - ObfuscationSettings::Quic(quic::Settings { - quic_endpoint: *endpoint, - wireguard_endpoint: SocketAddr::from((Ipv4Addr::LOCALHOST, 51820)), - hostname: hostname.to_owned(), - token: AUTH_HEADER.to_owned(), - #[cfg(target_os = "linux")] - fwmark, - }) + ObfuscatorConfig::Quic { + hostname, + endpoint, + auth_token, + } => { + let wireguard_endpoint = SocketAddr::from((Ipv4Addr::LOCALHOST, 51820)); + let settings = quic::Settings::new( + *endpoint, + hostname.to_owned(), + auth_token.parse().unwrap(), + wireguard_endpoint, + ); + #[cfg(target_os = "linux")] + if let Some(fwmark) = fwmark { + return ObfuscationSettings::Quic(settings.fwmark(fwmark)); + } + ObfuscationSettings::Quic(settings) } } } diff --git a/tunnel-obfuscation/Cargo.toml b/tunnel-obfuscation/Cargo.toml index 1bee10199b..452b9f9eed 100644 --- a/tunnel-obfuscation/Cargo.toml +++ b/tunnel-obfuscation/Cargo.toml @@ -15,6 +15,7 @@ log = { workspace = true } async-trait = "0.1" thiserror = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros", "net", "io-util"] } +tokio-util = { workspace = true } udp-over-tcp = { git = "https://github.com/mullvad/udp-over-tcp", rev = "87936ac29b68b902565955f138ab02294bcc8593" } shadowsocks = { workspace = true } mullvad-masque-proxy = { path = "../mullvad-masque-proxy" } diff --git a/tunnel-obfuscation/src/quic.rs b/tunnel-obfuscation/src/quic.rs index e0b16ecb66..e4ab2c40e2 100644 --- a/tunnel-obfuscation/src/quic.rs +++ b/tunnel-obfuscation/src/quic.rs @@ -4,9 +4,10 @@ use async_trait::async_trait; use mullvad_masque_proxy::client::{Client, ClientConfig}; use std::{ io, - net::{Ipv4Addr, SocketAddr}, + net::{Ipv4Addr, Ipv6Addr, SocketAddr}, }; use tokio::net::UdpSocket; +use tokio_util::sync::{CancellationToken, DropGuard}; use crate::Obfuscator; @@ -20,57 +21,177 @@ pub enum Error { MasqueProxyError(#[source] mullvad_masque_proxy::client::Error), } +#[derive(Debug)] pub struct Quic { local_endpoint: SocketAddr, task: tokio::task::JoinHandle<Result<()>>, + _shutdown: DropGuard, } #[derive(Debug)] pub struct Settings { /// Remote Quic endpoint - pub quic_endpoint: SocketAddr, + quic_endpoint: SocketAddr, /// Remote Wireguard endpoint - pub wireguard_endpoint: SocketAddr, + wireguard_endpoint: SocketAddr, /// Hostname to use for QUIC - pub hostname: String, - /// Auth token to use for QUIC. Must NOT be prefixed with "Bearer". - pub token: String, + hostname: String, + /// Authentication token to set for the CONNECT request when establishing a QUIC connection. + /// Must NOT be prefixed with "Bearer". + token: AuthToken, /// fwmark to apply to use for the QUIC connection #[cfg(target_os = "linux")] - pub fwmark: Option<u32>, + fwmark: Option<u32>, + /// MTU for the QUIC client. This needs to account for the *additional* headers other than IP + /// and UDP, but not for those specifically. + mtu: Option<u16>, } -impl Quic { - pub(crate) async fn new(settings: &Settings) -> Result<Self> { - let local_socket = UdpSocket::bind(SocketAddr::from((Ipv4Addr::LOCALHOST, 0))) - .await - .map_err(Error::BindError)?; +impl Settings { + ///See [Settings] for details. + pub fn new( + quic_server_endpoint: SocketAddr, + hostname: String, + token: AuthToken, + target_endpoint: SocketAddr, + ) -> Self { + Self { + quic_endpoint: quic_server_endpoint, + wireguard_endpoint: target_endpoint, + hostname, + token, + mtu: None, + #[cfg(target_os = "linux")] + fwmark: None, + } + } + + /// Set an explicit MTU for the Quic obfuscator. + pub fn mtu(self, mtu: u16) -> Self { + debug_assert!(mtu <= 1500, "MTU is too high: {mtu}"); + let mtu = Some(mtu); + Self { mtu, ..self } + } + + /// Set `fwmark` for the Quic obfuscator. + #[cfg(target_os = "linux")] + pub fn fwmark(self, fwmark: u32) -> Self { + let fwmark = Some(fwmark); + Self { fwmark, ..self } + } + + /// The masque-proxy server expects the Authentication header to be prefixed with "Bearer ", so + /// prefix the auth token with that. + fn auth_header(&self) -> String { + format!("Bearer {token}", token = self.token.0) + } +} + +/// Authorization Token used when connecting to a masque-proxy. +#[derive(Clone, Debug, PartialEq)] +pub struct AuthToken(String); + +impl AuthToken { + /// Create a new token for constructing a valid Authorization header when connecting to a + /// masque-proxy. + pub fn new(token: String) -> Option<Self> { + // TODO: We could potentially do more validation, but the exact format of the auth token is + // not known to be stable (yet). + if token.starts_with("Bearer") { + return None; + }; + Some(Self(token)) + } +} - let local_endpoint = local_socket.local_addr().unwrap(); - let token = settings.token.clone(); +impl std::str::FromStr for AuthToken { + type Err = String; + fn from_str(token: &str) -> std::result::Result<Self, Self::Err> { + match Self::new(token.to_owned()) { + Some(token) => Ok(token), + None => Err( + "Authentication token must not start with \"Bearer\". Please just the token, the Authentication header will be formatted before starting the QUIC client." + .to_string()) + + } + } +} + +impl Quic { + pub(crate) async fn new(settings: &Settings) -> Result<Self> { + let (local_socket, local_udp_client_addr) = + Quic::create_local_udp_socket(settings.quic_endpoint.is_ipv4()).await?; + // The address family of the local QUIC client socket has to match the address family + // of the endpoint we're connecting to. The address itself is not important to consumers wanting + // to obfuscate traffic. It is solely used by the local proxy client to know where the QUIC + // obfuscator is running. + let quic_client_local_addr = if settings.quic_endpoint.is_ipv4() { + SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0)) + } else { + SocketAddr::from((Ipv6Addr::UNSPECIFIED, 0)) + }; let config_builder = ClientConfig::builder() .client_socket(local_socket) - .local_addr((Ipv4Addr::UNSPECIFIED, 0).into()) + .local_addr(quic_client_local_addr) .server_addr(settings.quic_endpoint) .server_host(settings.hostname.clone()) .target_addr(settings.wireguard_endpoint) - .auth_header(Some(format!("Bearer {token}").to_owned())); + .auth_header(Some(settings.auth_header())) + .mtu(settings.mtu.unwrap_or(1500)); #[cfg(target_os = "linux")] let config_builder = config_builder.fwmark(settings.fwmark); - let task = tokio::spawn(async move { - let client = Client::connect(config_builder.build()) - .await - .map_err(Error::MasqueProxyError)?; - client.run().await.map_err(Error::MasqueProxyError) - }); + let client = Client::connect(config_builder.build()) + .await + .map_err(Error::MasqueProxyError)?; + + let token = CancellationToken::new(); + + let local_proxy = tokio::spawn(Quic::run_forwarding(client, token.child_token())); + + let quic = Quic { + local_endpoint: local_udp_client_addr, + task: local_proxy, + _shutdown: token.drop_guard(), + }; + + Ok(quic) + } + + async fn run_forwarding( + masque_proxy_client: Client, + cancel_token: CancellationToken, + ) -> Result<()> { + log::trace!("Spawning QUIC client .."); + let mut client = tokio::spawn(masque_proxy_client.run()); + log::trace!("QUIC client is running! QUIC Obfuscator is serving traffic 🎉"); + tokio::select! { + _ = cancel_token.cancelled() => log::trace!("Stopping QUIC obfuscation"), + _result = &mut client => log::trace!("QUIC client closed"), + }; + + client.abort(); + Ok(()) + } + + /// Create a local proxy client. + /// + /// The resulting UdpSocket/the SocketAddr where programs that want to obfuscate their + /// traffic with QUIC will write to. + async fn create_local_udp_socket(ipv4: bool) -> Result<(UdpSocket, SocketAddr)> { + let random_bind_addr = if ipv4 { + SocketAddr::from((Ipv4Addr::LOCALHOST, 0)) + } else { + SocketAddr::from((Ipv6Addr::LOCALHOST, 0)) + }; + let local_udp_socket = UdpSocket::bind(random_bind_addr) + .await + .map_err(Error::BindError)?; + let udp_client_addr = local_udp_socket.local_addr().unwrap(); - Ok(Quic { - local_endpoint, - task, - }) + Ok((local_udp_socket, udp_client_addr)) } } |
