Skip to content

Commit 19896f5

Browse files
committed
Simplify obfuscation resolver
1 parent 48dfea4 commit 19896f5

File tree

1 file changed

+156
-154
lines changed
  • mullvad-relay-selector/src/relay_selector

1 file changed

+156
-154
lines changed

mullvad-relay-selector/src/relay_selector/mod.rs

Lines changed: 156 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ use mullvad_types::{
3838
};
3939
use std::{
4040
borrow::Borrow,
41+
ops::RangeInclusive,
4142
sync::{Arc, LazyLock, Mutex, RwLock},
4243
};
4344
use std::{net::IpAddr, ops::Deref};
@@ -881,7 +882,7 @@ impl RelaySelector {
881882
fn criteria(&self, predicate: Predicate) -> Vec<Criteria<'_, WireguardRelay>> {
882883
match predicate {
883884
Predicate::Singlehop(constraints) => {
884-
let mut singlehop_criteria = self.entry_criteria(constraints.clone());
885+
let entry_criteria = self.entry_criteria(constraints.clone());
885886

886887
let ownership = Criteria::new(move |relay| {
887888
matcher::filter_on_ownership(constraints.general.ownership.as_ref(), relay)
@@ -894,8 +895,7 @@ impl RelaySelector {
894895
let active =
895896
Criteria::new(|relay: &WireguardRelay| relay.active.if_false(Reason::Inactive));
896897
let location = self.location_criteria(constraints.general.location);
897-
singlehop_criteria.extend([active, location, ownership, providers]);
898-
singlehop_criteria
898+
vec![entry_criteria, active, location, ownership, providers]
899899
}
900900
Predicate::Autohop(constraints) => {
901901
// This case is identical to `singlehop`, except that it does not generally care about obfuscation, DAITA, etc.
@@ -955,7 +955,7 @@ impl RelaySelector {
955955
};
956956

957957
// Check criteria that apply specifically to entries
958-
let can_be_used_as_entry = Criteria::flatten(self.entry_criteria(constraints));
958+
let can_be_used_as_entry = self.entry_criteria(constraints);
959959

960960
let criteria = can_be_used_as_exit.and(
961961
// The relay must also be a valid entry.
@@ -993,7 +993,7 @@ impl RelaySelector {
993993

994994
// Except for the `can_be_used_as_exit` condition, the remainder of the work is
995995
// ~equiv to `Predicate::Singlehop`.
996-
let mut criteria = self.entry_criteria(entry.clone());
996+
let criteria = self.entry_criteria(entry.clone());
997997
let ownership = Criteria::new(move |relay| {
998998
matcher::filter_on_ownership(entry.general.ownership.as_ref(), relay)
999999
.if_false(Reason::Ownership)
@@ -1006,8 +1006,14 @@ impl RelaySelector {
10061006
let active =
10071007
Criteria::new(|relay: &WireguardRelay| relay.active.if_false(Reason::Inactive));
10081008
let location = self.location_criteria(entry.general.location);
1009-
criteria.extend([active, location, can_be_used_as_entry, ownership, providers]);
1010-
criteria
1009+
vec![
1010+
criteria,
1011+
active,
1012+
location,
1013+
can_be_used_as_entry,
1014+
ownership,
1015+
providers,
1016+
]
10111017
}
10121018
Predicate::Exit(MultihopConstraints { entry, exit }) => {
10131019
// If an entry is already selected, it should be rejected as a possible exit relay.
@@ -1059,147 +1065,12 @@ impl RelaySelector {
10591065
}
10601066

10611067
/// All criteria that apply for specifically for entry relays.
1062-
fn entry_criteria(&self, constraints: EntryConstraints) -> Vec<Criteria<'_, WireguardRelay>> {
1063-
let wg_endpoint_ip_version =
1064-
Criteria::new(move |relay: &WireguardRelay| match constraints.ip_version {
1065-
Constraint::Any => Verdict::Accept,
1066-
Constraint::Only(IpVersion::V4) => Verdict::Accept,
1067-
Constraint::Only(IpVersion::V6) => {
1068-
relay.ipv6_addr_in.is_some().if_false(Reason::IpVersion)
1069-
}
1070-
});
1071-
1072-
// Here we have to consider extra entry constraints, such as DAITA, obfuscation etc.
1073-
let constraints_clone = constraints.clone();
1074-
let obfuscation_ipversion_port = Criteria::new(move |relay: &WireguardRelay| {
1075-
match self.obfuscation_criteria(relay, &constraints_clone) {
1076-
ObfuscationVerdict::AcceptWireguardEndpoint => wg_endpoint_ip_version.eval(relay),
1077-
ObfuscationVerdict::AcceptSeparateEndpoint => Verdict::Accept,
1078-
ObfuscationVerdict::Reject(reason) => {
1079-
Verdict::reject(reason).and(wg_endpoint_ip_version.eval(relay))
1080-
}
1081-
}
1082-
});
1083-
1084-
let daita = Criteria::new(move |relay| {
1085-
let daita_on = constraints.daita.as_ref().map(|settings| settings.enabled);
1086-
matcher::filter_on_daita(daita_on, relay).if_false(Reason::Daita)
1087-
});
1088-
1089-
vec![daita, obfuscation_ipversion_port]
1090-
}
1091-
1092-
fn obfuscation_criteria(
1093-
&self,
1094-
relay: &WireguardRelay,
1095-
EntryConstraints {
1096-
obfuscation_settings,
1097-
ip_version,
1098-
..
1099-
}: &EntryConstraints,
1100-
) -> ObfuscationVerdict {
1101-
/// Returns `Ok(())` if any IP in `ip_list` matches `requested_ip_version`,
1102-
/// or `Err(Some(ip_version))` if switching to `ip_version` would yield a match (`Err(None)` otherwise).
1103-
fn any_ip_matches_version(
1104-
requested_ip_version: &Constraint<IpVersion>,
1105-
ip_list: impl IntoIterator<Item: Borrow<IpAddr>>,
1106-
) -> Result<(), Option<IpAvailability>> {
1107-
let (has_ipv4, has_ipv6) =
1108-
ip_list.into_iter().fold((false, false), |(v4, v6), addr| {
1109-
(v4 || addr.borrow().is_ipv4(), v6 || addr.borrow().is_ipv6())
1110-
});
1111-
match requested_ip_version {
1112-
Constraint::Any if has_ipv4 || has_ipv6 => Ok(()),
1113-
Constraint::Only(IpVersion::V4) if has_ipv4 => Ok(()),
1114-
Constraint::Only(IpVersion::V6) if has_ipv6 => Ok(()),
1115-
// No match — report whether the *other* IP version is available.
1116-
Constraint::Any => Err(None),
1117-
Constraint::Only(IpVersion::V4) => Err(Some(IpAvailability::Ipv6)),
1118-
Constraint::Only(IpVersion::V6) => Err(Some(IpAvailability::Ipv4)),
1119-
}
1120-
}
1121-
1122-
use ObfuscationVerdict::*;
1123-
match obfuscation_settings {
1124-
// Possible edge case that we have not implemented:
1125-
// - User has set IPv6=only and anti-censorship=auto
1126-
// - A relay doesn't have an IPv6 for its wg endpoint, but it does have an IPv6 extra shadowsocks addr.
1127-
// In this scenario, we could conceivably allow the relay by enabling shadowsocks to resolve the IP constraint.
1128-
// This would negatively affect the performance of the connection, so we have chosen to discard the relay for now.
1129-
Constraint::Any => AcceptWireguardEndpoint,
1130-
Constraint::Only(settings) => {
1131-
use mullvad_types::relay_constraints::SelectedObfuscation::*;
1132-
match settings.selected_obfuscation {
1133-
Shadowsocks => {
1134-
// The relay may have IPs specifically meant for shadowsocks,
1135-
// which we want to use if possible.
1136-
let ss_extra_addrs = &relay.endpoint().shadowsocks_extra_addr_in;
1137-
// Check if any of them matches the requested IP version.
1138-
match any_ip_matches_version(ip_version, ss_extra_addrs) {
1139-
Ok(()) => AcceptSeparateEndpoint,
1140-
// Otherwise, we must fall back to using the WireGuard endpoint.
1141-
Err(other_ip_matches) => {
1142-
// A few ports on the wg endpoint are dedicated to shadowsocks.
1143-
// If a specific port is requested and it lies outside this range,
1144-
// then we cannot resolve the constraint.
1145-
let cannot_use_wg_endpoint =
1146-
settings.shadowsocks.port.is_only_and(|port| {
1147-
!self.relay_list(|rl| {
1148-
rl.wireguard
1149-
.shadowsocks_port_ranges
1150-
.iter()
1151-
.any(|range| range.contains(&port))
1152-
})
1153-
});
1154-
match (cannot_use_wg_endpoint, other_ip_matches) {
1155-
(false, None | Some(_)) => {
1156-
// Port is usable on WireGuard endpoint, so fall back to it
1157-
AcceptWireguardEndpoint
1158-
}
1159-
(true, Some(_)) => {
1160-
// Switching IP version would unblock the relay.
1161-
// Note that the relay could also be unblocked by removing the port constraint
1162-
// so that a normal WireGuard endpoint can be used IFF that endpoint
1163-
// is available with the requested IP version. We cannot represent this, so we
1164-
// opt to only inform the user about the IP version.
1165-
Reject(Reason::IpVersion)
1166-
}
1167-
(true, None) => {
1168-
// No extra addresses are available at all, the the port must be changed
1169-
// so that a Wireguard endpoint can be used. This endpoint must
1170-
// then also be available with the requested IP version.
1171-
Reject(Reason::Port)
1172-
}
1173-
}
1174-
}
1175-
}
1176-
}
1177-
Quic => {
1178-
// TODO: Refactor using `if-let guards` once 1.95 is stable.
1179-
let Some(quic) = relay.endpoint().quic() else {
1180-
// QUIC is disabled
1181-
return Reject(Reason::Obfuscation);
1182-
};
1183-
match any_ip_matches_version(ip_version, quic.in_addr()) {
1184-
Ok(()) => AcceptSeparateEndpoint,
1185-
// Switching IP version would unblock the relay.
1186-
Err(Some(_)) => Reject(Reason::IpVersion),
1187-
// The relay has quic but no IPv4 or IPv6 addresses to use it.
1188-
// This scenario should be unreachable, but treat it as if obfuscation was
1189-
// unavailable just in case.
1190-
Err(None) => Reject(Reason::Obfuscation),
1191-
}
1192-
}
1193-
// LWO is only enabled on some relays
1194-
Lwo if relay.endpoint().lwo => AcceptWireguardEndpoint,
1195-
Lwo => Reject(Reason::Obfuscation),
1196-
// Other relays are always valid
1197-
// TODO:^ This might not be true. We might want to consider the selected port for
1198-
// udp2tcp & wireguard port ..
1199-
Off | Auto | WireguardPort | Udp2Tcp => AcceptWireguardEndpoint,
1200-
}
1201-
}
1202-
}
1068+
fn entry_criteria(&self, constraints: EntryConstraints) -> Criteria<'_, WireguardRelay> {
1069+
let shadowsocks_port_ranges =
1070+
self.relay_list(|rl| rl.wireguard.shadowsocks_port_ranges.clone());
1071+
Criteria::new(move |relay: &WireguardRelay| {
1072+
entry_criteria_inner(relay, &constraints, &shadowsocks_port_ranges)
1073+
})
12031074
}
12041075

12051076
fn location_criteria(
@@ -1219,10 +1090,146 @@ impl RelaySelector {
12191090

12201091
enum ObfuscationVerdict {
12211092
AcceptWireguardEndpoint,
1222-
AcceptSeparateEndpoint,
1093+
AcceptObfuscationEndpoint,
12231094
Reject(Reason),
12241095
}
12251096

1097+
fn entry_criteria_inner(
1098+
relay: &WireguardRelay,
1099+
constraints: &EntryConstraints,
1100+
shadowsocks_port_ranges: &[RangeInclusive<u16>],
1101+
) -> Verdict {
1102+
let wg_endpoint_ip_version = match constraints.ip_version {
1103+
Constraint::Any => Verdict::Accept,
1104+
Constraint::Only(IpVersion::V4) => Verdict::Accept,
1105+
Constraint::Only(IpVersion::V6) => relay.ipv6_addr_in.is_some().if_false(Reason::IpVersion),
1106+
};
1107+
1108+
// Here we have to consider extra entry constraints, such as DAITA, obfuscation etc.
1109+
let constraints_clone = constraints.clone();
1110+
let obfuscation_ipversion_port = {
1111+
match obfuscation_criteria(shadowsocks_port_ranges, relay, &constraints_clone) {
1112+
ObfuscationVerdict::AcceptWireguardEndpoint => wg_endpoint_ip_version,
1113+
ObfuscationVerdict::AcceptObfuscationEndpoint => Verdict::Accept,
1114+
ObfuscationVerdict::Reject(reason) => {
1115+
Verdict::reject(reason).and(wg_endpoint_ip_version)
1116+
}
1117+
}
1118+
};
1119+
1120+
let daita = {
1121+
let daita_on = constraints.daita.as_ref().map(|settings| settings.enabled);
1122+
matcher::filter_on_daita(daita_on, relay).if_false(Reason::Daita)
1123+
};
1124+
1125+
daita.and(obfuscation_ipversion_port)
1126+
}
1127+
1128+
fn obfuscation_criteria(
1129+
shadowsocks_port_ranges: &[RangeInclusive<u16>],
1130+
relay: &WireguardRelay,
1131+
EntryConstraints {
1132+
obfuscation_settings,
1133+
ip_version,
1134+
..
1135+
}: &EntryConstraints,
1136+
) -> ObfuscationVerdict {
1137+
enum IpVersionMatch {
1138+
Ok,
1139+
Other,
1140+
None,
1141+
}
1142+
fn any_ip_matches_version(
1143+
requested_ip_version: &Constraint<IpVersion>,
1144+
ip_list: impl IntoIterator<Item: Borrow<IpAddr>>,
1145+
) -> IpVersionMatch {
1146+
let (has_ipv4, has_ipv6) = ip_list.into_iter().fold((false, false), |(v4, v6), addr| {
1147+
(v4 || addr.borrow().is_ipv4(), v6 || addr.borrow().is_ipv6())
1148+
});
1149+
match requested_ip_version {
1150+
Constraint::Any if has_ipv4 || has_ipv6 => IpVersionMatch::Ok,
1151+
Constraint::Only(IpVersion::V4) if has_ipv4 => IpVersionMatch::Ok,
1152+
Constraint::Only(IpVersion::V6) if has_ipv6 => IpVersionMatch::Ok,
1153+
// No match — report whether the *other* IP version is available.
1154+
Constraint::Any => IpVersionMatch::None,
1155+
Constraint::Only(IpVersion::V4) => IpVersionMatch::Other,
1156+
Constraint::Only(IpVersion::V6) => IpVersionMatch::Other,
1157+
}
1158+
}
1159+
1160+
use ObfuscationVerdict::*;
1161+
match obfuscation_settings {
1162+
// Possible edge case that we have not implemented:
1163+
// - User has set IPv6=only and anti-censorship=auto
1164+
// - A relay doesn't have an IPv6 for its wg endpoint, but it does have an IPv6 extra shadowsocks addr.
1165+
// In this scenario, we could conceivably allow the relay by enabling shadowsocks to resolve the IP constraint.
1166+
// This would negatively affect the performance of the connection, so we have chosen to discard the relay for now.
1167+
Constraint::Any => AcceptWireguardEndpoint,
1168+
Constraint::Only(settings) => {
1169+
use mullvad_types::relay_constraints::SelectedObfuscation::*;
1170+
match settings.selected_obfuscation {
1171+
Shadowsocks => {
1172+
// The relay may have IPs specifically meant for shadowsocks.
1173+
// Use them if they match the requested IP version.
1174+
match any_ip_matches_version(
1175+
ip_version,
1176+
&relay.endpoint().shadowsocks_extra_addr_in,
1177+
) {
1178+
IpVersionMatch::Ok => AcceptObfuscationEndpoint,
1179+
// Check if we can fall back to using the WireGuard endpoint instead.
1180+
// A few port ranges on it are dedicated to shadowsocks. If a specific port
1181+
// is requested it must lie within these ranges.
1182+
_ if !settings.shadowsocks.port.is_only_and(|port| {
1183+
!shadowsocks_port_ranges
1184+
.iter()
1185+
.any(|range| range.contains(&port))
1186+
}) =>
1187+
{
1188+
AcceptWireguardEndpoint
1189+
}
1190+
// -- We cannot resolve the relay on any endpoint, so reject it --
1191+
1192+
// Switching IP version would unblock the relay, so give that as the reject reason.
1193+
// Note that the relay could also be unblocked by removing the port constraint
1194+
// so that a normal WireGuard endpoint can be used IFF that endpoint
1195+
// is available with the requested IP version. We cannot represent this, so we
1196+
// opt to only inform the user about the IP version.
1197+
IpVersionMatch::Other => Reject(Reason::IpVersion),
1198+
// No extra addresses are available at all, the port must be changed
1199+
// so that a Wireguard endpoint can be used. This endpoint must
1200+
// then also be available with the requested IP version, which
1201+
// is checked for outside this function.
1202+
IpVersionMatch::None => Reject(Reason::Port),
1203+
}
1204+
}
1205+
Quic => {
1206+
// TODO: Refactor using `if-let guards` once 1.95 is stable.
1207+
let Some(quic) = relay.endpoint().quic() else {
1208+
// QUIC is disabled
1209+
return Reject(Reason::Obfuscation);
1210+
};
1211+
match any_ip_matches_version(ip_version, quic.in_addr()) {
1212+
IpVersionMatch::Ok => AcceptObfuscationEndpoint,
1213+
// Switching IP version would unblock the relay.
1214+
IpVersionMatch::Other => Reject(Reason::IpVersion),
1215+
// The relay has quic but no IPv4 or IPv6 addresses to use it.
1216+
// This scenario should be unreachable, but treat it as if obfuscation was
1217+
// unavailable just in case.
1218+
IpVersionMatch::None => Reject(Reason::Obfuscation),
1219+
}
1220+
}
1221+
// LWO is only enabled on some relays
1222+
Lwo if relay.endpoint().lwo => AcceptWireguardEndpoint,
1223+
Lwo => Reject(Reason::Obfuscation),
1224+
// Other relays are always valid
1225+
// TODO:^ This might not be true. We might want to consider the selected port for
1226+
// udp2tcp & wireguard port ..
1227+
Off | Auto | WireguardPort | Udp2Tcp => AcceptWireguardEndpoint,
1228+
}
1229+
}
1230+
}
1231+
}
1232+
12261233
/// A criteria is a function from a _single_ constraint and a relay to a [`Verdict`].
12271234
///
12281235
/// Multiple [`Criteria`] can be evaluated against a single relay at once by [`Criteria::eval`]. A
@@ -1273,11 +1280,6 @@ impl<'a> Criteria<'a, WireguardRelay> {
12731280
.map(|criteria| criteria.eval(relay))
12741281
.fold(Verdict::Accept, Verdict::and)
12751282
}
1276-
1277-
/// Flatten a nested structure of different criteria into one.
1278-
fn flatten(criterias: Vec<Self>) -> Self {
1279-
Criteria::new(move |relay| Criteria::fold(criterias.iter(), relay))
1280-
}
12811283
}
12821284

12831285
/// If a relay is accepted or rejected. If it is rejected, all [reasons](Reason) for that judgement

0 commit comments

Comments
 (0)