diff options
| author | David Lönnhager <david.l@mullvad.net> | 2024-08-16 09:14:56 +0200 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2024-08-16 09:14:56 +0200 |
| commit | fe705988688962c96401686cd3d1309c296eeae1 (patch) | |
| tree | 71fd3c6759e403d94550d99a216523d5bad4fd10 | |
| parent | 532b8ade64f47be9d00655844f4b5fb9070f293e (diff) | |
| parent | 854c9babc336a01e4e49a75c67aaf81ba331540b (diff) | |
| download | mullvadvpn-fe705988688962c96401686cd3d1309c296eeae1.tar.xz mullvadvpn-fe705988688962c96401686cd3d1309c296eeae1.zip | |
Merge branch 'add-shadowsocks-obfuscation'
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?)) -} |
