Skip to content

Commit b1fd392

Browse files
WIP Implement relay selection algorithm anew
1 parent 78fdff8 commit b1fd392

File tree

3 files changed

+210
-29
lines changed

3 files changed

+210
-29
lines changed

mullvad-management-interface/src/types/conversions/relay_constraints.rs

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,9 @@ impl TryFrom<&proto::WireguardConstraints>
4444
.entry_location
4545
.clone()
4646
.and_then(|loc| {
47-
Constraint::<mullvad_types::relay_constraints::LocationConstraint>::try_from(
48-
loc,
49-
)
50-
.ok()
47+
mullvad_types::relay_constraints::LocationConstraint::try_from(loc).ok()
5148
})
52-
.unwrap_or(Constraint::Any),
49+
.into(),
5350
entry_providers: try_providers_constraint_from_proto(&constraints.entry_providers)?,
5451
entry_ownership: try_ownership_constraint_from_i32(constraints.entry_ownership)?,
5552
})
@@ -89,8 +86,10 @@ impl TryFrom<proto::RelaySettings> for mullvad_types::relay_constraints::RelaySe
8986
proto::relay_settings::Endpoint::Normal(settings) => {
9087
let location = settings
9188
.location
92-
.and_then(|loc| Constraint::<mullvad_types::relay_constraints::LocationConstraint>::try_from(loc).ok())
93-
.unwrap_or(Constraint::Any);
89+
.and_then(|loc| {
90+
mullvad_types::relay_constraints::LocationConstraint::try_from(loc).ok()
91+
})
92+
.into();
9493
let providers = try_providers_constraint_from_proto(&settings.providers)?;
9594
let ownership = try_ownership_constraint_from_i32(settings.ownership)?;
9695

@@ -277,26 +276,28 @@ impl From<mullvad_types::relay_constraints::LocationConstraint> for proto::Locat
277276
}
278277
}
279278

280-
impl TryFrom<proto::LocationConstraint>
281-
for Constraint<mullvad_types::relay_constraints::LocationConstraint>
282-
{
279+
impl TryFrom<proto::LocationConstraint> for mullvad_types::relay_constraints::LocationConstraint {
283280
type Error = FromProtobufTypeError;
284281

285282
fn try_from(location: proto::LocationConstraint) -> Result<Self, Self::Error> {
286283
use mullvad_types::relay_constraints::LocationConstraint;
287-
match location.r#type {
288-
Some(proto::location_constraint::Type::Location(location)) => Ok(Constraint::Only(
284+
let Some(typ) = location.r#type else {
285+
return Err(FromProtobufTypeError::InvalidArgument(
286+
"Type of location constraint was not provided",
287+
));
288+
};
289+
match typ {
290+
proto::location_constraint::Type::Location(location) => Ok(
289291
LocationConstraint::Location(GeographicLocationConstraint::try_from(location)?),
290-
)),
291-
Some(proto::location_constraint::Type::CustomList(list_id)) => {
292+
),
293+
proto::location_constraint::Type::CustomList(list_id) => {
292294
let location = LocationConstraint::CustomList {
293295
list_id: Id::from_str(&list_id).map_err(|_| {
294296
FromProtobufTypeError::InvalidArgument("Id could not be parsed to a uuid")
295297
})?,
296298
};
297-
Ok(Constraint::Only(location))
299+
Ok(location)
298300
}
299-
None => Ok(Constraint::Any),
300301
}
301302
}
302303
}
@@ -492,6 +493,17 @@ impl TryFrom<proto::RelayOverride> for mullvad_types::relay_constraints::RelayOv
492493
}
493494
}
494495

