@@ -16,7 +16,7 @@ use crate::{
1616 query:: { ObfuscationQuery , RelayQuery , RelayQueryExt , WireguardRelayQuery } ,
1717} ;
1818
19- use itertools:: Itertools ;
19+ use itertools:: { Either , Itertools } ;
2020pub use mullvad_types:: relay_list:: Relay ;
2121use 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.
834963pub 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+
856991fn apply_ip_availability (
857992 runtime_ip_availability : IpAvailability ,
858993 user_query : & mut RelayQuery ,
0 commit comments