summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2025-09-18 17:22:18 +0200
committerDavid Lönnhager <david.l@mullvad.net>2025-09-18 17:22:18 +0200
commitc072667ffed6b4b698cec5a4a8adff00be88c3e5 (patch)
treec1fe4b5354c23e939f97577ef7aadaf6eab5731e
parent923414f3f00b033dde8ed538ad05c18da4da6b27 (diff)
parenta074cb8e3625d5378c0be7954b1f5423479d071c (diff)
downloadmullvadvpn-c072667ffed6b4b698cec5a4a8adff00be88c3e5.tar.xz
mullvadvpn-c072667ffed6b4b698cec5a4a8adff00be88c3e5.zip
Merge branch 'add-staggered-obfuscator'
-rw-r--r--Cargo.lock23
-rw-r--r--android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt9
-rw-r--r--desktop/packages/mullvad-vpn/src/main/grpc-type-convertions.ts27
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/main-view/ConnectionDetails.tsx6
-rw-r--r--desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts1
-rw-r--r--mullvad-cli/src/format.rs39
-rw-r--r--mullvad-daemon/Cargo.toml2
-rw-r--r--mullvad-daemon/src/tunnel.rs7
-rw-r--r--mullvad-management-interface/proto/management_interface.proto21
-rw-r--r--mullvad-management-interface/src/types/conversions/net.rs193
-rw-r--r--mullvad-relay-selector/Cargo.toml4
-rw-r--r--mullvad-relay-selector/src/relay_selector/helpers.rs86
-rw-r--r--mullvad-relay-selector/src/relay_selector/mod.rs39
-rw-r--r--mullvad-relay-selector/tests/relay_selector.rs21
-rw-r--r--mullvad-types/src/features.rs20
-rw-r--r--talpid-core/src/firewall/linux.rs12
-rw-r--r--talpid-core/src/firewall/macos.rs48
-rw-r--r--talpid-core/src/firewall/mod.rs78
-rw-r--r--talpid-core/src/firewall/windows/mod.rs16
-rw-r--r--talpid-core/src/firewall/windows/winfw/mod.rs89
-rw-r--r--talpid-core/src/firewall/windows/winfw/sys.rs6
-rw-r--r--talpid-core/src/tunnel_state_machine/connected_state.rs15
-rw-r--r--talpid-core/src/tunnel_state_machine/connecting_state.rs15
-rw-r--r--talpid-types/src/net/mod.rs106
-rw-r--r--talpid-types/src/net/obfuscation.rs76
-rw-r--r--talpid-types/src/net/wireguard.rs8
-rw-r--r--talpid-wireguard/src/config.rs6
-rw-r--r--talpid-wireguard/src/lib.rs8
-rw-r--r--talpid-wireguard/src/obfuscation.rs50
-rw-r--r--tunnel-obfuscation/Cargo.toml4
-rw-r--r--tunnel-obfuscation/src/lib.rs11
-rw-r--r--tunnel-obfuscation/src/lwo.rs2
-rw-r--r--tunnel-obfuscation/src/multiplexer.rs466
-rw-r--r--tunnel-obfuscation/src/quic.rs2
-rw-r--r--tunnel-obfuscation/src/shadowsocks.rs2
-rw-r--r--tunnel-obfuscation/src/udp2tcp.rs2
-rw-r--r--windows/winfw/src/winfw/fwcontext.cpp22
-rw-r--r--windows/winfw/src/winfw/fwcontext.h4
-rw-r--r--windows/winfw/src/winfw/winfw.cpp64
-rw-r--r--windows/winfw/src/winfw/winfw.h10
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,