summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMarkus Pettersson <markus.pettersson@mullvad.net>2025-07-09 15:52:35 +0200
committerMarkus Pettersson <markus.pettersson@mullvad.net>2025-07-09 15:52:35 +0200
commitab2cf79c4b36e7ecf0485e57c89b50738b4fc482 (patch)
tree9efb23c08d38004266e2459e7e8876dfa6f7620b
parent1a3c9c16b64eb8c30db150a2719a1ab028de9cab (diff)
parent1d874a65d41ff343f7308da8ad4e3c20f5b67264 (diff)
downloadmullvadvpn-ab2cf79c4b36e7ecf0485e57c89b50738b4fc482.tar.xz
mullvadvpn-ab2cf79c4b36e7ecf0485e57c89b50738b4fc482.zip
Merge branch 'add-quic-to-relay-selector-des-2265'
-rw-r--r--Cargo.lock1
-rw-r--r--Cargo.toml1
-rw-r--r--docs/relay-selector.md7
-rw-r--r--mullvad-api/src/bin/relay_list.rs6
-rw-r--r--mullvad-api/src/relay_list.rs23
-rw-r--r--mullvad-ios/src/tunnel_obfuscator_proxy/mod.rs10
-rw-r--r--mullvad-management-interface/src/types/conversions/relay_list.rs11
-rw-r--r--mullvad-masque-proxy/src/client/mod.rs49
-rw-r--r--mullvad-relay-selector/src/relay_selector/helpers.rs22
-rw-r--r--mullvad-relay-selector/src/relay_selector/matcher.rs7
-rw-r--r--mullvad-relay-selector/src/relay_selector/mod.rs22
-rw-r--r--mullvad-relay-selector/src/relay_selector/query.rs22
-rw-r--r--mullvad-relay-selector/tests/relay_selector.rs60
-rw-r--r--mullvad-types/src/relay_list.rs117
-rw-r--r--talpid-types/src/net/obfuscation.rs6
-rw-r--r--talpid-wireguard/src/obfuscation.rs53
-rw-r--r--tunnel-obfuscation/Cargo.toml1
-rw-r--r--tunnel-obfuscation/src/quic.rs173
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))
}
}