summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift
blob: 941462934455faf7032f519a8138edce0e20ddcd (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
//
//  LocalShadowsocksLoader.swift
//  MullvadREST
//
//  Created by Mojgan on 2024-01-08.
//  Copyright © 2025 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadSettings
import MullvadTypes

public final class ShadowsocksLoader: ShadowsocksLoaderProtocol, SwiftShadowsocksBridgeProviding, Sendable {
    let cache: ShadowsocksConfigurationCacheProtocol
    let relaySelector: ShadowsocksRelaySelectorProtocol
    let settingsUpdater: SettingsUpdater

    nonisolated(unsafe) private var observer: SettingsObserverBlock!
    nonisolated(unsafe) private var tunnelSettings = LatestTunnelSettings()
    private let settingsStrategy = TunnelSettingsStrategy()

    deinit {
        self.settingsUpdater.removeObserver(observer)
    }

    public init(
        cache: ShadowsocksConfigurationCacheProtocol,
        relaySelector: ShadowsocksRelaySelectorProtocol,
        settingsUpdater: SettingsUpdater
    ) {
        self.cache = cache
        self.relaySelector = relaySelector
        self.settingsUpdater = settingsUpdater
        self.addObservers()
    }

    private func addObservers() {
        observer =
            SettingsObserverBlock(
                didUpdateSettings: { [weak self] latestTunnelSettings in
                    guard let self else { return }
                    if settingsStrategy.shouldReconnectToNewRelay(
                        oldSettings: tunnelSettings,
                        newSettings: latestTunnelSettings
                    ) {
                        try? clear()
                    }
                    tunnelSettings = latestTunnelSettings
                }
            )
        settingsUpdater.addObserver(self.observer)
    }

    public func clear() throws {
        try self.cache.clear()
    }

    /// Returns the last used shadowsocks configuration, otherwise a new randomized configuration.
    public func load() throws -> ShadowsocksConfiguration {
        do {
            // If a previous shadowsocks configuration was in cache, return it directly.
            return try cache.read()
        } catch {
            // There is no previous configuration either if this is the first time this code ran
            let newConfiguration = try create()
            try cache.write(newConfiguration)
            return newConfiguration
        }
    }

    /// Returns a randomly selected shadowsocks configuration.
    private func create() throws -> ShadowsocksConfiguration {
        let bridgeConfiguration = try relaySelector.getBridges()
        let closestRelay = try relaySelector.selectRelay(with: tunnelSettings)

        guard let bridgeAddress = closestRelay?.ipv4AddrIn,
            let bridgeConfiguration
        else { throw POSIXError(.ENOENT) }

        return ShadowsocksConfiguration(
            address: .ipv4(bridgeAddress),
            port: bridgeConfiguration.port,
            password: bridgeConfiguration.password,
            cipher: bridgeConfiguration.cipher
        )
    }

    public func bridge() -> ShadowsocksConfiguration? {
        try? load()
    }
}