496+
// TODO: Rename
497+
pub fn try_providers_from_proto(
498+
providers: Vec<proto::relay_selector::Provider>,
499+
) -> Result<Constraint<mullvad_types::relay_constraints::Providers>, FromProtobufTypeError> {
500+
let providers: Vec<_> = providers
501+
.into_iter()
502+
.map(|provider| provider.name)
503+
.collect();
504+
try_providers_constraint_from_proto(&providers)
505+
}
506+
495507
pub fn try_providers_constraint_from_proto(
496508
providers: &[String],
497509
) -> Result<Constraint<mullvad_types::relay_constraints::Providers>, FromProtobufTypeError> {

mullvad-management-interface/src/types/conversions/relay_selector.rs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,48 @@
1-
use crate::types::relay_selector::*;
1+
use crate::types::{
2+
FromProtobufTypeError,
3+
relay_constraints::{try_ownership_constraint_from_i32, try_providers_from_proto},
4+
relay_selector::*,
5+
};
26

37
impl TryFrom<Predicate> for mullvad_relay_selector::Predicate {
4-
type Error = String;
8+
type Error = FromProtobufTypeError;
59

610
fn try_from(predicate: Predicate) -> Result<Self, Self::Error> {
711
let Some(context) = predicate.context else {
812
todo!("Return early");
913
};
1014
match context {
11-
predicate::Context::Singlehop(_constraints) => Ok(Self::Singlehop),
15+
predicate::Context::Singlehop(constraints) => {
16+
let EntryConstraints {
17+
general_constraints,
18+
obfuscation_settings: _, // TODO: Consider this parameter
19+
daita_settings: _, // TODO: Consider this parameter
20+
ip_version: _, // TODO: Consider this parameter
21+
} = constraints;
22+
// TODO: It might be beneficial to consolidate this whole conversion into a single
23+
// type.
24+
let (location, providers, ownership) = {
25+
match general_constraints {
26+
None => Default::default(),
27+
Some(constraints) => {
28+
let location = constraints
29+
.location
30+
.map(mullvad_types::relay_constraints::LocationConstraint::try_from)
31+
.transpose()?
32+
.into();
33+
let providers = try_providers_from_proto(constraints.providers)?;
34+
let ownership =
35+
try_ownership_constraint_from_i32(constraints.ownership)?;
36+
(location, providers, ownership)
37+
}
38+
}
39+
};
40+
Ok(Self::Singlehop {
41+
location,
42+
providers,
43+
ownership,
44+
})
45+
}
1246
predicate::Context::Autohop(_constraints) => Ok(Self::Autohop),
1347
predicate::Context::Entry(_constraints) => Ok(Self::Entry),
1448
predicate::Context::Exit(_constraints) => Ok(Self::Exit),

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

Lines changed: 145 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use crate::{
1616
query::{ObfuscationQuery, RelayQuery, RelayQueryExt, WireguardRelayQuery},
1717
};
1818

19-
use itertools::Itertools;
19+
use itertools::{Either, Itertools};
2020
pub use mullvad_types::relay_list::Relay;
2121
use mullvad_types::{
2222
CustomTunnelEndpoint, Intersection,
@@ -25,7 +25,8 @@ use mullvad_types::{
2525
endpoint::MullvadEndpoint,
2626
location::Coordinates,
2727
relay_constraints::{
28-
ObfuscationSettings, RelayConstraints, RelaySettings, WireguardConstraints,
28+
LocationConstraint, ObfuscationSettings, Ownership, Providers, RelayConstraints,
29+
RelaySettings, WireguardConstraints,
2930
},
3031
relay_list::{Bridge, BridgeList, RelayList, WireguardRelay},
3132
settings::Settings,
@@ -813,26 +814,158 @@ impl RelaySelector {
813814
Ok((endpoint, bridge))
814815
}
815816

816-
// NEW relay selector API.
817+
// == NEW relay selector API. ==
817818
// Starting afresh, but this should be used in existing functions.
818819

819820
/// As oppossed to the prior [`Self::get_relay_by_query`], this function is stateless with
820821
/// regards to any particular config / settings.
821822
pub fn partition_relays(
822-
&self, // TODO: If relay list is an in-parameter, we don't need this to be an associated
823-
// function.
824-
_predicate: Predicate,
825-
// relays: &'a [Relay], // Implicit argument for now.
823+
// TODO: If relay list is an in-parameter, we don't need this to be an associated function.
824+
&self,
825+
predicate: Predicate,
826826
) -> RelayPartitions {
827-
let partitions: RelayPartitions = RelayPartitions::default();
828-
partitions
827+
// Implicit argument for now. Might as well be an explicit in-parameter.
828+
// let relays = self.get_relays();
829+
let relays: Vec<Relay> = vec![];
830+
// The relay selection algorithm is embarrassingly parallel: https://en.wikipedia.org/wiki/Embarrassingly_parallel.
831+
// We may explore the entire search space (`relays` x `criteria`) without any synchronisation
832+
// between different branches.
833+
let verdicts: Vec<(Relay, Verdict)> = match predicate {
834+
Predicate::Singlehop {
835+
location,
836+
providers,
837+
ownership,
838+
} => {
839+
/* pseudo-code (implemented mostly in `Criteria`).
840+
*
841+
* let relay := relays.pop()
842+
* let mut rejections := []
843+
*
844+
* if let Some(reason) critera(relay) {
845+
* rejections.push(reason)
846+
* }
847+
* ..
848+
* if rejections.empty() {
849+
* Accept(relay)
850+
* } else {
851+
* Fail(relay, rejections)
852+
* }
853+
* */
854+
let criteria = [
855+
Criteria::otherwise(|relay| relay.active, |_| Reject::Inactive),
856+
Criteria::otherwise(|relay| relay.hostname == "se", |_| Reject::Inactive),
857+
];
858+
relays
859+
.into_iter()
860+
// This part of the algorithm maps each relay to a verdict: Either Accept or
861+
// Reject(Reason).
862+
.map(|relay| {
863+
let verdict = Criteria::eval(criteria.iter(), &relay);
864+
(relay, verdict)
865+
})
866+
.collect()
867+
}
868+
Predicate::Autohop => todo!("Implement partition_relays(Autohop)"),
869+
Predicate::Entry => todo!("Implement partition_relays(Entry)"),
870+
Predicate::Exit => todo!("Implement partition_relays(Exit)"),
871+
};
872+
// After this mapping, a single reduce is performed to partition the relays based on
873+
// their assigned verdict.
874+
verdicts
875+
.into_iter()
876+
.partition_map(|(relay, verdict)| match verdict {
877+
Verdict::Accept => Either::Left(relay),
878+
Verdict::Fail(rejected) => Either::Right((relay, rejected)),
879+
})
880+
.into()
881+
}
882+
}
883+
884+
/// A criteria is a function from a _single_ constraint and a relay to a [`Verdict`].
885+
///
886+
/// Multiple [`Criteria`] can be evaluated against a single relay at once by [`Criteria::eval`]. A
887+
/// final verdict is then compiled. If applicable, all reject reasons are accumulated and presented
888+
/// as a single [`Verdict::Fail`].
889+
struct Criteria<'a> {
890+
f: Box<dyn Fn(&Relay) -> Verdict + 'a>,
891+
}
892+
893+
impl<'a> Criteria<'a> {
894+
/// Create a new [`Criteria`].
895+
fn new(f: impl Fn(&Relay) -> Verdict + 'a) -> Self {
896+
Criteria { f: Box::new(f) }
897+
}
898+
899+
/// If the given criteria [`f`] evaulates to `false`, the second provided function `reason` is
900+
/// run to provide a single [`Reject`] reason. `reason` gets access to the failing relay, which
901+
/// means that `reason` may derivce additional information for why this particular relay was
902+
/// rejected.
903+
///
904+
/// This is a short-hand for how most common [`Criteria`]s will be formulated, and it allows the
905+
/// caller to nicely separate the scrutinizing rejection logic from the logic extracting data to
906+
/// provide together with the final rejection. In the happy case this carries minimal additional
907+
/// runtime overhead compared to [`Criteria::new`], but upon a rejection two functions will run
908+
/// instead of one. For more fine-grained control over this behavior, prefer [`Criteria::new`].
909+
fn otherwise(f: impl Fn(&Relay) -> bool + 'a, reason: impl Fn(&Relay) -> Reject + 'a) -> Self {
910+
Criteria::new(move |relay| {
911+
if f(relay) {
912+
Verdict::Accept
913+
} else {
914+
Verdict::Fail(vec![reason(relay)])
915+
}
916+
})
917+
}
918+
919+
/// Evaluate a single [`Criteria`] for a single [`Relay`].
920+
fn run(&self, relay: &Relay) -> Verdict {
921+
(self.f)(relay)
922+
}
923+
924+
/// Evaluate all criterias for a given relay, resulting in a single final verdict.
925+
fn eval(criterias: impl Iterator<Item = &'a Criteria<'a>>, relay: &Relay) -> Verdict {
926+
let mut rejections = vec![];
927+
for criteria in criterias.into_iter() {
928+
if let Verdict::Fail(reasons) = criteria.run(relay) {
929+
for reason in reasons {
930+
rejections.push(reason);
931+
}
932+
};
933+
}
934+
match rejections.is_empty() {
935+
true => Verdict::Accept,
936+
false => Verdict::Fail(rejections),
937+
}
938+
}
939+
}
940+
941+
/// If a relay is accepted or rejected .
942+
///
943+
/// # Note
944+
/// The associated relay is implied from the environment.
945+
#[derive(Debug)]
946+
enum Verdict {
947+
Accept,
948+
Fail(Vec<Reject>),
949+
}
950+
951+
impl From<(Vec<Relay>, Vec<(Relay, Vec<Reject>)>)> for RelayPartitions {
952+
/// Map the result of [`Itertools::partition_map`] to [`RelayPartitions`].
953+
fn from(partitions: (Vec<Relay>, Vec<(Relay, Vec<Reject>)>)) -> Self {
954+
Self {
955+
matches: partitions.0,
956+
discards: partitions.1,
957+
}
829958
}
830959
}
831960

832961
/// Specify the constraints that should be applied when selecting relays,
833962
/// along with a context that may affect the selection behavior.
834963
pub enum Predicate {
835-
Singlehop,
964+
Singlehop {
965+
location: Constraint<LocationConstraint>,
966+
providers: Constraint<Providers>,
967+
ownership: Constraint<Ownership>,
968+
},
836969
Autohop,
837970
// Multihop-only
838971
Entry,
@@ -853,6 +986,8 @@ pub enum Reject {
853986
// TODO: Add more reasons - at least all in `relay_selector.proto`.
854987
}
855988

989+
// == End of new relay selector API. ==
990+
856991
fn apply_ip_availability(
857992
runtime_ip_availability: IpAvailability,
858993
user_query: &mut RelayQuery,

0 commit comments

Comments
 (0)