@@ -38,6 +38,7 @@ use mullvad_types::{
3838} ;
3939use std:: {
4040 borrow:: Borrow ,
41+ ops:: RangeInclusive ,
4142 sync:: { Arc , LazyLock , Mutex , RwLock } ,
4243} ;
4344use 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
12201091enum 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