summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadREST/Relay/RelayPicking/MultihopPicker.swift
blob: 2452b376f50a0ce6e358366d1fcb0a10a59875ce (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
//
//  MultihopPicker.swift
//  MullvadVPN
//
//  Created by Jon Petersson on 2024-12-11.
//  Copyright © 2025 Mullvad VPN AB. All rights reserved.
//

import MullvadSettings
import MullvadTypes

struct MultihopPicker: RelayPicking {
    let obfuscation: RelayObfuscation
    let tunnelSettings: LatestTunnelSettings
    let connectionAttemptCount: UInt

    func pick() throws -> SelectedRelays {
        let constraints = tunnelSettings.relayConstraints
        let daitaSettings = tunnelSettings.daita

        // Guarantee that the entry relay supports selected obfuscation
        let obfuscationBypass = UnsupportedObfuscationProvider(
            relayConstraint: constraints.entryLocations,
            relays: obfuscation.obfuscatedRelays,
            filterConstraint: constraints.filter,
            daitaEnabled: daitaSettings.daitaState.isEnabled
        )

        let supportedObfuscation = RelayObfuscator(
            relays: obfuscation.allRelays,
            tunnelSettings: tunnelSettings,
            connectionAttemptCount: connectionAttemptCount,
            obfuscationBypass: obfuscationBypass
        ).obfuscate()

        let entryCandidates = try RelaySelector.WireGuard.findCandidates(
            by: daitaSettings.isAutomaticRouting ? .any : constraints.entryLocations,
            in: supportedObfuscation.obfuscatedRelays,
            filterConstraint: constraints.filter,
            daitaEnabled: daitaSettings.daitaState.isEnabled
        )

        let exitCandidates = try RelaySelector.WireGuard.findCandidates(
            by: constraints.exitLocations,
            in: supportedObfuscation.allRelays,
            filterConstraint: constraints.filter,
            daitaEnabled: false
        )

        // Create a new picker so that it can use the new obfuscation object.
        let picker = MultihopPicker(
            obfuscation: supportedObfuscation,
            tunnelSettings: tunnelSettings,
            connectionAttemptCount: connectionAttemptCount
        )

        /*
         Relay selection is prioritised in the following order:
         1. Both entry and exit constraints match only a single relay. Both relays are selected.
         2. Entry constraint matches only a single relay and the other multiple relays. The single relay
            is selected and excluded from the list of multiple relays.
         3. Exit constraint matches multiple relays and the other a single relay. The single relay
            is selected and excluded from the list of multiple relays.
         4. Both entry and exit constraints match multiple relays. Exit relay is picked first and then
            excluded from the list of entry relays.
         */
        let decisionFlow = OneToOne(
            next: OneToMany(
                next: ManyToOne(
                    next: ManyToMany(
                        next: nil,
                        relayPicker: picker
                    ),
                    relayPicker: picker
                ),
                relayPicker: picker
            ),
            relayPicker: picker
        )

        return try decisionFlow.pick(
            entryCandidates: entryCandidates,
            exitCandidates: exitCandidates,
            daitaAutomaticRouting: daitaSettings.isAutomaticRouting
        )
    }

    func exclude(
        relay: SelectedRelay,
        from candidates: [RelayWithLocation<REST.ServerRelay>],
        closeTo location: Location? = nil,
        applyObfuscatedIps: Bool
    ) throws -> SelectedRelay {
        let filteredCandidates = candidates.filter { relayWithLocation in
            relayWithLocation.relay.hostname != relay.hostname
        }

        return try findBestMatch(
            from: filteredCandidates,
            closeTo: location,
            applyObfuscatedIps: applyObfuscatedIps
        )
    }
}