summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--docs/relay-selector.md10
-rw-r--r--ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift3
-rw-r--r--ios/MullvadMockData/MullvadREST/SelectedRelaysStub+Stubs.swift3
-rw-r--r--ios/MullvadREST/Relay/ObfuscationMethodSelector.swift9
-rw-r--r--ios/MullvadREST/Relay/RelayPicking/RelayPicking.swift3
-rw-r--r--ios/MullvadREST/Relay/RelaySelectorProtocol.swift7
-rw-r--r--ios/MullvadRustRuntime/TunnelObfuscator.swift40
-rw-r--r--ios/MullvadRustRuntime/include/mullvad_rust_runtime.h31
-rw-r--r--ios/MullvadSettings/WireGuardObfuscationSettings.swift4
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ChipView/ChipContainerView.swift3
-rw-r--r--ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift3
-rw-r--r--ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift11
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Relay/ObfuscationMethodSelectorTests.swift30
-rw-r--r--ios/MullvadVPNUITests/Base/BaseUITestCase.swift6
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActor.swift38
-rw-r--r--ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift32
-rw-r--r--ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift3
-rw-r--r--ios/PacketTunnelCoreTests/EphemeralPeerExchangingPipelineTests.swift6
-rw-r--r--ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift5
-rw-r--r--ios/PacketTunnelCoreTests/MultiHopEphemeralPeerExchangerTests.swift6
-rw-r--r--ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift30
-rw-r--r--ios/PacketTunnelCoreTests/SingleHopEphemeralPeerExchangerTests.swift7
-rw-r--r--mullvad-ios/src/api_client/access_method_settings.rs6
-rw-r--r--mullvad-ios/src/api_client/address_cache_provider.rs5
-rw-r--r--mullvad-ios/src/api_client/helpers.rs22
-rw-r--r--mullvad-ios/src/api_client/mod.rs33
-rw-r--r--mullvad-ios/src/api_client/storekit.rs9
-rw-r--r--mullvad-ios/src/lib.rs44
-rw-r--r--mullvad-ios/src/tunnel_obfuscator_proxy/ffi.rs121
-rw-r--r--mullvad-ios/src/tunnel_obfuscator_proxy/mod.rs40
-rw-r--r--talpid-wireguard/src/obfuscation.rs4
-rw-r--r--tunnel-obfuscation/src/quic.rs8
32 files changed, 345 insertions, 237 deletions
diff --git a/docs/relay-selector.md b/docs/relay-selector.md
index 2096a21865..3f74ffd6ae 100644
--- a/docs/relay-selector.md
+++ b/docs/relay-selector.md
@@ -54,9 +54,9 @@ constraints the following default ones will take effect
#### Tunnel protocol is Wireguard
- The first attempt will connect to a Wireguard relay on a random port
-- The second attempt will connect to a Wireguard relay on port 443
-- The third attempt will connect to a Wireguard relay over IPv6 (if IPv6 is configured on the host) on a random port
-- The fourth attempt will connect to a Wireguard relay on a random port using Shadowsocks for obfuscation
+- The second attempt will connect to a Wireguard relay over IPv6 (if IPv6 is configured on the host) on a random port
+- The third attempt will connect to a Wireguard relay on a random port using Shadowsocks for obfuscation
+- The fourth attempt will connect to a Wireguard relay using QUIC for obfuscation (if QUIC is implemented)
- The fifth attempt will connect to a Wireguard relay on a random port using [UDP2TCP obfuscation](https://github.com/mullvad/udp-over-tcp)
- The sixth attempt will connect to a Wireguard relay over IPv6 on a random port using UDP2TCP obfuscation (if IPv6 is configured on the host)
@@ -73,8 +73,8 @@ Note: This is not applicable to Android nor iOS.
The iOS platform does not support OpenVPN, or connecting to a relay over IPv6.
As such, the above algorithm is simplified to the following version:
- The first attempt will connect to a Wireguard relay on a random port
- - The second attempt will connect to a Wireguard relay on port 443
- - The third attempt will connect to a Wireguard relay on a random port using Shadowsocks for obfuscation
+ - The second attempt will connect to a Wireguard relay on a random port using Shadowsocks for obfuscation
+ - The third attempt will connect to a Wireguard relay using QUIC for obfuscation
- The fourth attempt will connect to a Wireguard relay on a random port using [UDP2TCP obfuscation](https://github.com/mullvad/udp-over-tcp)
### Random Ports for UDP2TCP and Shadowsocks
diff --git a/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift b/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift
index 64dbf3a943..d7333dc915 100644
--- a/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift
+++ b/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift
@@ -63,7 +63,8 @@ extension RelaySelectorStub {
cityCode: "got",
latitude: 0,
longitude: 0
- )
+ ),
+ features: nil
)
return SelectedRelays(
diff --git a/ios/MullvadMockData/MullvadREST/SelectedRelaysStub+Stubs.swift b/ios/MullvadMockData/MullvadREST/SelectedRelaysStub+Stubs.swift
index ce265aa90a..57576da35c 100644
--- a/ios/MullvadMockData/MullvadREST/SelectedRelaysStub+Stubs.swift
+++ b/ios/MullvadMockData/MullvadREST/SelectedRelaysStub+Stubs.swift
@@ -29,7 +29,8 @@ public struct SelectedRelaysStub {
cityCode: "got",
latitude: 42,
longitude: 42
- )
+ ),
+ features: nil
),
retryAttempt: 0
)
diff --git a/ios/MullvadREST/Relay/ObfuscationMethodSelector.swift b/ios/MullvadREST/Relay/ObfuscationMethodSelector.swift
index f73509affe..73944a8093 100644
--- a/ios/MullvadREST/Relay/ObfuscationMethodSelector.swift
+++ b/ios/MullvadREST/Relay/ObfuscationMethodSelector.swift
@@ -11,15 +11,18 @@ import MullvadSettings
public struct ObfuscationMethodSelector {
/// This retry logic used is explained at the following link:
/// https://github.com/mullvad/mullvadvpn-app/blob/main/docs/relay-selector.md#default-constraints-for-tunnel-endpoints
+ ///
+ /// - Note: This method should never return `.automatic`.
public static func obfuscationMethodBy(
connectionAttemptCount: UInt,
tunnelSettings: LatestTunnelSettings
) -> WireGuardObfuscationState {
- // TODO: Revisit this when QUIC obfuscation is added
if tunnelSettings.wireGuardObfuscation.state == .automatic {
- if connectionAttemptCount.isOrdered(nth: 2, forEverySetOf: 3) {
+ if connectionAttemptCount.isOrdered(nth: 2, forEverySetOf: 4) {
.shadowsocks
- } else if connectionAttemptCount.isOrdered(nth: 3, forEverySetOf: 3) {
+ } else if connectionAttemptCount.isOrdered(nth: 3, forEverySetOf: 4) {
+ .quic
+ } else if connectionAttemptCount.isOrdered(nth: 4, forEverySetOf: 4) {
.udpOverTcp
} else {
.off
diff --git a/ios/MullvadREST/Relay/RelayPicking/RelayPicking.swift b/ios/MullvadREST/Relay/RelayPicking/RelayPicking.swift
index 2edd67a7ea..18421de3f3 100644
--- a/ios/MullvadREST/Relay/RelayPicking/RelayPicking.swift
+++ b/ios/MullvadREST/Relay/RelayPicking/RelayPicking.swift
@@ -39,7 +39,8 @@ extension RelayPicking {
return SelectedRelay(
endpoint: match.endpoint,
hostname: match.relay.hostname,
- location: match.location
+ location: match.location,
+ features: match.relay.features
)
}
diff --git a/ios/MullvadREST/Relay/RelaySelectorProtocol.swift b/ios/MullvadREST/Relay/RelaySelectorProtocol.swift
index 7b2f91c761..174ff9ccb1 100644
--- a/ios/MullvadREST/Relay/RelaySelectorProtocol.swift
+++ b/ios/MullvadREST/Relay/RelaySelectorProtocol.swift
@@ -6,7 +6,6 @@
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//
-import Foundation
import MullvadSettings
import MullvadTypes
@@ -34,11 +33,15 @@ public struct SelectedRelay: Equatable, Codable, Sendable {
/// Relay geo location.
public let location: Location
+ /// Relay features, such as `DAITA` or `QUIC`.
+ public let features: REST.ServerRelay.Features?
+
/// Designated initializer.
- public init(endpoint: MullvadEndpoint, hostname: String, location: Location) {
+ public init(endpoint: MullvadEndpoint, hostname: String, location: Location, features: REST.ServerRelay.Features?) {
self.endpoint = endpoint
self.hostname = hostname
self.location = location
+ self.features = features
}
}
diff --git a/ios/MullvadRustRuntime/TunnelObfuscator.swift b/ios/MullvadRustRuntime/TunnelObfuscator.swift
index 495930b222..6f74089265 100644
--- a/ios/MullvadRustRuntime/TunnelObfuscator.swift
+++ b/ios/MullvadRustRuntime/TunnelObfuscator.swift
@@ -14,7 +14,7 @@ import Network
public enum TunnelObfuscationProtocol {
case udpOverTcp
case shadowsocks
- case quic
+ case quic(hostname: String, token: String)
}
public protocol TunnelObfuscation {
@@ -74,22 +74,34 @@ public final class TunnelObfuscator: TunnelObfuscation {
stateLock.withLock {
guard !isStarted else { return }
- let obfuscationProtocol = switch obfuscationProtocol {
- case .udpOverTcp: TunnelObfuscatorProtocol(0)
- case .shadowsocks: TunnelObfuscatorProtocol(1)
- case .quic: TunnelObfuscatorProtocol(2)
- }
-
let result = withUnsafeMutablePointer(to: &proxyHandle) { proxyHandlePointer in
let addressData = remoteAddress.rawValue
- return start_tunnel_obfuscator_proxy(
- addressData.map { $0 },
- UInt(addressData.count),
- tcpPort,
- obfuscationProtocol,
- proxyHandlePointer
- )
+ return switch obfuscationProtocol {
+ case .udpOverTcp:
+ start_udp2tcp_obfuscator_proxy(
+ addressData.map { $0 },
+ UInt(addressData.count),
+ tcpPort,
+ proxyHandlePointer
+ )
+ case .shadowsocks:
+ start_shadowsocks_obfuscator_proxy(
+ addressData.map { $0 },
+ UInt(addressData.count),
+ tcpPort,
+ proxyHandlePointer
+ )
+ case let .quic(hostname, token):
+ start_quic_obfuscator_proxy(
+ addressData.map { $0 },
+ UInt(addressData.count),
+ tcpPort,
+ hostname,
+ token,
+ proxyHandlePointer
+ )
+ }
}
assert(result == 0)
diff --git a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h
index d4c0bf334f..ad15b43d39 100644
--- a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h
+++ b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h
@@ -17,16 +17,6 @@ enum SwiftAccessMethodKind {
};
typedef uint8_t SwiftAccessMethodKind;
-/**
- * SAFETY: `TunnelObfuscatorProtocol` values must either be `0` or `1`
- */
-enum TunnelObfuscatorProtocol {
- UdpOverTcp = 0,
- Shadowsocks,
- Quic,
-};
-typedef uint8_t TunnelObfuscatorProtocol;
-
typedef struct ApiContext ApiContext;
/**
@@ -930,10 +920,21 @@ int32_t start_shadowsocks_proxy(const uint8_t *forward_address,
*/
int32_t stop_shadowsocks_proxy(struct ProxyHandle *proxy_config);
-int32_t start_tunnel_obfuscator_proxy(const uint8_t *peer_address,
- uintptr_t peer_address_len,
- uint16_t peer_port,
- TunnelObfuscatorProtocol obfuscation_protocol,
- struct ProxyHandle *proxy_handle);
+int32_t start_udp2tcp_obfuscator_proxy(const uint8_t *peer_address,
+ uintptr_t peer_address_len,
+ uint16_t peer_port,
+ struct ProxyHandle *proxy_handle);
+
+int32_t start_shadowsocks_obfuscator_proxy(const uint8_t *peer_address,
+ uintptr_t peer_address_len,
+ uint16_t peer_port,
+ struct ProxyHandle *proxy_handle);
+
+int32_t start_quic_obfuscator_proxy(const uint8_t *peer_address,
+ uintptr_t peer_address_len,
+ uint16_t peer_port,
+ const char *hostname,
+ const char *token,
+ struct ProxyHandle *proxy_handle);
int32_t stop_tunnel_obfuscator_proxy(struct ProxyHandle *proxy_handle);
diff --git a/ios/MullvadSettings/WireGuardObfuscationSettings.swift b/ios/MullvadSettings/WireGuardObfuscationSettings.swift
index 80a4a6d425..b32175030f 100644
--- a/ios/MullvadSettings/WireGuardObfuscationSettings.swift
+++ b/ios/MullvadSettings/WireGuardObfuscationSettings.swift
@@ -18,9 +18,7 @@ public enum WireGuardObfuscationState: Codable, Sendable {
case automatic
case udpOverTcp
case shadowsocks
- #if DEBUG
case quic
- #endif
case off
public init(from decoder: Decoder) throws {
@@ -45,10 +43,8 @@ public enum WireGuardObfuscationState: Codable, Sendable {
self = .udpOverTcp
case .shadowsocks:
self = .shadowsocks
- #if DEBUG
case .quic:
self = .quic
- #endif
case .off:
self = .off
}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ChipView/ChipContainerView.swift b/ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ChipView/ChipContainerView.swift
index 6c31610be4..744beededc 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ChipView/ChipContainerView.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ChipView/ChipContainerView.swift
@@ -115,7 +115,8 @@ struct ChipContainerView<ViewModel>: View where ViewModel: ChipViewModelProtocol
cityCode: "gbg",
latitude: 1234,
longitude: 1234
- )
+ ),
+ features: nil
),
retryAttempt: 0
),
diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift
index eca4097f31..91b0a5db98 100644
--- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift
+++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift
@@ -224,7 +224,6 @@ final class VPNSettingsCellFactory: @preconcurrency CellFactoryProtocol {
self?.delegate?.showDetails(for: .wireguardOverShadowsocks)
}
- #if DEBUG
case .wireGuardObfuscationQuic:
guard let cell = cell as? SelectableSettingsCell else { return }
@@ -238,7 +237,7 @@ final class VPNSettingsCellFactory: @preconcurrency CellFactoryProtocol {
cell.setAccessibilityIdentifier(item.accessibilityIdentifier)
cell.detailTitleLabel.setAccessibilityIdentifier(.wireGuardObfuscationQuic)
cell.applySubCellStyling()
- #endif
+
case .wireGuardObfuscationOff:
guard let cell = cell as? SelectableSettingsCell else { return }
diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift
index 84df606905..356ae9bf5c 100644
--- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift
+++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift
@@ -83,9 +83,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource<
case wireGuardObfuscationAutomatic
case wireGuardObfuscationUdpOverTcp
case wireGuardObfuscationShadowsocks
- #if DEBUG
case wireGuardObfuscationQuic
- #endif
case wireGuardObfuscationOff
case wireGuardObfuscationPort(_ port: WireGuardObfuscationUdpOverTcpPort)
case quantumResistanceAutomatic
@@ -151,10 +149,8 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource<
.wireGuardObfuscationUdpOverTcp
case .wireGuardObfuscationShadowsocks:
.wireGuardObfuscationShadowsocks
- #if DEBUG
case .wireGuardObfuscationQuic:
.wireGuardObfuscationQuic
- #endif
case .wireGuardObfuscationOff:
.wireGuardObfuscationOff
case .wireGuardObfuscationPort:
@@ -182,13 +178,8 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource<
.wireGuardPort
case .wireGuardCustomPort:
.wireGuardCustomPort
- #if DEBUG
case .wireGuardObfuscationAutomatic, .wireGuardObfuscationOff, .wireGuardObfuscationQuic:
.wireGuardObfuscation
- #else
- case .wireGuardObfuscationAutomatic, .wireGuardObfuscationOff:
- .wireGuardObfuscation
- #endif
case .wireGuardObfuscationUdpOverTcp, .wireGuardObfuscationShadowsocks:
.wireGuardObfuscationOption
case .wireGuardObfuscationPort:
@@ -228,9 +219,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource<
case .off: .wireGuardObfuscationOff
case .on, .udpOverTcp: .wireGuardObfuscationUdpOverTcp
case .shadowsocks: .wireGuardObfuscationShadowsocks
- #if DEBUG
case .quic: .wireGuardObfuscationQuic
- #endif
}
let quantumResistanceItem: Item = switch viewModel.quantumResistance {
diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/ObfuscationMethodSelectorTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/ObfuscationMethodSelectorTests.swift
index 3694076db0..3526606d72 100644
--- a/ios/MullvadVPNTests/MullvadREST/Relay/ObfuscationMethodSelectorTests.swift
+++ b/ios/MullvadVPNTests/MullvadREST/Relay/ObfuscationMethodSelectorTests.swift
@@ -29,7 +29,7 @@ class ObfuscationMethodSelectorTests: XCTestCase {
connectionAttemptCount: attempt,
tunnelSettings: tunnelSettings
)
- if attempt.isOrdered(nth: 1, forEverySetOf: 3) {
+ if attempt.isOrdered(nth: 1, forEverySetOf: 4) {
XCTAssertEqual(method, .off)
} else {
XCTAssertNotEqual(method, .off)
@@ -53,7 +53,7 @@ class ObfuscationMethodSelectorTests: XCTestCase {
connectionAttemptCount: attempt,
tunnelSettings: tunnelSettings
)
- if attempt.isOrdered(nth: 2, forEverySetOf: 3) {
+ if attempt.isOrdered(nth: 2, forEverySetOf: 4) {
XCTAssertEqual(method, .shadowsocks)
} else {
XCTAssertNotEqual(method, .shadowsocks)
@@ -61,6 +61,30 @@ class ObfuscationMethodSelectorTests: XCTestCase {
}
}
+ func testMethodSelectionQuic() throws {
+ (UInt(0) ... 10).forEach { attempt in
+ tunnelSettings.wireGuardObfuscation = WireGuardObfuscationSettings(state: .quic)
+
+ var method = ObfuscationMethodSelector.obfuscationMethodBy(
+ connectionAttemptCount: attempt,
+ tunnelSettings: tunnelSettings
+ )
+ XCTAssertEqual(method, .quic)
+
+ tunnelSettings.wireGuardObfuscation = WireGuardObfuscationSettings(state: .automatic)
+
+ method = ObfuscationMethodSelector.obfuscationMethodBy(
+ connectionAttemptCount: attempt,
+ tunnelSettings: tunnelSettings
+ )
+ if attempt.isOrdered(nth: 3, forEverySetOf: 4) {
+ XCTAssertEqual(method, .quic)
+ } else {
+ XCTAssertNotEqual(method, .quic)
+ }
+ }
+ }
+
func testMethodSelectionUdpOverTcp() throws {
(UInt(0) ... 10).forEach { attempt in
tunnelSettings.wireGuardObfuscation = WireGuardObfuscationSettings(state: .udpOverTcp)
@@ -77,7 +101,7 @@ class ObfuscationMethodSelectorTests: XCTestCase {
connectionAttemptCount: attempt,
tunnelSettings: tunnelSettings
)
- if attempt.isOrdered(nth: 3, forEverySetOf: 3) {
+ if attempt.isOrdered(nth: 4, forEverySetOf: 4) {
XCTAssertEqual(method, .udpOverTcp)
} else {
XCTAssertNotEqual(method, .udpOverTcp)
diff --git a/ios/MullvadVPNUITests/Base/BaseUITestCase.swift b/ios/MullvadVPNUITests/Base/BaseUITestCase.swift
index df03759409..ebae341a33 100644
--- a/ios/MullvadVPNUITests/Base/BaseUITestCase.swift
+++ b/ios/MullvadVPNUITests/Base/BaseUITestCase.swift
@@ -32,9 +32,9 @@ class BaseUITestCase: XCTestCase {
/// Default relay to use in tests
static let testsDefaultRelayName = "se-got-wg-001"
- static let testsDefaultQuicCountryName = "Relay Software Country"
- static let testsDefaultQuicCityName = "Relay Software city"
- static let testsDefaultQuicRelayName = "se-got-wg-881"
+ static let testsDefaultQuicCountryName = "Ireland"
+ static let testsDefaultQuicCityName = "Dublin"
+ static let testsDefaultQuicRelayName = "ie-dub-wg-001"
/// True when the current test case is capturing packets
private var currentTestCaseShouldCapturePackets = false
diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift
index bf4feac3c1..7cd230e2fb 100644
--- a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift
+++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift
@@ -215,40 +215,6 @@ extension PacketTunnelActor {
}
/**
- Reconnect tunnel to new relays. Enters error state on failure.
-
- - Parameters:
- - nextRelay: next relays to connect to
- - reason: reason for reconnect
- */
- private func reconnect(to nextRelays: NextRelays, reason: ActorReconnectReason) async {
- do {
- switch state {
- // There is no connection monitoring going on when exchanging keys.
- // The procedure starts from scratch for each reconnection attempts.
- case .connecting, .connected, .reconnecting, .error, .negotiatingEphemeralPeer:
- switch reason {
- case .connectionLoss:
- // Tunnel monitor is already paused at this point. Avoid calling stop() to prevent the reset of
- // internal state
- break
- case .userInitiated:
- tunnelMonitor.stop()
- }
-
- try await tryStart(nextRelays: nextRelays, reason: reason)
-
- case .disconnected, .disconnecting, .initial:
- break
- }
- } catch {
- logger.error(error: error, message: "Failed to reconnect the tunnel.")
-
- await setErrorStateInternal(with: error)
- }
- }
-
- /**
Entry point for attempting to start the tunnel by performing the following steps:
- Read settings
@@ -430,7 +396,9 @@ extension PacketTunnelActor {
let obfuscated = protocolObfuscator.obfuscate(
connectionState.connectedEndpoint,
settings: settings.tunnelSettings,
- retryAttempts: connectionState.selectedRelays.retryAttempt
+ retryAttempts: connectionState.selectedRelays.retryAttempt,
+ relayFeatures: connectionState.selectedRelays.entry?.features ?? connectionState.selectedRelays.exit
+ .features
)
let transportLayer = protocolObfuscator.transportLayer.map { $0 } ?? .udp
diff --git a/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift b/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift
index b6588d6de6..868effbe71 100644
--- a/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift
+++ b/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift
@@ -21,7 +21,8 @@ public protocol ProtocolObfuscation {
func obfuscate(
_ endpoint: MullvadEndpoint,
settings: LatestTunnelSettings,
- retryAttempts: UInt
+ retryAttempts: UInt,
+ relayFeatures: REST.ServerRelay.Features?
) -> ProtocolObfuscationResult
var transportLayer: TransportLayer? { get }
var remotePort: UInt16 { get }
@@ -46,7 +47,8 @@ public class ProtocolObfuscator<Obfuscator: TunnelObfuscation>: ProtocolObfuscat
public func obfuscate(
_ endpoint: MullvadEndpoint,
settings: LatestTunnelSettings,
- retryAttempts: UInt = 0
+ retryAttempts: UInt = 0,
+ relayFeatures: REST.ServerRelay.Features?
) -> ProtocolObfuscationResult {
let obfuscationMethod = ObfuscationMethodSelector.obfuscationMethodBy(
connectionAttemptCount: retryAttempts,
@@ -55,17 +57,29 @@ public class ProtocolObfuscator<Obfuscator: TunnelObfuscation>: ProtocolObfuscat
remotePort = endpoint.ipv4Relay.port
- guard obfuscationMethod != .off else {
+ #if DEBUG
+ let obfuscationProtocol: TunnelObfuscationProtocol? = switch obfuscationMethod {
+ case .udpOverTcp:
+ .udpOverTcp
+ case .shadowsocks:
+ .shadowsocks
+ case .quic:
+ if let relayFeatures = relayFeatures?.quic {
+ .quic(hostname: relayFeatures.domain, token: relayFeatures.token)
+ } else {
+ nil
+ }
+ default:
+ // This is fine, since ObfuscationMethodSelector.obfuscationMethodBy` above should never
+ // return .automatic.
+ nil
+ }
+
+ guard let obfuscationProtocol else {
tunnelObfuscator = nil
return .init(endpoint: endpoint, method: .off)
}
- #if DEBUG
- let obfuscationProtocol: TunnelObfuscationProtocol = switch obfuscationMethod {
- case .shadowsocks: .shadowsocks
- case .quic: .quic
- default: .udpOverTcp
- }
let obfuscator = Obfuscator(
remoteAddress: endpoint.ipv4Relay.ip,
tcpPort: remotePort,
diff --git a/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift
index 16332271ed..527f6f49ca 100644
--- a/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift
+++ b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift
@@ -138,7 +138,8 @@ final class AppMessageHandlerTests: XCTestCase {
exit: SelectedRelay(
endpoint: match.endpoint,
hostname: match.relay.hostname,
- location: match.location
+ location: match.location,
+ features: nil
),
retryAttempt: 0
)
diff --git a/ios/PacketTunnelCoreTests/EphemeralPeerExchangingPipelineTests.swift b/ios/PacketTunnelCoreTests/EphemeralPeerExchangingPipelineTests.swift
index d340974131..23b231f8d3 100644
--- a/ios/PacketTunnelCoreTests/EphemeralPeerExchangingPipelineTests.swift
+++ b/ios/PacketTunnelCoreTests/EphemeralPeerExchangingPipelineTests.swift
@@ -51,12 +51,14 @@ final class EphemeralPeerExchangingPipelineTests: XCTestCase {
entryRelay = SelectedRelay(
endpoint: entryMatch.endpoint,
hostname: entryMatch.relay.hostname,
- location: entryMatch.location
+ location: entryMatch.location,
+ features: nil
)
exitRelay = SelectedRelay(
endpoint: exitMatch.endpoint,
hostname: exitMatch.relay.hostname,
- location: exitMatch.location
+ location: exitMatch.location,
+ features: nil
)
}
diff --git a/ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift b/ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift
index 77087782a3..4f65cb65e9 100644
--- a/ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift
+++ b/ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift
@@ -6,7 +6,7 @@
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//
-import Foundation
+@testable import MullvadREST
@testable import MullvadSettings
@testable import MullvadTypes
@testable import PacketTunnelCore
@@ -17,7 +17,8 @@ struct ProtocolObfuscationStub: ProtocolObfuscation {
func obfuscate(
_ endpoint: MullvadEndpoint,
settings: LatestTunnelSettings,
- retryAttempts: UInt
+ retryAttempts: UInt,
+ relayFeatures: REST.ServerRelay.Features?
) -> ProtocolObfuscationResult {
.init(endpoint: endpoint, method: .off)
}
diff --git a/ios/PacketTunnelCoreTests/MultiHopEphemeralPeerExchangerTests.swift b/ios/PacketTunnelCoreTests/MultiHopEphemeralPeerExchangerTests.swift
index c77211b5fa..9d2f5289ca 100644
--- a/ios/PacketTunnelCoreTests/MultiHopEphemeralPeerExchangerTests.swift
+++ b/ios/PacketTunnelCoreTests/MultiHopEphemeralPeerExchangerTests.swift
@@ -50,12 +50,14 @@ final class MultiHopEphemeralPeerExchangerTests: XCTestCase {
entryRelay = SelectedRelay(
endpoint: entryMatch.endpoint,
hostname: entryMatch.relay.hostname,
- location: entryMatch.location
+ location: entryMatch.location,
+ features: nil
)
exitRelay = SelectedRelay(
endpoint: exitMatch.endpoint,
hostname: exitMatch.relay.hostname,
- location: exitMatch.location
+ location: exitMatch.location,
+ features: nil
)
}
diff --git a/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift b/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift
index ba506a0519..1ef7dfe597 100644
--- a/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift
+++ b/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift
@@ -6,6 +6,7 @@
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//
+@testable import MullvadREST
@testable import MullvadSettings
@testable import MullvadTypes
import Network
@@ -33,14 +34,14 @@ final class ProtocolObfuscatorTests: XCTestCase {
func testObfuscateOffDoesNotChangeEndpoint() {
let settings = settings(.off, obfuscationPort: .automatic)
- let nonObfuscated = obfuscator.obfuscate(endpoint, settings: settings)
+ let nonObfuscated = obfuscator.obfuscate(endpoint, settings: settings, relayFeatures: nil)
XCTAssertEqual(endpoint, nonObfuscated.endpoint)
}
func testObfuscateUdpOverTcp() throws {
let settings = settings(.udpOverTcp, obfuscationPort: .automatic)
- let obfuscated = obfuscator.obfuscate(endpoint, settings: settings)
+ let obfuscated = obfuscator.obfuscate(endpoint, settings: settings, relayFeatures: nil)
let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub)
validate(obfuscated.endpoint, against: obfuscationProtocol)
@@ -48,7 +49,19 @@ final class ProtocolObfuscatorTests: XCTestCase {
func testObfuscateShadowsocks() throws {
let settings = settings(.shadowsocks, obfuscationPort: .automatic)
- let obfuscated = obfuscator.obfuscate(endpoint, settings: settings)
+ let obfuscated = obfuscator.obfuscate(endpoint, settings: settings, relayFeatures: nil)
+ let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub)
+
+ validate(obfuscated.endpoint, against: obfuscationProtocol)
+ }
+
+ func testObfuscateQuic() throws {
+ let settings = settings(.quic, obfuscationPort: .automatic)
+ let obfuscated = obfuscator.obfuscate(
+ endpoint,
+ settings: settings,
+ relayFeatures: .init(daita: nil, quic: .init(addrIn: [], domain: "", token: ""))
+ )
let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub)
validate(obfuscated.endpoint, against: obfuscationProtocol)
@@ -57,13 +70,18 @@ final class ProtocolObfuscatorTests: XCTestCase {
func testObfuscateAutomatic() throws {
let settings = settings(.automatic, obfuscationPort: .automatic)
- try (UInt(0) ... 2).forEach { attempt in
- let obfuscated = obfuscator.obfuscate(endpoint, settings: settings, retryAttempts: attempt)
+ try (UInt(0) ... 3).forEach { attempt in
+ let obfuscated = obfuscator.obfuscate(
+ endpoint,
+ settings: settings,
+ retryAttempts: attempt,
+ relayFeatures: .init(daita: nil, quic: .init(addrIn: [], domain: "", token: ""))
+ )
switch attempt {
case 0:
XCTAssertEqual(endpoint, obfuscated.endpoint)
- case 1, 2:
+ case 1, 2, 3:
let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub)
validate(obfuscated.endpoint, against: obfuscationProtocol)
default:
diff --git a/ios/PacketTunnelCoreTests/SingleHopEphemeralPeerExchangerTests.swift b/ios/PacketTunnelCoreTests/SingleHopEphemeralPeerExchangerTests.swift
index 7707ffabf7..314adbee33 100644
--- a/ios/PacketTunnelCoreTests/SingleHopEphemeralPeerExchangerTests.swift
+++ b/ios/PacketTunnelCoreTests/SingleHopEphemeralPeerExchangerTests.swift
@@ -35,7 +35,12 @@ final class SingleHopEphemeralPeerExchangerTests: XCTestCase {
numberOfFailedAttempts: 0
)
- exitRelay = SelectedRelay(endpoint: match.endpoint, hostname: match.relay.hostname, location: match.location)
+ exitRelay = SelectedRelay(
+ endpoint: match.endpoint,
+ hostname: match.relay.hostname,
+ location: match.location,
+ features: nil
+ )
}
func testEphemeralPeerExchangeFailsWhenNegotiationCannotStart() async {
diff --git a/mullvad-ios/src/api_client/access_method_settings.rs b/mullvad-ios/src/api_client/access_method_settings.rs
index 35a90d1f0c..2870047e24 100644
--- a/mullvad-ios/src/api_client/access_method_settings.rs
+++ b/mullvad-ios/src/api_client/access_method_settings.rs
@@ -11,7 +11,7 @@ use mullvad_types::access_method::{
};
use talpid_types::net::proxy::{self, Shadowsocks, Socks5Remote};
-use super::helpers::convert_c_string;
+use crate::get_string;
/// Converts parameters into a `Box<AccessMethodSetting>` raw representation that
/// can be passed across the FFI boundary
@@ -54,9 +54,9 @@ fn convert_builtin_access_method_setting_inner(
proxy_configuration: *const c_void,
) -> Option<AccessMethodSetting> {
// SAFETY: See `convert_builtin_access_method_setting`
- let id = Id::from_string(unsafe { convert_c_string(unique_identifier) })?;
+ let id = unsafe { Id::from_string(get_string(unique_identifier))? };
// SAFETY: See `convert_builtin_access_method_setting`
- let name = unsafe { convert_c_string(name) };
+ let name = unsafe { get_string(name) };
match method_kind {
SwiftAccessMethodKind::KindDirect => Some(AccessMethodSetting::with_id(
id,
diff --git a/mullvad-ios/src/api_client/address_cache_provider.rs b/mullvad-ios/src/api_client/address_cache_provider.rs
index f813683445..9d5f656c62 100644
--- a/mullvad-ios/src/api_client/address_cache_provider.rs
+++ b/mullvad-ios/src/api_client/address_cache_provider.rs
@@ -1,7 +1,8 @@
-use super::helpers::convert_c_string;
use libc::c_char;
use std::{ffi::c_void, net::SocketAddr};
+use super::get_string;
+
extern "C" {
/// Return the latest available endpoint, or a default one if none are cached
///
@@ -58,7 +59,7 @@ impl SwiftAddressCacheProviderContext {
// SAFETY: The pointer contained in the late deallocator returned by `swift_get_cached_endpoint`
// is guaranteed to point to a valid UTF-8 String
// It is also guaranteed to be a valid representation of either an IPv4 or IPv6 address
- let cached_address = unsafe { convert_c_string(deallocator.ptr) }
+ let cached_address = unsafe { get_string(deallocator.ptr) }
.parse()
.expect("Invalid socket address in cache");
diff --git a/mullvad-ios/src/api_client/helpers.rs b/mullvad-ios/src/api_client/helpers.rs
index f331e56a9b..556b679d71 100644
--- a/mullvad-ios/src/api_client/helpers.rs
+++ b/mullvad-ios/src/api_client/helpers.rs
@@ -1,10 +1,12 @@
use std::{
- ffi::{c_char, c_void, CStr},
+ ffi::{c_char, c_void},
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
};
use talpid_types::net::proxy::{Shadowsocks, Socks5Remote, SocksAuth};
+use super::get_string;
+
/// Constructs a new IP address from a pointer containing bytes representing an IP address.
///
/// SAFETY: `addr` pointer must be non-null, aligned, and point to at least addr_len bytes
@@ -30,16 +32,6 @@ pub(crate) unsafe fn parse_ip_addr(addr: *const u8, addr_len: usize) -> Option<I
}
}
-/// Converts a pointer to a C style string into an owned Rust `String`
-///
-/// # SAFETY
-/// `c_str` must point to a valid, null terminated C string.
-pub unsafe fn convert_c_string(c_str: *const c_char) -> String {
- // SAFETY: c_str points to a valid region of memory and contains a null terminator.
- let str = unsafe { CStr::from_ptr(c_str) };
- String::from_utf8_lossy(str.to_bytes()).into_owned()
-}
-
/// Converts parameters into a boxed `Shadowsocks` configuration that is safe
/// to send across the FFI boundary
///
@@ -60,8 +52,8 @@ pub unsafe extern "C" fn new_shadowsocks_access_method_setting(
return std::ptr::null();
};
- let password = convert_c_string(c_password);
- let cipher = convert_c_string(c_cipher);
+ let password = get_string(c_password);
+ let cipher = get_string(c_cipher);
let shadowsocks_configuration = Shadowsocks {
endpoint,
@@ -97,8 +89,8 @@ pub unsafe extern "C" fn new_socks5_access_method_setting(
if c_username.is_null() || c_password.is_null() {
None
} else {
- let username = convert_c_string(c_username);
- let password = convert_c_string(c_password);
+ let username = get_string(c_username);
+ let password = get_string(c_password);
SocksAuth::new(username, password).ok()
}
};
diff --git a/mullvad-ios/src/api_client/mod.rs b/mullvad-ios/src/api_client/mod.rs
index 4e875fe4c6..1c45fb3397 100644
--- a/mullvad-ios/src/api_client/mod.rs
+++ b/mullvad-ios/src/api_client/mod.rs
@@ -1,9 +1,9 @@
-use std::{ffi::c_char, ffi::CStr, future::Future, sync::Arc};
+use std::{ffi::c_char, future::Future, sync::Arc};
+use crate::get_string;
use access_method_resolver::SwiftAccessMethodResolver;
use access_method_settings::SwiftAccessMethodSettingsWrapper;
use address_cache_provider::SwiftAddressCacheWrapper;
-use helpers::convert_c_string;
use mullvad_api::{
access_mode::{AccessModeSelector, AccessModeSelectorHandle},
rest::{self, MullvadRestHandle},
@@ -108,8 +108,8 @@ pub unsafe extern "C" fn mullvad_api_use_access_method(
access_method_id: *const c_char,
) {
let api_context = api_context.rust_context();
- // SAFETY: See Safety notes for `convert_c_string`
- let id = unsafe { convert_c_string(access_method_id) };
+ // SAFETY: See Safety notes for `get_string`
+ let id = get_string(access_method_id);
let Some(id) = Id::from_string(id) else {
return;
@@ -214,14 +214,9 @@ pub extern "C" fn mullvad_api_init_inner(
settings_provider: SwiftAccessMethodSettingsWrapper,
address_cache: SwiftAddressCacheWrapper,
) -> SwiftApiContext {
- // Safety: See notes for `convert_c_string`
- let (host, address, domain) = unsafe {
- (
- convert_c_string(host),
- convert_c_string(address),
- convert_c_string(domain),
- )
- };
+ // Safety: See notes for `get_string`
+ let (host, address, domain) =
+ unsafe { (get_string(host), get_string(address), get_string(domain)) };
// The iOS client provides a different default endpoint based on its configuration
// Debug and Release builds use the standard endpoints
@@ -314,17 +309,3 @@ where
retry_future(future_factory, should_retry, retry_strategy.delays()).await
}
-
-/// Try to convert a C string to an owned [String]. if `ptr` is null, an empty [String] is
-/// returned.
-///
-/// # Safety
-/// - `ptr` must uphold all safety invariants as required by [CStr::from_ptr].
-fn get_string(ptr: *const c_char) -> String {
- if ptr.is_null() {
- return String::new();
- }
- // Safety: See function doc comment.
- let cstr = unsafe { CStr::from_ptr(ptr) };
- cstr.to_str().map(ToOwned::to_owned).unwrap_or_default()
-}
diff --git a/mullvad-ios/src/api_client/storekit.rs b/mullvad-ios/src/api_client/storekit.rs
index 61967eec9b..994e6628d7 100644
--- a/mullvad-ios/src/api_client/storekit.rs
+++ b/mullvad-ios/src/api_client/storekit.rs
@@ -9,8 +9,7 @@ use mullvad_types::account::AccountNumber;
use super::{
cancellation::{RequestCancelHandle, SwiftCancelHandle},
completion::{CompletionCookie, SwiftCompletionHandler},
- do_request,
- helpers::convert_c_string,
+ do_request, get_string,
response::SwiftMullvadApiResponse,
retry_strategy::{RetryStrategy, SwiftRetryStrategy},
SwiftApiContext,
@@ -58,7 +57,7 @@ pub unsafe extern "C" fn mullvad_ios_legacy_storekit_payment(
let completion = completion_handler.clone();
// SAFETY: See param documentation for `account_number`.
- let account_number = unsafe { AccountNumber::from(convert_c_string(account_number)) };
+ let account_number = AccountNumber::from(get_string(account_number));
// SAFETY: See param documentation for `body`.
let body = unsafe { std::slice::from_raw_parts(body, body_size) }.to_vec();
@@ -133,7 +132,7 @@ pub unsafe extern "C" fn mullvad_ios_init_storekit_payment(
let completion = completion_handler.clone();
// SAFETY: See param documentation for `account_number`.
- let account_number = unsafe { AccountNumber::from(convert_c_string(account_number)) };
+ let account_number = AccountNumber::from(get_string(account_number));
let task = tokio_handle.spawn(async move {
match mullvad_ios_init_storekit_payment_inner(
@@ -208,7 +207,7 @@ pub unsafe extern "C" fn mullvad_ios_check_storekit_payment(
let completion = completion_handler.clone();
// SAFETY: See param documentation for `account_number`.
- let account_number = unsafe { AccountNumber::from(convert_c_string(account_number)) };
+ let account_number = AccountNumber::from(get_string(account_number));
// SAFETY: See param documentation for `body`.
let body = unsafe { std::slice::from_raw_parts(body, body_size) }.to_vec();
diff --git a/mullvad-ios/src/lib.rs b/mullvad-ios/src/lib.rs
index fa23672e29..b98960f158 100644
--- a/mullvad-ios/src/lib.rs
+++ b/mullvad-ios/src/lib.rs
@@ -1,4 +1,9 @@
#![cfg(target_os = "ios")]
+use libc::c_char;
+use std::ffi::CStr;
+use std::sync::OnceLock;
+use tokio::runtime::{Builder, Handle, Runtime};
+
mod api_client;
mod encrypted_dns_proxy;
mod ephemeral_peer_proxy;
@@ -14,23 +19,30 @@ pub struct ProxyHandle {
#[unsafe(no_mangle)]
pub static CONFIG_SERVICE_PORT: u16 = talpid_tunnel_config_client::CONFIG_SERVICE_PORT;
-mod ios {
- use std::sync::OnceLock;
- use tokio::runtime::{Builder, Handle, Runtime};
-
- static RUNTIME: OnceLock<Result<Runtime, String>> = OnceLock::new();
+static RUNTIME: OnceLock<Result<Runtime, String>> = OnceLock::new();
- pub fn mullvad_ios_runtime() -> Result<Handle, String> {
- match RUNTIME.get_or_init(|| {
- Builder::new_multi_thread()
- .enable_all()
- .build()
- .map_err(|error| ToString::to_string(&error))
- }) {
- Ok(runtime) => Ok(runtime.handle().clone()),
- Err(error) => Err(error.clone()),
- }
+fn mullvad_ios_runtime() -> Result<Handle, String> {
+ match RUNTIME.get_or_init(|| {
+ Builder::new_multi_thread()
+ .enable_all()
+ .build()
+ .map_err(|error| ToString::to_string(&error))
+ }) {
+ Ok(runtime) => Ok(runtime.handle().clone()),
+ Err(error) => Err(error.clone()),
}
}
-use ios::*;
+/// Try to convert a C string to an owned [String]. if `ptr` is null, an empty [String] is
+/// returned.
+///
+/// # Safety
+/// - `ptr` must uphold all safety invariants as required by [CStr::from_ptr].
+unsafe fn get_string(ptr: *const c_char) -> String {
+ if ptr.is_null() {
+ return String::new();
+ }
+ // Safety: See function doc comment.
+ let cstr = unsafe { CStr::from_ptr(ptr) };
+ cstr.to_str().map(ToOwned::to_owned).unwrap_or_default()
+}
diff --git a/mullvad-ios/src/tunnel_obfuscator_proxy/ffi.rs b/mullvad-ios/src/tunnel_obfuscator_proxy/ffi.rs
index 732d889e89..849d4336cc 100644
--- a/mullvad-ios/src/tunnel_obfuscator_proxy/ffi.rs
+++ b/mullvad-ios/src/tunnel_obfuscator_proxy/ffi.rs
@@ -1,52 +1,131 @@
+use libc::c_char;
+
use super::{TunnelObfuscatorHandle, TunnelObfuscatorRuntime};
use crate::ProxyHandle;
use std::{net::SocketAddr, sync::Once};
-use crate::api_client::helpers::parse_ip_addr;
+use crate::{api_client::helpers::parse_ip_addr, get_string};
static INIT_LOGGING: Once = Once::new();
-/// SAFETY: `TunnelObfuscatorProtocol` values must either be `0` or `1`
-#[repr(u8)]
-pub enum TunnelObfuscatorProtocol {
- UdpOverTcp = 0,
- Shadowsocks,
- Quic,
+macro_rules! throw_int_error {
+ ($result:expr) => {
+ match $result {
+ Ok(value) => value,
+ Err(value) => return value,
+ }
+ };
+}
+
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn start_udp2tcp_obfuscator_proxy(
+ peer_address: *const u8,
+ peer_address_len: usize,
+ peer_port: u16,
+ proxy_handle: *mut ProxyHandle,
+) -> i32 {
+ init_logging();
+
+ let peer_sock_addr = throw_int_error!(get_socket_address(
+ peer_address,
+ peer_address_len,
+ peer_port
+ ));
+ let result = TunnelObfuscatorRuntime::new_udp2tcp(peer_sock_addr).run();
+
+ start(proxy_handle, result)
+}
+
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn start_shadowsocks_obfuscator_proxy(
+ peer_address: *const u8,
+ peer_address_len: usize,
+ peer_port: u16,
+ proxy_handle: *mut ProxyHandle,
+) -> i32 {
+ init_logging();
+
+ let peer_sock_addr = throw_int_error!(get_socket_address(
+ peer_address,
+ peer_address_len,
+ peer_port
+ ));
+ let result = TunnelObfuscatorRuntime::new_shadowsocks(peer_sock_addr).run();
+
+ start(proxy_handle, result)
}
#[unsafe(no_mangle)]
-pub unsafe extern "C" fn start_tunnel_obfuscator_proxy(
+pub unsafe extern "C" fn start_quic_obfuscator_proxy(
peer_address: *const u8,
peer_address_len: usize,
peer_port: u16,
- obfuscation_protocol: TunnelObfuscatorProtocol,
+ hostname: *const c_char,
+ token: *const c_char,
proxy_handle: *mut ProxyHandle,
) -> i32 {
+ init_logging();
+
+ let peer_sock_addr = throw_int_error!(get_socket_address(
+ peer_address,
+ peer_address_len,
+ peer_port
+ ));
+ let hostname = get_string(hostname);
+ let token = get_string(token);
+ let result = TunnelObfuscatorRuntime::new_quic(peer_sock_addr, hostname, token).run();
+
+ start(proxy_handle, result)
+}
+
+fn init_logging() {
INIT_LOGGING.call_once(|| {
let _ = oslog::OsLogger::new("net.mullvad.MullvadVPN.TunnelObfuscatorProxy")
.level_filter(log::LevelFilter::Info)
.init();
});
+}
- let peer_sock_addr: SocketAddr =
- if let Some(ip_address) = parse_ip_addr(peer_address, peer_address_len) {
+/// Constructs a new IP address from a pointer containing bytes representing an IP address.
+///
+/// SAFETY: `addr` pointer must be non-null, aligned, and point to at least addr_len bytes
+unsafe fn get_socket_address(
+ peer_address: *const u8,
+ peer_address_len: usize,
+ peer_port: u16,
+) -> Result<SocketAddr, i32> {
+ let peer_sock_addr =
+ // SAFETY: See notes for `parse_ip_addr`.
+ if let Some(ip_address) = unsafe { parse_ip_addr(peer_address, peer_address_len) } {
SocketAddr::new(ip_address, peer_port)
} else {
- return -1;
+ return Err(-1);
};
+ Ok(peer_sock_addr)
+}
- let result = TunnelObfuscatorRuntime::new(peer_sock_addr, obfuscation_protocol).run();
-
+/// # Safety
+///
+/// Behavior is undefined if any of the following conditions are violated:
+///
+/// * `proxy_handle` must be [valid] for writes.
+/// * `proxy_handle` must be properly aligned. Use [`write_unaligned`] if this is not the
+/// case.
+unsafe fn start(
+ proxy_handle: *mut ProxyHandle,
+ result: Result<(SocketAddr, TunnelObfuscatorHandle), std::io::Error>,
+) -> i32 {
match result {
Ok((local_endpoint, obfuscator_handle)) => {
let boxed_handle = Box::new(obfuscator_handle);
- std::ptr::write(
- proxy_handle,
- ProxyHandle {
- context: Box::into_raw(boxed_handle) as *mut _,
- port: local_endpoint.port(),
- },
- );
+ let source_handle = ProxyHandle {
+ context: Box::into_raw(boxed_handle) as *mut _,
+ port: local_endpoint.port(),
+ };
+
+ // SAFETY: Caller guarantees that this pointer is valid.
+ unsafe { std::ptr::write(proxy_handle, source_handle) };
+
0
}
Err(err) => {
diff --git a/mullvad-ios/src/tunnel_obfuscator_proxy/mod.rs b/mullvad-ios/src/tunnel_obfuscator_proxy/mod.rs
index bb208db57e..5faeab7162 100644
--- a/mullvad-ios/src/tunnel_obfuscator_proxy/mod.rs
+++ b/mullvad-ios/src/tunnel_obfuscator_proxy/mod.rs
@@ -1,4 +1,3 @@
-use ffi::TunnelObfuscatorProtocol;
use std::{
io,
net::{Ipv4Addr, SocketAddr},
@@ -17,27 +16,26 @@ pub struct TunnelObfuscatorRuntime {
}
impl TunnelObfuscatorRuntime {
- pub fn new(peer: SocketAddr, obfuscation_protocol: TunnelObfuscatorProtocol) -> Self {
- let settings: ObfuscationSettings = match obfuscation_protocol {
- TunnelObfuscatorProtocol::UdpOverTcp => {
- ObfuscationSettings::Udp2Tcp(udp2tcp::Settings { peer })
- }
- TunnelObfuscatorProtocol::Shadowsocks => {
- ObfuscationSettings::Shadowsocks(shadowsocks::Settings {
- shadowsocks_endpoint: peer,
- wireguard_endpoint: SocketAddr::from((Ipv4Addr::LOCALHOST, 51820)),
- })
- }
- TunnelObfuscatorProtocol::Quic => {
- ObfuscationSettings::Quic(quic::Settings {
- quic_endpoint: peer,
- wireguard_endpoint: SocketAddr::from((Ipv4Addr::LOCALHOST, 51820)),
- // TODO: fetch the real hostname from the relay list
- hostname: "www.mullvad.net".to_string(),
- })
- }
- };
+ pub fn new_udp2tcp(peer: SocketAddr) -> Self {
+ let settings = ObfuscationSettings::Udp2Tcp(udp2tcp::Settings { peer });
+ Self { settings }
+ }
+ pub fn new_shadowsocks(peer: SocketAddr) -> Self {
+ let settings = ObfuscationSettings::Shadowsocks(shadowsocks::Settings {
+ shadowsocks_endpoint: peer,
+ wireguard_endpoint: SocketAddr::from((Ipv4Addr::LOCALHOST, 51820)),
+ });
+ Self { settings }
+ }
+
+ pub fn new_quic(peer: SocketAddr, hostname: String, token: String) -> Self {
+ let settings = ObfuscationSettings::Quic(quic::Settings {
+ quic_endpoint: peer,
+ wireguard_endpoint: SocketAddr::from((Ipv4Addr::LOCALHOST, 51820)),
+ hostname,
+ token,
+ });
Self { settings }
}
diff --git a/talpid-wireguard/src/obfuscation.rs b/talpid-wireguard/src/obfuscation.rs
index b214e89ae0..6677d630cb 100644
--- a/talpid-wireguard/src/obfuscation.rs
+++ b/talpid-wireguard/src/obfuscation.rs
@@ -16,6 +16,9 @@ use tunnel_obfuscation::{
create_obfuscator, quic, shadowsocks, udp2tcp, Settings as ObfuscationSettings,
};
+/// Test authentication header to set for the CONNECT request.
+const AUTH_HEADER: &str = "test";
+
/// Begin running obfuscation machine, if configured. This function will patch `config`'s endpoint
/// to point to an endpoint on localhost
pub async fn apply_obfuscation_config(
@@ -101,6 +104,7 @@ fn settings_from_config(
quic_endpoint: *endpoint,
wireguard_endpoint: SocketAddr::from((Ipv4Addr::LOCALHOST, 51820)),
hostname: hostname.to_owned(),
+ token: AUTH_HEADER.to_owned(),
#[cfg(target_os = "linux")]
fwmark,
})
diff --git a/tunnel-obfuscation/src/quic.rs b/tunnel-obfuscation/src/quic.rs
index 1230e53ad1..e0b16ecb66 100644
--- a/tunnel-obfuscation/src/quic.rs
+++ b/tunnel-obfuscation/src/quic.rs
@@ -12,9 +12,6 @@ use crate::Obfuscator;
type Result<T> = std::result::Result<T, Error>;
-/// Authentication header to set for the CONNECT request
-const AUTH_HEADER: &str = "Bearer test";
-
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Failed to bind UDP socket")]
@@ -36,6 +33,8 @@ pub struct Settings {
pub wireguard_endpoint: SocketAddr,
/// Hostname to use for QUIC
pub hostname: String,
+ /// Auth token to use for QUIC. Must NOT be prefixed with "Bearer".
+ pub token: String,
/// fwmark to apply to use for the QUIC connection
#[cfg(target_os = "linux")]
pub fwmark: Option<u32>,
@@ -48,6 +47,7 @@ impl Quic {
.map_err(Error::BindError)?;
let local_endpoint = local_socket.local_addr().unwrap();
+ let token = settings.token.clone();
let config_builder = ClientConfig::builder()
.client_socket(local_socket)
@@ -55,7 +55,7 @@ impl Quic {
.server_addr(settings.quic_endpoint)
.server_host(settings.hostname.clone())
.target_addr(settings.wireguard_endpoint)
- .auth_header(Some(AUTH_HEADER.to_owned()));
+ .auth_header(Some(format!("Bearer {token}").to_owned()));
#[cfg(target_os = "linux")]
let config_builder = config_builder.fwmark(settings.fwmark);