summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2024-08-16 09:14:56 +0200
committerDavid Lönnhager <david.l@mullvad.net>2024-08-16 09:14:56 +0200
commitfe705988688962c96401686cd3d1309c296eeae1 (patch)
tree71fd3c6759e403d94550d99a216523d5bad4fd10
parent532b8ade64f47be9d00655844f4b5fb9070f293e (diff)
parent854c9babc336a01e4e49a75c67aaf81ba331540b (diff)
downloadmullvadvpn-fe705988688962c96401686cd3d1309c296eeae1.tar.xz
mullvadvpn-fe705988688962c96401686cd3d1309c296eeae1.zip
Merge branch 'add-shadowsocks-obfuscation'
-rw-r--r--Cargo.lock2
-rw-r--r--android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt10
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationType.kt3
-rw-r--r--gui/src/main/daemon-rpc.ts20
-rw-r--r--mullvad-api/src/relay_list.rs25
-rw-r--r--mullvad-cli/src/cmds/relay.rs2
-rw-r--r--mullvad-daemon/src/lib.rs8
-rw-r--r--mullvad-daemon/src/logging.rs1
-rw-r--r--mullvad-management-interface/proto/management_interface.proto42
-rw-r--r--mullvad-management-interface/src/types/conversions/features.rs2
-rw-r--r--mullvad-management-interface/src/types/conversions/net.rs53
-rw-r--r--mullvad-management-interface/src/types/conversions/relay_constraints.rs40
-rw-r--r--mullvad-management-interface/src/types/conversions/relay_list.rs65
-rw-r--r--mullvad-relay-selector/src/error.rs2
-rw-r--r--mullvad-relay-selector/src/relay_selector/detailer.rs68
-rw-r--r--mullvad-relay-selector/src/relay_selector/helpers.rs282
-rw-r--r--mullvad-relay-selector/src/relay_selector/matcher.rs76
-rw-r--r--mullvad-relay-selector/src/relay_selector/mod.rs42
-rw-r--r--mullvad-relay-selector/src/relay_selector/query.rs67
-rw-r--r--mullvad-relay-selector/tests/relay_selector.rs226
-rw-r--r--mullvad-types/src/constraints/constraint.rs7
-rw-r--r--mullvad-types/src/features.rs2
-rw-r--r--mullvad-types/src/relay_constraints.rs38
-rw-r--r--mullvad-types/src/relay_list.rs14
-rw-r--r--talpid-types/src/net/mod.rs22
-rw-r--r--talpid-types/src/net/obfuscation.rs1
-rw-r--r--talpid-wireguard/src/lib.rs100
-rw-r--r--test/test-manager/src/tests/tunnel.rs2
-rw-r--r--tunnel-obfuscation/Cargo.toml2
-rw-r--r--tunnel-obfuscation/src/lib.rs25
-rw-r--r--tunnel-obfuscation/src/main.rs4
-rw-r--r--tunnel-obfuscation/src/shadowsocks.rs256
-rw-r--r--tunnel-obfuscation/src/udp2tcp.rs13
33 files changed, 1272 insertions, 250 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 7683c0e820..6c25a0ade8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4494,6 +4494,8 @@ name = "tunnel-obfuscation"
version = "0.0.0"
dependencies = [
"async-trait",
+ "log",
+ "shadowsocks",
"thiserror",
"tokio",
"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 50599ad7f4..9baa426696 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
@@ -166,11 +166,13 @@ internal fun ManagementInterface.ObfuscationEndpoint.toDomain(): ObfuscationEndp
obfuscationType = obfuscationType.toDomain()
)
-internal fun ManagementInterface.ObfuscationType.toDomain(): ObfuscationType =
+internal fun ManagementInterface.ObfuscationEndpoint.ObfuscationType.toDomain(): ObfuscationType =
when (this) {
- ManagementInterface.ObfuscationType.UDP2TCP -> ObfuscationType.Udp2Tcp
- ManagementInterface.ObfuscationType.UNRECOGNIZED ->
+ ManagementInterface.ObfuscationEndpoint.ObfuscationType.UDP2TCP -> ObfuscationType.Udp2Tcp
+ ManagementInterface.ObfuscationEndpoint.ObfuscationType.UNRECOGNIZED ->
throw IllegalArgumentException("Unrecognized obfuscation type")
+ ManagementInterface.ObfuscationEndpoint.ObfuscationType.SHADOWSOCKS ->
+ throw IllegalArgumentException("Shadowsocks is unsupported")
}
internal fun ManagementInterface.TransportProtocol.toDomain(): TransportProtocol =
@@ -340,6 +342,8 @@ internal fun ManagementInterface.ObfuscationSettings.SelectedObfuscation.toDomai
ManagementInterface.ObfuscationSettings.SelectedObfuscation.OFF -> SelectedObfuscation.Off
ManagementInterface.ObfuscationSettings.SelectedObfuscation.UDP2TCP ->
SelectedObfuscation.Udp2Tcp
+ ManagementInterface.ObfuscationSettings.SelectedObfuscation.SHADOWSOCKS ->
+ throw IllegalArgumentException("Shadowsocks is unsupported")
ManagementInterface.ObfuscationSettings.SelectedObfuscation.UNRECOGNIZED ->
throw IllegalArgumentException("Unrecognized selected obfuscation")
}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationType.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationType.kt
index cd71d645af..4e7cb1f5a8 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationType.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationType.kt
@@ -1,5 +1,6 @@
package net.mullvad.mullvadvpn.lib.model
enum class ObfuscationType {
- Udp2Tcp
+ Udp2Tcp,
+ Shadowsocks
}
diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts
index 3461a85ac5..65e3c6b1c5 100644
--- a/gui/src/main/daemon-rpc.ts
+++ b/gui/src/main/daemon-rpc.ts
@@ -29,7 +29,6 @@ import {
DeviceEvent,
DeviceState,
DirectMethod,
- EndpointObfuscationType,
ErrorState,
ErrorStateCause,
FirewallPolicyError,
@@ -403,6 +402,8 @@ export class DaemonRpc {
grpcObfuscationSettings.setUdp2tcp(grpcUdp2tcpSettings);
}
+ grpcObfuscationSettings.setShadowsocks(new grpcTypes.ShadowsocksSettings());
+
await this.call<grpcTypes.ObfuscationSettings, Empty>(
this.client.setObfuscationSettings,
grpcObfuscationSettings,
@@ -1136,9 +1137,9 @@ function convertFromTunnelType(tunnelType: grpcTypes.TunnelType): TunnelType {
}
function convertFromProxyEndpoint(proxyEndpoint: grpcTypes.ProxyEndpoint.AsObject): IProxyEndpoint {
- const proxyTypeMap: Record<grpcTypes.ProxyType, ProxyType> = {
- [grpcTypes.ProxyType.CUSTOM]: 'custom',
- [grpcTypes.ProxyType.SHADOWSOCKS]: 'shadowsocks',
+ const proxyTypeMap: Record<grpcTypes.ProxyEndpoint.ProxyType, ProxyType> = {
+ [grpcTypes.ProxyEndpoint.ProxyType.CUSTOM]: 'custom',
+ [grpcTypes.ProxyEndpoint.ProxyType.SHADOWSOCKS]: 'shadowsocks',
};
return {
@@ -1151,14 +1152,17 @@ function convertFromProxyEndpoint(proxyEndpoint: grpcTypes.ProxyEndpoint.AsObjec
function convertFromObfuscationEndpoint(
obfuscationEndpoint: grpcTypes.ObfuscationEndpoint.AsObject,
): IObfuscationEndpoint {
- const obfuscationTypes: Record<grpcTypes.ObfuscationType, EndpointObfuscationType> = {
- [grpcTypes.ObfuscationType.UDP2TCP]: 'udp2tcp',
- };
+ // TODO: Handle Shadowsocks (and other implemented protocols)
+ if (
+ obfuscationEndpoint.obfuscationType !== grpcTypes.ObfuscationEndpoint.ObfuscationType.UDP2TCP
+ ) {
+ throw new Error('unsupported obfuscation protocol');
+ }
return {
...obfuscationEndpoint,
protocol: convertFromTransportProtocol(obfuscationEndpoint.protocol),
- obfuscationType: obfuscationTypes[obfuscationEndpoint.obfuscationType],
+ obfuscationType: 'udp2tcp',
};
}
diff --git a/mullvad-api/src/relay_list.rs b/mullvad-api/src/relay_list.rs
index 6039abab66..22410549b8 100644
--- a/mullvad-api/src/relay_list.rs
+++ b/mullvad-api/src/relay_list.rs
@@ -9,7 +9,8 @@ use talpid_types::net::wireguard;
use std::{
collections::BTreeMap,
future::Future,
- net::{Ipv4Addr, Ipv6Addr},
+ net::{IpAddr, Ipv4Addr, Ipv6Addr},
+ ops::RangeInclusive,
time::Duration,
};
@@ -244,20 +245,37 @@ struct Wireguard {
port_ranges: Vec<(u16, u16)>,
ipv4_gateway: Ipv4Addr,
ipv6_gateway: Ipv6Addr,
+ /// Shadowsocks port ranges available on all WireGuard relays
+ #[serde(default)]
+ shadowsocks_port_ranges: Vec<(u16, u16)>,
relays: Vec<WireGuardRelay>,
}
impl From<&Wireguard> for relay_list::WireguardEndpointData {
fn from(wg: &Wireguard) -> Self {
Self {
- port_ranges: wg.port_ranges.clone(),
+ port_ranges: inclusive_range_from_pair_set(wg.port_ranges.clone()).collect(),
ipv4_gateway: wg.ipv4_gateway,
ipv6_gateway: wg.ipv6_gateway,
+ shadowsocks_port_ranges: inclusive_range_from_pair_set(
+ wg.shadowsocks_port_ranges.clone(),
+ )
+ .collect(),
udp2tcp_ports: vec![],
}
}
}
+fn inclusive_range_from_pair_set<T>(
+ set: impl IntoIterator<Item = (T, T)>,
+) -> impl Iterator<Item = RangeInclusive<T>> {
+ set.into_iter().map(inclusive_range_from_pair)
+}
+
+fn inclusive_range_from_pair<T>(pair: (T, T)) -> RangeInclusive<T> {
+ RangeInclusive::new(pair.0, pair.1)
+}
+
impl Wireguard {
/// Consumes `self` and appends all its relays to `countries`.
fn extract_relays(
@@ -305,6 +323,8 @@ struct WireGuardRelay {
public_key: wireguard::PublicKey,
#[serde(default)]
daita: bool,
+ #[serde(default)]
+ shadowsocks_extra_addr_in: Vec<IpAddr>,
}
impl WireGuardRelay {
@@ -315,6 +335,7 @@ impl WireGuardRelay {
relay_list::RelayEndpointData::Wireguard(relay_list::WireguardRelayEndpointData {
public_key: self.public_key,
daita: self.daita,
+ shadowsocks_extra_addr_in: self.shadowsocks_extra_addr_in,
}),
)
}
diff --git a/mullvad-cli/src/cmds/relay.rs b/mullvad-cli/src/cmds/relay.rs
index 146d37c2b2..68292c0ad1 100644
--- a/mullvad-cli/src/cmds/relay.rs
+++ b/mullvad-cli/src/cmds/relay.rs
@@ -669,7 +669,7 @@ impl Relay {
let is_valid_port = wireguard
.port_ranges
.into_iter()
- .any(|(first, last)| first <= specific_port && specific_port <= last);
+ .any(|range| range.contains(&specific_port));
if !is_valid_port {
return Err(anyhow!("The specified port is invalid"));
}
diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs
index 468f4fca38..1a456a48bb 100644
--- a/mullvad-daemon/src/lib.rs
+++ b/mullvad-daemon/src/lib.rs
@@ -2931,6 +2931,13 @@ impl Daemon {
.as_ref()
.filter(|obfuscation| obfuscation.obfuscation_type == ObfuscationType::Udp2Tcp)
.is_some();
+ let shadowsocks = endpoint
+ .obfuscation
+ .as_ref()
+ .filter(|obfuscation| {
+ obfuscation.obfuscation_type == ObfuscationType::Shadowsocks
+ })
+ .is_some();
let mtu = settings.tunnel_options.wireguard.mtu.is_some();
@@ -2941,6 +2948,7 @@ impl Daemon {
(quantum_resistant, FeatureIndicator::QuantumResistance),
(multihop, FeatureIndicator::Multihop),
(udp_tcp, FeatureIndicator::Udp2Tcp),
+ (shadowsocks, FeatureIndicator::Shadowsocks),
(mtu, FeatureIndicator::CustomMtu),
#[cfg(daita)]
(daita, FeatureIndicator::Daita),
diff --git a/mullvad-daemon/src/logging.rs b/mullvad-daemon/src/logging.rs
index e6a29aed37..6e91e86ea6 100644
--- a/mullvad-daemon/src/logging.rs
+++ b/mullvad-daemon/src/logging.rs
@@ -47,6 +47,7 @@ pub const SILENCED_CRATES: &[&str] = &[
"hickory_proto",
"hickory_server",
"hickory_resolver",
+ "shadowsocks::relay::udprelay",
];
const SLIGHTLY_SILENCED_CRATES: &[&str] = &["mnl", "nftnl", "udp_over_tcp"];
diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto
index 7e78cffde8..e705c63788 100644
--- a/mullvad-management-interface/proto/management_interface.proto
+++ b/mullvad-management-interface/proto/management_interface.proto
@@ -254,37 +254,39 @@ enum FeatureIndicator {
SPLIT_TUNNELING = 3;
LOCKDOWN_MODE = 4;
UDP_2_TCP = 5;
- LAN_SHARING = 6;
- DNS_CONTENT_BLOCKERS = 7;
- CUSTOM_DNS = 8;
- SERVER_IP_OVERRIDE = 9;
- CUSTOM_MTU = 10;
- CUSTOM_MSS_FIX = 11;
- DAITA = 12;
-}
-
-enum ObfuscationType {
- UDP2TCP = 0;
+ SHADOWSOCKS = 6;
+ LAN_SHARING = 7;
+ DNS_CONTENT_BLOCKERS = 8;
+ CUSTOM_DNS = 9;
+ SERVER_IP_OVERRIDE = 10;
+ CUSTOM_MTU = 11;
+ CUSTOM_MSS_FIX = 12;
+ DAITA = 13;
}
message ObfuscationEndpoint {
+ enum ObfuscationType {
+ UDP2TCP = 0;
+ SHADOWSOCKS = 1;
+ }
+
string address = 1;
uint32 port = 2;
TransportProtocol protocol = 3;
ObfuscationType obfuscation_type = 4;
}
-enum ProxyType {
- SHADOWSOCKS = 0;
- CUSTOM = 1;
-}
-
message Endpoint {
string address = 1;
TransportProtocol protocol = 2;
}
message ProxyEndpoint {
+ enum ProxyType {
+ SHADOWSOCKS = 0;
+ CUSTOM = 1;
+ }
+
string address = 1;
TransportProtocol protocol = 2;
ProxyType proxy_type = 3;
@@ -352,14 +354,18 @@ message BridgeState {
message Udp2TcpObfuscationSettings { optional uint32 port = 1; }
+message ShadowsocksSettings { optional uint32 port = 1; }
+
message ObfuscationSettings {
enum SelectedObfuscation {
AUTO = 0;
OFF = 1;
UDP2TCP = 2;
+ SHADOWSOCKS = 3;
}
SelectedObfuscation selected_obfuscation = 1;
Udp2TcpObfuscationSettings udp2tcp = 2;
+ ShadowsocksSettings shadowsocks = 3;
}
message CustomList {
@@ -631,6 +637,7 @@ message Relay {
message WireguardRelayEndpointData {
bytes public_key = 1;
bool daita = 2;
+ repeated string shadowsocks_extra_addr_in = 3;
}
message Location {
@@ -686,7 +693,8 @@ message WireguardEndpointData {
repeated PortRange port_ranges = 1;
string ipv4_gateway = 2;
string ipv6_gateway = 3;
- repeated uint32 udp2tcp_ports = 4;
+ repeated PortRange shadowsocks_port_ranges = 4;
+ repeated uint32 udp2tcp_ports = 5;
}
message PortRange {
diff --git a/mullvad-management-interface/src/types/conversions/features.rs b/mullvad-management-interface/src/types/conversions/features.rs
index ae04fc9099..d21441a610 100644
--- a/mullvad-management-interface/src/types/conversions/features.rs
+++ b/mullvad-management-interface/src/types/conversions/features.rs
@@ -10,6 +10,7 @@ impl From<mullvad_types::features::FeatureIndicator> for proto::FeatureIndicator
mullvad_types::features::FeatureIndicator::SplitTunneling => SplitTunneling,
mullvad_types::features::FeatureIndicator::LockdownMode => LockdownMode,
mullvad_types::features::FeatureIndicator::Udp2Tcp => Udp2Tcp,
+ mullvad_types::features::FeatureIndicator::Shadowsocks => Shadowsocks,
mullvad_types::features::FeatureIndicator::LanSharing => LanSharing,
mullvad_types::features::FeatureIndicator::DnsContentBlockers => DnsContentBlockers,
mullvad_types::features::FeatureIndicator::CustomDns => CustomDns,
@@ -30,6 +31,7 @@ impl From<proto::FeatureIndicator> for mullvad_types::features::FeatureIndicator
proto::FeatureIndicator::SplitTunneling => Self::SplitTunneling,
proto::FeatureIndicator::LockdownMode => Self::LockdownMode,
proto::FeatureIndicator::Udp2Tcp => Self::Udp2Tcp,
+ proto::FeatureIndicator::Shadowsocks => Self::Shadowsocks,
proto::FeatureIndicator::LanSharing => Self::LanSharing,
proto::FeatureIndicator::DnsContentBlockers => Self::DnsContentBlockers,
proto::FeatureIndicator::CustomDns => Self::CustomDns,
diff --git a/mullvad-management-interface/src/types/conversions/net.rs b/mullvad-management-interface/src/types/conversions/net.rs
index e9c8a94838..249ca5f406 100644
--- a/mullvad-management-interface/src/types/conversions/net.rs
+++ b/mullvad-management-interface/src/types/conversions/net.rs
@@ -17,8 +17,12 @@ impl From<talpid_types::net::TunnelEndpoint> for proto::TunnelEndpoint {
address: proxy_ep.endpoint.address.to_string(),
protocol: i32::from(proto::TransportProtocol::from(proxy_ep.endpoint.protocol)),
proxy_type: match proxy_ep.proxy_type {
- net::proxy::ProxyType::Shadowsocks => i32::from(proto::ProxyType::Shadowsocks),
- net::proxy::ProxyType::Custom => i32::from(proto::ProxyType::Custom),
+ net::proxy::ProxyType::Shadowsocks => {
+ i32::from(proto::proxy_endpoint::ProxyType::Shadowsocks)
+ }
+ net::proxy::ProxyType::Custom => {
+ i32::from(proto::proxy_endpoint::ProxyType::Custom)
+ }
},
}),
obfuscation: endpoint.obfuscation.map(|obfuscation_endpoint| {
@@ -29,7 +33,12 @@ impl From<talpid_types::net::TunnelEndpoint> for proto::TunnelEndpoint {
obfuscation_endpoint.endpoint.protocol,
)),
obfuscation_type: match obfuscation_endpoint.obfuscation_type {
- net::ObfuscationType::Udp2Tcp => i32::from(proto::ObfuscationType::Udp2tcp),
+ net::ObfuscationType::Udp2Tcp => {
+ i32::from(proto::obfuscation_endpoint::ObfuscationType::Udp2tcp)
+ }
+ net::ObfuscationType::Shadowsocks => {
+ i32::from(proto::obfuscation_endpoint::ObfuscationType::Shadowsocks)
+ }
},
}
}),
@@ -72,11 +81,15 @@ impl TryFrom<proto::TunnelEndpoint> for talpid_types::net::TunnelEndpoint {
)?,
protocol: try_transport_protocol_from_i32(proxy_ep.protocol)?,
},
- proxy_type: match proto::ProxyType::try_from(proxy_ep.proxy_type) {
- Ok(proto::ProxyType::Shadowsocks) => {
+ proxy_type: match proto::proxy_endpoint::ProxyType::try_from(
+ proxy_ep.proxy_type,
+ ) {
+ Ok(proto::proxy_endpoint::ProxyType::Shadowsocks) => {
talpid_net::proxy::ProxyType::Shadowsocks
}
- Ok(proto::ProxyType::Custom) => talpid_net::proxy::ProxyType::Custom,
+ Ok(proto::proxy_endpoint::ProxyType::Custom) => {
+ talpid_net::proxy::ProxyType::Custom
+ }
Err(_) => {
return Err(FromProtobufTypeError::InvalidArgument(
"unknown proxy type",
@@ -100,18 +113,22 @@ impl TryFrom<proto::TunnelEndpoint> for talpid_types::net::TunnelEndpoint {
),
protocol: try_transport_protocol_from_i32(obfs_ep.protocol)?,
},
- obfuscation_type: match proto::ObfuscationType::try_from(
- obfs_ep.obfuscation_type,
- ) {
- Ok(proto::ObfuscationType::Udp2tcp) => {
- talpid_net::ObfuscationType::Udp2Tcp
- }
- Err(_) => {
- return Err(FromProtobufTypeError::InvalidArgument(
- "unknown obfuscation type",
- ))
- }
- },
+ 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
+ }
+ Err(_) => {
+ return Err(FromProtobufTypeError::InvalidArgument(
+ "unknown obfuscation type",
+ ))
+ }
+ },
})
})
.transpose()?,
diff --git a/mullvad-management-interface/src/types/conversions/relay_constraints.rs b/mullvad-management-interface/src/types/conversions/relay_constraints.rs
index 47d097abe9..ef0afd02b4 100644
--- a/mullvad-management-interface/src/types/conversions/relay_constraints.rs
+++ b/mullvad-management-interface/src/types/conversions/relay_constraints.rs
@@ -152,10 +152,14 @@ impl From<&mullvad_types::relay_constraints::ObfuscationSettings> for proto::Obf
SelectedObfuscation::Udp2Tcp => {
proto::obfuscation_settings::SelectedObfuscation::Udp2tcp
}
+ SelectedObfuscation::Shadowsocks => {
+ proto::obfuscation_settings::SelectedObfuscation::Shadowsocks
+ }
});
Self {
selected_obfuscation,
udp2tcp: Some(proto::Udp2TcpObfuscationSettings::from(&settings.udp2tcp)),
+ shadowsocks: Some(proto::ShadowsocksSettings::from(&settings.shadowsocks)),
}
}
}
@@ -176,6 +180,14 @@ impl From<&mullvad_types::relay_constraints::Udp2TcpObfuscationSettings>
}
}
+impl From<&mullvad_types::relay_constraints::ShadowsocksSettings> for proto::ShadowsocksSettings {
+ fn from(settings: &mullvad_types::relay_constraints::ShadowsocksSettings) -> Self {
+ Self {
+ port: settings.port.map(u32::from).option(),
+ }
+ }
+}
+
impl From<mullvad_types::relay_constraints::BridgeSettings> for proto::BridgeSettings {
fn from(settings: mullvad_types::relay_constraints::BridgeSettings) -> Self {
use proto::bridge_settings;
@@ -438,9 +450,10 @@ impl TryFrom<proto::ObfuscationSettings> for mullvad_types::relay_constraints::O
Ok(IpcSelectedObfuscation::Auto) => SelectedObfuscation::Auto,
Ok(IpcSelectedObfuscation::Off) => SelectedObfuscation::Off,
Ok(IpcSelectedObfuscation::Udp2tcp) => SelectedObfuscation::Udp2Tcp,
+ Ok(IpcSelectedObfuscation::Shadowsocks) => SelectedObfuscation::Shadowsocks,
Err(_) => {
return Err(FromProtobufTypeError::InvalidArgument(
- "invalid selected obfuscator",
+ "invalid obfuscation settings",
));
}
};
@@ -451,7 +464,17 @@ impl TryFrom<proto::ObfuscationSettings> for mullvad_types::relay_constraints::O
}
None => {
return Err(FromProtobufTypeError::InvalidArgument(
- "invalid selected obfuscator",
+ "invalid udp2tcp settings",
+ ));
+ }
+ };
+ let shadowsocks = match settings.shadowsocks {
+ Some(settings) => {
+ mullvad_types::relay_constraints::ShadowsocksSettings::try_from(&settings)?
+ }
+ None => {
+ return Err(FromProtobufTypeError::InvalidArgument(
+ "invalid shadowsocks settings",
));
}
};
@@ -459,6 +482,7 @@ impl TryFrom<proto::ObfuscationSettings> for mullvad_types::relay_constraints::O
Ok(Self {
selected_obfuscation,
udp2tcp,
+ shadowsocks,
})
}
}
@@ -475,6 +499,18 @@ impl TryFrom<&proto::Udp2TcpObfuscationSettings>
}
}
+impl TryFrom<&proto::ShadowsocksSettings>
+ for mullvad_types::relay_constraints::ShadowsocksSettings
+{
+ type Error = FromProtobufTypeError;
+
+ fn try_from(settings: &proto::ShadowsocksSettings) -> Result<Self, Self::Error> {
+ Ok(Self {
+ port: Constraint::from(settings.port.map(|port| port as u16)),
+ })
+ }
+}
+
impl TryFrom<proto::BridgeState> for mullvad_types::relay_constraints::BridgeState {
type Error = FromProtobufTypeError;
diff --git a/mullvad-management-interface/src/types/conversions/relay_list.rs b/mullvad-management-interface/src/types/conversions/relay_list.rs
index 4e0a363702..e38b32eeed 100644
--- a/mullvad-management-interface/src/types/conversions/relay_list.rs
+++ b/mullvad-management-interface/src/types/conversions/relay_list.rs
@@ -1,5 +1,6 @@
use std::{
net::{Ipv4Addr, Ipv6Addr},
+ ops::RangeInclusive,
str::FromStr,
};
@@ -65,18 +66,29 @@ impl From<mullvad_types::relay_list::WireguardEndpointData> for proto::Wireguard
port_ranges: wireguard
.port_ranges
.into_iter()
- .map(|(first, last)| proto::PortRange {
- first: u32::from(first),
- last: u32::from(last),
- })
+ .map(proto::PortRange::from)
.collect(),
ipv4_gateway: wireguard.ipv4_gateway.to_string(),
ipv6_gateway: wireguard.ipv6_gateway.to_string(),
+ shadowsocks_port_ranges: wireguard
+ .shadowsocks_port_ranges
+ .into_iter()
+ .map(proto::PortRange::from)
+ .collect(),
udp2tcp_ports: wireguard.udp2tcp_ports.into_iter().map(u32::from).collect(),
}
}
}
+impl From<RangeInclusive<u16>> for proto::PortRange {
+ fn from(range: RangeInclusive<u16>) -> Self {
+ proto::PortRange {
+ first: u32::from(*range.start()),
+ last: u32::from(*range.end()),
+ }
+ }
+}
+
impl From<mullvad_types::relay_list::RelayListCountry> for proto::RelayListCountry {
fn from(country: mullvad_types::relay_list::RelayListCountry) -> Self {
let mut proto_country = proto::RelayListCountry {
@@ -123,6 +135,11 @@ impl From<mullvad_types::relay_list::Relay> for proto::Relay {
proto::WireguardRelayEndpointData {
public_key: data.public_key.as_bytes().to_vec(),
daita: data.daita,
+ shadowsocks_extra_addr_in: data
+ .shadowsocks_extra_addr_in
+ .iter()
+ .map(|addr| addr.to_string())
+ .collect(),
},
)),
_ => None,
@@ -238,6 +255,17 @@ impl TryFrom<proto::Relay> for mullvad_types::relay_list::Relay {
mullvad_types::relay_list::WireguardRelayEndpointData {
public_key: bytes_to_pubkey(&data.public_key)?,
daita: data.daita,
+ shadowsocks_extra_addr_in: data
+ .shadowsocks_extra_addr_in
+ .iter()
+ .map(|addr| {
+ addr.parse().map_err(|_err| {
+ FromProtobufTypeError::InvalidArgument(
+ "invalid relay IPv6 address",
+ )
+ })
+ })
+ .collect::<Result<_, FromProtobufTypeError>>()?,
},
)
}
@@ -346,20 +374,20 @@ impl TryFrom<proto::WireguardEndpointData> for mullvad_types::relay_list::Wiregu
let port_ranges = wireguard
.port_ranges
.into_iter()
- .map(|range| {
- let first = u16::try_from(range.first)
- .map_err(|_| FromProtobufTypeError::InvalidArgument("invalid wg port"))?;
- let last = u16::try_from(range.last)
- .map_err(|_| FromProtobufTypeError::InvalidArgument("invalid wg port"))?;
- Ok((first, last))
- })
- .collect::<Result<Vec<(u16, u16)>, FromProtobufTypeError>>()?;
+ .map(RangeInclusive::try_from)
+ .collect::<Result<Vec<_>, FromProtobufTypeError>>()?;
let ipv4_gateway = Ipv4Addr::from_str(&wireguard.ipv4_gateway)
.map_err(|_| FromProtobufTypeError::InvalidArgument("Invalid IPv4 gateway"))?;
let ipv6_gateway = Ipv6Addr::from_str(&wireguard.ipv6_gateway)
.map_err(|_| FromProtobufTypeError::InvalidArgument("Invalid IPv6 gateway"))?;
+ let shadowsocks_port_ranges = wireguard
+ .shadowsocks_port_ranges
+ .into_iter()
+ .map(RangeInclusive::try_from)
+ .collect::<Result<Vec<_>, FromProtobufTypeError>>()?;
+
let udp2tcp_ports = wireguard
.udp2tcp_ports
.into_iter()
@@ -373,7 +401,20 @@ impl TryFrom<proto::WireguardEndpointData> for mullvad_types::relay_list::Wiregu
port_ranges,
ipv4_gateway,
ipv6_gateway,
+ shadowsocks_port_ranges,
udp2tcp_ports,
})
}
}
+
+impl TryFrom<proto::PortRange> for RangeInclusive<u16> {
+ type Error = FromProtobufTypeError;
+
+ fn try_from(range: proto::PortRange) -> Result<Self, Self::Error> {
+ let first = u16::try_from(range.first)
+ .map_err(|_| FromProtobufTypeError::InvalidArgument("invalid port"))?;
+ let last = u16::try_from(range.last)
+ .map_err(|_| FromProtobufTypeError::InvalidArgument("invalid port"))?;
+ Ok(first..=last)
+ }
+}
diff --git a/mullvad-relay-selector/src/error.rs b/mullvad-relay-selector/src/error.rs
index 3e70f2429b..aefd931da6 100644
--- a/mullvad-relay-selector/src/error.rs
+++ b/mullvad-relay-selector/src/error.rs
@@ -20,7 +20,7 @@ pub enum Error {
NoBridge,
#[error("No obfuscators matching current constraints")]
- NoObfuscator,
+ NoObfuscator(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("No endpoint could be constructed due to {} for relay {:?}", .internal, .relay)]
NoEndpoint {
diff --git a/mullvad-relay-selector/src/relay_selector/detailer.rs b/mullvad-relay-selector/src/relay_selector/detailer.rs
index 5c387c41f5..7b5f24f94c 100644
--- a/mullvad-relay-selector/src/relay_selector/detailer.rs
+++ b/mullvad-relay-selector/src/relay_selector/detailer.rs
@@ -41,10 +41,8 @@ pub enum Error {
MissingPublicKey,
#[error("The selected relay does not support IPv6")]
NoIPv6(Box<Relay>),
- #[error("Invalid port argument: port {0} is not in any valid Wireguard port range")]
- PortNotInRange(u16),
- #[error("Port selection algorithm is broken")]
- PortSelectionAlgorithm,
+ #[error("Failed to select port")]
+ PortSelectionError,
}
/// Constructs a [`MullvadWireguardEndpoint`] with details for how to connect to a Wireguard relay.
@@ -161,34 +159,29 @@ fn get_address_for_wireguard_relay(
query: &WireguardRelayQuery,
relay: &Relay,
) -> Result<IpAddr, Error> {
- match query.ip_version {
- Constraint::Any | Constraint::Only(IpVersion::V4) => Ok(relay.ipv4_addr_in.into()),
- Constraint::Only(IpVersion::V6) => relay
+ match resolve_ip_version(query.ip_version) {
+ IpVersion::V4 => Ok(relay.ipv4_addr_in.into()),
+ IpVersion::V6 => relay
.ipv6_addr_in
.map(|addr| addr.into())
.ok_or(Error::NoIPv6(Box::new(relay.clone()))),
}
}
+pub fn resolve_ip_version(ip_version: Constraint<IpVersion>) -> IpVersion {
+ match ip_version {
+ Constraint::Any | Constraint::Only(IpVersion::V4) => IpVersion::V4,
+ Constraint::Only(IpVersion::V6) => IpVersion::V6,
+ }
+}
+
/// Try to pick a valid Wireguard port.
fn get_port_for_wireguard_relay(
query: &WireguardRelayQuery,
data: &WireguardEndpointData,
) -> Result<u16, Error> {
- match query.port {
- Constraint::Any => select_random_port(&data.port_ranges),
- Constraint::Only(port) => {
- if data
- .port_ranges
- .iter()
- .any(|range| (range.0 <= port && port <= range.1))
- {
- Ok(port)
- } else {
- Err(Error::PortNotInRange(port))
- }
- }
- }
+ super::helpers::desired_or_random_port_from_range(&data.port_ranges, query.port)
+ .map_err(|_err| Error::PortSelectionError)
}
/// Read the [`PublicKey`] of a relay. This will only succeed if [relay][`Relay`] is a
@@ -200,39 +193,6 @@ const fn get_public_key(relay: &Relay) -> Result<&PublicKey, Error> {
}
}
-/// Selects a random port number from a list of provided port ranges.
-///
-/// This function iterates over a list of port ranges, each represented as a tuple (u16, u16)
-/// where the first element is the start of the range and the second is the end (inclusive),
-/// and selects a random port from the set of all ranges.
-///
-/// # Parameters
-/// - `port_ranges`: A slice of tuples, each representing a range of valid port numbers.
-///
-/// # Returns
-/// - `Option<u16>`: A randomly selected port number within the given ranges, or `None` if the input
-/// is empty or the total number of available ports is zero.
-fn select_random_port(port_ranges: &[(u16, u16)]) -> Result<u16, Error> {
- use rand::Rng;
- let get_port_amount = |range: &(u16, u16)| -> u64 { (1 + range.1 - range.0) as u64 };
- let port_amount: u64 = port_ranges.iter().map(get_port_amount).sum();
-
- if port_amount < 1 {
- return Err(Error::PortSelectionAlgorithm);
- }
-
- let mut port_index = rand::thread_rng().gen_range(0..port_amount);
-
- for range in port_ranges.iter() {
- let ports_in_range = get_port_amount(range);
- if port_index < ports_in_range {
- return Ok(port_index as u16 + range.0);
- }
- port_index -= ports_in_range;
- }
- Err(Error::PortSelectionAlgorithm)
-}
-
/// Constructs an [`Endpoint`] with details for how to connect to an OpenVPN relay.
///
/// If this endpoint is to be used in conjunction with a bridge, the resulting endpoint is
diff --git a/mullvad-relay-selector/src/relay_selector/helpers.rs b/mullvad-relay-selector/src/relay_selector/helpers.rs
index 442b1f596f..c49a7df04d 100644
--- a/mullvad-relay-selector/src/relay_selector/helpers.rs
+++ b/mullvad-relay-selector/src/relay_selector/helpers.rs
@@ -1,16 +1,34 @@
//! This module contains various helper functions for the relay selector implementation.
-use std::net::SocketAddr;
+use std::{
+ net::{IpAddr, SocketAddr},
+ ops::{RangeBounds, RangeInclusive},
+};
use mullvad_types::{
- constraints::Constraint, endpoint::MullvadWireguardEndpoint,
- relay_constraints::Udp2TcpObfuscationSettings, relay_list::Relay,
+ constraints::Constraint,
+ endpoint::MullvadWireguardEndpoint,
+ relay_constraints::{ShadowsocksSettings, Udp2TcpObfuscationSettings},
+ relay_list::Relay,
+};
+use rand::{
+ seq::{IteratorRandom, SliceRandom},
+ thread_rng, Rng,
};
-use rand::{seq::SliceRandom, thread_rng, Rng};
use talpid_types::net::obfuscation::ObfuscatorConfig;
use crate::SelectedObfuscator;
+/// Port ranges available for WireGuard relays that have extra IPs for Shadowsocks.
+/// For relays that have no additional IPs, only ports provided by the relay list are available.
+const SHADOWSOCKS_EXTRA_PORT_RANGES: &[RangeInclusive<u16>] = &[1..=u16::MAX];
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error("Found no valid port matching the selected settings")]
+ NoMatchingPort,
+}
+
/// Picks a relay using [pick_random_relay_weighted], using the `weight` member of each relay
/// as the weight function.
pub fn pick_random_relay(relays: &[Relay]) -> Option<&Relay> {
@@ -62,21 +80,21 @@ pub fn get_udp2tcp_obfuscator(
udp2tcp_ports: &[u16],
relay: Relay,
endpoint: &MullvadWireguardEndpoint,
-) -> Option<SelectedObfuscator> {
+) -> Result<SelectedObfuscator, 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),
};
- Some(SelectedObfuscator { config, relay })
+ Ok(SelectedObfuscator { config, relay })
}
-pub fn get_udp2tcp_obfuscator_port(
+fn get_udp2tcp_obfuscator_port(
obfuscation_settings: &Udp2TcpObfuscationSettings,
udp2tcp_ports: &[u16],
-) -> Option<u16> {
- if let Constraint::Only(desired_port) = obfuscation_settings.port {
+) -> Result<u16, Error> {
+ let port = if let Constraint::Only(desired_port) = obfuscation_settings.port {
udp2tcp_ports
.iter()
.find(|&candidate| desired_port == *candidate)
@@ -84,5 +102,251 @@ pub fn get_udp2tcp_obfuscator_port(
} else {
// There are no specific obfuscation settings to take into consideration in this case.
udp2tcp_ports.choose(&mut thread_rng()).copied()
+ };
+ port.ok_or(Error::NoMatchingPort)
+}
+
+pub fn get_shadowsocks_obfuscator(
+ settings: &ShadowsocksSettings,
+ non_extra_port_ranges: &[RangeInclusive<u16>],
+ relay: Relay,
+ endpoint: &MullvadWireguardEndpoint,
+) -> Result<SelectedObfuscator, Error> {
+ let port = settings.port;
+ let extra_addrs = match &relay.endpoint_data {
+ mullvad_types::relay_list::RelayEndpointData::Wireguard(wg) => {
+ &wg.shadowsocks_extra_addr_in
+ }
+ _ => panic!("expected wireguard relay"),
+ };
+
+ let endpoint = get_shadowsocks_obfuscator_inner(
+ endpoint.peer.endpoint.ip(),
+ non_extra_port_ranges,
+ extra_addrs,
+ port,
+ )?;
+
+ Ok(SelectedObfuscator {
+ config: ObfuscatorConfig::Shadowsocks { endpoint },
+ relay,
+ })
+}
+
+/// Return an obfuscation config for the wireguard server at `wg_in_addr` or one of `extra_in_addrs`
+/// (unless empty). `wg_in_addr_port_ranges` contains all valid ports for `wg_in_addr`, and
+/// `SHADOWSOCKS_EXTRA_PORT_RANGES` contains valid ports for `extra_in_addrs`.
+fn get_shadowsocks_obfuscator_inner<R: RangeBounds<u16> + Iterator<Item = u16> + Clone>(
+ wg_in_addr: IpAddr,
+ wg_in_addr_port_ranges: &[R],
+ extra_in_addrs: &[IpAddr],
+ desired_port: Constraint<u16>,
+) -> Result<SocketAddr, Error> {
+ // Filter out addresses for the wrong address family
+ let extra_in_addrs: Vec<_> = extra_in_addrs
+ .iter()
+ .filter(|addr| addr.is_ipv4() == wg_in_addr.is_ipv4())
+ .copied()
+ .collect();
+
+ let in_ip = extra_in_addrs
+ .iter()
+ .choose(&mut rand::thread_rng())
+ .copied()
+ .unwrap_or(wg_in_addr);
+
+ let selected_port = if extra_in_addrs.is_empty() {
+ desired_or_random_port_from_range(wg_in_addr_port_ranges, desired_port)
+ } else {
+ desired_or_random_port_from_range(SHADOWSOCKS_EXTRA_PORT_RANGES, desired_port)
+ }?;
+
+ Ok(SocketAddr::from((in_ip, selected_port)))
+}
+
+/// 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.
+pub fn desired_or_random_port_from_range<R: RangeBounds<u16> + Iterator<Item = u16> + Clone>(
+ port_ranges: &[R],
+ desired_port: Constraint<u16>,
+) -> Result<u16, Error> {
+ desired_port
+ .map(|port| port_if_in_range(port_ranges, port))
+ .unwrap_or_else(|| select_random_port(port_ranges))
+}
+
+/// Return `Ok(port)`, if and only if `port` is in `port_ranges`. Otherwise, return an error.
+fn port_if_in_range<R: RangeBounds<u16>>(port_ranges: &[R], port: u16) -> Result<u16, Error> {
+ port_ranges
+ .iter()
+ .find_map(|range| {
+ if range.contains(&port) {
+ Some(port)
+ } else {
+ None
+ }
+ })
+ .ok_or(Error::NoMatchingPort)
+}
+
+/// Selects a random port number from a list of provided port ranges.
+///
+/// # Parameters
+/// - `port_ranges`: A slice of port numbers.
+///
+/// # Returns
+/// - On success, a randomly selected port number within the given ranges. Otherwise,
+/// an error is returned.
+pub fn select_random_port<R: RangeBounds<u16> + Iterator<Item = u16> + Clone>(
+ port_ranges: &[R],
+) -> Result<u16, Error> {
+ port_ranges
+ .iter()
+ .cloned()
+ .flatten()
+ .choose(&mut rand::thread_rng())
+ .ok_or(Error::NoMatchingPort)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{
+ get_shadowsocks_obfuscator_inner, port_if_in_range, SHADOWSOCKS_EXTRA_PORT_RANGES,
+ };
+ use mullvad_types::constraints::Constraint;
+ use std::{net::IpAddr, ops::RangeInclusive};
+
+ /// Test whether select ports are available when relay has no extra IPs
+ #[test]
+ fn test_shadowsocks_no_extra_addrs() {
+ const PORT_RANGES: &[RangeInclusive<u16>] = &[100..=200, 1000..=2000];
+ const WITHIN_RANGE_PORT: u16 = 100;
+ const OUT_OF_RANGE_PORT: u16 = 1;
+ let wg_in_ip: IpAddr = "1.2.3.4".parse().unwrap();
+
+ let selected_addr =
+ get_shadowsocks_obfuscator_inner(wg_in_ip, PORT_RANGES, &[], Constraint::Any)
+ .expect("should find valid port without constraint");
+
+ assert_eq!(selected_addr.ip(), wg_in_ip);
+ assert!(
+ port_if_in_range(PORT_RANGES, selected_addr.port()).is_ok(),
+ "expected port in port range"
+ );
+
+ let selected_addr = get_shadowsocks_obfuscator_inner(
+ wg_in_ip,
+ PORT_RANGES,
+ &[],
+ Constraint::Only(WITHIN_RANGE_PORT),
+ )
+ .expect("should find within-range port");
+
+ assert_eq!(selected_addr.ip(), wg_in_ip);
+ assert!(
+ port_if_in_range(PORT_RANGES, selected_addr.port()).is_ok(),
+ "expected port in port range"
+ );
+
+ let selected_addr = get_shadowsocks_obfuscator_inner(
+ wg_in_ip,
+ PORT_RANGES,
+ &[],
+ Constraint::Only(OUT_OF_RANGE_PORT),
+ );
+ assert!(
+ selected_addr.is_err(),
+ "expected no relay for port outside range, found {selected_addr:?}"
+ );
+ }
+
+ /// All ports should be available when relay has extra IPs, and only extra IPs should be used
+ #[test]
+ fn test_shadowsocks_extra_addrs() {
+ const PORT_RANGES: &[RangeInclusive<u16>] = &[100..=200, 1000..=2000];
+ const OUT_OF_RANGE_PORT: u16 = 1;
+ let wg_in_ip: IpAddr = "1.2.3.4".parse().unwrap();
+
+ let extra_in_addrs: &[IpAddr] =
+ &["1.3.3.7".parse().unwrap(), "192.0.2.123".parse().unwrap()];
+
+ let selected_addr = get_shadowsocks_obfuscator_inner(
+ wg_in_ip,
+ PORT_RANGES,
+ extra_in_addrs,
+ Constraint::Any,
+ )
+ .expect("should find valid port without constraint");
+
+ assert!(
+ extra_in_addrs.contains(&selected_addr.ip()),
+ "expected extra IP to be selected"
+ );
+ assert!(port_if_in_range(SHADOWSOCKS_EXTRA_PORT_RANGES, selected_addr.port(),).is_ok());
+
+ let selected_addr = get_shadowsocks_obfuscator_inner(
+ wg_in_ip,
+ PORT_RANGES,
+ extra_in_addrs,
+ Constraint::Only(OUT_OF_RANGE_PORT),
+ )
+ .expect("expected selected address to be returned");
+ assert!(
+ extra_in_addrs.contains(&selected_addr.ip()),
+ "expected extra IP to be selected, got {selected_addr:?}"
+ );
+ assert_eq!(
+ selected_addr.port(),
+ OUT_OF_RANGE_PORT,
+ "expected selected port, got {selected_addr:?}"
+ );
+ }
+
+ /// Extra addresses that belong to the wrong IP family should be ignored
+ #[test]
+ fn test_shadowsocks_irrelevant_extra_addrs() {
+ const PORT_RANGES: &[RangeInclusive<u16>] = &[100..=200, 1000..=2000];
+ const IN_RANGE_PORT: u16 = 100;
+ const OUT_OF_RANGE_PORT: u16 = 1;
+ let wg_in_ip: IpAddr = "1.2.3.4".parse().unwrap();
+
+ let extra_in_addrs: &[IpAddr] = &["::2".parse().unwrap()];
+
+ let selected_addr = get_shadowsocks_obfuscator_inner(
+ wg_in_ip,
+ PORT_RANGES,
+ extra_in_addrs,
+ Constraint::Any,
+ )
+ .expect("should find valid port without constraint");
+
+ assert_eq!(
+ selected_addr.ip(),
+ wg_in_ip,
+ "expected extra IP to be ignored"
+ );
+
+ let selected_addr = get_shadowsocks_obfuscator_inner(
+ wg_in_ip,
+ PORT_RANGES,
+ extra_in_addrs,
+ Constraint::Only(OUT_OF_RANGE_PORT),
+ );
+ assert!(
+ selected_addr.is_err(),
+ "expected no match for out-of-range port"
+ );
+
+ let selected_addr = get_shadowsocks_obfuscator_inner(
+ wg_in_ip,
+ PORT_RANGES,
+ extra_in_addrs,
+ Constraint::Only(IN_RANGE_PORT),
+ );
+ assert!(
+ selected_addr.is_ok(),
+ "expected match for within-range port"
+ );
}
}
diff --git a/mullvad-relay-selector/src/relay_selector/matcher.rs b/mullvad-relay-selector/src/relay_selector/matcher.rs
index f6c244d3f2..8277619cb6 100644
--- a/mullvad-relay-selector/src/relay_selector/matcher.rs
+++ b/mullvad-relay-selector/src/relay_selector/matcher.rs
@@ -1,26 +1,31 @@
//! This module is responsible for filtering the whole relay list based on queries.
-use std::collections::HashSet;
+use std::{collections::HashSet, ops::RangeInclusive};
use mullvad_types::{
constraints::{Constraint, Match},
custom_list::CustomListsSettings,
relay_constraints::{
GeographicLocationConstraint, InternalBridgeConstraints, LocationConstraint, Ownership,
- Providers,
+ Providers, ShadowsocksSettings,
},
relay_list::{Relay, RelayEndpointData, WireguardRelayEndpointData},
};
-use talpid_types::net::TunnelType;
+use talpid_types::net::{IpVersion, TunnelType};
-use super::query::RelayQuery;
+use super::{
+ parsed_relays::ParsedRelays,
+ query::{ObfuscationQuery, RelayQuery, WireguardRelayQuery},
+};
/// Filter a list of relays and their endpoints based on constraints.
/// Only relays with (and including) matching endpoints are returned.
-pub fn filter_matching_relay_list<'a, R: Iterator<Item = &'a Relay> + Clone>(
+pub fn filter_matching_relay_list(
query: &RelayQuery,
- relays: R,
+ relay_list: &ParsedRelays,
custom_lists: &CustomListsSettings,
) -> Vec<Relay> {
+ let relays = relay_list.relays();
+
let locations = ResolvedLocationConstraint::from_constraint(&query.location, custom_lists);
let shortlist = relays
// Filter on tunnel type
@@ -34,7 +39,9 @@ pub fn filter_matching_relay_list<'a, R: Iterator<Item = &'a Relay> + Clone>(
// Filter by providers
.filter(|relay| filter_on_providers(&query.providers, relay))
// Filter by DAITA support
- .filter(|relay| filter_on_daita(&query.wireguard_constraints.daita, relay));
+ .filter(|relay| filter_on_daita(&query.wireguard_constraints.daita, relay))
+ // Filter by obfuscation support
+ .filter(|relay| filter_on_obfuscation(&query.wireguard_constraints, relay_list, relay));
// The last filtering to be done is on the `include_in_country` attribute found on each
// relay. When the location constraint is based on country, a relay which has
@@ -123,6 +130,61 @@ pub fn filter_on_daita(filter: &Constraint<bool>, relay: &Relay) -> bool {
}
}
+/// Returns whether `relay` satisfies the obfuscation settings.
+fn filter_on_obfuscation(
+ query: &WireguardRelayQuery,
+ relay_list: &ParsedRelays,
+ relay: &Relay,
+) -> bool {
+ match &query.obfuscation {
+ // Shadowsocks has relay-specific constraints
+ ObfuscationQuery::Shadowsocks(settings) => {
+ let wg_data = &relay_list.parsed_list().wireguard;
+ filter_on_shadowsocks(
+ &wg_data.shadowsocks_port_ranges,
+ &query.ip_version,
+ settings,
+ relay,
+ )
+ }
+
+ // If Shadowsocks is not a requirement, then there are no relay-specific constraints
+ _ => true,
+ }
+}
+
+/// Returns whether `relay` satisfies the Shadowsocks filter posed by `port`.
+fn filter_on_shadowsocks(
+ port_ranges: &[RangeInclusive<u16>],
+ ip_version: &Constraint<IpVersion>,
+ settings: &ShadowsocksSettings,
+ relay: &Relay,
+) -> bool {
+ let ip_version = super::detailer::resolve_ip_version(*ip_version);
+
+ match (settings, &relay.endpoint_data) {
+ // If Shadowsocks is specifically asked for, we must check if the specific relay supports our port.
+ // If there are extra addresses, then all ports are available, so we do not need to do this.
+ (
+ ShadowsocksSettings {
+ port: Constraint::Only(desired_port),
+ },
+ RelayEndpointData::Wireguard(wg_data),
+ ) => {
+ let filtered_extra_addrs = wg_data
+ .shadowsocks_extra_addr_in
+ .iter()
+ .find(|&&addr| IpVersion::from(addr) == ip_version);
+
+ filtered_extra_addrs.is_some()
+ || port_ranges.iter().any(|range| range.contains(desired_port))
+ }
+
+ // Otherwise, any relay works.
+ _ => true,
+ }
+}
+
/// Returns whether the relay is an OpenVPN relay.
pub const fn filter_openvpn(relay: &Relay) -> bool {
matches!(relay.endpoint_data, RelayEndpointData::Openvpn)
diff --git a/mullvad-relay-selector/src/relay_selector/mod.rs b/mullvad-relay-selector/src/relay_selector/mod.rs
index 7e6acab43a..b60bb86f3c 100644
--- a/mullvad-relay-selector/src/relay_selector/mod.rs
+++ b/mullvad-relay-selector/src/relay_selector/mod.rs
@@ -704,7 +704,7 @@ impl RelaySelector {
custom_lists: &CustomListsSettings,
parsed_relays: &ParsedRelays,
) -> Result<WireguardConfig, Error> {
- let candidates = filter_matching_relay_list(query, parsed_relays.relays(), custom_lists);
+ let candidates = filter_matching_relay_list(query, parsed_relays, custom_lists);
helpers::pick_random_relay(&candidates)
.cloned()
.map(WireguardConfig::singlehop)
@@ -736,9 +736,9 @@ impl RelaySelector {
// DAITA should only be enabled for the entry relay
exit_relay_query.wireguard_constraints.daita = Constraint::Only(false);
let exit_candidates =
- filter_matching_relay_list(&exit_relay_query, parsed_relays.relays(), custom_lists);
+ filter_matching_relay_list(&exit_relay_query, parsed_relays, custom_lists);
let entry_candidates =
- filter_matching_relay_list(&entry_relay_query, parsed_relays.relays(), custom_lists);
+ filter_matching_relay_list(&entry_relay_query, parsed_relays, custom_lists);
fn pick_random_excluding<'a>(list: &'a [Relay], exclude: &'a Relay) -> Option<&'a Relay> {
list.iter()
@@ -790,18 +790,35 @@ impl RelaySelector {
endpoint: &MullvadWireguardEndpoint,
parsed_relays: &ParsedRelays,
) -> Result<Option<SelectedObfuscator>, Error> {
+ let obfuscator_relay = match relay {
+ WireguardConfig::Singlehop { exit } => exit,
+ WireguardConfig::Multihop { entry, .. } => entry,
+ };
+ let box_obfsucation_error = |error: helpers::Error| Error::NoObfuscator(Box::new(error));
+
match &query.wireguard_constraints.obfuscation {
ObfuscationQuery::Off | ObfuscationQuery::Auto => Ok(None),
- ObfuscationQuery::Udp2tcp { port } => {
- let obfuscator_relay = match relay {
- WireguardConfig::Singlehop { exit } => exit,
- WireguardConfig::Multihop { entry, .. } => entry,
- };
+ ObfuscationQuery::Udp2tcp(settings) => {
let udp2tcp_ports = &parsed_relays.parsed_list().wireguard.udp2tcp_ports;
- helpers::get_udp2tcp_obfuscator(port, udp2tcp_ports, obfuscator_relay, endpoint)
+ helpers::get_udp2tcp_obfuscator(settings, udp2tcp_ports, obfuscator_relay, endpoint)
.map(Some)
- .ok_or(Error::NoObfuscator)
+ .map_err(box_obfsucation_error)
+ }
+ ObfuscationQuery::Shadowsocks(settings) => {
+ let port_ranges = &parsed_relays
+ .parsed_list()
+ .wireguard
+ .shadowsocks_port_ranges;
+ let obfuscation = helpers::get_shadowsocks_obfuscator(
+ settings,
+ port_ranges,
+ obfuscator_relay,
+ endpoint,
+ )
+ .map_err(box_obfsucation_error)?;
+
+ Ok(Some(obfuscation))
}
}
}
@@ -1020,7 +1037,7 @@ impl RelaySelector {
}
let matching_locations: Vec<Location> =
- filter_matching_relay_list(query, parsed_relays.relays(), custom_lists)
+ filter_matching_relay_list(query, parsed_relays, custom_lists)
.into_iter()
.filter_map(|relay| relay.location)
.unique_by(|location| location.city.clone())
@@ -1042,8 +1059,7 @@ impl RelaySelector {
parsed_relays: &ParsedRelays,
) -> Option<Relay> {
// Filter among all valid relays
- let relays = parsed_relays.relays();
- let candidates = filter_matching_relay_list(query, relays, custom_lists);
+ let candidates = filter_matching_relay_list(query, parsed_relays, custom_lists);
// Pick one of the valid relays.
helpers::pick_random_relay(&candidates).cloned()
}
diff --git a/mullvad-relay-selector/src/relay_selector/query.rs b/mullvad-relay-selector/src/relay_selector/query.rs
index 48ad5bd1dc..0faa9c1ca9 100644
--- a/mullvad-relay-selector/src/relay_selector/query.rs
+++ b/mullvad-relay-selector/src/relay_selector/query.rs
@@ -33,8 +33,8 @@ use mullvad_types::{
constraints::Constraint,
relay_constraints::{
BridgeConstraints, LocationConstraint, ObfuscationSettings, OpenVpnConstraints, Ownership,
- Providers, RelayConstraints, RelaySettings, SelectedObfuscation, TransportPort,
- Udp2TcpObfuscationSettings, WireguardConstraints,
+ Providers, RelayConstraints, RelaySettings, SelectedObfuscation, ShadowsocksSettings,
+ TransportPort, Udp2TcpObfuscationSettings, WireguardConstraints,
},
Intersection,
};
@@ -159,9 +159,8 @@ pub enum ObfuscationQuery {
Off,
#[default]
Auto,
- Udp2tcp {
- port: Udp2TcpObfuscationSettings,
- },
+ Udp2tcp(Udp2TcpObfuscationSettings),
+ Shadowsocks(ShadowsocksSettings),
}
impl From<ObfuscationSettings> for ObfuscationQuery {
@@ -173,9 +172,10 @@ impl From<ObfuscationSettings> for ObfuscationQuery {
match obfuscation.selected_obfuscation {
SelectedObfuscation::Off => ObfuscationQuery::Off,
SelectedObfuscation::Auto => ObfuscationQuery::Auto,
- SelectedObfuscation::Udp2Tcp => ObfuscationQuery::Udp2tcp {
- port: obfuscation.udp2tcp,
- },
+ SelectedObfuscation::Udp2Tcp => ObfuscationQuery::Udp2tcp(obfuscation.udp2tcp),
+ SelectedObfuscation::Shadowsocks => {
+ ObfuscationQuery::Shadowsocks(obfuscation.shadowsocks)
+ }
}
}
}
@@ -185,11 +185,13 @@ impl Intersection for ObfuscationQuery {
match (self, other) {
(ObfuscationQuery::Off, _) | (_, ObfuscationQuery::Off) => Some(ObfuscationQuery::Off),
(ObfuscationQuery::Auto, other) | (other, ObfuscationQuery::Auto) => Some(other),
- (ObfuscationQuery::Udp2tcp { port: a }, ObfuscationQuery::Udp2tcp { port: b }) => {
- Some(ObfuscationQuery::Udp2tcp {
- port: a.intersection(b)?,
- })
+ (ObfuscationQuery::Udp2tcp(a), ObfuscationQuery::Udp2tcp(b)) => {
+ Some(ObfuscationQuery::Udp2tcp(a.intersection(b)?))
}
+ (ObfuscationQuery::Shadowsocks(a), ObfuscationQuery::Shadowsocks(b)) => {
+ Some(ObfuscationQuery::Shadowsocks(a.intersection(b)?))
+ }
+ _ => None,
}
}
}
@@ -343,7 +345,7 @@ pub mod builder {
constraints::Constraint,
relay_constraints::{
BridgeConstraints, LocationConstraint, RelayConstraints, SelectedObfuscation,
- TransportPort, Udp2TcpObfuscationSettings,
+ ShadowsocksSettings, TransportPort, Udp2TcpObfuscationSettings,
},
};
use talpid_types::net::TunnelType;
@@ -543,8 +545,28 @@ pub mod builder {
obfuscation: obfuscation.clone(),
daita: self.protocol.daita,
};
+ self.query.wireguard_constraints.obfuscation = ObfuscationQuery::Udp2tcp(obfuscation);
+ RelayQueryBuilder {
+ query: self.query,
+ protocol,
+ }
+ }
+
+ /// Enable Shadowsocks obufscation. This will in turn enable the option to configure the
+ /// port.
+ pub fn shadowsocks(
+ mut self,
+ ) -> RelayQueryBuilder<Wireguard<Multihop, ShadowsocksSettings, Daita>> {
+ let obfuscation = ShadowsocksSettings {
+ port: Constraint::Any,
+ };
+ let protocol = Wireguard {
+ multihop: self.protocol.multihop,
+ obfuscation: obfuscation.clone(),
+ daita: self.protocol.daita,
+ };
self.query.wireguard_constraints.obfuscation =
- ObfuscationQuery::Udp2tcp { port: obfuscation };
+ ObfuscationQuery::Shadowsocks(obfuscation);
RelayQueryBuilder {
query: self.query,
protocol,
@@ -557,9 +579,8 @@ pub mod builder {
/// protocol should use to connect to a relay.
pub fn udp2tcp_port(mut self, port: u16) -> Self {
self.protocol.obfuscation.port = Constraint::Only(port);
- self.query.wireguard_constraints.obfuscation = ObfuscationQuery::Udp2tcp {
- port: self.protocol.obfuscation.clone(),
- };
+ self.query.wireguard_constraints.obfuscation =
+ ObfuscationQuery::Udp2tcp(self.protocol.obfuscation.clone());
self
}
}
@@ -680,7 +701,10 @@ pub mod builder {
mod test {
use mullvad_types::{
constraints::Constraint,
- relay_constraints::{ObfuscationSettings, SelectedObfuscation, Udp2TcpObfuscationSettings},
+ relay_constraints::{
+ ObfuscationSettings, SelectedObfuscation, ShadowsocksSettings,
+ Udp2TcpObfuscationSettings,
+ },
};
use proptest::prelude::*;
@@ -765,11 +789,14 @@ mod test {
/// When obfuscation is set to automatic in [`ObfuscationSettings`], the query should not
/// contain any specific obfuscation protocol settings.
#[test]
- fn test_auto_obfuscation_settings(port in constraint(proptest::arbitrary::any::<u16>())) {
+ fn test_auto_obfuscation_settings(port1 in constraint(proptest::arbitrary::any::<u16>()), port2 in constraint(proptest::arbitrary::any::<u16>())) {
let query = ObfuscationQuery::from(ObfuscationSettings {
selected_obfuscation: SelectedObfuscation::Auto,
udp2tcp: Udp2TcpObfuscationSettings {
- port,
+ port: port1,
+ },
+ shadowsocks: ShadowsocksSettings {
+ port: port2,
},
});
assert_eq!(query, ObfuscationQuery::Auto);
diff --git a/mullvad-relay-selector/tests/relay_selector.rs b/mullvad-relay-selector/tests/relay_selector.rs
index 053de20c1a..ee0716aed1 100644
--- a/mullvad-relay-selector/tests/relay_selector.rs
+++ b/mullvad-relay-selector/tests/relay_selector.rs
@@ -1,26 +1,29 @@
//! Tests for verifying that the relay selector works as expected.
use once_cell::sync::Lazy;
-use std::collections::HashSet;
+use std::{
+ collections::HashSet,
+ net::{IpAddr, Ipv4Addr, Ipv6Addr},
+};
use talpid_types::net::{
obfuscation::ObfuscatorConfig,
wireguard::PublicKey,
- Endpoint,
+ Endpoint, IpVersion,
TransportProtocol::{Tcp, Udp},
TunnelType,
};
use mullvad_relay_selector::{
query::{builder::RelayQueryBuilder, BridgeQuery, ObfuscationQuery, OpenVpnRelayQuery},
- Error, GetRelay, RelaySelector, RuntimeParameters, SelectorConfig, WireguardConfig,
- RETRY_ORDER,
+ Error, GetRelay, RelaySelector, RuntimeParameters, SelectedObfuscator, SelectorConfig,
+ WireguardConfig, RETRY_ORDER,
};
use mullvad_types::{
constraints::Constraint,
endpoint::MullvadEndpoint,
relay_constraints::{
- BridgeConstraints, BridgeState, GeographicLocationConstraint, Ownership, Providers,
- TransportPort,
+ BridgeConstraints, BridgeState, GeographicLocationConstraint, LocationConstraint,
+ Ownership, Providers, RelayOverride, TransportPort,
},
relay_list::{
BridgeEndpointData, OpenVpnEndpoint, OpenVpnEndpointData, Relay, RelayEndpointData,
@@ -55,6 +58,7 @@ static RELAYS: Lazy<RelayList> = Lazy::new(|| RelayList {
)
.unwrap(),
daita: false,
+ shadowsocks_extra_addr_in: vec![],
}),
location: None,
},
@@ -73,6 +77,7 @@ static RELAYS: Lazy<RelayList> = Lazy::new(|| RelayList {
)
.unwrap(),
daita: false,
+ shadowsocks_extra_addr_in: vec![],
}),
location: None,
},
@@ -112,6 +117,7 @@ static RELAYS: Lazy<RelayList> = Lazy::new(|| RelayList {
endpoint_data: RelayEndpointData::Bridge,
location: None,
},
+ SHADOWSOCKS_RELAY.clone(),
],
}],
}],
@@ -155,18 +161,48 @@ static RELAYS: Lazy<RelayList> = Lazy::new(|| RelayList {
},
wireguard: WireguardEndpointData {
port_ranges: vec![
- (53, 53),
- (443, 443),
- (4000, 33433),
- (33565, 51820),
- (52000, 60000),
+ 53..=53,
+ 443..=443,
+ 4000..=33433,
+ 33565..=51820,
+ 52000..=60000,
],
ipv4_gateway: "10.64.0.1".parse().unwrap(),
ipv6_gateway: "fc00:bbbb:bbbb:bb01::1".parse().unwrap(),
udp2tcp_ports: vec![],
+ shadowsocks_port_ranges: vec![100..=200, 1000..=2000],
},
});
+/// A Shadowsocks relay with additional addresses
+static SHADOWSOCKS_RELAY: Lazy<Relay> = Lazy::new(|| Relay {
+ hostname: SHADOWSOCKS_RELAY_LOCATION
+ .get_hostname()
+ .unwrap()
+ .to_owned(),
+ ipv4_addr_in: SHADOWSOCKS_RELAY_IPV4,
+ ipv6_addr_in: Some(SHADOWSOCKS_RELAY_IPV6),
+ include_in_country: true,
+ active: true,
+ owned: true,
+ provider: "provider0".to_string(),
+ weight: 1,
+ endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData {
+ public_key: PublicKey::from_base64("eaNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=").unwrap(),
+ daita: false,
+ shadowsocks_extra_addr_in: SHADOWSOCKS_RELAY_EXTRA_ADDRS.to_vec(),
+ }),
+ location: None,
+});
+const SHADOWSOCKS_RELAY_IPV4: Ipv4Addr = Ipv4Addr::new(123, 123, 123, 1);
+const SHADOWSOCKS_RELAY_IPV6: Ipv6Addr = Ipv6Addr::new(0x123, 0, 0, 0, 0, 0, 0, 2);
+const SHADOWSOCKS_RELAY_EXTRA_ADDRS: &[IpAddr; 2] = &[
+ IpAddr::V4(Ipv4Addr::new(123, 123, 123, 2)),
+ IpAddr::V6(Ipv6Addr::new(0x123, 0, 0, 0, 0, 0, 0, 2)),
+];
+static SHADOWSOCKS_RELAY_LOCATION: Lazy<GeographicLocationConstraint> =
+ Lazy::new(|| GeographicLocationConstraint::hostname("se", "got", "se1337-wireguard"));
+
// Helper functions
fn unwrap_relay(get_result: GetRelay) -> Relay {
match get_result {
@@ -314,7 +350,8 @@ fn test_retry_order() {
assert!(match &query.wireguard_constraints.obfuscation {
ObfuscationQuery::Auto => true,
ObfuscationQuery::Off => obfuscator.is_none(),
- ObfuscationQuery::Udp2tcp { .. } => obfuscator.is_some(),
+ ObfuscationQuery::Udp2tcp(_) | ObfuscationQuery::Shadowsocks(_) =>
+ obfuscator.is_some(),
});
}
GetRelay::OpenVpn {
@@ -437,6 +474,7 @@ fn test_wireguard_entry() {
)
.unwrap(),
daita: false,
+ shadowsocks_extra_addr_in: vec![],
}),
location: None,
},
@@ -455,6 +493,7 @@ fn test_wireguard_entry() {
)
.unwrap(),
daita: false,
+ shadowsocks_extra_addr_in: vec![],
}),
location: None,
},
@@ -467,15 +506,16 @@ fn test_wireguard_entry() {
},
wireguard: WireguardEndpointData {
port_ranges: vec![
- (53, 53),
- (443, 443),
- (4000, 33433),
- (33565, 51820),
- (52000, 60000),
+ 53..=53,
+ 443..=443,
+ 4000..=33433,
+ 33565..=51820,
+ 52000..=60000,
],
ipv4_gateway: "10.64.0.1".parse().unwrap(),
ipv6_gateway: "fc00:bbbb:bbbb:bb01::1".parse().unwrap(),
udp2tcp_ports: vec![],
+ shadowsocks_port_ranges: vec![100..=200, 1000..=2000],
},
};
@@ -706,6 +746,140 @@ fn test_selecting_any_relay_will_consider_multihop() {
}
}
+/// Test whether Shadowsocks is always selected as the obfuscation protocol when Shadowsocks is selected.
+#[test]
+fn test_selecting_wireguard_over_shadowsocks() {
+ let relay_selector = RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone());
+
+ let mut query = RelayQueryBuilder::new().wireguard().shadowsocks().build();
+ query.wireguard_constraints.use_multihop = Constraint::Only(false);
+
+ let relay = relay_selector.get_relay_by_query(query).unwrap();
+ match relay {
+ GetRelay::Wireguard {
+ obfuscator,
+ inner: WireguardConfig::Singlehop { .. },
+ ..
+ } => {
+ assert!(obfuscator.is_some_and(|obfuscator| matches!(
+ obfuscator.config,
+ ObfuscatorConfig::Shadowsocks { .. }
+ )))
+ }
+ wrong_relay => panic!(
+ "Relay selector should have picked a Wireguard relay with Shadowsocks, instead chose {wrong_relay:?}"
+ ),
+ }
+}
+
+/// Test whether extra Shadowsocks IPs are selected when available
+#[test]
+fn test_selecting_wireguard_over_shadowsocks_extra_ips() {
+ let relay_selector = RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone());
+
+ let mut query = RelayQueryBuilder::new().wireguard().shadowsocks().build();
+ query.wireguard_constraints.use_multihop = Constraint::Only(false);
+ query.location = Constraint::Only(LocationConstraint::Location(
+ SHADOWSOCKS_RELAY_LOCATION.clone(),
+ ));
+
+ let relay = relay_selector.get_relay_by_query(query).unwrap();
+ match relay {
+ GetRelay::Wireguard {
+ obfuscator: Some(SelectedObfuscator { config: ObfuscatorConfig::Shadowsocks { endpoint }, .. }),
+ inner: WireguardConfig::Singlehop { .. },
+ ..
+ } => {
+ assert!(SHADOWSOCKS_RELAY_EXTRA_ADDRS.contains(&endpoint.ip()), "{} is not an additional IP", endpoint);
+ }
+ wrong_relay => panic!(
+ "Relay selector should have picked a Wireguard relay with Shadowsocks, instead chose {wrong_relay:?}"
+ ),
+ }
+}
+
+/// Ignore extra IPv4 addresses when overrides are set
+#[test]
+fn test_selecting_wireguard_ignore_extra_ips_override_v4() {
+ const OVERRIDE_IPV4: Ipv4Addr = Ipv4Addr::new(1, 3, 3, 7);
+
+ let config = mullvad_relay_selector::SelectorConfig {
+ relay_overrides: vec![RelayOverride {
+ hostname: SHADOWSOCKS_RELAY_LOCATION
+ .get_hostname()
+ .unwrap()
+ .to_string(),
+ ipv4_addr_in: Some(OVERRIDE_IPV4),
+ ipv6_addr_in: None,
+ }],
+ ..Default::default()
+ };
+
+ let relay_selector = RelaySelector::from_list(config, RELAYS.clone());
+
+ let mut query_v4 = RelayQueryBuilder::new().wireguard().shadowsocks().build();
+ query_v4.wireguard_constraints.use_multihop = Constraint::Only(false);
+ query_v4.location = Constraint::Only(LocationConstraint::Location(
+ SHADOWSOCKS_RELAY_LOCATION.clone(),
+ ));
+ query_v4.wireguard_constraints.ip_version = Constraint::Only(IpVersion::V4);
+
+ let relay = relay_selector.get_relay_by_query(query_v4).unwrap();
+ match relay {
+ GetRelay::Wireguard {
+ obfuscator: Some(SelectedObfuscator { config: ObfuscatorConfig::Shadowsocks { endpoint }, .. }),
+ inner: WireguardConfig::Singlehop { .. },
+ ..
+ } => {
+ assert_eq!(endpoint.ip(), IpAddr::from(OVERRIDE_IPV4));
+ }
+ wrong_relay => panic!(
+ "Relay selector should have picked a Wireguard relay with Shadowsocks, instead chose {wrong_relay:?}"
+ ),
+ }
+}
+
+/// Ignore extra IPv6 addresses when overrides are set
+#[test]
+fn test_selecting_wireguard_ignore_extra_ips_override_v6() {
+ const OVERRIDE_IPV6: Ipv6Addr = Ipv6Addr::new(1, 0, 0, 0, 0, 0, 10, 10);
+
+ let config = SelectorConfig {
+ relay_overrides: vec![RelayOverride {
+ hostname: SHADOWSOCKS_RELAY_LOCATION
+ .get_hostname()
+ .unwrap()
+ .to_string(),
+ ipv4_addr_in: None,
+ ipv6_addr_in: Some(OVERRIDE_IPV6),
+ }],
+ ..Default::default()
+ };
+
+ let relay_selector = RelaySelector::from_list(config, RELAYS.clone());
+
+ let mut query_v6 = RelayQueryBuilder::new().wireguard().shadowsocks().build();
+ query_v6.wireguard_constraints.use_multihop = Constraint::Only(false);
+ query_v6.location = Constraint::Only(LocationConstraint::Location(
+ SHADOWSOCKS_RELAY_LOCATION.clone(),
+ ));
+ query_v6.wireguard_constraints.ip_version = Constraint::Only(IpVersion::V6);
+
+ let relay = relay_selector.get_relay_by_query(query_v6).unwrap();
+ match relay {
+ GetRelay::Wireguard {
+ obfuscator: Some(SelectedObfuscator { config: ObfuscatorConfig::Shadowsocks { endpoint }, .. }),
+ inner: WireguardConfig::Singlehop { .. },
+ ..
+ } => {
+ assert_eq!(endpoint.ip(), IpAddr::from(OVERRIDE_IPV6));
+ }
+ wrong_relay => panic!(
+ "Relay selector should have picked a Wireguard relay with Shadowsocks, instead chose {wrong_relay:?}"
+ ),
+ }
+}
+
/// Construct a query for a Wireguard configuration where UDP2TCP obfuscation is selected and
/// multihop is explicitly turned off. Assert that the relay selector always return an obfuscator
/// configuration.
@@ -733,7 +907,7 @@ fn test_selecting_wireguard_endpoint_with_udp2tcp_obfuscation() {
}
}
-/// Construct a query for a Wireguard configuration where UDP2TCP obfuscation is set to "Auto" and
+/// Construct a query for a Wireguard configuration where obfuscation is set to "Auto" and
/// multihop is explicitly turned off. Assert that the relay selector does *not* return an
/// obfuscator config.
///
@@ -778,10 +952,10 @@ fn test_selected_wireguard_endpoints_use_correct_port_ranges() {
let Some(obfuscator) = obfuscator else {
panic!("Relay selector should have picked an obfuscator")
};
- assert!(match obfuscator.config {
- ObfuscatorConfig::Udp2Tcp { endpoint } =>
+ assert!(matches!(obfuscator.config,
+ ObfuscatorConfig::Udp2Tcp { endpoint } if
TCP2UDP_PORTS.contains(&endpoint.port()),
- })
+ ))
}
wrong_relay => panic!(
"Relay selector should have picked a Wireguard relay, instead chose {wrong_relay:?}"
@@ -954,6 +1128,7 @@ fn test_include_in_country() {
"BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=",
)
.unwrap(),
+ shadowsocks_extra_addr_in: vec![],
daita: false,
}),
location: None,
@@ -972,6 +1147,7 @@ fn test_include_in_country() {
"BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=",
)
.unwrap(),
+ shadowsocks_extra_addr_in: vec![],
daita: false,
}),
location: None,
@@ -999,10 +1175,11 @@ fn test_include_in_country() {
shadowsocks: vec![],
},
wireguard: WireguardEndpointData {
- port_ranges: vec![(53, 53), (4000, 33433), (33565, 51820), (52000, 60000)],
+ port_ranges: vec![53..=53, 4000..=33433, 33565..=51820, 52000..=60000],
ipv4_gateway: "10.64.0.1".parse().unwrap(),
ipv6_gateway: "fc00:bbbb:bbbb:bb01::1".parse().unwrap(),
udp2tcp_ports: vec![],
+ shadowsocks_port_ranges: vec![],
},
};
@@ -1172,6 +1349,7 @@ fn test_daita() {
"BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=",
)
.unwrap(),
+ shadowsocks_extra_addr_in: vec![],
daita: false,
}),
location: None,
@@ -1190,6 +1368,7 @@ fn test_daita() {
"BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=",
)
.unwrap(),
+ shadowsocks_extra_addr_in: vec![],
daita: true,
}),
location: None,
@@ -1202,9 +1381,10 @@ fn test_daita() {
shadowsocks: vec![],
},
wireguard: WireguardEndpointData {
- port_ranges: vec![(53, 53), (4000, 33433), (33565, 51820), (52000, 60000)],
+ port_ranges: vec![53..=53, 4000..=33433, 33565..=51820, 52000..=60000],
ipv4_gateway: "10.64.0.1".parse().unwrap(),
ipv6_gateway: "fc00:bbbb:bbbb:bb01::1".parse().unwrap(),
+ shadowsocks_port_ranges: vec![],
udp2tcp_ports: vec![],
},
};
diff --git a/mullvad-types/src/constraints/constraint.rs b/mullvad-types/src/constraints/constraint.rs
index 2554a97d25..35b83e1320 100644
--- a/mullvad-types/src/constraints/constraint.rs
+++ b/mullvad-types/src/constraints/constraint.rs
@@ -56,6 +56,13 @@ impl<T> Constraint<T> {
}
}
+ pub fn unwrap_or_else<F: FnOnce() -> T>(self, or_else: F) -> T {
+ match self {
+ Constraint::Only(value) => value,
+ Constraint::Any => or_else(),
+ }
+ }
+
pub fn or(self, other: Constraint<T>) -> Constraint<T> {
match self {
Constraint::Any => other,
diff --git a/mullvad-types/src/features.rs b/mullvad-types/src/features.rs
index 9a6b7c7e64..30455bd0bc 100644
--- a/mullvad-types/src/features.rs
+++ b/mullvad-types/src/features.rs
@@ -31,6 +31,7 @@ pub enum FeatureIndicator {
SplitTunneling,
LockdownMode,
Udp2Tcp,
+ Shadowsocks,
LanSharing,
DnsContentBlockers,
CustomDns,
@@ -49,6 +50,7 @@ impl std::fmt::Display for FeatureIndicator {
FeatureIndicator::SplitTunneling => "Split Tunneling",
FeatureIndicator::LockdownMode => "Lockdown Mode",
FeatureIndicator::Udp2Tcp => "Udp2Tcp",
+ FeatureIndicator::Shadowsocks => "Shadowsocks",
FeatureIndicator::LanSharing => "LAN Sharing",
FeatureIndicator::DnsContentBlockers => "Dns Content Blocker",
FeatureIndicator::CustomDns => "Custom Dns",
diff --git a/mullvad-types/src/relay_constraints.rs b/mullvad-types/src/relay_constraints.rs
index 37d4708602..b348727685 100644
--- a/mullvad-types/src/relay_constraints.rs
+++ b/mullvad-types/src/relay_constraints.rs
@@ -5,7 +5,7 @@ use crate::{
constraints::{Constraint, Match},
custom_list::{CustomListsSettings, Id},
location::{CityCode, CountryCode, Hostname},
- relay_list::Relay,
+ relay_list::{Relay, RelayEndpointData},
CustomTunnelEndpoint, Intersection,
};
use serde::{Deserialize, Serialize};
@@ -198,6 +198,13 @@ impl GeographicLocationConstraint {
pub fn is_country(&self) -> bool {
matches!(self, GeographicLocationConstraint::Country(_))
}
+
+ pub fn get_hostname(&self) -> Option<&Hostname> {
+ match self {
+ GeographicLocationConstraint::Hostname(_, _, hostname) => Some(hostname),
+ _ => None,
+ }
+ }
}
impl Match<Relay> for GeographicLocationConstraint {
@@ -477,6 +484,7 @@ pub enum SelectedObfuscation {
Off,
#[cfg_attr(feature = "clap", clap(name = "udp2tcp"))]
Udp2Tcp,
+ Shadowsocks,
}
impl Intersection for SelectedObfuscation {
@@ -500,6 +508,7 @@ impl fmt::Display for SelectedObfuscation {
SelectedObfuscation::Auto => "auto".fmt(f),
SelectedObfuscation::Off => "off".fmt(f),
SelectedObfuscation::Udp2Tcp => "udp2tcp".fmt(f),
+ SelectedObfuscation::Shadowsocks => "shadowsocks".fmt(f),
}
}
}
@@ -519,6 +528,21 @@ impl fmt::Display for Udp2TcpObfuscationSettings {
}
}
+#[derive(Default, Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Intersection)]
+#[serde(rename_all = "snake_case")]
+pub struct ShadowsocksSettings {
+ pub port: Constraint<u16>,
+}
+
+impl fmt::Display for ShadowsocksSettings {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self.port {
+ Constraint::Any => write!(f, "any port"),
+ Constraint::Only(port) => write!(f, "port {port}"),
+ }
+ }
+}
+
/// Contains obfuscation settings
#[derive(Default, Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
@@ -526,6 +550,7 @@ impl fmt::Display for Udp2TcpObfuscationSettings {
pub struct ObfuscationSettings {
pub selected_obfuscation: SelectedObfuscation,
pub udp2tcp: Udp2TcpObfuscationSettings,
+ pub shadowsocks: ShadowsocksSettings,
}
/// Limits the set of bridge servers to use in `mullvad-daemon`.
@@ -642,5 +667,16 @@ impl RelayOverride {
);
relay.ipv6_addr_in = Some(ipv6_addr_in);
}
+
+ // Additional IPs should be ignored when overrides are present
+ if let RelayEndpointData::Wireguard(data) = &mut relay.endpoint_data {
+ data.shadowsocks_extra_addr_in.retain(|addr| {
+ let not_overridden_v4 = self.ipv4_addr_in.is_none() && addr.is_ipv4();
+ let not_overridden_v6 = self.ipv6_addr_in.is_none() && addr.is_ipv6();
+
+ // Keep address if it's not overridden
+ not_overridden_v4 || not_overridden_v6
+ });
+ }
}
}
diff --git a/mullvad-types/src/relay_list.rs b/mullvad-types/src/relay_list.rs
index 054752d592..173b50b4f9 100644
--- a/mullvad-types/src/relay_list.rs
+++ b/mullvad-types/src/relay_list.rs
@@ -1,6 +1,9 @@
use crate::location::{CityCode, CountryCode, Location};
use serde::{Deserialize, Serialize};
-use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
+use std::{
+ net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
+ ops::RangeInclusive,
+};
use talpid_types::net::{
proxy::{CustomProxy, Shadowsocks},
wireguard, TransportProtocol,
@@ -111,6 +114,7 @@ impl PartialEq for Relay {
/// # )
/// # .unwrap(),
/// # daita: false,
+ /// # shadowsocks_extra_addr_in: vec![],
/// # }),
/// # location: None,
/// };
@@ -168,10 +172,12 @@ pub struct OpenVpnEndpoint {
#[serde(rename_all = "snake_case")]
pub struct WireguardEndpointData {
/// Port to connect to
- pub port_ranges: Vec<(u16, u16)>,
+ pub port_ranges: Vec<RangeInclusive<u16>>,
/// Gateways to be used with the tunnel
pub ipv4_gateway: Ipv4Addr,
pub ipv6_gateway: Ipv6Addr,
+ /// Shadowsocks port ranges available on all WireGuard relays
+ pub shadowsocks_port_ranges: Vec<RangeInclusive<u16>>,
pub udp2tcp_ports: Vec<u16>,
}
@@ -181,6 +187,7 @@ impl Default for WireguardEndpointData {
port_ranges: vec![],
ipv4_gateway: "0.0.0.0".parse().unwrap(),
ipv6_gateway: "::".parse().unwrap(),
+ shadowsocks_port_ranges: vec![],
udp2tcp_ports: vec![],
}
}
@@ -194,6 +201,9 @@ pub struct WireguardRelayEndpointData {
/// Whether the server supports DAITA
#[serde(default)]
pub daita: bool,
+ /// Optional IP addresses used by Shadowsocks
+ #[serde(default)]
+ pub shadowsocks_extra_addr_in: Vec<IpAddr>,
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
diff --git a/talpid-types/src/net/mod.rs b/talpid-types/src/net/mod.rs
index 485f1d779f..5875f33482 100644
--- a/talpid-types/src/net/mod.rs
+++ b/talpid-types/src/net/mod.rs
@@ -119,6 +119,10 @@ impl TunnelParameters {
address: *endpoint,
protocol: TransportProtocol::Tcp,
},
+ ObfuscatorConfig::Shadowsocks { endpoint } => Endpoint {
+ address: *endpoint,
+ protocol: TransportProtocol::Udp,
+ },
}
}
@@ -250,12 +254,14 @@ impl fmt::Display for TunnelEndpoint {
pub enum ObfuscationType {
#[serde(rename = "udp2tcp")]
Udp2Tcp,
+ Shadowsocks,
}
impl fmt::Display for ObfuscationType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
ObfuscationType::Udp2Tcp => "Udp2Tcp".fmt(f),
+ ObfuscationType::Shadowsocks => "Shadowsocks".fmt(f),
}
}
}
@@ -277,6 +283,13 @@ impl From<&ObfuscatorConfig> for ObfuscationEndpoint {
},
ObfuscationType::Udp2Tcp,
),
+ ObfuscatorConfig::Shadowsocks { endpoint } => (
+ Endpoint {
+ address: *endpoint,
+ protocol: TransportProtocol::Udp,
+ },
+ ObfuscationType::Shadowsocks,
+ ),
};
ObfuscationEndpoint {
@@ -461,6 +474,15 @@ pub enum IpVersion {
V6,
}
+impl From<IpAddr> for IpVersion {
+ fn from(value: IpAddr) -> Self {
+ match value {
+ IpAddr::V4(_) => IpVersion::V4,
+ IpAddr::V6(_) => IpVersion::V6,
+ }
+ }
+}
+
impl fmt::Display for IpVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match *self {
diff --git a/talpid-types/src/net/obfuscation.rs b/talpid-types/src/net/obfuscation.rs
index a39e9bf919..ddd93a4a39 100644
--- a/talpid-types/src/net/obfuscation.rs
+++ b/talpid-types/src/net/obfuscation.rs
@@ -4,4 +4,5 @@ use std::net::SocketAddr;
#[derive(Clone, Eq, PartialEq, Deserialize, Serialize, Debug)]
pub enum ObfuscatorConfig {
Udp2Tcp { endpoint: SocketAddr },
+ Shadowsocks { endpoint: SocketAddr },
}
diff --git a/talpid-wireguard/src/lib.rs b/talpid-wireguard/src/lib.rs
index 6cc9e0be0d..b9a85560ee 100644
--- a/talpid-wireguard/src/lib.rs
+++ b/talpid-wireguard/src/lib.rs
@@ -5,7 +5,7 @@
use self::config::Config;
#[cfg(windows)]
use futures::channel::mpsc;
-use futures::future::{abortable, AbortHandle as FutureAbortHandle, BoxFuture, Future};
+use futures::future::{BoxFuture, Future};
#[cfg(target_os = "linux")]
use once_cell::sync::Lazy;
#[cfg(target_os = "android")]
@@ -16,7 +16,7 @@ use std::env;
use std::io;
use std::{
convert::Infallible,
- net::IpAddr,
+ net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
path::Path,
pin::Pin,
sync::{mpsc as sync_mpsc, Arc, Mutex},
@@ -39,7 +39,8 @@ use talpid_types::{
};
use tokio::sync::Mutex as AsyncMutex;
use tunnel_obfuscation::{
- create_obfuscator, Error as ObfuscationError, Settings as ObfuscationSettings, Udp2TcpSettings,
+ create_obfuscator, shadowsocks, udp2tcp, Error as ObfuscationError,
+ Settings as ObfuscationSettings,
};
/// WireGuard config data-types
@@ -158,18 +159,18 @@ const PSK_EXCHANGE_TIMEOUT_MULTIPLIER: u32 = 2;
/// Simple wrapper that automatically cancels the future which runs an obfuscator.
struct ObfuscatorHandle {
- abort_handle: FutureAbortHandle,
+ obfuscation_task: tokio::task::JoinHandle<()>,
#[cfg(target_os = "android")]
remote_socket_fd: std::os::unix::io::RawFd,
}
impl ObfuscatorHandle {
pub fn new(
- abort_handle: FutureAbortHandle,
+ obfuscation_task: tokio::task::JoinHandle<()>,
#[cfg(target_os = "android")] remote_socket_fd: std::os::unix::io::RawFd,
) -> Self {
Self {
- abort_handle,
+ obfuscation_task,
#[cfg(target_os = "android")]
remote_socket_fd,
}
@@ -181,13 +182,13 @@ impl ObfuscatorHandle {
}
pub fn abort(&self) {
- self.abort_handle.abort();
+ self.obfuscation_task.abort();
}
}
impl Drop for ObfuscatorHandle {
fn drop(&mut self) {
- self.abort_handle.abort();
+ self.obfuscation_task.abort();
}
}
@@ -204,48 +205,63 @@ async fn maybe_create_obfuscator(
close_msg_sender: sync_mpsc::Sender<CloseMsg>,
) -> Result<Option<ObfuscatorHandle>> {
if let Some(ref obfuscator_config) = config.obfuscator_config {
- match obfuscator_config {
+ let settings = match obfuscator_config {
ObfuscatorConfig::Udp2Tcp { endpoint } => {
- log::trace!("Connecting to Udp2Tcp endpoint {:?}", *endpoint);
- let settings = Udp2TcpSettings {
+ ObfuscationSettings::Udp2Tcp(udp2tcp::Settings {
peer: *endpoint,
#[cfg(target_os = "linux")]
fwmark: config.fwmark,
- };
- let obfuscator = create_obfuscator(&ObfuscationSettings::Udp2Tcp(settings))
- .await
- .map_err(Error::CreateObfuscatorError)?;
- let endpoint = obfuscator.endpoint();
+ })
+ }
+ ObfuscatorConfig::Shadowsocks { endpoint } => {
+ ObfuscationSettings::Shadowsocks(shadowsocks::Settings {
+ shadowsocks_endpoint: *endpoint,
+ // TODO: Temporary since we may different entry IPs later?
+ wireguard_endpoint: if config.entry_peer.endpoint.is_ipv4() {
+ SocketAddr::from((Ipv4Addr::LOCALHOST, 51820))
+ } else {
+ SocketAddr::from((Ipv6Addr::LOCALHOST, 51820))
+ },
+ //wireguard_endpoint: config.entry_peer.endpoint,
+ #[cfg(target_os = "linux")]
+ fwmark: config.fwmark,
+ })
+ }
+ };
- log::trace!("Patching first WireGuard peer to become {:?}", endpoint);
- config.entry_peer.endpoint = endpoint;
+ log::trace!("Obfuscation settings: {settings:?}");
- #[cfg(target_os = "android")]
- let remote_socket_fd = obfuscator.remote_socket_fd();
+ let obfuscator = create_obfuscator(&settings)
+ .await
+ .map_err(Error::CreateObfuscatorError)?;
+ let endpoint = obfuscator.endpoint();
- let (runner, abort_handle) = abortable(async move {
- match obfuscator.run().await {
- Ok(_) => {
- let _ = close_msg_sender.send(CloseMsg::ObfuscatorExpired);
- }
- Err(error) => {
- log::error!(
- "{}",
- error.display_chain_with_msg("Obfuscation controller failed")
- );
- let _ = close_msg_sender
- .send(CloseMsg::ObfuscatorFailed(Error::ObfuscatorError(error)));
- }
- }
- });
- tokio::spawn(runner);
- return Ok(Some(ObfuscatorHandle::new(
- abort_handle,
- #[cfg(target_os = "android")]
- remote_socket_fd,
- )));
+ log::trace!("Patching first WireGuard peer to become {endpoint}");
+ config.entry_peer.endpoint = endpoint;
+
+ #[cfg(target_os = "android")]
+ let remote_socket_fd = obfuscator.remote_socket_fd();
+
+ let obfuscation_task = tokio::spawn(async move {
+ match obfuscator.run().await {
+ Ok(_) => {
+ let _ = close_msg_sender.send(CloseMsg::ObfuscatorExpired);
+ }
+ Err(error) => {
+ log::error!(
+ "{}",
+ error.display_chain_with_msg("Obfuscation controller failed")
+ );
+ let _ = close_msg_sender
+ .send(CloseMsg::ObfuscatorFailed(Error::ObfuscatorError(error)));
+ }
}
- }
+ });
+ return Ok(Some(ObfuscatorHandle::new(
+ obfuscation_task,
+ #[cfg(target_os = "android")]
+ remote_socket_fd,
+ )));
}
Ok(None)
}
diff --git a/test/test-manager/src/tests/tunnel.rs b/test/test-manager/src/tests/tunnel.rs
index 540f02802e..c671f28966 100644
--- a/test/test-manager/src/tests/tunnel.rs
+++ b/test/test-manager/src/tests/tunnel.rs
@@ -151,6 +151,7 @@ pub async fn test_udp2tcp_tunnel(
udp2tcp: Udp2TcpObfuscationSettings {
port: Constraint::Any,
},
+ ..Default::default()
})
.await
.expect("failed to enable udp2tcp");
@@ -578,6 +579,7 @@ pub async fn test_quantum_resistant_multihop_udp2tcp_tunnel(
udp2tcp: Udp2TcpObfuscationSettings {
port: Constraint::Any,
},
+ ..Default::default()
})
.await
.expect("Failed to enable obfuscation");
diff --git a/tunnel-obfuscation/Cargo.toml b/tunnel-obfuscation/Cargo.toml
index 25c6951352..b5f71fdb62 100644
--- a/tunnel-obfuscation/Cargo.toml
+++ b/tunnel-obfuscation/Cargo.toml
@@ -11,7 +11,9 @@ rust-version.workspace = true
workspace = true
[dependencies]
+log = { workspace = true }
async-trait = "0.1"
thiserror = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "net", "io-util"] }
udp-over-tcp = { git = "https://github.com/mullvad/udp-over-tcp", rev = "87936ac29b68b902565955f138ab02294bcc8593" }
+shadowsocks = { workspace = true }
diff --git a/tunnel-obfuscation/src/lib.rs b/tunnel-obfuscation/src/lib.rs
index d1a0fa46e0..62528ed609 100644
--- a/tunnel-obfuscation/src/lib.rs
+++ b/tunnel-obfuscation/src/lib.rs
@@ -1,8 +1,8 @@
use async_trait::async_trait;
use std::net::SocketAddr;
-mod udp2tcp;
-pub use udp2tcp::Udp2TcpSettings;
+pub mod shadowsocks;
+pub mod udp2tcp;
pub type Result<T> = std::result::Result<T, Error>;
@@ -13,6 +13,12 @@ pub enum Error {
#[error("Failed to run Udp2Tcp obfuscator")]
RunUdp2TcpObfuscator(#[source] udp2tcp::Error),
+
+ #[error("Failed to initialize Shadowsocks")]
+ CreateShadowsocksObfuscator(#[source] shadowsocks::Error),
+
+ #[error("Failed to run Shadowsocks")]
+ RunShadowsocksObfuscator(#[source] shadowsocks::Error),
}
#[async_trait]
@@ -27,14 +33,25 @@ pub trait Obfuscator: Send {
fn remote_socket_fd(&self) -> std::os::unix::io::RawFd;
}
+#[derive(Debug)]
pub enum Settings {
- Udp2Tcp(Udp2TcpSettings),
+ Udp2Tcp(udp2tcp::Settings),
+ Shadowsocks(shadowsocks::Settings),
}
pub async fn create_obfuscator(settings: &Settings) -> Result<Box<dyn Obfuscator>> {
match settings {
- Settings::Udp2Tcp(s) => udp2tcp::create_obfuscator(s)
+ Settings::Udp2Tcp(s) => udp2tcp::Udp2Tcp::new(s)
.await
+ .map(box_obfuscator)
.map_err(Error::CreateUdp2TcpObfuscator),
+ Settings::Shadowsocks(s) => shadowsocks::Shadowsocks::new(s)
+ .await
+ .map(box_obfuscator)
+ .map_err(Error::CreateShadowsocksObfuscator),
}
}
+
+fn box_obfuscator(obfs: impl Obfuscator + 'static) -> Box<dyn Obfuscator> {
+ Box::new(obfs) as Box<dyn Obfuscator>
+}
diff --git a/tunnel-obfuscation/src/main.rs b/tunnel-obfuscation/src/main.rs
index 50235ae7e8..d08329e0a1 100644
--- a/tunnel-obfuscation/src/main.rs
+++ b/tunnel-obfuscation/src/main.rs
@@ -1,5 +1,5 @@
use std::{env::args, net::SocketAddr};
-use tunnel_obfuscation::{create_obfuscator, Obfuscator, Settings, Udp2TcpSettings};
+use tunnel_obfuscation::{create_obfuscator, udp2tcp, Obfuscator, Settings};
#[tokio::main]
async fn main() {
@@ -19,7 +19,7 @@ async fn main() {
async fn instantiate_requested(obfuscator_type: &str) -> Box<dyn Obfuscator> {
match obfuscator_type {
"udp2tcp" => {
- let settings = Udp2TcpSettings {
+ let settings = udp2tcp::Settings {
peer: SocketAddr::new("127.0.0.1".parse().unwrap(), 3030),
#[cfg(target_os = "linux")]
fwmark: Some(1337),
diff --git a/tunnel-obfuscation/src/shadowsocks.rs b/tunnel-obfuscation/src/shadowsocks.rs
new file mode 100644
index 0000000000..0f7492a3ca
--- /dev/null
+++ b/tunnel-obfuscation/src/shadowsocks.rs
@@ -0,0 +1,256 @@
+use super::Obfuscator;
+use async_trait::async_trait;
+use shadowsocks::{
+ config::{ServerConfig, ServerType},
+ context::Context,
+ crypto::CipherKind,
+ net::ConnectOpts,
+ relay::{udprelay::proxy_socket::ProxySocketError, Address},
+ ProxySocket,
+};
+use std::{io, net::SocketAddr, sync::Arc};
+use tokio::{net::UdpSocket, sync::oneshot};
+
+const SHADOWSOCKS_CIPHER: CipherKind = CipherKind::AES_256_GCM;
+const SHADOWSOCKS_PASSWORD: &str = "mullvad";
+
+type Result<T> = std::result::Result<T, Error>;
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ /// Failed to bind local UDP socket
+ #[error("Failed to bind UDP socket")]
+ BindUdp(#[source] io::Error),
+ /// Missing UDP listener address
+ #[error("Failed to retrieve UDP socket bind address")]
+ GetUdpLocalAddress(#[source] io::Error),
+ /// Failed to wait for UDP client
+ #[error("Failed to wait for UDP client")]
+ WaitForUdpClient(#[source] io::Error),
+ /// Failed to create UDP stream
+ #[error("Failed to create UDP stream")]
+ CreateUdpStream(#[source] io::Error),
+ /// Failed to connect to Shadowsocks endpoint
+ #[error("Failed to connect to Shadowsocks endpoint")]
+ ConnectShadowsocks(#[from] ProxySocketError),
+}
+
+pub struct Shadowsocks {
+ udp_client_addr: SocketAddr,
+ server: tokio::task::JoinHandle<Result<()>>,
+ // The receiver will implicitly shut down when this is dropped
+ _shutdown_tx: oneshot::Sender<()>,
+}
+
+#[derive(Debug)]
+pub struct Settings {
+ /// Remote Shadowsocks endpoint
+ pub shadowsocks_endpoint: SocketAddr,
+ /// Remote WireGuard endpoint
+ pub wireguard_endpoint: SocketAddr,
+ #[cfg(target_os = "linux")]
+ pub fwmark: Option<u32>,
+}
+
+impl Shadowsocks {
+ pub(crate) async fn new(settings: &Settings) -> Result<Self> {
+ let (local_udp_socket, udp_client_addr) =
+ create_local_udp_socket(settings.shadowsocks_endpoint.is_ipv4()).await?;
+
+ let (shutdown_tx, shutdown_rx) = oneshot::channel();
+
+ let server = tokio::spawn(run_obfuscation(
+ local_udp_socket,
+ settings.shadowsocks_endpoint,
+ settings.wireguard_endpoint,
+ shutdown_rx,
+ #[cfg(target_os = "linux")]
+ settings.fwmark,
+ ));
+
+ Ok(Shadowsocks {
+ udp_client_addr,
+ server,
+ _shutdown_tx: shutdown_tx,
+ })
+ }
+}
+
+async fn run_obfuscation(
+ local_udp_socket: UdpSocket,
+ shadowsocks_endpoint: SocketAddr,
+ wireguard_endpoint: SocketAddr,
+ shutdown_rx: oneshot::Receiver<()>,
+ #[cfg(target_os = "linux")] fwmark: Option<u32>,
+) -> Result<()> {
+ wait_for_local_udp_client(&local_udp_socket)
+ .await
+ .map_err(Error::WaitForUdpClient)?;
+
+ let shadowsocks = create_shadowsocks_client(
+ shadowsocks_endpoint,
+ #[cfg(target_os = "linux")]
+ fwmark,
+ )
+ .await?;
+
+ let local_udp = Arc::new(local_udp_socket);
+ let shadowsocks = Arc::new(shadowsocks);
+
+ let wg_addr = Address::SocketAddress(wireguard_endpoint);
+
+ let mut client = tokio::spawn(handle_outgoing(
+ shadowsocks.clone(),
+ local_udp.clone(),
+ wg_addr.clone(),
+ ));
+ let mut server = tokio::spawn(handle_incoming(shadowsocks, local_udp, wg_addr));
+
+ tokio::select! {
+ _ = shutdown_rx => {
+ log::trace!("Stopping shadowsocks obfuscation");
+ }
+ _result = &mut server => log::trace!("Shadowsocks client closed"),
+ _result = &mut client => log::trace!("Local UDP client closed"),
+ }
+
+ client.abort();
+ let _ = client.await;
+ server.abort();
+ let _ = server.await;
+
+ Ok(())
+}
+
+async fn create_shadowsocks_client(
+ shadowsocks_endpoint: SocketAddr,
+ #[cfg(target_os = "linux")] fwmark: Option<u32>,
+) -> std::result::Result<ProxySocket, ProxySocketError> {
+ let ss_context = Context::new_shared(ServerType::Local);
+ let ss_config: ServerConfig = ServerConfig::new(
+ shadowsocks_endpoint,
+ SHADOWSOCKS_PASSWORD,
+ SHADOWSOCKS_CIPHER,
+ );
+ let connect_opts = ConnectOpts {
+ #[cfg(target_os = "linux")]
+ fwmark,
+ ..Default::default()
+ };
+ ProxySocket::connect_with_opts(ss_context, &ss_config, &connect_opts).await
+}
+
+async fn create_local_udp_socket(ipv4: bool) -> Result<(UdpSocket, SocketAddr)> {
+ let random_bind_addr = if ipv4 {
+ SocketAddr::new("127.0.0.1".parse().unwrap(), 0)
+ } else {
+ SocketAddr::new("::1".parse().unwrap(), 0)
+ };
+ let local_udp_socket = UdpSocket::bind(random_bind_addr)
+ .await
+ .map_err(Error::BindUdp)?;
+ let udp_client_addr = local_udp_socket
+ .local_addr()
+ .map_err(Error::GetUdpLocalAddress)?;
+
+ Ok((local_udp_socket, udp_client_addr))
+}
+
+/// Wait for a client to connect to `udp_listener` and connect the socket to that address
+async fn wait_for_local_udp_client(udp_listener: &UdpSocket) -> io::Result<()> {
+ log::trace!("Waiting for UDP socket client");
+ let client_addr = udp_listener.peek_sender().await?;
+
+ log::trace!("UDP connection from {client_addr}");
+ udp_listener.connect(client_addr).await
+}
+
+async fn handle_outgoing(
+ ss_write: Arc<ProxySocket>,
+ local_udp_read: Arc<UdpSocket>,
+ wg_addr: Address,
+) {
+ let mut rx_buffer = vec![0u8; u16::MAX as usize];
+
+ loop {
+ let read_n = match local_udp_read.recv(&mut rx_buffer).await {
+ Ok(read_n) => read_n,
+ Err(error) => {
+ log::error!("Failed to read from local UDP socket: {error}");
+ break;
+ }
+ };
+
+ if let Err(error) = ss_write.send(&wg_addr, &rx_buffer[0..read_n]).await {
+ log::error!("Failed to write to Shadowsocks client: {error}");
+ if is_fatal_socket_error(&error) {
+ break;
+ }
+ }
+ }
+}
+
+async fn handle_incoming(
+ ss_read: Arc<ProxySocket>,
+ local_udp_write: Arc<UdpSocket>,
+ wg_addr: Address,
+) {
+ let mut rx_buffer = vec![0u8; u16::MAX as usize];
+
+ loop {
+ let (read_n, addr, _ctrl) = match ss_read.recv(&mut rx_buffer).await {
+ Ok(read_n) => read_n,
+ Err(error) => {
+ log::error!("Failed to read from Shadowsocks client: {error}");
+ break;
+ }
+ };
+
+ if addr != wg_addr {
+ log::trace!("Ignoring packet from unexpected source: {addr}");
+ continue;
+ }
+
+ if let Err(error) = local_udp_write.send(&rx_buffer[0..read_n]).await {
+ log::error!("Failed to write to local UDP socket: {error}");
+ if is_fatal_socket_io_error(&error) {
+ break;
+ }
+ }
+ }
+}
+
+#[async_trait]
+impl Obfuscator for Shadowsocks {
+ fn endpoint(&self) -> SocketAddr {
+ self.udp_client_addr
+ }
+
+ async fn run(self: Box<Self>) -> crate::Result<()> {
+ self.server
+ .await
+ .expect("server handle panicked")
+ .map_err(crate::Error::RunShadowsocksObfuscator)
+ }
+
+ #[cfg(target_os = "android")]
+ fn remote_socket_fd(&self) -> std::os::unix::io::RawFd {
+ todo!("return remote socket fd")
+ }
+}
+
+/// Return whether retrying is a lost cause
+fn is_fatal_socket_error(error: &ProxySocketError) -> bool {
+ matches!(error, ProxySocketError::IoError(e) if is_fatal_socket_io_error(e))
+}
+
+fn is_fatal_socket_io_error(error: &io::Error) -> bool {
+ matches!(
+ error.kind(),
+ io::ErrorKind::NotConnected
+ | io::ErrorKind::ConnectionReset
+ | io::ErrorKind::ConnectionRefused
+ | io::ErrorKind::ConnectionAborted
+ | io::ErrorKind::BrokenPipe
+ )
+}
diff --git a/tunnel-obfuscation/src/udp2tcp.rs b/tunnel-obfuscation/src/udp2tcp.rs
index 9469441564..450d8d3bba 100644
--- a/tunnel-obfuscation/src/udp2tcp.rs
+++ b/tunnel-obfuscation/src/udp2tcp.rs
@@ -6,13 +6,14 @@ use udp_over_tcp::{
TcpOptions,
};
-pub struct Udp2TcpSettings {
+#[derive(Debug)]
+pub struct Settings {
pub peer: SocketAddr,
#[cfg(target_os = "linux")]
pub fwmark: Option<u32>,
}
-pub type Result<T> = std::result::Result<T, Error>;
+type Result<T> = std::result::Result<T, Error>;
#[derive(thiserror::Error, Debug)]
pub enum Error {
@@ -29,13 +30,13 @@ pub enum Error {
RunObfuscator(#[source] udp2tcp::Error),
}
-struct Udp2Tcp {
+pub struct Udp2Tcp {
local_addr: SocketAddr,
instance: Udp2TcpImpl,
}
impl Udp2Tcp {
- pub async fn new(settings: &Udp2TcpSettings) -> Result<Self> {
+ pub(crate) async fn new(settings: &Settings) -> Result<Self> {
let listen_addr = if settings.peer.is_ipv4() {
SocketAddr::new("127.0.0.1".parse().unwrap(), 0)
} else {
@@ -85,7 +86,3 @@ impl Obfuscator for Udp2Tcp {
self.instance.remote_tcp_fd()
}
}
-
-pub async fn create_obfuscator(settings: &Udp2TcpSettings) -> Result<Box<dyn Obfuscator>> {
- Ok(Box::new(Udp2Tcp::new(settings).await?))
-}