diff options
| author | David Lönnhager <david.l@mullvad.net> | 2025-09-18 17:22:18 +0200 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2025-09-18 17:22:18 +0200 |
| commit | c072667ffed6b4b698cec5a4a8adff00be88c3e5 (patch) | |
| tree | c1fe4b5354c23e939f97577ef7aadaf6eab5731e | |
| parent | 923414f3f00b033dde8ed538ad05c18da4da6b27 (diff) | |
| parent | a074cb8e3625d5378c0be7954b1f5423479d071c (diff) | |
| download | mullvadvpn-c072667ffed6b4b698cec5a4a8adff00be88c3e5.tar.xz mullvadvpn-c072667ffed6b4b698cec5a4a8adff00be88c3e5.zip | |
Merge branch 'add-staggered-obfuscator'
40 files changed, 1322 insertions, 298 deletions
diff --git a/Cargo.lock b/Cargo.lock index b4917a5807..55b56b6873 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1758,7 +1758,7 @@ dependencies = [ "indexmap 2.2.6", "slab", "tokio", - "tokio-util 0.7.10", + "tokio-util 0.7.16", "tracing", ] @@ -1777,7 +1777,7 @@ dependencies = [ "indexmap 2.2.6", "slab", "tokio", - "tokio-util 0.7.10", + "tokio-util 0.7.16", "tracing", ] @@ -1818,7 +1818,7 @@ dependencies = [ "h3-datagram", "quinn", "tokio", - "tokio-util 0.7.10", + "tokio-util 0.7.16", ] [[package]] @@ -1939,7 +1939,7 @@ dependencies = [ "thiserror 1.0.59", "time", "tokio", - "tokio-util 0.7.10", + "tokio-util 0.7.16", "tracing", ] @@ -6089,7 +6089,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", - "tokio-util 0.7.10", + "tokio-util 0.7.16", ] [[package]] @@ -6125,16 +6125,16 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", "futures-sink", + "futures-util", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -6237,7 +6237,7 @@ dependencies = [ "rand 0.8.5", "slab", "tokio", - "tokio-util 0.7.10", + "tokio-util 0.7.16", "tower-layer", "tower-service", "tracing", @@ -6405,7 +6405,7 @@ dependencies = [ "nix 0.29.0", "thiserror 2.0.9", "tokio", - "tokio-util 0.7.10", + "tokio-util 0.7.16", "windows-sys 0.59.0", "wintun-bindings", ] @@ -6416,6 +6416,7 @@ version = "0.0.0" dependencies = [ "async-trait", "criterion", + "futures", "log", "mullvad-masque-proxy", "nix 0.30.1", @@ -6425,7 +6426,7 @@ dependencies = [ "talpid-types", "thiserror 2.0.9", "tokio", - "tokio-util 0.7.10", + "tokio-util 0.7.16", "udp-over-tcp", ] diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt index 7889d1ea2d..4601995d78 100644 --- a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt +++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt @@ -193,8 +193,8 @@ internal fun ManagementInterface.TunnelEndpoint.toDomain(): TunnelEndpoint = }, quantumResistant = quantumResistant, obfuscation = - if (hasObfuscation()) { - obfuscation.toDomain() + if (hasObfuscation() && obfuscation.hasSingle()) { + obfuscation.single.toDomain() } else { null }, @@ -204,7 +204,10 @@ internal fun ManagementInterface.TunnelEndpoint.toDomain(): TunnelEndpoint = internal fun ManagementInterface.ObfuscationEndpoint.toDomain(): ObfuscationEndpoint = ObfuscationEndpoint( endpoint = - Endpoint(address = InetSocketAddress(address, port), protocol = protocol.toDomain()), + Endpoint( + address = endpoint.address.toInetSocketAddress(), + protocol = endpoint.protocol.toDomain(), + ), obfuscationType = obfuscationType.toDomain(), ) diff --git a/desktop/packages/mullvad-vpn/src/main/grpc-type-convertions.ts b/desktop/packages/mullvad-vpn/src/main/grpc-type-convertions.ts index aa7b19615a..d691973775 100644 --- a/desktop/packages/mullvad-vpn/src/main/grpc-type-convertions.ts +++ b/desktop/packages/mullvad-vpn/src/main/grpc-type-convertions.ts @@ -354,7 +354,13 @@ function convertFromTunnelStateRelayInfo( proxy: state.tunnelEndpoint.proxy && convertFromProxyEndpoint(state.tunnelEndpoint.proxy), obfuscationEndpoint: state.tunnelEndpoint.obfuscation && - convertFromObfuscationEndpoint(state.tunnelEndpoint.obfuscation), + state.tunnelEndpoint.obfuscation.single && + state.tunnelEndpoint.obfuscation.single.endpoint && + // TODO: Handle multiplexer? + convertFromObfuscationEndpoint( + state.tunnelEndpoint.obfuscation.single.obfuscationType, + state.tunnelEndpoint.obfuscation.single.endpoint, + ), entryEndpoint: state.tunnelEndpoint.entryEndpoint && convertFromEntryEndpoint(state.tunnelEndpoint.entryEndpoint), @@ -432,30 +438,31 @@ function convertFromProxyEndpoint(proxyEndpoint: grpcTypes.ProxyEndpoint.AsObjec } function convertFromObfuscationEndpoint( - obfuscationEndpoint: grpcTypes.ObfuscationEndpoint.AsObject, + obfuscationType: grpcTypes.ObfuscationEndpoint.ObfuscationType, + obfuscationEndpoint: grpcTypes.Endpoint.AsObject, ): IObfuscationEndpoint { - let obfuscationType: EndpointObfuscationType; - switch (obfuscationEndpoint.obfuscationType) { + let translatedType: EndpointObfuscationType; + switch (obfuscationType) { case grpcTypes.ObfuscationEndpoint.ObfuscationType.UDP2TCP: - obfuscationType = 'udp2tcp'; + translatedType = 'udp2tcp'; break; case grpcTypes.ObfuscationEndpoint.ObfuscationType.SHADOWSOCKS: - obfuscationType = 'shadowsocks'; + translatedType = 'shadowsocks'; break; case grpcTypes.ObfuscationEndpoint.ObfuscationType.QUIC: - obfuscationType = 'quic'; + translatedType = 'quic'; break; case grpcTypes.ObfuscationEndpoint.ObfuscationType.LWO: - obfuscationType = 'lwo'; + translatedType = 'lwo'; break; default: throw new Error('unsupported obfuscation protocol'); } return { - ...obfuscationEndpoint, + address: obfuscationEndpoint.address, protocol: convertFromTransportProtocol(obfuscationEndpoint.protocol), - obfuscationType: obfuscationType, + obfuscationType: translatedType, }; } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/main-view/ConnectionDetails.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/main-view/ConnectionDetails.tsx index 3f24480339..859c249a4d 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/main-view/ConnectionDetails.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/main-view/ConnectionDetails.tsx @@ -193,9 +193,11 @@ function tunnelEndpointToObfuscationEndpoint( return undefined; } + const socketAddr = parseSocketAddress(endpoint.obfuscationEndpoint.address); + return { - ip: endpoint.obfuscationEndpoint.address, - port: endpoint.obfuscationEndpoint.port, + ip: socketAddr.host, + port: socketAddr.port, protocol: endpoint.obfuscationEndpoint.protocol, obfuscationType: endpoint.obfuscationEndpoint.obfuscationType, }; diff --git a/desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts b/desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts index 12de883318..117f002289 100644 --- a/desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts +++ b/desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts @@ -157,7 +157,6 @@ export interface IEndpoint { export interface IObfuscationEndpoint { address: string; - port: number; protocol: RelayProtocol; obfuscationType: EndpointObfuscationType; } diff --git a/mullvad-cli/src/format.rs b/mullvad-cli/src/format.rs index 31369e7f3e..0797a1dced 100644 --- a/mullvad-cli/src/format.rs +++ b/mullvad-cli/src/format.rs @@ -224,12 +224,12 @@ fn format_relay_connection( verbose: bool, ) -> String { let first_hop = endpoint.entry_endpoint.as_ref().map(|entry| { - let endpoint = format_endpoint( + let endpoint = format_endpoints( location.and_then(|l| l.entry_hostname.as_deref()), // Check if we *actually* want to print an obfuscator endpoint .. match endpoint.obfuscation { - Some(ref obfuscation) => &obfuscation.endpoint, - _ => entry, + Some(ref info) => info.get_endpoints(), + _ => vec![*entry], }, verbose, ); @@ -246,13 +246,13 @@ fn format_relay_connection( format!(" via {proxy_endpoint}") }); - let exit_endpoint = format_endpoint( + let exit_endpoint = format_endpoints( location.and_then(|l| l.hostname.as_deref()), // Check if we *actually* want to print an obfuscator endpoint .. // The obfuscator information should be printed for the exit relay if multihop is disabled - match (endpoint.obfuscation, &first_hop) { - (Some(ref obfuscation), None) => &obfuscation.endpoint, - _ => &endpoint.endpoint, + match (&endpoint.obfuscation, &first_hop) { + (Some(obfuscation), None) => obfuscation.get_endpoints(), + _ => vec![endpoint.endpoint], }, verbose, ); @@ -264,6 +264,31 @@ fn format_relay_connection( ) } +fn format_endpoints( + hostname: Option<&str>, + endpoints: impl AsRef<[Endpoint]>, + verbose: bool, +) -> String { + let endpoints = endpoints.as_ref(); + if endpoints.len() == 1 { + return format_endpoint(hostname, &endpoints[0], verbose); + } + + let mut endpoints_str = String::new(); + for (i, endpoint) in endpoints.iter().enumerate() { + if i > 0 { + endpoints_str.push_str(" | "); + } + endpoints_str.push_str(&endpoint.to_string()); + } + + match (hostname, verbose) { + (Some(hostname), true) => format!("{hostname} ({endpoints_str})"), + (None, _) => endpoints_str, + (Some(hostname), false) => hostname.to_string(), + } +} + fn format_endpoint(hostname: Option<&str>, endpoint: &Endpoint, verbose: bool) -> String { match (hostname, verbose) { (Some(hostname), true) => format!("{hostname} ({endpoint})"), diff --git a/mullvad-daemon/Cargo.toml b/mullvad-daemon/Cargo.toml index 7abf9bf7cc..b444631bf1 100644 --- a/mullvad-daemon/Cargo.toml +++ b/mullvad-daemon/Cargo.toml @@ -14,7 +14,7 @@ workspace = true # Allow the API server to use to be configured api-override = ["mullvad-api/api-override"] boringtun = ["talpid-core/boringtun"] - +staggered-obfuscation = ["mullvad-relay-selector/staggered-obfuscation"] [dependencies] anyhow = { workspace = true } diff --git a/mullvad-daemon/src/tunnel.rs b/mullvad-daemon/src/tunnel.rs index 87ae2d5077..4c35ee2be8 100644 --- a/mullvad-daemon/src/tunnel.rs +++ b/mullvad-daemon/src/tunnel.rs @@ -10,11 +10,10 @@ use mullvad_types::{ use talpid_core::tunnel_state_machine::TunnelParametersGenerator; #[cfg(not(target_os = "android"))] use talpid_types::net::{ - Endpoint, TunnelParameters, obfuscation::ObfuscatorConfig, openvpn, proxy::CustomProxy, - wireguard, + Endpoint, TunnelParameters, obfuscation::Obfuscators, openvpn, proxy::CustomProxy, wireguard, }; #[cfg(target_os = "android")] -use talpid_types::net::{TunnelParameters, obfuscation::ObfuscatorConfig, wireguard}; +use talpid_types::net::{TunnelParameters, obfuscation::Obfuscators, wireguard}; use talpid_types::{ErrorExt, net::IpAvailability, tunnel::ParameterGenerationError}; @@ -233,7 +232,7 @@ impl InnerParametersGenerator { &self, endpoint: MullvadWireguardEndpoint, data: PrivateAccountAndDevice, - obfuscator_config: Option<ObfuscatorConfig>, + obfuscator_config: Option<Obfuscators>, ) -> TunnelParameters { let tunnel_ipv4 = data.device.wg_data.addresses.ipv4_address.ip(); let tunnel_ipv6 = data.device.wg_data.addresses.ipv6_address.ip(); diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto index 46701136a9..ff3206cb6a 100644 --- a/mullvad-management-interface/proto/management_interface.proto +++ b/mullvad-management-interface/proto/management_interface.proto @@ -305,7 +305,7 @@ message TunnelEndpoint { TunnelType tunnel_type = 3; bool quantum_resistant = 4; ProxyEndpoint proxy = 5; - ObfuscationEndpoint obfuscation = 6; + ObfuscationInfo obfuscation = 6; Endpoint entry_endpoint = 7; TunnelMetadata tunnel_metadata = 8; bool daita = 9; @@ -332,6 +332,19 @@ enum FeatureIndicator { DAITA_MULTIHOP = 15; } +message ObfuscationInfo { + oneof type { + ObfuscationEndpoint single = 1; + MultiplexObfuscation multiple = 2; + } +} + +message MultiplexObfuscation { + // optional direct endpoint + Endpoint direct = 1; + repeated ObfuscationEndpoint obfuscators = 2; +} + message ObfuscationEndpoint { enum ObfuscationType { UDP2TCP = 0; @@ -340,10 +353,8 @@ message ObfuscationEndpoint { LWO = 3; } - string address = 1; - uint32 port = 2; - TransportProtocol protocol = 3; - ObfuscationType obfuscation_type = 4; + Endpoint endpoint = 1; + ObfuscationType obfuscation_type = 2; } message Endpoint { diff --git a/mullvad-management-interface/src/types/conversions/net.rs b/mullvad-management-interface/src/types/conversions/net.rs index 478400346f..bc3f3a6e0c 100644 --- a/mullvad-management-interface/src/types/conversions/net.rs +++ b/mullvad-management-interface/src/types/conversions/net.rs @@ -1,5 +1,4 @@ use crate::types::{FromProtobufTypeError, conversions::arg_from_str, proto}; -use std::net::SocketAddr; impl From<talpid_types::net::TunnelEndpoint> for proto::TunnelEndpoint { fn from(endpoint: talpid_types::net::TunnelEndpoint) -> Self { @@ -25,33 +24,8 @@ impl From<talpid_types::net::TunnelEndpoint> for proto::TunnelEndpoint { } }, }), - obfuscation: endpoint.obfuscation.map(|obfuscation_endpoint| { - proto::ObfuscationEndpoint { - address: obfuscation_endpoint.endpoint.address.ip().to_string(), - port: u32::from(obfuscation_endpoint.endpoint.address.port()), - protocol: i32::from(proto::TransportProtocol::from( - obfuscation_endpoint.endpoint.protocol, - )), - obfuscation_type: match obfuscation_endpoint.obfuscation_type { - net::ObfuscationType::Udp2Tcp => { - i32::from(proto::obfuscation_endpoint::ObfuscationType::Udp2tcp) - } - net::ObfuscationType::Shadowsocks => { - i32::from(proto::obfuscation_endpoint::ObfuscationType::Shadowsocks) - } - net::ObfuscationType::Quic => { - i32::from(proto::obfuscation_endpoint::ObfuscationType::Quic) - } - net::ObfuscationType::Lwo => { - i32::from(proto::obfuscation_endpoint::ObfuscationType::Lwo) - } - }, - } - }), - entry_endpoint: endpoint.entry_endpoint.map(|entry| proto::Endpoint { - address: entry.address.to_string(), - protocol: i32::from(proto::TransportProtocol::from(entry.protocol)), - }), + obfuscation: endpoint.obfuscation.map(proto::ObfuscationInfo::from), + entry_endpoint: endpoint.entry_endpoint.map(proto::Endpoint::from), tunnel_metadata: endpoint .tunnel_interface .map(|tunnel_interface| proto::TunnelMetadata { tunnel_interface }), @@ -63,6 +37,110 @@ impl From<talpid_types::net::TunnelEndpoint> for proto::TunnelEndpoint { } } +impl From<talpid_types::net::Endpoint> for proto::Endpoint { + fn from(value: talpid_types::net::Endpoint) -> Self { + proto::Endpoint { + address: value.address.to_string(), + protocol: i32::from(proto::TransportProtocol::from(value.protocol)), + } + } +} + +impl TryFrom<proto::Endpoint> for talpid_types::net::Endpoint { + type Error = FromProtobufTypeError; + + fn try_from(endpoint: proto::Endpoint) -> Result<Self, FromProtobufTypeError> { + Ok(talpid_types::net::Endpoint { + address: arg_from_str(&endpoint.address, "invalid endpoint address")?, + protocol: try_transport_protocol_from_i32(endpoint.protocol)?, + }) + } +} + +impl From<talpid_types::net::ObfuscationInfo> for proto::ObfuscationInfo { + fn from(info: talpid_types::net::ObfuscationInfo) -> Self { + match info { + talpid_types::net::ObfuscationInfo::Single(endpoint) => proto::ObfuscationInfo { + r#type: Some(proto::obfuscation_info::Type::Single( + proto::ObfuscationEndpoint::from(endpoint), + )), + }, + talpid_types::net::ObfuscationInfo::Multiplexer { + direct, + obfuscators, + } => proto::ObfuscationInfo { + r#type: Some(proto::obfuscation_info::Type::Multiple( + proto::MultiplexObfuscation { + direct: direct.map(proto::Endpoint::from), + obfuscators: obfuscators + .iter() + .cloned() + .map(proto::ObfuscationEndpoint::from) + .collect(), + }, + )), + }, + } + } +} + +impl From<talpid_types::net::ObfuscationEndpoint> for proto::ObfuscationEndpoint { + fn from(endpoint: talpid_types::net::ObfuscationEndpoint) -> Self { + proto::ObfuscationEndpoint { + endpoint: Some(proto::Endpoint::from(endpoint.endpoint)), + obfuscation_type: match endpoint.obfuscation_type { + talpid_types::net::ObfuscationType::Udp2Tcp => { + i32::from(proto::obfuscation_endpoint::ObfuscationType::Udp2tcp) + } + talpid_types::net::ObfuscationType::Shadowsocks => { + i32::from(proto::obfuscation_endpoint::ObfuscationType::Shadowsocks) + } + talpid_types::net::ObfuscationType::Quic => { + i32::from(proto::obfuscation_endpoint::ObfuscationType::Quic) + } + talpid_types::net::ObfuscationType::Lwo => { + i32::from(proto::obfuscation_endpoint::ObfuscationType::Lwo) + } + }, + } + } +} + +impl TryFrom<proto::ObfuscationEndpoint> for talpid_types::net::ObfuscationEndpoint { + type Error = FromProtobufTypeError; + + fn try_from(endpoint: proto::ObfuscationEndpoint) -> Result<Self, Self::Error> { + use talpid_types::net as talpid_net; + + Ok(talpid_net::ObfuscationEndpoint { + endpoint: talpid_net::Endpoint::try_from(endpoint.endpoint.ok_or( + FromProtobufTypeError::InvalidArgument("missing obfuscation endpoint"), + )?)?, + obfuscation_type: match proto::obfuscation_endpoint::ObfuscationType::try_from( + endpoint.obfuscation_type, + ) { + Ok(proto::obfuscation_endpoint::ObfuscationType::Udp2tcp) => { + talpid_net::ObfuscationType::Udp2Tcp + } + Ok(proto::obfuscation_endpoint::ObfuscationType::Shadowsocks) => { + talpid_net::ObfuscationType::Shadowsocks + } + Ok(proto::obfuscation_endpoint::ObfuscationType::Quic) => { + talpid_net::ObfuscationType::Quic + } + Ok(proto::obfuscation_endpoint::ObfuscationType::Lwo) => { + talpid_net::ObfuscationType::Lwo + } + Err(_) => { + return Err(FromProtobufTypeError::InvalidArgument( + "unknown obfuscation type", + )); + } + }, + }) + } +} + impl TryFrom<proto::TunnelEndpoint> for talpid_types::net::TunnelEndpoint { type Error = FromProtobufTypeError; @@ -107,41 +185,30 @@ impl TryFrom<proto::TunnelEndpoint> for talpid_types::net::TunnelEndpoint { .transpose()?, obfuscation: endpoint .obfuscation - .map(|obfs_ep| { - Ok(talpid_net::ObfuscationEndpoint { - endpoint: talpid_net::Endpoint { - address: SocketAddr::new( - arg_from_str( - &obfs_ep.address, - "invalid obfuscation endpoint address", - )?, - obfs_ep.port as u16, - ), - protocol: try_transport_protocol_from_i32(obfs_ep.protocol)?, - }, - obfuscation_type: - match proto::obfuscation_endpoint::ObfuscationType::try_from( - obfs_ep.obfuscation_type, - ) { - Ok(proto::obfuscation_endpoint::ObfuscationType::Udp2tcp) => { - talpid_net::ObfuscationType::Udp2Tcp - } - Ok(proto::obfuscation_endpoint::ObfuscationType::Shadowsocks) => { - talpid_net::ObfuscationType::Shadowsocks - } - Ok(proto::obfuscation_endpoint::ObfuscationType::Quic) => { - talpid_net::ObfuscationType::Quic - } - Ok(proto::obfuscation_endpoint::ObfuscationType::Lwo) => { - talpid_net::ObfuscationType::Lwo - } - Err(_) => { - return Err(FromProtobufTypeError::InvalidArgument( - "unknown obfuscation type", - )); - } - }, - }) + .map(|info| match info.r#type { + Some(proto::obfuscation_info::Type::Single(endpoint)) => { + Ok(talpid_types::net::ObfuscationInfo::Single( + talpid_net::ObfuscationEndpoint::try_from(endpoint)?, + )) + } + Some(proto::obfuscation_info::Type::Multiple(multiple)) => { + let direct = multiple + .direct + .map(talpid_net::Endpoint::try_from) + .transpose()?; + let obfuscators = multiple + .obfuscators + .into_iter() + .map(talpid_net::ObfuscationEndpoint::try_from) + .collect::<Result<Vec<_>, _>>()?; + Ok(talpid_types::net::ObfuscationInfo::Multiplexer { + direct, + obfuscators, + }) + } + None => Err(FromProtobufTypeError::InvalidArgument( + "unknown obfuscation info type", + )), }) .transpose()?, entry_endpoint: endpoint diff --git a/mullvad-relay-selector/Cargo.toml b/mullvad-relay-selector/Cargo.toml index 3bb0f7665e..8c5e49971f 100644 --- a/mullvad-relay-selector/Cargo.toml +++ b/mullvad-relay-selector/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true [lints] workspace = true +[features] +default = [] +staggered-obfuscation = [] + [dependencies] chrono = { workspace = true } thiserror = { workspace = true } diff --git a/mullvad-relay-selector/src/relay_selector/helpers.rs b/mullvad-relay-selector/src/relay_selector/helpers.rs index 7e9ae866fc..f80942d0f5 100644 --- a/mullvad-relay-selector/src/relay_selector/helpers.rs +++ b/mullvad-relay-selector/src/relay_selector/helpers.rs @@ -18,6 +18,7 @@ use rand::{ }; use talpid_types::net::{IpVersion, obfuscation::ObfuscatorConfig}; +#[cfg(feature = "staggered-obfuscation")] use crate::SelectedObfuscator; /// Port ranges available for WireGuard relays that have extra IPs for Shadowsocks. @@ -84,19 +85,74 @@ pub fn pick_random_relay_weighted<'a, RelayType>( } } +/// Create a multiplexer obfuscator config +/// +/// # Arguments +/// * `udp2tcp_ports` - Available ports for UDP2TCP obfuscation +/// * `shadowsocks_ports` - Available port ranges for Shadowsocks obfuscation +/// * `obfuscator_relay` - The relay that will host the obfuscation services +/// * `endpoint` - Selected endpoint +#[cfg(feature = "staggered-obfuscation")] +pub fn get_multiplexer_obfuscator( + udp2tcp_ports: &[u16], + shadowsocks_ports: &[RangeInclusive<u16>], + obfuscator_relay: Relay, + endpoint: &MullvadWireguardEndpoint, +) -> Result<SelectedObfuscator, Error> { + use talpid_types::net::obfuscation::Obfuscators; + + // Direct (no obfuscation) method + let direct = Some(endpoint.peer.endpoint); + + // Add obfuscation methods + let mut configs = vec![]; + + let udp2tcp = get_udp2tcp_obfuscator( + &Udp2TcpObfuscationSettings::default(), + udp2tcp_ports, + obfuscator_relay.clone(), + endpoint, + )?; + configs.push(udp2tcp.0); + + let shadowsocks = get_shadowsocks_obfuscator( + &ShadowsocksSettings::default(), + shadowsocks_ports, + obfuscator_relay.clone(), + endpoint, + )?; + configs.push(shadowsocks.0); + + let ip_version = match endpoint.peer.endpoint { + SocketAddr::V4(_) => IpVersion::V4, + SocketAddr::V6(_) => IpVersion::V6, + }; + if let Some(quic) = get_quic_obfuscator(obfuscator_relay.clone(), ip_version) { + configs.push(quic.0); + } + + let config = + Obfuscators::multiplexer(direct, &configs).expect("non-zero number of obfuscators"); + + Ok(SelectedObfuscator { + config, + relay: obfuscator_relay, + }) +} + pub fn get_udp2tcp_obfuscator( obfuscation_settings_constraint: &Udp2TcpObfuscationSettings, udp2tcp_ports: &[u16], relay: Relay, endpoint: &MullvadWireguardEndpoint, -) -> Result<SelectedObfuscator, Error> { +) -> Result<(ObfuscatorConfig, Relay), Error> { let udp2tcp_endpoint_port = get_udp2tcp_obfuscator_port(obfuscation_settings_constraint, udp2tcp_ports)?; let config = ObfuscatorConfig::Udp2Tcp { endpoint: SocketAddr::new(endpoint.peer.endpoint.ip(), udp2tcp_endpoint_port), }; - Ok(SelectedObfuscator { config, relay }) + Ok((config, relay)) } fn get_udp2tcp_obfuscator_port( @@ -120,7 +176,7 @@ pub fn get_shadowsocks_obfuscator( non_extra_port_ranges: &[RangeInclusive<u16>], relay: Relay, endpoint: &MullvadWireguardEndpoint, -) -> Result<SelectedObfuscator, Error> { +) -> Result<(ObfuscatorConfig, Relay), Error> { let port = settings.port; let extra_addrs = match &relay.endpoint_data { mullvad_types::relay_list::RelayEndpointData::Wireguard(wg) => { @@ -136,13 +192,13 @@ pub fn get_shadowsocks_obfuscator( port, )?; - Ok(SelectedObfuscator { - config: ObfuscatorConfig::Shadowsocks { endpoint }, - relay, - }) + Ok((ObfuscatorConfig::Shadowsocks { endpoint }, relay)) } -pub fn get_quic_obfuscator(relay: Relay, ip_version: IpVersion) -> Option<SelectedObfuscator> { +pub fn get_quic_obfuscator( + relay: Relay, + ip_version: IpVersion, +) -> Option<(ObfuscatorConfig, Relay)> { let quic = relay.wireguard()?.quic()?; let config = { let hostname = quic.hostname().to_string(); @@ -158,14 +214,13 @@ pub fn get_quic_obfuscator(relay: Relay, ip_version: IpVersion) -> Option<Select } }; - let obfuscator = SelectedObfuscator { config, relay }; - Some(obfuscator) + Some((config, relay)) } pub fn get_lwo_obfuscator( relay: Relay, endpoint: &MullvadWireguardEndpoint, -) -> Option<SelectedObfuscator> { +) -> Option<(ObfuscatorConfig, Relay)> { let _wg = relay.wireguard()?; // TODO: check if LWO is supported on this relay @@ -179,8 +234,7 @@ pub fn get_lwo_obfuscator( let config = ObfuscatorConfig::Lwo { endpoint }; - let obfuscator = SelectedObfuscator { config, relay }; - Some(obfuscator) + Some((config, relay)) } /// Return an obfuscation config for the wireguard server at `wg_in_addr` or one of `extra_in_addrs` @@ -214,8 +268,8 @@ fn get_shadowsocks_obfuscator_inner<R: RangeBounds<u16> + Iterator<Item = u16> + } /// Return `desired_port` if it is specified and included in `port_ranges`. -/// If `desired_port` isn't specified, a random port from the ranges is returned. -/// If `desired_port` is specified but not in range, an error is returned. +/// If `desired_port` isn't specified, return a random port from the ranges. +/// If `desired_port` is specified but not in range, return an error. pub fn desired_or_random_port_from_range<R: RangeBounds<u16> + Iterator<Item = u16> + Clone>( port_ranges: &[R], desired_port: Constraint<u16>, @@ -239,7 +293,7 @@ fn port_if_in_range<R: RangeBounds<u16>>(port_ranges: &[R], port: u16) -> Result .ok_or(Error::NoMatchingPort) } -/// Selects a random port number from a list of provided port ranges. +/// Select a random port number from a list of provided port ranges. /// /// # Parameters /// - `port_ranges`: A slice of port numbers. diff --git a/mullvad-relay-selector/src/relay_selector/mod.rs b/mullvad-relay-selector/src/relay_selector/mod.rs index bb6f251c0b..b286d8f38b 100644 --- a/mullvad-relay-selector/src/relay_selector/mod.rs +++ b/mullvad-relay-selector/src/relay_selector/mod.rs @@ -49,7 +49,7 @@ use talpid_types::{ ErrorExt, net::{ Endpoint, IpAvailability, IpVersion, TransportProtocol, TunnelType, - obfuscation::ObfuscatorConfig, + obfuscation::{ObfuscatorConfig, Obfuscators}, proxy::{CustomProxy, Shadowsocks}, }, }; @@ -269,10 +269,19 @@ impl SelectedBridge { #[derive(Clone, Debug)] pub struct SelectedObfuscator { - pub config: ObfuscatorConfig, + pub config: Obfuscators, pub relay: Relay, } +impl From<(ObfuscatorConfig, Relay)> for SelectedObfuscator { + fn from((config, relay): (ObfuscatorConfig, Relay)) -> Self { + SelectedObfuscator { + config: Obfuscators::Single(config), + relay, + } + } +} + impl Default for SelectorConfig { fn default() -> Self { let default_settings = Settings::default(); @@ -924,12 +933,27 @@ impl RelaySelector { let box_obfsucation_error = |error: helpers::Error| Error::NoObfuscator(Box::new(error)); match &query.wireguard_constraints().obfuscation { - ObfuscationQuery::Off | ObfuscationQuery::Auto => Ok(None), + ObfuscationQuery::Off => Ok(None), + #[cfg(not(feature = "staggered-obfuscation"))] + ObfuscationQuery::Auto => Ok(None), + #[cfg(feature = "staggered-obfuscation")] + ObfuscationQuery::Auto => { + let shadowsocks_ports = &parsed_relays.wireguard.shadowsocks_port_ranges; + let udp2tcp_ports = &parsed_relays.wireguard.udp2tcp_ports; + helpers::get_multiplexer_obfuscator( + udp2tcp_ports, + shadowsocks_ports, + obfuscator_relay, + endpoint, + ) + .map(Some) + .map_err(box_obfsucation_error) + } ObfuscationQuery::Udp2tcp(settings) => { let udp2tcp_ports = &parsed_relays.wireguard.udp2tcp_ports; helpers::get_udp2tcp_obfuscator(settings, udp2tcp_ports, obfuscator_relay, endpoint) - .map(Some) + .map(|obfs| Some(obfs.into())) .map_err(box_obfsucation_error) } ObfuscationQuery::Shadowsocks(settings) => { @@ -940,15 +964,18 @@ impl RelaySelector { obfuscator_relay, endpoint, ) + .map(|obfs| obfs.into()) .map_err(box_obfsucation_error)?; Ok(Some(obfuscation)) } ObfuscationQuery::Quic => { let ip_version = resolve_ip_version(query.wireguard_constraints().ip_version); - Ok(helpers::get_quic_obfuscator(obfuscator_relay, ip_version)) + Ok(helpers::get_quic_obfuscator(obfuscator_relay, ip_version).map(Into::into)) + } + ObfuscationQuery::Lwo => { + Ok(helpers::get_lwo_obfuscator(obfuscator_relay, endpoint).map(Into::into)) } - ObfuscationQuery::Lwo => Ok(helpers::get_lwo_obfuscator(obfuscator_relay, endpoint)), } } diff --git a/mullvad-relay-selector/tests/relay_selector.rs b/mullvad-relay-selector/tests/relay_selector.rs index e623b0f168..3e60e39e96 100644 --- a/mullvad-relay-selector/tests/relay_selector.rs +++ b/mullvad-relay-selector/tests/relay_selector.rs @@ -9,7 +9,7 @@ use talpid_types::net::{ Endpoint, IpVersion, TransportProtocol::{Tcp, Udp}, TunnelType, - obfuscation::ObfuscatorConfig, + obfuscation::{ObfuscatorConfig, Obfuscators}, wireguard::PublicKey, }; @@ -842,7 +842,7 @@ fn test_selecting_wireguard_over_shadowsocks() { } => { assert!(obfuscator.is_some_and(|obfuscator| matches!( obfuscator.config, - ObfuscatorConfig::Shadowsocks { .. } + Obfuscators::Single(ObfuscatorConfig::Shadowsocks { .. }) ))) } wrong_relay => panic!( @@ -867,7 +867,7 @@ fn test_selecting_wireguard_over_shadowsocks_extra_ips() { GetRelay::Wireguard { obfuscator: Some(SelectedObfuscator { - config: ObfuscatorConfig::Shadowsocks { endpoint }, + config: Obfuscators::Single(ObfuscatorConfig::Shadowsocks { endpoint }), .. }), inner: WireguardConfig::Singlehop { exit }, @@ -903,7 +903,7 @@ fn test_selecting_wireguard_over_quic() { } => { assert!(obfuscator.is_some_and(|obfuscator| matches!( obfuscator.config, - ObfuscatorConfig::Quic { .. }, + Obfuscators::Single(ObfuscatorConfig::Quic { .. }), ))) } wrong_relay => panic!( @@ -987,7 +987,7 @@ fn test_selecting_wireguard_ignore_extra_ips_override_v4() { GetRelay::Wireguard { obfuscator: Some(SelectedObfuscator { - config: ObfuscatorConfig::Shadowsocks { endpoint }, + config: Obfuscators::Single(ObfuscatorConfig::Shadowsocks { endpoint }), .. }), inner: WireguardConfig::Singlehop { exit }, @@ -1034,7 +1034,7 @@ fn test_selecting_wireguard_ignore_extra_ips_override_v6() { GetRelay::Wireguard { obfuscator: Some(SelectedObfuscator { - config: ObfuscatorConfig::Shadowsocks { endpoint }, + config: Obfuscators::Single(ObfuscatorConfig::Shadowsocks { endpoint }), .. }), inner: WireguardConfig::Singlehop { exit }, @@ -1068,7 +1068,7 @@ fn test_selecting_wireguard_endpoint_with_udp2tcp_obfuscation() { } => { assert!(obfuscator.is_some_and(|obfuscator| matches!( obfuscator.config, - ObfuscatorConfig::Udp2Tcp { .. } + Obfuscators::Single(ObfuscatorConfig::Udp2Tcp { .. }) ))) } wrong_relay => panic!( @@ -1082,6 +1082,7 @@ fn test_selecting_wireguard_endpoint_with_udp2tcp_obfuscation() { /// obfuscator config. /// /// [`RelaySelector::get_relay`] may still enable obfuscation if it is present in [`RETRY_ORDER`]. +#[cfg(not(feature = "staggered-obfuscation"))] #[test] fn test_selecting_wireguard_endpoint_with_auto_obfuscation() { let relay_selector = default_relay_selector(); @@ -1126,7 +1127,7 @@ fn test_selected_wireguard_endpoints_use_correct_port_ranges() { panic!("Relay selector should have picked an obfuscator") }; assert!(matches!(obfuscator.config, - ObfuscatorConfig::Udp2Tcp { endpoint } if + Obfuscators::Single(ObfuscatorConfig::Udp2Tcp { endpoint }) if TCP2UDP_PORTS.contains(&endpoint.port()), )) } @@ -1736,10 +1737,10 @@ fn test_shadowsocks_runtime_ipv4_unavailable() { assert!( matches!(user_result, GetRelay::Wireguard { obfuscator: Some(SelectedObfuscator { - config: ObfuscatorConfig::Shadowsocks { + config: Obfuscators::Single(ObfuscatorConfig::Shadowsocks { endpoint, .. - }, + }), .. }), .. diff --git a/mullvad-types/src/features.rs b/mullvad-types/src/features.rs index c181a52dbb..a16f7ca33f 100644 --- a/mullvad-types/src/features.rs +++ b/mullvad-types/src/features.rs @@ -5,7 +5,7 @@ use std::{ use crate::settings::{DnsState, Settings}; use serde::{Deserialize, Serialize}; -use talpid_types::net::{ObfuscationType, TunnelEndpoint, TunnelType}; +use talpid_types::net::{ObfuscationInfo, ObfuscationType, TunnelEndpoint, TunnelType}; /// Feature indicators are active settings that should be shown to the user to make them aware of /// what is affecting their connection at any given time. @@ -171,11 +171,12 @@ pub fn compute_feature_indicators( TunnelType::Wireguard => { let quantum_resistant = endpoint.quantum_resistant; - let has_obfuscation = |obfs| { - endpoint - .obfuscation + let has_obfuscation = |obfs| match &endpoint.obfuscation { + Some(ObfuscationInfo::Single(endpoint)) => endpoint.obfuscation_type == obfs, + Some(ObfuscationInfo::Multiplexer { obfuscators, .. }) => obfuscators .iter() - .any(|obfuscation| obfuscation.obfuscation_type == obfs) + .any(|single| single.obfuscation_type == obfs), + None => false, }; let udp_tcp = has_obfuscation(ObfuscationType::Udp2Tcp); let shadowsocks = has_obfuscation(ObfuscationType::Shadowsocks); @@ -359,19 +360,22 @@ mod tests { expected_indicators ); - endpoint.obfuscation = Some(ObfuscationEndpoint { + endpoint.obfuscation = Some(ObfuscationInfo::Single(ObfuscationEndpoint { endpoint: Endpoint { address: SocketAddr::from(([1, 2, 3, 4], 443)), protocol: TransportProtocol::Tcp, }, obfuscation_type: ObfuscationType::Udp2Tcp, - }); + })); expected_indicators.0.insert(FeatureIndicator::Udp2Tcp); assert_eq!( compute_feature_indicators(&settings, &endpoint, false), expected_indicators ); - endpoint.obfuscation.as_mut().unwrap().obfuscation_type = ObfuscationType::Shadowsocks; + let Some(ObfuscationInfo::Single(ref mut obfs)) = endpoint.obfuscation else { + unreachable!() + }; + obfs.obfuscation_type = ObfuscationType::Shadowsocks; expected_indicators.0.remove(&FeatureIndicator::Udp2Tcp); expected_indicators.0.insert(FeatureIndicator::Shadowsocks); assert_eq!( diff --git a/talpid-core/src/firewall/linux.rs b/talpid-core/src/firewall/linux.rs index 1a418e8ca6..f49733c9f2 100644 --- a/talpid-core/src/firewall/linux.rs +++ b/talpid-core/src/firewall/linux.rs @@ -561,13 +561,15 @@ impl<'a> PolicyBatch<'a> { fn add_policy_specific_rules(&mut self, policy: &FirewallPolicy, fwmark: u32) -> Result<()> { let allow_lan = match policy { FirewallPolicy::Connecting { - peer_endpoint, + peer_endpoints, tunnel, allow_lan, allowed_endpoint, allowed_tunnel_traffic, } => { - self.add_allow_tunnel_endpoint_rules(peer_endpoint, fwmark); + for endpoint in peer_endpoints { + self.add_allow_tunnel_endpoint_rules(endpoint, fwmark); + } self.add_allow_endpoint_rules(allowed_endpoint); // Important to block DNS after allow relay rule (so the relay can operate @@ -595,12 +597,14 @@ impl<'a> PolicyBatch<'a> { *allow_lan } FirewallPolicy::Connected { - peer_endpoint, + peer_endpoints, tunnel, allow_lan, dns_config, } => { - self.add_allow_tunnel_endpoint_rules(peer_endpoint, fwmark); + for endpoint in peer_endpoints { + self.add_allow_tunnel_endpoint_rules(endpoint, fwmark); + } for server in dns_config.tunnel_config() { self.add_allow_tunnel_dns_rule( diff --git a/talpid-core/src/firewall/macos.rs b/talpid-core/src/firewall/macos.rs index d8bf5a5136..020b8d713c 100644 --- a/talpid-core/src/firewall/macos.rs +++ b/talpid-core/src/firewall/macos.rs @@ -172,7 +172,7 @@ impl Firewall { } } - let Some(peer) = policy.peer_endpoint().map(|endpoint| endpoint.endpoint) else { + let Some(peers) = policy.peer_endpoints() else { // If there's no peer, there's also no tunnel. We have no states to preserve return Ok(true); }; @@ -192,9 +192,20 @@ impl Firewall { } } } else { - // Clear all states except traffic destined for the VPN endpoint. - // Ephemeral peer exchange becomes unreliable otherwise. - peer.address != remote_address || as_pfctl_proto(peer.protocol) != proto + let mut should_delete = true; + for peer in peers { + let peer = peer.endpoint; + + // Clear all states except traffic destined for some VPN endpoint. + // Ephemeral peer exchange becomes unreliable otherwise. + should_delete &= + peer.address != remote_address || as_pfctl_proto(peer.protocol) != proto; + + if !should_delete { + break; + } + } + should_delete }; Ok(should_delete) @@ -274,12 +285,12 @@ impl Firewall { /// bit too clever. fn get_nat_rules(&mut self, policy: &FirewallPolicy) -> Result<Vec<pfctl::NatRule>> { let (FirewallPolicy::Connected { - peer_endpoint, + peer_endpoints, tunnel, .. } | FirewallPolicy::Connecting { - peer_endpoint, + peer_endpoints, tunnel: Some(tunnel), .. }) = policy @@ -309,11 +320,13 @@ impl Firewall { } // no nat to [vpn ip] - let no_nat_to_vpn_server = pfctl::NatRuleBuilder::default() - .action(pfctl::NatRuleAction::NoNat) - .to(peer_endpoint.endpoint.address) - .build()?; - rules.push(no_nat_to_vpn_server); + for peer_endpoint in peer_endpoints { + let no_nat_to_vpn_server = pfctl::NatRuleBuilder::default() + .action(pfctl::NatRuleAction::NoNat) + .to(peer_endpoint.endpoint.address) + .build()?; + rules.push(no_nat_to_vpn_server); + } // no nat on [tun interface] let no_nat_on_tun = pfctl::NatRuleBuilder::default() @@ -346,14 +359,17 @@ impl Firewall { ) -> Result<Vec<pfctl::FilterRule>> { match policy { FirewallPolicy::Connecting { - peer_endpoint, + peer_endpoints, tunnel, allow_lan, allowed_endpoint, allowed_tunnel_traffic, redirect_interface, } => { - let mut rules = vec![self.get_allow_relay_rule(peer_endpoint)?]; + let mut rules = vec![]; + for peer in peer_endpoints { + rules.push(self.get_allow_relay_rule(peer)?); + } rules.push(self.get_allowed_endpoint_rule(allowed_endpoint)?); // Important to block DNS after allow relay rule (so the relay can operate @@ -393,7 +409,7 @@ impl Firewall { Ok(rules) } FirewallPolicy::Connected { - peer_endpoint, + peer_endpoints, tunnel, allow_lan, dns_config, @@ -412,7 +428,9 @@ impl Firewall { ); } - rules.push(self.get_allow_relay_rule(peer_endpoint)?); + for peer in peer_endpoints { + rules.push(self.get_allow_relay_rule(peer)?); + } // Important to block DNS *before* we allow the tunnel and allow LAN. So DNS // can't leak to the wrong IPs in the tunnel or on the LAN. diff --git a/talpid-core/src/firewall/mod.rs b/talpid-core/src/firewall/mod.rs index 22d4954d64..a13a447aa1 100644 --- a/talpid-core/src/firewall/mod.rs +++ b/talpid-core/src/firewall/mod.rs @@ -81,8 +81,8 @@ pub fn is_local_address(address: &IpAddr) -> bool { pub enum FirewallPolicy { /// Allow traffic only to server Connecting { - /// The peer endpoint that should be allowed. - peer_endpoint: AllowedEndpoint, + /// The peer endpoints that should be allowed. + peer_endpoints: Vec<AllowedEndpoint>, /// IP of the exit endpoint, iff it differs from `peer_endpoint` #[cfg(target_os = "windows")] exit_endpoint_ip: Option<IpAddr>, @@ -101,8 +101,8 @@ pub enum FirewallPolicy { /// Allow traffic only to server and over tunnel interface Connected { - /// The peer endpoint that should be allowed. - peer_endpoint: AllowedEndpoint, + /// The peer endpoints that should be allowed. + peer_endpoints: Vec<AllowedEndpoint>, /// IP of the exit endpoint, iff it differs from `peer_endpoint` #[cfg(target_os = "windows")] exit_endpoint_ip: Option<IpAddr>, @@ -129,10 +129,10 @@ pub enum FirewallPolicy { impl FirewallPolicy { /// Return the tunnel peer endpoint, if available - pub fn peer_endpoint(&self) -> Option<&AllowedEndpoint> { + pub fn peer_endpoints(&self) -> Option<&[AllowedEndpoint]> { match self { - FirewallPolicy::Connecting { peer_endpoint, .. } - | FirewallPolicy::Connected { peer_endpoint, .. } => Some(peer_endpoint), + FirewallPolicy::Connecting { peer_endpoints, .. } + | FirewallPolicy::Connected { peer_endpoints, .. } => Some(peer_endpoints), _ => None, } } @@ -201,9 +201,25 @@ impl FirewallPolicy { impl fmt::Display for FirewallPolicy { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn print_peer_endpoints( + f: &mut fmt::Formatter<'_>, + endpoints: &[AllowedEndpoint], + ) -> fmt::Result { + if let Some((first, remaining)) = endpoints.split_first() { + write!(f, "{{ {first} ")?; + for endpoint in remaining { + write!(f, "| {endpoint} ")?; + } + write!(f, "}}")?; + } else { + write!(f, "unknown")?; + } + Ok(()) + } + match self { FirewallPolicy::Connecting { - peer_endpoint, + peer_endpoints, tunnel, allow_lan, allowed_endpoint, @@ -211,10 +227,12 @@ impl fmt::Display for FirewallPolicy { .. } => { if let Some(tunnel) = tunnel { + write!(f, "Connecting to ")?; + print_peer_endpoints(f, peer_endpoints)?; + write!( f, - "Connecting to {} over \"{}\" (ip: {}, v4 gw: {}, v6 gw: {:?}, allowed in-tunnel traffic: {}), {} LAN. Allowing endpoint {}", - peer_endpoint, + " over \"{}\" (ip: {}, v4 gw: {}, v6 gw: {:?}, allowed in-tunnel traffic: {}), {} LAN. Allowing endpoint {}", tunnel.interface, tunnel .ips @@ -229,35 +247,39 @@ impl fmt::Display for FirewallPolicy { allowed_endpoint, ) } else { + write!(f, "Connecting to ")?; + print_peer_endpoints(f, peer_endpoints)?; write!( f, - "Connecting to {}, {} LAN, interface: none. Allowing endpoint {}", - peer_endpoint, + ", {} LAN, interface: none. Allowing endpoint {}", if *allow_lan { "Allowing" } else { "Blocking" }, allowed_endpoint, ) } } FirewallPolicy::Connected { - peer_endpoint, + peer_endpoints, tunnel, allow_lan, .. - } => write!( - f, - "Connected to {} over \"{}\" (ip: {}, v4 gw: {}, v6 gw: {:?}), {} LAN", - peer_endpoint, - tunnel.interface, - tunnel - .ips - .iter() - .map(|ip| ip.to_string()) - .collect::<Vec<_>>() - .join(","), - tunnel.ipv4_gateway, - tunnel.ipv6_gateway, - if *allow_lan { "Allowing" } else { "Blocking" } - ), + } => { + write!(f, "Connected to ")?; + print_peer_endpoints(f, peer_endpoints)?; + write!( + f, + " over \"{}\" (ip: {}, v4 gw: {}, v6 gw: {:?}), {} LAN", + tunnel.interface, + tunnel + .ips + .iter() + .map(|ip| ip.to_string()) + .collect::<Vec<_>>() + .join(","), + tunnel.ipv4_gateway, + tunnel.ipv6_gateway, + if *allow_lan { "Allowing" } else { "Blocking" } + ) + } FirewallPolicy::Blocked { allow_lan, allowed_endpoint, diff --git a/talpid-core/src/firewall/windows/mod.rs b/talpid-core/src/firewall/windows/mod.rs index 20d8901c4a..1aa207dc47 100644 --- a/talpid-core/src/firewall/windows/mod.rs +++ b/talpid-core/src/firewall/windows/mod.rs @@ -124,7 +124,7 @@ impl Firewall { let apply_result = match policy { FirewallPolicy::Connecting { - peer_endpoint, + peer_endpoints, exit_endpoint_ip, tunnel, allow_lan, @@ -133,7 +133,7 @@ impl Firewall { } => { let cfg = &WinFwSettings::new(allow_lan); self.set_connecting_state( - &peer_endpoint, + &peer_endpoints, exit_endpoint_ip, cfg, tunnel.as_ref(), @@ -142,7 +142,7 @@ impl Firewall { ) } FirewallPolicy::Connected { - peer_endpoint, + peer_endpoints, exit_endpoint_ip, tunnel, allow_lan, @@ -150,7 +150,7 @@ impl Firewall { } => { let cfg = &WinFwSettings::new(allow_lan); self.set_connected_state( - &peer_endpoint, + &peer_endpoints, exit_endpoint_ip, cfg, &tunnel, @@ -199,7 +199,7 @@ impl Firewall { fn set_connecting_state( &mut self, - peer_endpoint: &AllowedEndpoint, + peer_endpoints: &[AllowedEndpoint], exit_endpoint_ip: Option<IpAddr>, winfw_settings: &WinFwSettings, tunnel_metadata: Option<&TunnelMetadata>, @@ -209,7 +209,7 @@ impl Firewall { log::trace!("Applying 'connecting' firewall policy"); let tunnel_interface = tunnel_metadata.map(|metadata| metadata.interface.as_ref()); winfw::apply_policy_connecting( - peer_endpoint, + peer_endpoints, exit_endpoint_ip, winfw_settings, tunnel_interface, @@ -221,7 +221,7 @@ impl Firewall { fn set_connected_state( &mut self, - endpoint: &AllowedEndpoint, + peer_endpoints: &[AllowedEndpoint], exit_endpoint_ip: Option<IpAddr>, winfw_settings: &WinFwSettings, tunnel_metadata: &TunnelMetadata, @@ -230,7 +230,7 @@ impl Firewall { log::trace!("Applying 'connected' firewall policy"); let tunnel_interface = &tunnel_metadata.interface; winfw::apply_policy_connected( - endpoint, + peer_endpoints, exit_endpoint_ip, winfw_settings, tunnel_interface, diff --git a/talpid-core/src/firewall/windows/winfw/mod.rs b/talpid-core/src/firewall/windows/winfw/mod.rs index 9a2d5a7c8a..772408ff12 100644 --- a/talpid-core/src/firewall/windows/winfw/mod.rs +++ b/talpid-core/src/firewall/windows/winfw/mod.rs @@ -85,22 +85,42 @@ pub(super) fn apply_policy_blocked( } pub(super) fn apply_policy_connecting( - peer_endpoint: &AllowedEndpoint, + peer_endpoints: &[AllowedEndpoint], exit_endpoint_ip: Option<IpAddr>, winfw_settings: &WinFwSettings, tunnel_interface: Option<&str>, allowed_endpoint: AllowedEndpoint, allowed_tunnel_traffic: &AllowedTunnelTraffic, ) -> Result<(), FirewallPolicyError> { - let ip_str = widestring_ip(peer_endpoint.endpoint.address.ip()); - let winfw_relay = WinFwEndpoint { - ip: ip_str.as_ptr(), - port: peer_endpoint.endpoint.address.port(), - protocol: WinFwProt::from(peer_endpoint.endpoint.protocol), - }; + let mut winfw_relays = vec![]; + let mut ip_strs = vec![]; + let mut clients = None; + + for peer in peer_endpoints { + if clients.is_none() { + clients = Some(peer.clients.clone()); + } else if clients.as_ref() != Some(&peer.clients) { + log::error!("Relay endpoints must have same allowed clients"); + return Err(FirewallPolicyError::Generic); + } + + let ip_str = widestring_ip(peer.endpoint.address.ip()); + let ip_str_ptr = ip_str.as_ptr(); + // Keep pointer valid for the duration of this function + ip_strs.push(ip_str); - let relay_client_wstrs: Vec<_> = peer_endpoint - .clients + winfw_relays.push(WinFwEndpoint { + ip: ip_str_ptr, + port: peer.endpoint.address.port(), + protocol: WinFwProt::from(peer.endpoint.protocol), + }); + } + + let relay_client_wstrs: Vec<_> = clients + .ok_or_else(|| { + log::error!("Allowed clients missing. Zero peer endpoints?"); + FirewallPolicyError::Generic + })? .iter() .map(WideCString::from_os_str_truncate) .collect(); @@ -175,7 +195,8 @@ pub(super) fn apply_policy_connecting( let res = unsafe { WinFw_ApplyPolicyConnecting( winfw_settings, - &winfw_relay, + winfw_relays.len(), + winfw_relays.as_ptr(), exit_endpoint_ip_ptr, relay_client_wstr_ptrs.as_ptr(), relay_client_wstr_ptrs_len, @@ -193,31 +214,51 @@ pub(super) fn apply_policy_connecting( drop(endpoint2); drop(relay_client_wstrs); drop(exit_endpoint_ip_wstr); + drop(winfw_relays); + drop(ip_strs); res.into_result() } pub(super) fn apply_policy_connected( - endpoint: &AllowedEndpoint, + peer_endpoints: &[AllowedEndpoint], exit_endpoint_ip: Option<IpAddr>, winfw_settings: &WinFwSettings, tunnel_interface: &str, dns_config: &crate::dns::ResolvedDnsConfig, ) -> Result<(), FirewallPolicyError> { - let ip_str = widestring_ip(endpoint.endpoint.address.ip()); + let mut winfw_relays = vec![]; + let mut ip_strs = vec![]; + let mut clients = None; - let tunnel_alias = WideCString::from_str_truncate(tunnel_interface); + for peer in peer_endpoints { + if clients.is_none() { + clients = Some(peer.clients.clone()); + } else if clients.as_ref() != Some(&peer.clients) { + log::error!("Relay endpoints must have same allowed clients"); + return Err(FirewallPolicyError::Generic); + } - // ip_str, gateway_str and tunnel_alias have to outlive winfw_relay - let winfw_relay = WinFwEndpoint { - ip: ip_str.as_ptr(), - port: endpoint.endpoint.address.port(), - protocol: WinFwProt::from(endpoint.endpoint.protocol), - }; + let ip_str = widestring_ip(peer.endpoint.address.ip()); + let ip_str_ptr = ip_str.as_ptr(); + // Keep pointer valid for the duration of this function + ip_strs.push(ip_str); + + winfw_relays.push(WinFwEndpoint { + ip: ip_str_ptr, + port: peer.endpoint.address.port(), + protocol: WinFwProt::from(peer.endpoint.protocol), + }); + } + + let tunnel_alias = WideCString::from_str_truncate(tunnel_interface); // SAFETY: `relay_client_wstrs` must not be dropped until `WinFw_ApplyPolicyConnected` has // returned. - let relay_client_wstrs: Vec<_> = endpoint - .clients + let relay_client_wstrs: Vec<_> = clients + .ok_or_else(|| { + log::error!("Allowed clients missing. Zero peer endpoints?"); + FirewallPolicyError::Generic + })? .iter() .map(WideCString::from_os_str_truncate) .collect(); @@ -255,7 +296,8 @@ pub(super) fn apply_policy_connected( let result = unsafe { WinFw_ApplyPolicyConnected( winfw_settings, - &winfw_relay, + winfw_relays.len(), + winfw_relays.as_ptr(), exit_endpoint_ip_ptr, relay_client_wstr_ptrs.as_ptr(), relay_client_wstr_ptrs_len, @@ -269,7 +311,8 @@ pub(super) fn apply_policy_connected( // SAFETY: All of these must remain allocated until `WinFw_ApplyPolicyConnected` has returned. drop(exit_endpoint_ip_wstr); - drop(ip_str); + drop(ip_strs); + drop(winfw_relays); drop(relay_client_wstrs); result.into_result() } diff --git a/talpid-core/src/firewall/windows/winfw/sys.rs b/talpid-core/src/firewall/windows/winfw/sys.rs index 40bb61599c..a47c2b4192 100644 --- a/talpid-core/src/firewall/windows/winfw/sys.rs +++ b/talpid-core/src/firewall/windows/winfw/sys.rs @@ -131,7 +131,8 @@ unsafe extern "system" { #[link_name = "WinFw_ApplyPolicyConnecting"] pub fn WinFw_ApplyPolicyConnecting( settings: &WinFwSettings, - relay: &WinFwEndpoint, + numRelays: usize, + relays: *const WinFwEndpoint, exitEndpointIp: *const libc::wchar_t, relayClient: *const *const libc::wchar_t, relayClientLen: usize, @@ -143,7 +144,8 @@ unsafe extern "system" { #[link_name = "WinFw_ApplyPolicyConnected"] pub fn WinFw_ApplyPolicyConnected( settings: &WinFwSettings, - relay: &WinFwEndpoint, + numRelays: usize, + relays: *const WinFwEndpoint, exitEndpointIp: *const libc::wchar_t, relayClient: *const *const libc::wchar_t, relayClientLen: usize, diff --git a/talpid-core/src/tunnel_state_machine/connected_state.rs b/talpid-core/src/tunnel_state_machine/connected_state.rs index 7f23f3622f..5076338653 100644 --- a/talpid-core/src/tunnel_state_machine/connected_state.rs +++ b/talpid-core/src/tunnel_state_machine/connected_state.rs @@ -104,7 +104,7 @@ impl ConnectedState { } fn get_firewall_policy(&self, shared_values: &SharedTunnelStateValues) -> FirewallPolicy { - let endpoint = self.tunnel_parameters.get_next_hop_endpoint(); + let endpoints = self.tunnel_parameters.get_next_hop_endpoints(); #[cfg(target_os = "windows")] let clients = AllowedClients::from( @@ -130,10 +130,13 @@ impl ConnectedState { .get_exit_hop_endpoint() .map(|ep| ep.address.ip()); - #[cfg(target_os = "windows")] - debug_assert_ne!(exit_endpoint_ip, Some(endpoint.address.ip())); - - let peer_endpoint = AllowedEndpoint { endpoint, clients }; + let peer_endpoints = endpoints + .into_iter() + .map(|endpoint| AllowedEndpoint { + endpoint, + clients: clients.clone(), + }) + .collect(); #[cfg(target_os = "macos")] let redirect_interface = shared_values @@ -141,7 +144,7 @@ impl ConnectedState { .block_on(shared_values.split_tunnel.interface()); FirewallPolicy::Connected { - peer_endpoint, + peer_endpoints, #[cfg(target_os = "windows")] exit_endpoint_ip, tunnel: self.metadata.clone(), diff --git a/talpid-core/src/tunnel_state_machine/connecting_state.rs b/talpid-core/src/tunnel_state_machine/connecting_state.rs index 7ba803478c..479238af60 100644 --- a/talpid-core/src/tunnel_state_machine/connecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/connecting_state.rs @@ -167,7 +167,7 @@ impl ConnectingState { #[cfg(target_os = "linux")] shared_values.disable_connectivity_check(); - let endpoint = params.get_next_hop_endpoint(); + let endpoints = params.get_next_hop_endpoints(); #[cfg(target_os = "windows")] let clients = AllowedClients::from( @@ -186,10 +186,13 @@ impl ConnectingState { #[cfg(target_os = "windows")] let exit_endpoint_ip = params.get_exit_hop_endpoint().map(|ep| ep.address.ip()); - #[cfg(target_os = "windows")] - debug_assert_ne!(exit_endpoint_ip, Some(endpoint.address.ip())); - - let peer_endpoint = AllowedEndpoint { endpoint, clients }; + let peer_endpoints = endpoints + .into_iter() + .map(|endpoint| AllowedEndpoint { + endpoint, + clients: clients.clone(), + }) + .collect(); #[cfg(target_os = "macos")] let redirect_interface = shared_values @@ -197,7 +200,7 @@ impl ConnectingState { .block_on(shared_values.split_tunnel.interface()); let policy = FirewallPolicy::Connecting { - peer_endpoint, + peer_endpoints, #[cfg(target_os = "windows")] exit_endpoint_ip, tunnel: tunnel_metadata.clone(), diff --git a/talpid-types/src/net/mod.rs b/talpid-types/src/net/mod.rs index 5939bfebad..ddd1d6252f 100644 --- a/talpid-types/src/net/mod.rs +++ b/talpid-types/src/net/mod.rs @@ -1,12 +1,14 @@ +use crate::net::obfuscation::ObfuscatorConfig; + use self::proxy::{CustomProxy, Socks5Local}; #[cfg(target_os = "android")] use jnix::FromJava; -use obfuscation::ObfuscatorConfig; +use obfuscation::Obfuscators; use serde::{Deserialize, Serialize}; #[cfg(windows)] use std::path::PathBuf; use std::{ - fmt, + fmt, iter, net::{IpAddr, SocketAddr}, str::FromStr, }; @@ -54,7 +56,7 @@ impl TunnelParameters { .get_exit_endpoint() .unwrap_or_else(|| params.connection.get_endpoint()), proxy: None, - obfuscation: params.obfuscation.as_ref().map(ObfuscationEndpoint::from), + obfuscation: params.obfuscation.as_ref().map(ObfuscationInfo::from), entry_endpoint: params .connection .get_exit_endpoint() @@ -67,14 +69,14 @@ impl TunnelParameters { } /// Returns the endpoint that will be connected to - pub fn get_next_hop_endpoint(&self) -> Endpoint { + pub fn get_next_hop_endpoints(&self) -> Vec<Endpoint> { match self { TunnelParameters::OpenVpn(params) => params .proxy .as_ref() - .map(|proxy| proxy.get_remote_endpoint().endpoint) - .unwrap_or(params.config.endpoint), - TunnelParameters::Wireguard(params) => params.get_next_hop_endpoint(), + .map(|proxy| vec![proxy.get_remote_endpoint().endpoint]) + .unwrap_or_else(|| vec![params.config.endpoint]), + TunnelParameters::Wireguard(params) => params.get_next_hop_endpoints(), } } @@ -169,7 +171,7 @@ pub struct TunnelEndpoint { pub tunnel_type: TunnelType, pub quantum_resistant: bool, pub proxy: Option<proxy::ProxyEndpoint>, - pub obfuscation: Option<ObfuscationEndpoint>, + pub obfuscation: Option<ObfuscationInfo>, pub entry_endpoint: Option<Endpoint>, pub tunnel_interface: Option<String>, #[cfg(daita)] @@ -223,7 +225,86 @@ impl fmt::Display for ObfuscationType { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename = "obfuscation_info")] +pub enum ObfuscationInfo { + /// Single obfuscator + Single(ObfuscationEndpoint), + /// Multiplexer obfuscator + Multiplexer { + /// Direct endpoint, without obfuscation, if set + direct: Option<Endpoint>, + /// All other obfuscators + obfuscators: Vec<ObfuscationEndpoint>, + }, +} + +impl ObfuscationInfo { + pub fn get_endpoints(&self) -> Vec<Endpoint> { + match self { + ObfuscationInfo::Single(ep) => vec![ep.endpoint], + ObfuscationInfo::Multiplexer { + direct, + obfuscators, + } => { + let mut v = vec![]; + if let Some(direct) = direct { + v.push(*direct); + } + obfuscators.iter().for_each(|obfs| v.push(obfs.endpoint)); + v + } + } + } +} + +impl fmt::Display for ObfuscationInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + ObfuscationInfo::Single(obfs) => obfs.fmt(f), + ObfuscationInfo::Multiplexer { + direct, + obfuscators, + } => { + write!(f, "multiplex ")?; + + write!(f, "{{ ")?; + if let Some(direct) = direct { + write!(f, "direct {direct}")?; + } else { + write!(f, "no direct")?; + } + for obfuscator in obfuscators { + write!(f, " | {obfuscator}")?; + } + write!(f, " }}") + } + } + } +} + +impl From<&Obfuscators> for ObfuscationInfo { + fn from(config: &Obfuscators) -> Self { + match config { + Obfuscators::Multiplexer { + direct, + configs: (first_obfs, remaining_obfs), + } => ObfuscationInfo::Multiplexer { + direct: direct.map(|direct| Endpoint { + address: direct, + protocol: TransportProtocol::Udp, + }), + obfuscators: iter::once(first_obfs) + .chain(remaining_obfs) + .map(ObfuscationEndpoint::from) + .collect(), + }, + Obfuscators::Single(obfs) => ObfuscationInfo::Single(ObfuscationEndpoint::from(obfs)), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename = "obfuscation_endpoint")] pub struct ObfuscationEndpoint { pub endpoint: Endpoint, @@ -232,7 +313,6 @@ pub struct ObfuscationEndpoint { impl From<&ObfuscatorConfig> for ObfuscationEndpoint { fn from(config: &ObfuscatorConfig) -> ObfuscationEndpoint { - let endpoint = config.get_obfuscator_endpoint(); let obfuscation_type = match config { ObfuscatorConfig::Udp2Tcp { .. } => ObfuscationType::Udp2Tcp, ObfuscatorConfig::Shadowsocks { .. } => ObfuscationType::Shadowsocks, @@ -241,7 +321,7 @@ impl From<&ObfuscatorConfig> for ObfuscationEndpoint { }; ObfuscationEndpoint { - endpoint, + endpoint: config.endpoint(), obfuscation_type, } } @@ -254,7 +334,7 @@ impl fmt::Display for ObfuscationEndpoint { } /// Represents a network layer IP address together with the transport layer protocol and port. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] pub struct Endpoint { /// The socket address for the endpoint pub address: SocketAddr, @@ -470,7 +550,7 @@ impl FromStr for IpVersion { pub struct IpVersionParseError; /// Representation of a transport protocol, either UDP or TCP. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] #[serde(rename_all = "snake_case")] pub enum TransportProtocol { /// Represents the UDP transport protocol. diff --git a/talpid-types/src/net/obfuscation.rs b/talpid-types/src/net/obfuscation.rs index 7df09ecaf4..26e095844b 100644 --- a/talpid-types/src/net/obfuscation.rs +++ b/talpid-types/src/net/obfuscation.rs @@ -3,6 +3,23 @@ use std::net::SocketAddr; use super::{Endpoint, TransportProtocol}; +/// Available obfuscation configuration types. +#[derive(Clone, Eq, PartialEq, Deserialize, Serialize, Debug)] +pub enum Obfuscators { + /// A single obfuscation method + Single(ObfuscatorConfig), + /// Try multiple obfuscation methods (using `multiplexer` obfuscation). + /// + /// They are tested in the following order: `direct`, `config.0`, then + /// the remaining configs in `configs.1` in order. + Multiplexer { + /// Optional direct connection (no obfuscation) to try along with `configs`. + direct: Option<SocketAddr>, + /// Obfuscation configurations to try. + configs: (ObfuscatorConfig, Vec<ObfuscatorConfig>), + }, +} + #[derive(Clone, Eq, PartialEq, Deserialize, Serialize, Debug)] pub enum ObfuscatorConfig { Udp2Tcp { @@ -21,8 +38,65 @@ pub enum ObfuscatorConfig { }, } +impl Obfuscators { + /// Create a multiplexer obfuscator configuration. + /// + /// See [Obfuscators::Multiplexer] for more details. + /// + /// # Arguments + /// * `direct` - Optional direct connection endpoint (no obfuscation) + /// * `obfuscators` - List of obfuscation methods to try, at least one. + /// + /// # Returns + /// * `Some(Obfuscators::Multiplexer)` if at least one obfuscation method is provided + /// * `None` if the obfuscators list is empty + pub fn multiplexer( + direct: Option<SocketAddr>, + obfuscators: &[ObfuscatorConfig], + ) -> Option<Self> { + let [first, remaining @ ..] = obfuscators else { + return None; + }; + Some(Obfuscators::Multiplexer { + direct, + configs: (first.clone(), remaining.to_vec()), + }) + } + + /// Return all potential endpoints that this obfuscation configuration might connect to. + /// + /// For single obfuscators, return one endpoint. For `Obfuscators::Multiplexer`, return + /// all possible endpoints (direct + all obfuscated methods) that the multiplexer + /// might use, with duplicates removed. + pub fn endpoints(&self) -> Vec<Endpoint> { + match self { + Obfuscators::Single(config) => vec![config.endpoint()], + Obfuscators::Multiplexer { + direct, + configs: (first_config, remaining_configs), + } => { + let mut endpoints = vec![]; + if let Some(direct) = direct { + endpoints.push(Endpoint { + address: *direct, + protocol: TransportProtocol::Udp, + }); + } + endpoints.push(first_config.endpoint()); + endpoints.extend(remaining_configs.iter().map(|cfg| cfg.endpoint())); + + endpoints.sort(); + endpoints.dedup(); + + endpoints + } + } + } +} + impl ObfuscatorConfig { - pub fn get_obfuscator_endpoint(&self) -> Endpoint { + /// Return obfuscation endpoint, i.e. the first remote hop that will be connected to + pub fn endpoint(&self) -> Endpoint { match self { ObfuscatorConfig::Udp2Tcp { endpoint } => Endpoint { address: *endpoint, diff --git a/talpid-types/src/net/wireguard.rs b/talpid-types/src/net/wireguard.rs index d7fba0f7fb..3d7c96638f 100644 --- a/talpid-types/src/net/wireguard.rs +++ b/talpid-types/src/net/wireguard.rs @@ -16,16 +16,16 @@ pub struct TunnelParameters { pub connection: ConnectionConfig, pub options: TunnelOptions, pub generic_options: GenericTunnelOptions, - pub obfuscation: Option<super::obfuscation::ObfuscatorConfig>, + pub obfuscation: Option<super::obfuscation::Obfuscators>, } impl TunnelParameters { /// Returns the endpoint that will be connected to - pub fn get_next_hop_endpoint(&self) -> Endpoint { + pub fn get_next_hop_endpoints(&self) -> Vec<Endpoint> { self.obfuscation .as_ref() - .map(|proxy| proxy.get_obfuscator_endpoint()) - .unwrap_or_else(|| self.connection.get_endpoint()) + .map(|proxy| proxy.endpoints()) + .unwrap_or_else(|| vec![self.connection.get_endpoint()]) } } diff --git a/talpid-wireguard/src/config.rs b/talpid-wireguard/src/config.rs index 8bce0ee073..c8b943e0c1 100644 --- a/talpid-wireguard/src/config.rs +++ b/talpid-wireguard/src/config.rs @@ -4,7 +4,7 @@ use std::{ net::{Ipv4Addr, Ipv6Addr}, }; use talpid_types::net::wireguard::{PeerConfig, PrivateKey}; -use talpid_types::net::{GenericTunnelOptions, obfuscation::ObfuscatorConfig, wireguard}; +use talpid_types::net::{GenericTunnelOptions, obfuscation::Obfuscators, wireguard}; /// Name to use for the tunnel device #[cfg(target_os = "linux")] @@ -32,7 +32,7 @@ pub struct Config { #[cfg(target_os = "linux")] pub enable_ipv6: bool, /// Obfuscator config to be used for reaching the relay. - pub obfuscator_config: Option<ObfuscatorConfig>, + pub obfuscator_config: Option<Obfuscators>, /// Enable quantum-resistant PSK exchange pub quantum_resistant: bool, /// Enable DAITA @@ -71,7 +71,7 @@ impl Config { connection: &wireguard::ConnectionConfig, wg_options: &wireguard::TunnelOptions, generic_options: &GenericTunnelOptions, - obfuscator_config: &Option<ObfuscatorConfig>, + obfuscator_config: &Option<Obfuscators>, default_mtu: u16, ) -> Result<Config, Error> { let mut tunnel = connection.tunnel.clone(); diff --git a/talpid-wireguard/src/lib.rs b/talpid-wireguard/src/lib.rs index 9312827b08..ce7c256e9f 100644 --- a/talpid-wireguard/src/lib.rs +++ b/talpid-wireguard/src/lib.rs @@ -9,6 +9,8 @@ use futures::future::Future; use obfuscation::ObfuscatorHandle; #[cfg(windows)] use std::io; +#[cfg(not(target_os = "android"))] +use std::net::IpAddr; use std::{ convert::Infallible, path::Path, @@ -167,7 +169,11 @@ impl WireguardMonitor { let mut config = crate::config::Config::from_parameters(params, tunnel_mtu) .map_err(Error::WireguardConfigError)?; - let endpoint_addrs = [params.get_next_hop_endpoint().address.ip()]; + let endpoint_addrs: Vec<IpAddr> = params + .get_next_hop_endpoints() + .iter() + .map(|ep| ep.address.ip()) + .collect(); let (close_obfs_sender, close_obfs_listener) = sync_mpsc::channel(); // Start obfuscation server and patch the WireGuard config to point the endpoint to it. diff --git a/talpid-wireguard/src/obfuscation.rs b/talpid-wireguard/src/obfuscation.rs index c838ec98af..e20b4dd03d 100644 --- a/talpid-wireguard/src/obfuscation.rs +++ b/talpid-wireguard/src/obfuscation.rs @@ -5,15 +5,20 @@ use crate::{CloseMsg, config::Config}; #[cfg(target_os = "android")] use std::sync::{Arc, Mutex}; use std::{ + iter, net::{Ipv4Addr, Ipv6Addr, SocketAddr}, sync::mpsc as sync_mpsc, }; #[cfg(target_os = "android")] use talpid_tunnel::tun_provider::TunProvider; -use talpid_types::{ErrorExt, net::obfuscation::ObfuscatorConfig}; +use talpid_types::{ + ErrorExt, + net::obfuscation::{ObfuscatorConfig, Obfuscators}, +}; use tunnel_obfuscation::{ - Settings as ObfuscationSettings, create_obfuscator, lwo, quic, shadowsocks, udp2tcp, + Settings as ObfuscationSettings, create_obfuscator, lwo, multiplexer, quic, shadowsocks, + udp2tcp, }; /// Begin running obfuscation machine, if configured. This function will patch `config`'s endpoint @@ -83,6 +88,47 @@ fn patch_endpoint(config: &mut Config, endpoint: SocketAddr) { fn settings_from_config( config: &Config, + obfuscation_config: &Obfuscators, + mtu: u16, + #[cfg(target_os = "linux")] fwmark: Option<u32>, +) -> ObfuscationSettings { + match obfuscation_config { + Obfuscators::Single(obfuscation_config) => settings_from_single_config( + config, + obfuscation_config, + mtu, + #[cfg(target_os = "linux")] + fwmark, + ), + Obfuscators::Multiplexer { + direct, + configs: (first_obfs, remaining_obfs), + } => { + let mut transports = vec![]; + if let Some(direct) = direct { + transports.push(multiplexer::Transport::Direct(*direct)); + } + for obfs_config in iter::once(first_obfs).chain(remaining_obfs) { + let settings = settings_from_single_config( + config, + obfs_config, + mtu, + #[cfg(target_os = "linux")] + fwmark, + ); + transports.push(multiplexer::Transport::Obfuscated(settings)); + } + ObfuscationSettings::Multiplexer(multiplexer::Settings { + transports, + #[cfg(target_os = "linux")] + fwmark, + }) + } + } +} + +fn settings_from_single_config( + config: &Config, obfuscation_config: &ObfuscatorConfig, mtu: u16, #[cfg(target_os = "linux")] fwmark: Option<u32>, diff --git a/tunnel-obfuscation/Cargo.toml b/tunnel-obfuscation/Cargo.toml index 36ba26e344..7af41b6fb2 100644 --- a/tunnel-obfuscation/Cargo.toml +++ b/tunnel-obfuscation/Cargo.toml @@ -11,11 +11,12 @@ rust-version.workspace = true workspace = true [dependencies] +futures = { workspace = true } 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 } +tokio-util = { workspace = true, features = ["rt"] } udp-over-tcp = { git = "https://github.com/mullvad/udp-over-tcp", rev = "87936ac29b68b902565955f138ab02294bcc8593" } shadowsocks = { workspace = true } mullvad-masque-proxy = { path = "../mullvad-masque-proxy" } @@ -28,6 +29,7 @@ nix = { workspace = true, features = ["socket"]} [dev-dependencies] criterion = { version = "0.7.0", features = ["html_reports", "async_tokio"] } +tokio = { workspace = true, features = ["test-util"] } [[bench]] name = "lwo" diff --git a/tunnel-obfuscation/src/lib.rs b/tunnel-obfuscation/src/lib.rs index 814bd62333..4ee8f0c8a1 100644 --- a/tunnel-obfuscation/src/lib.rs +++ b/tunnel-obfuscation/src/lib.rs @@ -3,6 +3,7 @@ use std::net::SocketAddr; use tokio::io; pub mod lwo; +pub mod multiplexer; pub mod quic; pub mod shadowsocks; pub mod socket; @@ -42,6 +43,12 @@ pub enum Error { #[cfg(target_os = "linux")] #[error("Failed to set fwmark on remote socket")] SetFwmark(#[source] nix::Error), + + #[error("Failed to initialize multiplexer")] + CreateMultiplexerObfuscator(#[source] io::Error), + + #[error("Failed to run multiplexer")] + RunMultiplexerObfuscator(#[source] io::Error), } #[async_trait] @@ -61,12 +68,13 @@ pub trait Obfuscator: Send { fn packet_overhead(&self) -> u16; } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum Settings { Udp2Tcp(udp2tcp::Settings), Shadowsocks(shadowsocks::Settings), Quic(quic::Settings), Lwo(lwo::Settings), + Multiplexer(multiplexer::Settings), } pub async fn create_obfuscator(settings: &Settings) -> Result<Box<dyn Obfuscator>> { @@ -78,6 +86,7 @@ pub async fn create_obfuscator(settings: &Settings) -> Result<Box<dyn Obfuscator Settings::Shadowsocks(s) => shadowsocks::Shadowsocks::new(s).await.map(box_obfuscator), Settings::Quic(s) => quic::Quic::new(s).await.map(box_obfuscator), Settings::Lwo(s) => lwo::Lwo::new(s).await.map(box_obfuscator), + Settings::Multiplexer(s) => multiplexer::Multiplexer::new(s).await.map(box_obfuscator), } } diff --git a/tunnel-obfuscation/src/lwo.rs b/tunnel-obfuscation/src/lwo.rs index 54630dc299..b788d44401 100644 --- a/tunnel-obfuscation/src/lwo.rs +++ b/tunnel-obfuscation/src/lwo.rs @@ -31,7 +31,7 @@ pub enum Error { PeekUdpSender(#[source] io::Error), } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Settings { /// Remote LWO/WG server pub server_addr: SocketAddr, diff --git a/tunnel-obfuscation/src/multiplexer.rs b/tunnel-obfuscation/src/multiplexer.rs new file mode 100644 index 0000000000..daf0b6463f --- /dev/null +++ b/tunnel-obfuscation/src/multiplexer.rs @@ -0,0 +1,466 @@ +//! # Multiplexer Obfuscation +//! +//! This obfuscation module attempts to establish a connection through multiple obfuscation methods +//! simultaneously. It acts as a UDP proxy that forwards WireGuard traffic through other +//! obfuscation transports (UDP2TCP, Shadowsocks, QUIC, etc.) +//! and automatically selects the first one that successfully establishes a connection. +//! +//! ## How it works +//! +//! 1. **Initial Setup**: The multiplexer creates a local UDP socket that WireGuard connects to +//! 2. **Transport Spawning**: It progressively spawns different obfuscation transports at timed intervals +//! 3. **Traffic Fanout**: All incoming WireGuard packets are fanned out to all active transports +//! 4. **First Response Wins**: The first transport to receive a response from the server is selected +//! 5. **Connection Establishment**: Once a transport is selected, the multiplexer switches to a +//! direct forwarding mode between WireGuard and the selected transport +//! +//! ## Transport Types +//! +//! See the [Transport] enum. + +use std::{ + collections::{BTreeMap, VecDeque}, + io, + net::{Ipv4Addr, SocketAddr}, + sync::Arc, + time::Duration, +}; + +use async_trait::async_trait; +use tokio::net::UdpSocket; +use tokio_util::task::AbortOnDropHandle; + +use crate::socket::create_remote_socket; + +const MAX_DATAGRAM_SIZE: usize = u16::MAX as usize; + +/// Max number of initial outgoing packets to buffer for replaying to new transports +const MAX_INITIAL_PACKETS: usize = 100; + +/// An obfuscator that manages multiple other obfuscators and automatically +/// selects the first one that successfully establishes a connection. +/// +/// The multiplexer operates in two phases: +/// 1. **Discovery Phase**: Spawn transports progressively and fan out traffic to all of them +/// 2. **Connected Phase**: Once a transport responds, switch to forwarding to that transport only +pub struct Multiplexer { + /// Local UDP socket that WireGuard connects to + client_socket: Arc<UdpSocket>, + /// Address of the client socket that WireGuard should connect to + client_socket_addr: SocketAddr, + /// IPv4 socket for communicating with obfuscation proxies + proxy_socket_v4: Arc<UdpSocket>, + /// IPv6 socket for communicating with obfuscation proxies + proxy_socket_v6: Arc<UdpSocket>, + /// Map of currently active transport endpoints and their configurations + running_endpoints: BTreeMap<SocketAddr, Transport>, + /// Queue of transports to spawn (in priority order) + transports: VecDeque<Transport>, + /// Buffer of initial packets received from WireGuard to replay to new transports + initial_packets_to_send: Vec<Vec<u8>>, + /// Handles to spawned obfuscation tasks + tasks: Vec<AbortOnDropHandle<()>>, + /// Address of WG endpoint socket + wg_addr: Option<SocketAddr>, +} + +impl Multiplexer { + /// Create a new multiplexer with the specified transports (obfuscators) and settings. + /// + /// # Arguments + /// * `settings` - Configuration containing the list of transports to try and network settings + /// + /// # Returns + /// A new multiplexer instance ready to start obfuscation discovery + pub async fn new(settings: &Settings) -> crate::Result<Self> { + let client_socket = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)) + .await + .map_err(crate::Error::CreateMultiplexerObfuscator)?; + + let client_socket_addr = client_socket + .local_addr() + .map_err(crate::Error::CreateMultiplexerObfuscator)?; + + let proxy_socket_v4 = create_remote_socket( + true, + #[cfg(target_os = "linux")] + settings.fwmark, + ) + .await?; + let proxy_socket_v6 = create_remote_socket( + false, + #[cfg(target_os = "linux")] + settings.fwmark, + ) + .await?; + + Ok(Self { + client_socket: Arc::new(client_socket), + client_socket_addr, + proxy_socket_v4: Arc::new(proxy_socket_v4), + proxy_socket_v6: Arc::new(proxy_socket_v6), + running_endpoints: BTreeMap::new(), + transports: VecDeque::from(settings.transports.clone()), + tasks: vec![], + initial_packets_to_send: vec![], + wg_addr: None, + }) + } + + fn proxy_for_addr(&self, addr: SocketAddr) -> &Arc<UdpSocket> { + if addr.is_ipv4() { + &self.proxy_socket_v4 + } else { + &self.proxy_socket_v6 + } + } + + /// Start the multiplexer in discovery mode. + /// + /// Run the main event loop: + /// 1. Receive packets from WireGuard and fan them out to all active transports + /// 2. Receive responses from obfuscation proxies + /// 3. Spawn new transports at timed intervals + /// 4. Switch to connected mode when the first transport responds successfully + async fn start(mut self) -> io::Result<()> { + log::debug!("Running multiplexer obfuscation"); + + let mut wg_recv_buf = vec![0u8; MAX_DATAGRAM_SIZE]; + let mut obfs_recv_v4_buf = vec![0u8; MAX_DATAGRAM_SIZE]; + let mut obfs_recv_v6_buf = vec![0u8; MAX_DATAGRAM_SIZE]; + + let mut delay = tokio::time::interval(Duration::from_secs(1)); + + /// Helper to fan out a packet to all currently running endpoints + async fn send_to_all<'a>( + endpoints: &BTreeMap<SocketAddr, Transport>, + get_socket: impl Fn(SocketAddr) -> &'a Arc<UdpSocket>, + packet: &[u8], + ) { + let mut futs = vec![]; + for &addr in endpoints.keys() { + let udp = get_socket(addr); + futs.push(async move { + log::info!("Sending received packet to proxy {addr}"); + if let Err(err) = udp.send_to(packet, addr).await { + log::error!("Failed to send received packet to proxy {addr}: {err}"); + } else { + log::info!("Successfully sent traffic to obfuscator {addr}"); + } + }); + } + futures::future::join_all(futs).await; + } + + loop { + tokio::select! { + // From local WG + socket_recv = self.client_socket.recv_from(&mut wg_recv_buf) => { + match socket_recv { + Ok((bytes_received, from_addr)) => { + if let Some(prev_addr) = self.wg_addr && prev_addr != from_addr { + log::debug!( + "WireGuard endpoint address changed from {prev_addr} to {from_addr}" + ); + } + self.wg_addr = Some(from_addr); + let pkt = &wg_recv_buf[..bytes_received]; + + if self.initial_packets_to_send.len() >= MAX_INITIAL_PACKETS { + // Initial packets should be handshake initiation packets, so we + // should not end up here if there's some reasonable timeout. + // If we do, fail so we don't use excessive memory. + return Err(io::Error::other("Too many initial packets")); + } + + self.initial_packets_to_send.push(pkt.to_vec()); + + // Fan out latest WG packet to all currently spawned endpoints. + send_to_all( + &self.running_endpoints, + |addr| self.proxy_for_addr(addr), + pkt + ).await; + }, + Err(err) => { + log::error!("Failed to receive traffic from local WireGuard instance: {err}"); + return Ok(()); + } + } + }, + + // From any IPv4 proxy + obfuscator_recv = self.proxy_socket_v4.recv_from(&mut obfs_recv_v4_buf) => { + self.process_obfuscator_recv(obfuscator_recv.map(|(n, addr)| (&obfs_recv_v4_buf[..n], addr))).await?; + }, + + // From any IPv6 proxy + obfuscator_recv = self.proxy_socket_v6.recv_from(&mut obfs_recv_v6_buf) => { + self.process_obfuscator_recv(obfuscator_recv.map(|(n, addr)| (&obfs_recv_v6_buf[..n], addr))).await?; + }, + + // Spawning the next transport + _ = delay.tick() => { + let Some(transport) = self.transports.pop_front() else { continue; }; + if let Err(err) = self.spawn_new_transport(transport).await { + log::error!("Failed to spawn new transport: {err}"); + } + } + } + } + } + + /// Handler for packets received from any proxy. + /// + /// If received bytes were forwarded from an obfuscator back to wireguard, this indicates that + /// a handshake response was received (hopefully) and that we should switch to connected mode. + /// + /// If a packet was received, this continues running until `run_connected` returns. + async fn process_obfuscator_recv( + &self, + obfuscator_recv: io::Result<(&[u8], SocketAddr)>, + ) -> io::Result<()> { + match obfuscator_recv { + Ok((received, obfuscator_addr)) => { + let Some(transport_config) = self.running_endpoints.get(&obfuscator_addr) else { + log::trace!("Ignoring data from unexpected address {obfuscator_addr}"); + return Ok(()); + }; + let Some(wg_addr) = self.wg_addr else { + log::trace!( + "Received data from {obfuscator_addr} before receiving any data from WireGuard" + ); + return Ok(()); + }; + log::debug!( + "Selecting {:?} as valid transport configuration via {obfuscator_addr}", + transport_config + ); + let _ = self.client_socket.send_to(received, wg_addr).await; + self.run_connected(wg_addr, obfuscator_addr).await + } + Err(err) => { + log::error!("Failed to receive traffic from obfuscators: {err}"); + Err(err) + } + } + } + + /// Switch to connected mode after a transport has been successfully selected. + /// + /// In this mode, the multiplexer acts as a simple UDP proxy between WireGuard + /// and the selected obfuscation transport. + /// + /// # Arguments + /// * `local_address` - Address of the local WireGuard instance + /// * `proxy_address` - Address of the selected obfuscation proxy + async fn run_connected( + &self, + local_address: SocketAddr, + proxy_address: SocketAddr, + ) -> io::Result<()> { + let mut wg_recv_buf = vec![0u8; MAX_DATAGRAM_SIZE]; + let mut obfuscator_recv_buf = vec![0u8; MAX_DATAGRAM_SIZE]; + + self.client_socket + .connect(local_address) + .await + .inspect_err(|err| { + log::error!("Failed to connect client socket: {err}"); + })?; + + let proxy_socket = self.proxy_for_addr(proxy_address).clone(); + + let tx_client_socket = self.client_socket.clone(); + let tx_proxy_socket = proxy_socket.clone(); + + let tx_task = tokio::spawn(async move { + loop { + let n = tx_client_socket.recv(&mut wg_recv_buf).await?; + tx_proxy_socket + .send_to(&wg_recv_buf[..n], proxy_address) + .await?; + } + }); + let mut tx_task = AbortOnDropHandle::new(tx_task); + let client_socket = self.client_socket.clone(); + + let rx_task = tokio::spawn(async move { + loop { + let (n, _src) = proxy_socket.recv_from(&mut obfuscator_recv_buf).await?; + client_socket.send(&obfuscator_recv_buf[..n]).await?; + } + }); + let mut rx_task = AbortOnDropHandle::new(rx_task); + + tokio::select! { + Ok(result) = &mut tx_task => result, + Ok(result) = &mut rx_task => result, + else => Ok(()), + } + } + + /// Spawn a new obfuscation transport and add it to the active set. + /// + /// For direct transports, simply register the endpoint. For obfuscated + /// transports, start the obfuscation process in a background task. + /// + /// # Arguments + /// * `transport` - The obfuscation type to spawn + async fn spawn_new_transport(&mut self, transport: Transport) -> crate::Result<()> { + let endpoint = match transport.clone() { + Transport::Direct(addr) => { + self.running_endpoints.insert(addr, transport); + log::info!("Spawning direct forwarder"); + Ok(addr) + } + Transport::Obfuscated(obfuscator_settings) => { + let obfuscator = crate::create_obfuscator(&obfuscator_settings).await?; + let endpoint = obfuscator.endpoint(); + self.running_endpoints + .insert(endpoint, Transport::Obfuscated(obfuscator_settings)); + self.tasks + .push(AbortOnDropHandle::new(tokio::spawn(async move { + log::info!("Spawning new obfuscator"); + let _ = obfuscator.run().await; + }))); + Ok(endpoint) + } + }?; + + self.send_initial_packets_to(endpoint).await; + + Ok(()) + } + + async fn send_initial_packets_to(&self, endpoint: SocketAddr) { + let udp = self.proxy_for_addr(endpoint); + for packet in &self.initial_packets_to_send { + if let Err(err) = udp.send_to(packet, endpoint).await { + log::error!("Failed to forward packet to new obfuscator {endpoint}: {err}"); + } + } + } +} + +/// Configuration settings for multiplexer obfuscation +#[derive(Debug, Clone)] +pub struct Settings { + /// List of transports to try, ordered by priority (highest to lowest). + /// Spawn these transports progressively and select + /// the first one that successfully establishes a connection. + pub transports: Vec<Transport>, + /// Linux-specific firewall mark for outgoing connections + #[cfg(target_os = "linux")] + pub fwmark: Option<u32>, +} + +/// Represents a transport method that the multiplexer can use. +#[derive(Clone, Debug)] +pub enum Transport { + /// Direct UDP forwarding without any obfuscation + Direct(SocketAddr), + /// An obfuscated transport (UDP2TCP, Shadowsocks, QUIC, etc.) + Obfuscated(crate::Settings), +} + +#[async_trait] +impl crate::Obfuscator for Multiplexer { + fn endpoint(&self) -> SocketAddr { + self.client_socket_addr + } + + fn packet_overhead(&self) -> u16 { + // FIXME: This should ideally be the max overhead of all transports, + // and be lowered when a transport is selected. + 60 + } + + async fn run(self: Box<Self>) -> crate::Result<()> { + self.start() + .await + .map_err(crate::Error::RunMultiplexerObfuscator) + } + + #[cfg(target_os = "android")] + fn remote_socket_fd(&self) -> std::os::unix::io::RawFd { + unimplemented!("must return the socket fd of every obfuscator here") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Obfuscator; + + /// Test whether the multiplexer works with a direct transports + #[tokio::test(start_paused = true)] + async fn test_multiplexer_direct_forwarding() { + let server_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap(); + let server_addr = server_socket.local_addr().unwrap(); + + let server_socket2 = UdpSocket::bind("127.0.0.1:0").await.unwrap(); + let server_addr2 = server_socket2.local_addr().unwrap(); + + // Create multiplexer pointing to a single direct transport + let settings = Settings { + transports: vec![ + Transport::Direct(server_addr), + Transport::Direct(server_addr2), + ], + #[cfg(target_os = "linux")] + fwmark: None, + }; + + let multiplexer = Multiplexer::new(&settings).await.unwrap(); + let multiplexer_endpoint = multiplexer.endpoint(); + + let client_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap(); + + tokio::spawn(async move { + let boxed_multiplexer = Box::new(multiplexer); + boxed_multiplexer.run().await + }); + + // Send a test packet from client to multiplexer and verify that it is received + // NOTE: This may have to be an actual WireGuard handshake packet in the future + let test_data = b"Ping!"; + client_socket + .send_to(test_data, multiplexer_endpoint) + .await + .unwrap(); + + let mut server_buf = vec![0u8; 1024]; + let (bytes_received, client_addr) = server_socket.recv_from(&mut server_buf).await.unwrap(); + + assert_eq!(&server_buf[..bytes_received], test_data); + + // Our second socket should also receive this packet + let (bytes_received, _) = server_socket2.recv_from(&mut server_buf).await.unwrap(); + assert_eq!(&server_buf[..bytes_received], test_data); + + // Send a response back from the first server + let response_data = b"Pong!"; + server_socket + .send_to(response_data, client_addr) + .await + .unwrap(); + + // Verify that response was forwarded + let mut client_buf = vec![0u8; 1024]; + let (bytes_received, _) = client_socket.recv_from(&mut client_buf).await.unwrap(); + + assert_eq!(&client_buf[..bytes_received], response_data); + + // Test that packets are now forwarded directly (connected mode) + let second_test_data = b"Connected!"; + client_socket + .send_to(second_test_data, multiplexer_endpoint) + .await + .unwrap(); + + let (bytes_received, _) = server_socket.recv_from(&mut server_buf).await.unwrap(); + + assert_eq!(&server_buf[..bytes_received], second_test_data); + } +} diff --git a/tunnel-obfuscation/src/quic.rs b/tunnel-obfuscation/src/quic.rs index 8c587f4abd..c8387228d3 100644 --- a/tunnel-obfuscation/src/quic.rs +++ b/tunnel-obfuscation/src/quic.rs @@ -27,7 +27,7 @@ pub struct Quic { config: ClientConfig, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Settings { /// Remote Quic endpoint quic_endpoint: SocketAddr, diff --git a/tunnel-obfuscation/src/shadowsocks.rs b/tunnel-obfuscation/src/shadowsocks.rs index 081a442654..e1ecb18f99 100644 --- a/tunnel-obfuscation/src/shadowsocks.rs +++ b/tunnel-obfuscation/src/shadowsocks.rs @@ -51,7 +51,7 @@ pub struct Shadowsocks { outbound_fd: i32, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Settings { /// Remote Shadowsocks endpoint pub shadowsocks_endpoint: SocketAddr, diff --git a/tunnel-obfuscation/src/udp2tcp.rs b/tunnel-obfuscation/src/udp2tcp.rs index cf586bab76..6435629936 100644 --- a/tunnel-obfuscation/src/udp2tcp.rs +++ b/tunnel-obfuscation/src/udp2tcp.rs @@ -6,7 +6,7 @@ use udp_over_tcp::{ udp2tcp::{self, Udp2Tcp as Udp2TcpImpl}, }; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Settings { pub peer: SocketAddr, #[cfg(target_os = "linux")] diff --git a/windows/winfw/src/winfw/fwcontext.cpp b/windows/winfw/src/winfw/fwcontext.cpp index dc0a38b304..69a79537cd 100644 --- a/windows/winfw/src/winfw/fwcontext.cpp +++ b/windows/winfw/src/winfw/fwcontext.cpp @@ -191,7 +191,7 @@ FwContext::FwContext bool FwContext::applyPolicyConnecting ( const WinFwSettings &settings, - const WinFwEndpoint &relay, + const std::vector<WinFwEndpoint> &relays, const std::optional<wfp::IpAddress> &exitEndpointIp, const std::vector<std::wstring> &relayClients, const std::optional<std::wstring> &tunnelInterfaceAlias, @@ -203,7 +203,11 @@ bool FwContext::applyPolicyConnecting AppendNetBlockedRules(ruleset); AppendSettingsRules(ruleset, settings); - AppendRelayRules(ruleset, relay, relayClients); + + for (const auto &relay : relays) + { + AppendRelayRules(ruleset, relay, relayClients); + } if (allowedEndpoint.has_value()) { @@ -299,9 +303,9 @@ bool FwContext::applyPolicyConnecting bool FwContext::applyPolicyConnected ( const WinFwSettings &settings, - const WinFwEndpoint &relay, + const std::vector<WinFwEndpoint> &relays, const std::optional<wfp::IpAddress> &exitEndpointIp, - const std::vector<std::wstring> &relayClient, + const std::vector<std::wstring> &relayClients, const std::wstring &tunnelInterfaceAlias, const std::vector<wfp::IpAddress> &tunnelDnsServers, const std::vector<wfp::IpAddress> &nonTunnelDnsServers @@ -311,7 +315,11 @@ bool FwContext::applyPolicyConnected AppendNetBlockedRules(ruleset); AppendSettingsRules(ruleset, settings); - AppendRelayRules(ruleset, relay, relayClient); + + for (const auto &relay : relays) + { + AppendRelayRules(ruleset, relay, relayClients); + } if (!tunnelDnsServers.empty()) { @@ -327,14 +335,14 @@ bool FwContext::applyPolicyConnected } ruleset.emplace_back(std::make_unique<baseline::PermitVpnTunnel>( - relayClient, + relayClients, tunnelInterfaceAlias, std::nullopt, exitEndpointIp )); ruleset.emplace_back(std::make_unique<baseline::PermitVpnTunnelService>( - relayClient, + relayClients, tunnelInterfaceAlias, std::nullopt, exitEndpointIp diff --git a/windows/winfw/src/winfw/fwcontext.h b/windows/winfw/src/winfw/fwcontext.h index 8a4e4f0301..c8f3c2b4e5 100644 --- a/windows/winfw/src/winfw/fwcontext.h +++ b/windows/winfw/src/winfw/fwcontext.h @@ -27,7 +27,7 @@ public: bool applyPolicyConnecting ( const WinFwSettings &settings, - const WinFwEndpoint &relay, + const std::vector<WinFwEndpoint> &relays, const std::optional<wfp::IpAddress> &exitEndpointIp, const std::vector<std::wstring> &relayClients, const std::optional<std::wstring> &tunnelInterfaceAlias, @@ -38,7 +38,7 @@ public: bool applyPolicyConnected ( const WinFwSettings &settings, - const WinFwEndpoint &relay, + const std::vector<WinFwEndpoint> &relays, const std::optional<wfp::IpAddress> &exitEndpointIp, const std::vector<std::wstring> &relayClients, const std::wstring &tunnelInterfaceAlias, diff --git a/windows/winfw/src/winfw/winfw.cpp b/windows/winfw/src/winfw/winfw.cpp index 064532235c..6d79bf8356 100644 --- a/windows/winfw/src/winfw/winfw.cpp +++ b/windows/winfw/src/winfw/winfw.cpp @@ -328,7 +328,8 @@ WINFW_POLICY_STATUS WINFW_API WinFw_ApplyPolicyConnecting( const WinFwSettings *settings, - const WinFwEndpoint *relay, + size_t numRelays, + const WinFwEndpoint *relays, const wchar_t *exitEndpointIp, const wchar_t **relayClients, size_t relayClientsLen, @@ -349,9 +350,14 @@ WinFw_ApplyPolicyConnecting( THROW_ERROR("Invalid argument: settings"); } - if (nullptr == relay) + if (nullptr == relays) { - THROW_ERROR("Invalid argument: relay"); + THROW_ERROR("Invalid argument: relays"); + } + + if (0 == numRelays) + { + THROW_ERROR("Invalid argument: numRelays"); } if (nullptr == allowedTunnelTraffic) @@ -359,23 +365,33 @@ WinFw_ApplyPolicyConnecting( THROW_ERROR("Invalid argument: allowedTunnelTraffic"); } + std::vector<WinFwEndpoint> relayEndpoints; + relayEndpoints.reserve(numRelays); + for (size_t i = 0; i < numRelays; i++) + { + relayEndpoints.push_back(relays[i]); + } + const auto exitIpAddr = (exitEndpointIp != nullptr) ? std::make_optional(wfp::IpAddress(exitEndpointIp)) : std::nullopt; - const auto entryIpAddr = wfp::IpAddress(relay->ip); - if (entryIpAddr == exitIpAddr) + for (const auto &entryEndpoint : relayEndpoints) { - THROW_ERROR("Invalid argument: relay IP must not equal exitEndpointIp"); + const auto ipAddr = wfp::IpAddress(entryEndpoint.ip); + if (ipAddr == exitIpAddr) + { + THROW_ERROR("Invalid argument: relay IP must not equal exitEndpointIp"); + } } std::vector<std::wstring> relayClientWstrings; relayClientWstrings.reserve(relayClientsLen); - for(int i = 0; i < relayClientsLen; i++) { + for (size_t i = 0; i < relayClientsLen; i++) { relayClientWstrings.push_back(relayClients[i]); } return g_fwContext->applyPolicyConnecting( *settings, - *relay, + relayEndpoints, exitIpAddr, relayClientWstrings, tunnelInterfaceAlias != nullptr ? std::make_optional(tunnelInterfaceAlias) : std::nullopt, @@ -407,7 +423,8 @@ WINFW_POLICY_STATUS WINFW_API WinFw_ApplyPolicyConnected( const WinFwSettings *settings, - const WinFwEndpoint *relay, + size_t numRelays, + const WinFwEndpoint *relays, const wchar_t *exitEndpointIp, const wchar_t **relayClients, size_t relayClientsLen, @@ -430,9 +447,14 @@ WinFw_ApplyPolicyConnected( THROW_ERROR("Invalid argument: settings"); } - if (nullptr == relay) + if (nullptr == relays) { - THROW_ERROR("Invalid argument: relay"); + THROW_ERROR("Invalid argument: relays"); + } + + if (0 == numRelays) + { + THROW_ERROR("Invalid argument: numRelays"); } if (nullptr == tunnelInterfaceAlias) @@ -450,12 +472,22 @@ WinFw_ApplyPolicyConnected( THROW_ERROR("Invalid argument: nonTunnelDnsServers"); } + std::vector<WinFwEndpoint> relayEndpoints; + relayEndpoints.reserve(numRelays); + for (size_t i = 0; i < numRelays; i++) + { + relayEndpoints.push_back(relays[i]); + } + const auto exitIpAddr = (exitEndpointIp != nullptr) ? std::make_optional(wfp::IpAddress(exitEndpointIp)) : std::nullopt; - const auto entryIpAddr = wfp::IpAddress(relay->ip); - if (entryIpAddr == exitIpAddr) + for (const auto &entryEndpoint : relayEndpoints) { - THROW_ERROR("Invalid argument: relay IP must not equal exitEndpointIp"); + const auto ipAddr = wfp::IpAddress(entryEndpoint.ip); + if (ipAddr == exitIpAddr) + { + THROW_ERROR("Invalid argument: relay IP must not equal exitEndpointIp"); + } } std::vector<wfp::IpAddress> convertedTunnelDnsServers; @@ -499,13 +531,13 @@ WinFw_ApplyPolicyConnected( std::vector<std::wstring> relayClientWstrings; relayClientWstrings.reserve(relayClientsLen); - for(int i = 0; i < relayClientsLen; i++) { + for (size_t i = 0; i < relayClientsLen; i++) { relayClientWstrings.push_back(relayClients[i]); } return g_fwContext->applyPolicyConnected( *settings, - *relay, + relayEndpoints, exitIpAddr, relayClientWstrings, tunnelInterfaceAlias, diff --git a/windows/winfw/src/winfw/winfw.h b/windows/winfw/src/winfw/winfw.h index f3e26ea4aa..f4cd3b4bca 100644 --- a/windows/winfw/src/winfw/winfw.h +++ b/windows/winfw/src/winfw/winfw.h @@ -159,7 +159,7 @@ enum WINFW_POLICY_STATUS // // Apply restrictions in the firewall that block all traffic, except: // - What is specified by settings -// - Communication with the relay server +// - Communication with the relay server(s) // - Specified in-tunnel traffic, except DNS. // // Parameters: @@ -174,7 +174,8 @@ WINFW_POLICY_STATUS WINFW_API WinFw_ApplyPolicyConnecting( const WinFwSettings *settings, - const WinFwEndpoint *relay, + size_t numRelays, + const WinFwEndpoint *relays, const wchar_t *exitEndpointIp, const wchar_t **relayClient, size_t relayClientLen, @@ -188,7 +189,7 @@ WinFw_ApplyPolicyConnecting( // // Apply restrictions in the firewall that block all traffic, except: // - What is specified by settings -// - Communication with the relay server +// - Communication with the relay server(s) // - Non-DNS traffic inside the VPN tunnel // - DNS requests inside the VPN tunnel to any server in 'tunnelDnsServers' // - DNS requests outside the VPN tunnel to any server in 'nonTunnelDnsServers' @@ -208,7 +209,8 @@ WINFW_POLICY_STATUS WINFW_API WinFw_ApplyPolicyConnected( const WinFwSettings *settings, - const WinFwEndpoint *relay, + size_t numRelays, + const WinFwEndpoint *relays, const wchar_t *exitEndpointIp, const wchar_t **relayClient, size_t relayClientLen, |
