diff options
| author | Emīls <emils@mullvad.net> | 2024-02-06 23:07:37 +0100 |
|---|---|---|
| committer | Bug Magnet <marco.nikic@mullvad.net> | 2024-05-21 13:32:17 +0200 |
| commit | e5ee91b2e65041bd2b347b8c5651cebf09861c64 (patch) | |
| tree | 42a3c1181627b961d92318a8ccc2cda9efebbe49 | |
| parent | cba171ca2232838b3e7628221ed1b5a56f93230f (diff) | |
| download | mullvadvpn-e5ee91b2e65041bd2b347b8c5651cebf09861c64.tar.xz mullvadvpn-e5ee91b2e65041bd2b347b8c5651cebf09861c64.zip | |
Implement PQ PSK
50 files changed, 2009 insertions, 442 deletions
diff --git a/Cargo.lock b/Cargo.lock index 34c23cef16..07fb64dcd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4070,9 +4070,11 @@ dependencies = [ name = "talpid-tunnel-config-client" version = "0.0.0" dependencies = [ + "cbindgen", "classic-mceliece-rust", "libc", "log", + "oslog", "pqc_kyber", "prost", "rand 0.8.5", diff --git a/Cargo.toml b/Cargo.toml index aa3b9145c8..f7fcd70642 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,7 +80,7 @@ shadowsocks-service = { version = "1.16" } windows-sys = "0.52.0" -chrono = { version = "0.4.26", default-features = false} +chrono = { version = "0.4.26", default-features = false } clap = { version = "4.4.18", features = ["cargo", "derive"] } once_cell = "1.13" diff --git a/ios/MullvadPostQuantum/MullvadPostQuantum.h b/ios/MullvadPostQuantum/MullvadPostQuantum.h new file mode 100644 index 0000000000..f48ca77519 --- /dev/null +++ b/ios/MullvadPostQuantum/MullvadPostQuantum.h @@ -0,0 +1,19 @@ +// +// MullvadPostQuantum.h +// MullvadPostQuantum +// +// Created by Marco Nikic on 2024-02-27. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +#import <Foundation/Foundation.h> + +//! Project version number for MullvadPostQuantum. +FOUNDATION_EXPORT double MullvadPostQuantumVersionNumber; + +//! Project version string for MullvadPostQuantum. +FOUNDATION_EXPORT const unsigned char MullvadPostQuantumVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import <MullvadPostQuantum/PublicHeader.h> + + diff --git a/ios/MullvadPostQuantum/PacketTunnelProvider+TCPConnection.swift b/ios/MullvadPostQuantum/PacketTunnelProvider+TCPConnection.swift new file mode 100644 index 0000000000..8b06c7b178 --- /dev/null +++ b/ios/MullvadPostQuantum/PacketTunnelProvider+TCPConnection.swift @@ -0,0 +1,78 @@ +// +// PacketTunnelProvider+TCPConnection.swift +// PacketTunnel +// +// Created by Marco Nikic on 2024-02-15. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadTypes +import NetworkExtension +import TalpidTunnelConfigClientProxy +import WireGuardKitTypes + +@_cdecl("swift_nw_tcp_connection_send") +func tcpConnectionSend( + connection: UnsafeMutableRawPointer?, + data: UnsafeMutableRawPointer, + dataLength: UInt, + sender: UnsafeMutableRawPointer? +) { + guard let connection, let sender else { return } + let tcpConnection = Unmanaged<NWTCPConnection>.fromOpaque(connection).takeUnretainedValue() + let rawData = Data(bytes: data, count: Int(dataLength)) + + // The guarantee that no more than 2 writes happen in parallel is done by virtue of not returning the execution context + // to Rust before this closure is done executing. + tcpConnection.write(rawData, completionHandler: { maybeError in + if maybeError != nil { + handle_sent(0, sender) + } else { + handle_sent(dataLength, sender) + } + }) +} + +@_cdecl("swift_nw_tcp_connection_read") +func tcpConnectionReceive( + connection: UnsafeMutableRawPointer?, + sender: UnsafeMutableRawPointer? +) { + guard let connection, let sender else { return } + let tcpConnection = Unmanaged<NWTCPConnection>.fromOpaque(connection).takeUnretainedValue() + tcpConnection.readMinimumLength(0, maximumLength: Int(UInt16.max)) { data, maybeError in + if let data { + if maybeError != nil { + handle_recv(Data().map { $0 }, 0, sender) + } else { + handle_recv(data.map { $0 }, UInt(data.count), sender) + } + } + } +} + +@_cdecl("swift_post_quantum_key_ready") +func receivePostQuantumKey( + rawPacketTunnel: UnsafeMutableRawPointer?, + rawPresharedKey: UnsafeMutableRawPointer?, + rawEphemeralKey: UnsafeMutableRawPointer? +) { + guard + let rawPacketTunnel, + let postQuantumKeyReceiver = Unmanaged<NEPacketTunnelProvider>.fromOpaque(rawPacketTunnel) + .takeUnretainedValue() as? PostQuantumKeyReceiving + else { return } + + guard + let rawPresharedKey, + let rawEphemeralKey, + let ephemeralKey = PrivateKey(rawValue: Data(bytes: rawEphemeralKey, count: 32)), + let key = PreSharedKey(rawValue: Data(bytes: rawPresharedKey, count: 32)) + else { + postQuantumKeyReceiver.keyExchangeFailed() + return + } + + postQuantumKeyReceiver.receivePostQuantumKey(key, ephemeralKey: ephemeralKey) +} diff --git a/ios/MullvadPostQuantum/PostQuantumKeyNegotiator.swift b/ios/MullvadPostQuantum/PostQuantumKeyNegotiator.swift new file mode 100644 index 0000000000..8e813f47df --- /dev/null +++ b/ios/MullvadPostQuantum/PostQuantumKeyNegotiator.swift @@ -0,0 +1,54 @@ +// +// PostQuantumKeyNegotiator.swift +// PacketTunnel +// +// Created by Marco Nikic on 2024-02-16. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import NetworkExtension +import TalpidTunnelConfigClientProxy +import WireGuardKitTypes + +public class PostQuantumKeyNegotiator { + public init() {} + + var cancelToken: PostQuantumCancelToken? + + public func negotiateKey( + gatewayIP: IPv4Address, + devicePublicKey: PublicKey, + presharedKey: PrivateKey, + packetTunnel: NEPacketTunnelProvider, + tcpConnection: NWTCPConnection + ) { + let packetTunnelPointer = Unmanaged.passUnretained(packetTunnel).toOpaque() + let opaqueConnection = Unmanaged.passUnretained(tcpConnection).toOpaque() + var cancelToken = PostQuantumCancelToken() + + // TODO: Any non 0 return is considered a failure, and should be handled gracefully + let result = negotiate_post_quantum_key( + devicePublicKey.rawValue.map { $0 }, + presharedKey.rawValue.map { $0 }, + packetTunnelPointer, + opaqueConnection, + &cancelToken + ) + guard result == 0 else { + // Handle failure here + return + } + self.cancelToken = cancelToken + } + + public func cancelKeyNegotiation() { + guard var cancelToken else { return } + cancel_post_quantum_key_exchange(&cancelToken) + } + + deinit { + guard var cancelToken else { return } + drop_post_quantum_key_exchange_token(&cancelToken) + } +} diff --git a/ios/MullvadPostQuantum/module.private.modulemap b/ios/MullvadPostQuantum/module.private.modulemap new file mode 100644 index 0000000000..f831c7ca2e --- /dev/null +++ b/ios/MullvadPostQuantum/module.private.modulemap @@ -0,0 +1,5 @@ +framework module TalpidTunnelConfigClientProxy { + header "talpid_tunnel_config_client.h" + link "libtalpid_tunnel_config_client" + export * +} diff --git a/ios/MullvadPostQuantum/talpid-tunnel-config-client/include/talpid_tunnel_config_client.h b/ios/MullvadPostQuantum/talpid-tunnel-config-client/include/talpid_tunnel_config_client.h new file mode 100644 index 0000000000..7b9315c12f --- /dev/null +++ b/ios/MullvadPostQuantum/talpid-tunnel-config-client/include/talpid_tunnel_config_client.h @@ -0,0 +1,71 @@ +#include <stdarg.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdlib.h> + +/** + * Port used by the tunnel config service. + */ +#define CONFIG_SERVICE_PORT 1337 + +typedef struct PostQuantumCancelToken { + void *context; +} PostQuantumCancelToken; + +/** + * * # Safety * `sender` must be pointing to a valid instance of a `PostQuantumCancelToken` created by the `PacketTunnelProvider` + */ +void cancel_post_quantum_key_exchange(const struct PostQuantumCancelToken *sender); + +/** + * * # Safety * `sender` must be pointing to a valid instance of a `PostQuantumCancelToken` created by the `PacketTunnelProvider`. + */ +void drop_post_quantum_key_exchange_token(const struct PostQuantumCancelToken *sender); + +/** + * * # Safety * `sender` must be pointing to a valid instance of a `write_tx` created by the `IosTcpProvider` * * Callback to call when the TCP connection has written data. + */ +void handle_sent(uintptr_t bytes_sent, + const void *sender); + +/** + * * # Safety * `sender` must be pointing to a valid instance of a `read_tx` created by the `IosTcpProvider` * * Callback to call when the TCP connection has received data. + */ +void handle_recv(const uint8_t *data, + uintptr_t data_len, + const void *sender); + +/** + * Entry point for exchanging post quantum keys on iOS. + * The TCP connection must be created to go through the tunnel. + * # Safety + * This function is safe to call + */ +int32_t negotiate_post_quantum_key(const uint8_t *public_key, + const uint8_t *ephemeral_key, + const void *packet_tunnel, + const void *tcp_connection, + struct PostQuantumCancelToken *cancel_token); + +/** + * Called when there is data to send on the TCP connection. + * The TCP connection must write data on the wire, then call the `handle_sent` function. + */ +extern void swift_nw_tcp_connection_send(const void *connection, + const void *data, + uintptr_t data_len, + const void *sender); + +/** + * Called when there is data to read on the TCP connection. + * The TCP connection must read data from the wire, then call the `handle_read` function. + */ +extern void swift_nw_tcp_connection_read(const void *connection, const void *sender); + +/** + * Called when the preshared post quantum key is ready. + * `raw_preshared_key` might be NULL if the key negotiation failed. + */ +extern void swift_post_quantum_key_ready(const void *raw_packet_tunnel, + const uint8_t *raw_preshared_key, + const uint8_t *raw_ephemeral_private_key); diff --git a/ios/MullvadSettings/QuantumResistanceSettings.swift b/ios/MullvadSettings/QuantumResistanceSettings.swift index b5c12ae703..956b2fd0de 100644 --- a/ios/MullvadSettings/QuantumResistanceSettings.swift +++ b/ios/MullvadSettings/QuantumResistanceSettings.swift @@ -13,3 +13,10 @@ public enum TunnelQuantumResistance: Codable { case on case off } + +public extension TunnelQuantumResistance { + /// A single source of truth for whether the current state counts as on + var isEnabled: Bool { + self == .on + } +} diff --git a/ios/MullvadTypes/Protocols/PostQuantumKeyReceiving.swift b/ios/MullvadTypes/Protocols/PostQuantumKeyReceiving.swift index eb696fcca3..50809c50b1 100644 --- a/ios/MullvadTypes/Protocols/PostQuantumKeyReceiving.swift +++ b/ios/MullvadTypes/Protocols/PostQuantumKeyReceiving.swift @@ -10,18 +10,10 @@ import Foundation import WireGuardKitTypes public protocol PostQuantumKeyReceiving { - func receivePostQuantumKey(_ key: PreSharedKey) + func receivePostQuantumKey(_ key: PreSharedKey, ephemeralKey: PrivateKey) + func keyExchangeFailed() } public enum PostQuantumKeyReceivingError: Error { case invalidKey } - -public extension PostQuantumKeyReceiving { - func receivePostQuantumKey(_ keyData: Data) throws { - guard let key = PreSharedKey(rawValue: keyData) else { - throw PostQuantumKeyReceivingError.invalidKey - } - receivePostQuantumKey(key) - } -} diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 776943004b..c5b6fb9042 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -44,6 +44,9 @@ 449EBA262B975B9700DFA4EB /* PostQuantumKeyReceiving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449EBA252B975B9700DFA4EB /* PostQuantumKeyReceiving.swift */; }; 44B02E3B2BC5732D008EDF34 /* LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44B02E3A2BC5732D008EDF34 /* LoggingTests.swift */; }; 44B02E3C2BC5B8A5008EDF34 /* Bundle+ProductVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */; }; + 44BB5F972BE527F4002520EB /* TunnelState+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44BB5F962BE527F4002520EB /* TunnelState+UI.swift */; }; + 44BB5F982BE527F4002520EB /* TunnelState+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44BB5F962BE527F4002520EB /* TunnelState+UI.swift */; }; + 44BB5F9A2BE529FF002520EB /* TunnelStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44BB5F992BE529FE002520EB /* TunnelStateTests.swift */; }; 44DD7D242B6CFFD70005F67F /* StartTunnelOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D232B6CFFD70005F67F /* StartTunnelOperationTests.swift */; }; 44DD7D272B6D18FB0005F67F /* MockTunnelInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D262B6D18FB0005F67F /* MockTunnelInteractor.swift */; }; 44DD7D292B7113CA0005F67F /* MockTunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D282B7113CA0005F67F /* MockTunnel.swift */; }; @@ -683,15 +686,26 @@ A917352129FAAA5200D5DCFD /* TransportStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */; }; A91D78E32B03BDF200FCD5D3 /* TunnelObfuscation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5840231F2A406BF5007B27AC /* TunnelObfuscation.framework */; }; A91D78E42B03C01600FCD5D3 /* MullvadSettings.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58B2FDD32AA71D2A003EB5C6 /* MullvadSettings.framework */; }; + A9259FD22B8E06E90032C82B /* MullvadPostQuantum.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A944F25C2B8DEFDB00473F4C /* MullvadPostQuantum.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; A93181A12B727ED700E341D2 /* TunnelSettingsV4.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93181A02B727ED700E341D2 /* TunnelSettingsV4.swift */; }; A932D9EF2B5ADD0700999395 /* ProxyConfigurationTransportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A932D9EE2B5ADD0700999395 /* ProxyConfigurationTransportProvider.swift */; }; A932D9F32B5EB61100999395 /* HeadRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A932D9F22B5EB61100999395 /* HeadRequestTests.swift */; }; A932D9F52B5EBB9D00999395 /* RESTTransportStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = A932D9F42B5EBB9D00999395 /* RESTTransportStub.swift */; }; A935594C2B4C2DA900D5D524 /* APIAvailabilityTestRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A935594B2B4C2DA900D5D524 /* APIAvailabilityTestRequest.swift */; }; + A944F25F2B8DEFDB00473F4C /* MullvadPostQuantum.h in Headers */ = {isa = PBXBuildFile; fileRef = A944F25E2B8DEFDB00473F4C /* MullvadPostQuantum.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A944F2622B8DEFDB00473F4C /* MullvadPostQuantum.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A944F25C2B8DEFDB00473F4C /* MullvadPostQuantum.framework */; }; + A944F2632B8DEFDB00473F4C /* MullvadPostQuantum.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A944F25C2B8DEFDB00473F4C /* MullvadPostQuantum.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A944F26A2B8DF32900473F4C /* PostQuantumKeyNegotiator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9EB4F9C2B7FAB21002A2D7A /* PostQuantumKeyNegotiator.swift */; }; + A944F2722B8E02F600473F4C /* libtalpid_tunnel_config_client.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A944F2712B8E02E800473F4C /* libtalpid_tunnel_config_client.a */; }; + A948809B2BC9308D0090A44C /* PostQuantumKeyExchangeActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A948809A2BC9308D0090A44C /* PostQuantumKeyExchangeActor.swift */; }; A94D691A2ABAD66700413DD4 /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 58FE25E22AA72AE9003D1918 /* WireGuardKitTypes */; }; A94D691B2ABAD66700413DD4 /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 58FE25E72AA7399D003D1918 /* WireGuardKitTypes */; }; A95EEE362B722CD600A8A39B /* TunnelMonitorState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A95EEE352B722CD600A8A39B /* TunnelMonitorState.swift */; }; A95EEE382B722DFC00A8A39B /* PingStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = A95EEE372B722DFC00A8A39B /* PingStats.swift */; }; + A9630E3C2B8E0E7B00A65999 /* talpid_tunnel_config_client.h in Headers */ = {isa = PBXBuildFile; fileRef = A9630E3B2B8E0E7B00A65999 /* talpid_tunnel_config_client.h */; settings = {ATTRIBUTES = (Private, ); }; }; + A9630E432B8E10FB00A65999 /* MullvadTypes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223D5294C8E5E0029F5F8 /* MullvadTypes.framework */; }; + A9630E442B8E115A00A65999 /* MullvadPostQuantum.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A944F25C2B8DEFDB00473F4C /* MullvadPostQuantum.framework */; }; + A9630E492B921E6D00A65999 /* PacketTunnelProvider+TCPConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A557F42B7E3E5C0017ADA8 /* PacketTunnelProvider+TCPConnection.swift */; }; A970C89D2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = A970C89C2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift */; }; A97D25AE2B0BB18100946B2D /* ProtocolObfuscator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97D25AD2B0BB18100946B2D /* ProtocolObfuscator.swift */; }; A97D25B02B0BB5C400946B2D /* ProtocolObfuscationStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97D25AF2B0BB5C400946B2D /* ProtocolObfuscationStub.swift */; }; @@ -1195,6 +1209,20 @@ remoteGlobalIDString = 5840231E2A406BF5007B27AC; remoteInfo = TunnelObfuscation; }; + A944F2602B8DEFDB00473F4C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 58CE5E58224146200008646E /* Project object */; + proxyType = 1; + remoteGlobalIDString = A944F25B2B8DEFDB00473F4C; + remoteInfo = MullvadPostQuantum; + }; + A9630E412B8E10F700A65999 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 58CE5E58224146200008646E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 58D223D4294C8E5E0029F5F8; + remoteInfo = MullvadTypes; + }; A9EC20F12A5D79ED0040D56E /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 58CE5E58224146200008646E /* Project object */; @@ -1228,6 +1256,7 @@ F0ACE3112BE4E478006D5333 /* MullvadMockData.framework in Embed Frameworks */, 58D223E7294C8F120029F5F8 /* MullvadTypes.framework in Embed Frameworks */, 58D223FA294C8FF10029F5F8 /* MullvadLogging.framework in Embed Frameworks */, + A944F2632B8DEFDB00473F4C /* MullvadPostQuantum.framework in Embed Frameworks */, 58B2FDDA2AA71D2A003EB5C6 /* MullvadSettings.framework in Embed Frameworks */, A9EC20F02A5D79ED0040D56E /* TunnelObfuscation.framework in Embed Frameworks */, 7ABCA5B42A9349F20044A708 /* Routing.framework in Embed Frameworks */, @@ -1315,6 +1344,29 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + A906F94C2BA1E09A002BF22E /* WireGuardKitTypes in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + A906F94B2BA1E09A002BF22E /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + A906F94C2BA1E09A002BF22E /* WireGuardKitTypes in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + A9259FD52B8E06E90032C82B /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + A9259FD22B8E06E90032C82B /* MullvadPostQuantum.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -1371,6 +1423,8 @@ 449EB9FE2B95FF2500DFA4EB /* AccountMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMock.swift; sourceTree = "<group>"; }; 449EBA252B975B9700DFA4EB /* PostQuantumKeyReceiving.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostQuantumKeyReceiving.swift; sourceTree = "<group>"; }; 44B02E3A2BC5732D008EDF34 /* LoggingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggingTests.swift; sourceTree = "<group>"; }; + 44BB5F962BE527F4002520EB /* TunnelState+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelState+UI.swift"; sourceTree = "<group>"; }; + 44BB5F992BE529FE002520EB /* TunnelStateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelStateTests.swift; sourceTree = "<group>"; }; 44DD7D232B6CFFD70005F67F /* StartTunnelOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartTunnelOperationTests.swift; sourceTree = "<group>"; }; 44DD7D262B6D18FB0005F67F /* MockTunnelInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnelInteractor.swift; sourceTree = "<group>"; }; 44DD7D282B7113CA0005F67F /* MockTunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnel.swift; sourceTree = "<group>"; }; @@ -1998,9 +2052,14 @@ A932D9F42B5EBB9D00999395 /* RESTTransportStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTransportStub.swift; sourceTree = "<group>"; }; A935594B2B4C2DA900D5D524 /* APIAvailabilityTestRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIAvailabilityTestRequest.swift; sourceTree = "<group>"; }; A935594D2B4E919F00D5D524 /* Api.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Api.xcconfig; sourceTree = "<group>"; }; + A944F25C2B8DEFDB00473F4C /* MullvadPostQuantum.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MullvadPostQuantum.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A944F25E2B8DEFDB00473F4C /* MullvadPostQuantum.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MullvadPostQuantum.h; sourceTree = "<group>"; }; + A944F2712B8E02E800473F4C /* libtalpid_tunnel_config_client.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libtalpid_tunnel_config_client.a; path = "../target/aarch64-apple-ios/debug/libtalpid_tunnel_config_client.a"; sourceTree = "<group>"; }; A9467E7E2A29DEFE000DC21F /* RelayCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheTests.swift; sourceTree = "<group>"; }; + A948809A2BC9308D0090A44C /* PostQuantumKeyExchangeActor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostQuantumKeyExchangeActor.swift; sourceTree = "<group>"; }; A95EEE352B722CD600A8A39B /* TunnelMonitorState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorState.swift; sourceTree = "<group>"; }; A95EEE372B722DFC00A8A39B /* PingStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PingStats.swift; sourceTree = "<group>"; }; + A9630E3B2B8E0E7B00A65999 /* talpid_tunnel_config_client.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = talpid_tunnel_config_client.h; path = "talpid-tunnel-config-client/include/talpid_tunnel_config_client.h"; sourceTree = "<group>"; }; A970C89C2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Socks5UsernamePasswordCommand.swift; sourceTree = "<group>"; }; A97D25AD2B0BB18100946B2D /* ProtocolObfuscator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolObfuscator.swift; sourceTree = "<group>"; }; A97D25AF2B0BB5C400946B2D /* ProtocolObfuscationStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolObfuscationStub.swift; sourceTree = "<group>"; }; @@ -2016,6 +2075,7 @@ A99E5EDF2B7628150033F241 /* ProblemReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportViewModel.swift; sourceTree = "<group>"; }; A99E5EE12B762ED30033F241 /* ProblemReportViewController+ViewManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProblemReportViewController+ViewManagement.swift"; sourceTree = "<group>"; }; A9A1DE782AD5708E0073F689 /* TransportStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransportStrategy.swift; sourceTree = "<group>"; }; + A9A557F42B7E3E5C0017ADA8 /* PacketTunnelProvider+TCPConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelProvider+TCPConnection.swift"; sourceTree = "<group>"; }; A9A5F9A12ACB003D0083449F /* TunnelManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerTests.swift; sourceTree = "<group>"; }; A9A8A8EA2A262AB30086D569 /* FileCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCache.swift; sourceTree = "<group>"; }; A9B6AC172ADE8F4300F7802A /* MigrationManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationManagerTests.swift; sourceTree = "<group>"; }; @@ -2030,6 +2090,7 @@ A9E0317B2ACBFC7E0095D843 /* TunnelStore+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelStore+Stubs.swift"; sourceTree = "<group>"; }; A9E0317D2ACC32920095D843 /* TunnelStatusBlockObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStatusBlockObserver.swift; sourceTree = "<group>"; }; A9E034632ABB302000E59A5A /* UIEdgeInsets+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Extensions.swift"; sourceTree = "<group>"; }; + A9EB4F9C2B7FAB21002A2D7A /* PostQuantumKeyNegotiator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostQuantumKeyNegotiator.swift; sourceTree = "<group>"; }; A9EC20E72A5D3A8C0040D56E /* CoordinatesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatesTests.swift; sourceTree = "<group>"; }; A9F360332AAB626300F53531 /* VPNConnectionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNConnectionProtocol.swift; sourceTree = "<group>"; }; E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeViewController.swift; sourceTree = "<group>"; }; @@ -2196,6 +2257,7 @@ 58F0974E2A20C31100DA2DAD /* WireGuardKitTypes in Frameworks */, 58C7A4492A863F490060C66F /* PacketTunnelCore.framework in Frameworks */, 58D223F9294C8FF00029F5F8 /* MullvadLogging.framework in Frameworks */, + A944F2622B8DEFDB00473F4C /* MullvadPostQuantum.framework in Frameworks */, 58D223E6294C8F120029F5F8 /* MullvadTypes.framework in Frameworks */, 7ABCA5B32A9349F20044A708 /* Routing.framework in Frameworks */, 58D223CC294C8BCB0029F5F8 /* Operations.framework in Frameworks */, @@ -2209,6 +2271,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A9630E442B8E115A00A65999 /* MullvadPostQuantum.framework in Frameworks */, 589C6A7D2A45B06800DAD3EF /* TunnelObfuscation.framework in Frameworks */, 58FE25C62AA72779003D1918 /* PacketTunnelCore.framework in Frameworks */, 58FE25CE2AA72802003D1918 /* MullvadSettings.framework in Frameworks */, @@ -2294,6 +2357,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A944F2592B8DEFDB00473F4C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A9630E432B8E10FB00A65999 /* MullvadTypes.framework in Frameworks */, + A906F94A2BA1E09A002BF22E /* WireGuardKitTypes in Frameworks */, + A944F2722B8E02F600473F4C /* libtalpid_tunnel_config_client.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -2468,6 +2541,7 @@ 44DD7D262B6D18FB0005F67F /* MockTunnelInteractor.swift */, 44DD7D232B6CFFD70005F67F /* StartTunnelOperationTests.swift */, A9A5F9A12ACB003D0083449F /* TunnelManagerTests.swift */, + 44BB5F992BE529FE002520EB /* TunnelStateTests.swift */, A9E0317B2ACBFC7E0095D843 /* TunnelStore+Stubs.swift */, A9E031792ACB0AE70095D843 /* UIApplication+Stubs.swift */, 58165EBD2A262CBB00688EAD /* WgKeyRotationTests.swift */, @@ -2582,6 +2656,7 @@ 5820676326E771DB00655B05 /* TunnelManagerErrors.swift */, 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */, 58B93A1226C3F13600A55733 /* TunnelState.swift */, + 44BB5F962BE527F4002520EB /* TunnelState+UI.swift */, 5803B4B12940A48700C23744 /* TunnelStore.swift */, 5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */, 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */, @@ -2982,6 +3057,7 @@ 584F991F2902CBDD001F858D /* Frameworks */ = { isa = PBXGroup; children = ( + A944F2712B8E02E800473F4C /* libtalpid_tunnel_config_client.a */, 01EF6F332B6A590700125696 /* libmullvad_api.a */, 01EF6F312B6A58F000125696 /* debug */, 01EF6F2F2B6A588300125696 /* aarch64-apple-ios */, @@ -3392,6 +3468,9 @@ 7A83C3FC2A55B39500DFB83A /* TestPlans */, 584023202A406BF5007B27AC /* TunnelObfuscation */, 58695A9E2A4ADA9200328DB3 /* TunnelObfuscationTests */, + A944F25D2B8DEFDB00473F4C /* MullvadPostQuantum */, + 58CE5E61224146200008646E /* Products */, + 584F991F2902CBDD001F858D /* Frameworks */, ); sourceTree = "<group>"; }; @@ -3417,6 +3496,7 @@ 58B2FDD32AA71D2A003EB5C6 /* MullvadSettings.framework */, 852969252B4D9C1F007EAD4C /* MullvadVPNUITests.xctest */, F0ACE3082BE4E478006D5333 /* MullvadMockData.framework */, + A944F25C2B8DEFDB00473F4C /* MullvadPostQuantum.framework */, ); name = Products; sourceTree = "<group>"; @@ -3459,6 +3539,7 @@ 58F3F3682AA08E2200D3B0A4 /* PacketTunnelProvider */, 58915D662A25F9F20066445B /* DeviceCheck */, 588395612A9DF497008B63F6 /* WireGuardAdapter */, + A948809A2BC9308D0090A44C /* PostQuantumKeyExchangeActor.swift */, ); path = PacketTunnel; sourceTree = "<group>"; @@ -3910,6 +3991,17 @@ path = Socks5; sourceTree = "<group>"; }; + A944F25D2B8DEFDB00473F4C /* MullvadPostQuantum */ = { + isa = PBXGroup; + children = ( + A944F25E2B8DEFDB00473F4C /* MullvadPostQuantum.h */, + A9A557F42B7E3E5C0017ADA8 /* PacketTunnelProvider+TCPConnection.swift */, + A9EB4F9C2B7FAB21002A2D7A /* PostQuantumKeyNegotiator.swift */, + A9630E3B2B8E0E7B00A65999 /* talpid_tunnel_config_client.h */, + ); + path = MullvadPostQuantum; + sourceTree = "<group>"; + }; F028A5472A336E1900C0CAA3 /* RedeemVoucher */ = { isa = PBXGroup; children = ( @@ -4194,6 +4286,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A944F2572B8DEFDB00473F4C /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A944F25F2B8DEFDB00473F4C /* MullvadPostQuantum.h in Headers */, + A9630E3C2B8E0E7B00A65999 /* talpid_tunnel_config_client.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXHeadersBuildPhase section */ /* Begin PBXLegacyTarget section */ @@ -4255,7 +4356,6 @@ buildRules = ( ); dependencies = ( - F04F959F2B21D02700431E08 /* PBXTargetDependency */, A91614D32B108F4D00F416EB /* PBXTargetDependency */, ); name = TunnelObfuscation; @@ -4413,6 +4513,7 @@ 7ABCA5B62A9349F20044A708 /* PBXTargetDependency */, 58B2FDD82AA71D2A003EB5C6 /* PBXTargetDependency */, F0ACE30F2BE4E478006D5333 /* PBXTargetDependency */, + A944F2612B8DEFDB00473F4C /* PBXTargetDependency */, ); name = MullvadVPN; packageProductDependencies = ( @@ -4430,6 +4531,7 @@ 58CE5E75224146470008646E /* Sources */, 58CE5E76224146470008646E /* Frameworks */, 58CE5E77224146470008646E /* Resources */, + A9259FD52B8E06E90032C82B /* Embed Frameworks */, ); buildRules = ( ); @@ -4634,6 +4736,30 @@ productReference = F0ACE3082BE4E478006D5333 /* MullvadMockData.framework */; productType = "com.apple.product-type.framework"; }; + A944F25B2B8DEFDB00473F4C /* MullvadPostQuantum */ = { + isa = PBXNativeTarget; + buildConfigurationList = A944F2682B8DEFDB00473F4C /* Build configuration list for PBXNativeTarget "MullvadPostQuantum" */; + buildPhases = ( + A944F2692B8DF00C00473F4C /* Build Talpid Tunnel Config Client */, + A944F2572B8DEFDB00473F4C /* Headers */, + A944F2582B8DEFDB00473F4C /* Sources */, + A944F2592B8DEFDB00473F4C /* Frameworks */, + A944F25A2B8DEFDB00473F4C /* Resources */, + A906F94B2BA1E09A002BF22E /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + A9630E422B8E10F700A65999 /* PBXTargetDependency */, + ); + name = MullvadPostQuantum; + packageProductDependencies = ( + A906F9492BA1E09A002BF22E /* WireGuardKitTypes */, + ); + productName = MullvadPostQuantum; + productReference = A944F25C2B8DEFDB00473F4C /* MullvadPostQuantum.framework */; + productType = "com.apple.product-type.framework"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -4721,7 +4847,10 @@ TestTargetID = 58CE5E5F224146200008646E; }; F0ACE3072BE4E478006D5333 = { - CreatedOnToolsVersion = 15.3; + CreatedOnToolsVersion = 15.2; + }; + A944F25B2B8DEFDB00473F4C = { + CreatedOnToolsVersion = 15.2; }; }; }; @@ -4762,6 +4891,7 @@ 7A88DCCD2A8FABBE00D2FF0E /* Routing */, 7A88DCD62A8FABBE00D2FF0E /* RoutingTests */, F0ACE3072BE4E478006D5333 /* MullvadMockData */, + A944F25B2B8DEFDB00473F4C /* MullvadPostQuantum */, ); }; /* End PBXProject section */ @@ -4900,6 +5030,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A944F25A2B8DEFDB00473F4C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -4977,6 +5114,25 @@ shellPath = /bin/sh; shellScript = "CARGO_TARGET_DIR=${PROJECT_DIR}/../target bash ${PROJECT_DIR}/build-rust-library.sh tunnel-obfuscator-proxy\n"; }; + A944F2692B8DF00C00473F4C /* Build Talpid Tunnel Config Client */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Build Talpid Tunnel Config Client"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "CARGO_TARGET_DIR=${PROJECT_DIR}/../target bash ${PROJECT_DIR}/build-rust-library.sh talpid-tunnel-config-client\n"; + }; F05F39962B21C704006E60A7 /* Prebuild relays */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -5247,6 +5403,7 @@ 7A9BE5AB2B909A1700E2A7D0 /* LocationDataSourceProtocol.swift in Sources */, A9A5FA2A2ACB05160083449F /* CoordinatesTests.swift in Sources */, 44DD7D242B6CFFD70005F67F /* StartTunnelOperationTests.swift in Sources */, + 44BB5F982BE527F4002520EB /* TunnelState+UI.swift in Sources */, A9A5FA2B2ACB05160083449F /* CustomDateComponentsFormattingTests.swift in Sources */, A9A5FA2C2ACB05160083449F /* DeviceCheckOperationTests.swift in Sources */, A9A5FA2D2ACB05160083449F /* DurationTests.swift in Sources */, @@ -5424,6 +5581,7 @@ 7A9CCCB72A96302800DD6A34 /* RevokedCoordinator.swift in Sources */, 7A6389F82B864CDF008E77E1 /* LocationNode.swift in Sources */, 587D96742886D87C00CD8F1C /* DeviceManagementContentView.swift in Sources */, + 44BB5F972BE527F4002520EB /* TunnelState+UI.swift in Sources */, 7A11DD0B2A9495D400098CD8 /* AppRoutes.swift in Sources */, 5827B0902B0CAA0500CCBBA1 /* EditAccessMethodCoordinator.swift in Sources */, 5846227126E229F20035F7C2 /* StoreSubscription.swift in Sources */, @@ -5776,6 +5934,7 @@ 58F3F36A2AA08E3C00D3B0A4 /* PacketTunnelProvider.swift in Sources */, 58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */, 58C7A45B2A8640030060C66F /* PacketTunnelPathObserver.swift in Sources */, + A948809B2BC9308D0090A44C /* PostQuantumKeyExchangeActor.swift in Sources */, 580D6B8E2AB33BBF00B2D6E0 /* BlockedStateErrorMapper.swift in Sources */, 06AC116228F94C450037AF9A /* ApplicationConfiguration.swift in Sources */, 583FE02429C1ACB3006E85F9 /* RESTCreateApplePaymentResponse+Localization.swift in Sources */, @@ -5979,6 +6138,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A944F2582B8DEFDB00473F4C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A9630E492B921E6D00A65999 /* PacketTunnelProvider+TCPConnection.swift in Sources */, + A944F26A2B8DF32900473F4C /* PostQuantumKeyNegotiator.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -6179,6 +6347,16 @@ target = 5840231E2A406BF5007B27AC /* TunnelObfuscation */; targetProxy = A91D78E12B03BDE500FCD5D3 /* PBXContainerItemProxy */; }; + A944F2612B8DEFDB00473F4C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A944F25B2B8DEFDB00473F4C /* MullvadPostQuantum */; + targetProxy = A944F2602B8DEFDB00473F4C /* PBXContainerItemProxy */; + }; + A9630E422B8E10F700A65999 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 58D223D4294C8E5E0029F5F8 /* MullvadTypes */; + targetProxy = A9630E412B8E10F700A65999 /* PBXContainerItemProxy */; + }; A9EC20F22A5D79ED0040D56E /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 5840231E2A406BF5007B27AC /* TunnelObfuscation */; @@ -6804,6 +6982,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = minimal; SWIFT_VERSION = 5.0; }; name = Debug; @@ -6824,6 +7003,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_STRICT_CONCURRENCY = minimal; SWIFT_VERSION = 5.0; }; name = Release; @@ -6844,6 +7024,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).PacketTunnel"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = minimal; SWIFT_VERSION = 5.0; }; name = Debug; @@ -6863,6 +7044,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).PacketTunnel"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_STRICT_CONCURRENCY = minimal; SWIFT_VERSION = 5.0; }; name = Release; @@ -7473,6 +7655,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Mullvad VPN Development"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = minimal; SWIFT_VERSION = 5.0; }; name = Staging; @@ -7511,6 +7694,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Packet Tunnel Development"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = minimal; SWIFT_VERSION = 5.0; }; name = Staging; @@ -8010,6 +8194,210 @@ }; name = MockRelease; }; + A944F2642B8DEFDB00473F4C /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5808273928487E3E006B77A4 /* Base.xcconfig */; + buildSettings = { + APPLICATION_IDENTIFIER = net.mullvad.mullvadVPN; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = CKG9MXH72F; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 4; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Mullvad VPN AB. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios/debug"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios-sim/debug"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "$(PROJECT_DIR)/../target/x86_64-apple-ios/debug"; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MODULEMAP_PRIVATE_FILE = $PROJECT_DIR/MullvadPostQuantum/module.private.modulemap; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).MullvadPostQuantum"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + A944F2652B8DEFDB00473F4C /* Staging */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5808273928487E3E006B77A4 /* Base.xcconfig */; + buildSettings = { + APPLICATION_IDENTIFIER = net.mullvad.mullvadVPN; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = CKG9MXH72F; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 4; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Mullvad VPN AB. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ""; + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios/debug"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios-sim/debug"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "$(PROJECT_DIR)/../target/x86_64-apple-ios/debug"; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MODULEMAP_PRIVATE_FILE = $PROJECT_DIR/MullvadPostQuantum/module.private.modulemap; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).MullvadPostQuantum"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Staging; + }; + A944F2662B8DEFDB00473F4C /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5808273928487E3E006B77A4 /* Base.xcconfig */; + buildSettings = { + APPLICATION_IDENTIFIER = net.mullvad.mullvadVPN; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = CKG9MXH72F; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 4; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Mullvad VPN AB. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ""; + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios/release"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios-sim/release"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "$(PROJECT_DIR)/../target/x86_64-apple-ios/release"; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MODULEMAP_PRIVATE_FILE = $PROJECT_DIR/MullvadPostQuantum/module.private.modulemap; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).MullvadPostQuantum"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + A944F2672B8DEFDB00473F4C /* MockRelease */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5808273928487E3E006B77A4 /* Base.xcconfig */; + buildSettings = { + APPLICATION_IDENTIFIER = net.mullvad.mullvadVPN; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = CKG9MXH72F; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 4; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Mullvad VPN AB. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ""; + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios/release"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios-sim/release"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "$(PROJECT_DIR)/../target/x86_64-apple-ios/release"; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MODULEMAP_PRIVATE_FILE = $PROJECT_DIR/MullvadPostQuantum/module.private.modulemap; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).MullvadPostQuantum"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = MockRelease; + }; A9E99CE12B5195E600869AF2 /* MockRelease */ = { isa = XCBuildConfiguration; buildSettings = { @@ -8085,6 +8473,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Mullvad VPN Development"; + SWIFT_STRICT_CONCURRENCY = minimal; SWIFT_VERSION = 5.0; }; name = MockRelease; @@ -8119,6 +8508,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).PacketTunnel"; PRODUCT_NAME = "$(TARGET_NAME)"; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Packet Tunnel Development"; + SWIFT_STRICT_CONCURRENCY = minimal; SWIFT_VERSION = 5.0; }; name = MockRelease; @@ -8998,6 +9388,17 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + A944F2682B8DEFDB00473F4C /* Build configuration list for PBXNativeTarget "MullvadPostQuantum" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A944F2642B8DEFDB00473F4C /* Debug */, + A944F2652B8DEFDB00473F4C /* Staging */, + A944F2662B8DEFDB00473F4C /* Release */, + A944F2672B8DEFDB00473F4C /* MockRelease */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -9075,6 +9476,11 @@ package = 58F097482A20C30000DA2DAD /* XCRemoteSwiftPackageReference "wireguard-apple" */; productName = WireGuardKitTypes; }; + A906F9492BA1E09A002BF22E /* WireGuardKitTypes */ = { + isa = XCSwiftPackageProductDependency; + package = 58F097482A20C30000DA2DAD /* XCRemoteSwiftPackageReference "wireguard-apple" */; + productName = WireGuardKitTypes; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 58CE5E58224146200008646E /* Project object */; diff --git a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadPostQuantum.xcscheme b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadPostQuantum.xcscheme new file mode 100644 index 0000000000..0d18c5e1d9 --- /dev/null +++ b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadPostQuantum.xcscheme @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1520" + version = "1.7"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "A944F25B2B8DEFDB00473F4C" + BuildableName = "MullvadPostQuantum.framework" + BlueprintName = "MullvadPostQuantum" + ReferencedContainer = "container:MullvadVPN.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "A944F25B2B8DEFDB00473F4C" + BuildableName = "MullvadPostQuantum.framework" + BlueprintName = "MullvadPostQuantum" + ReferencedContainer = "container:MullvadVPN.xcodeproj"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift index 0c2b8678f8..80352cdda2 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -980,14 +980,10 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo guard tunnelManager.deviceState.isLoggedIn else { return false } switch tunnelManager.tunnelStatus.state { - case .connected, .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .error: + case .connected, .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .error, + .negotiatingPostQuantumKey: tunnelManager.reconnectTunnel(selectNewRelay: true) - #if DEBUG - case .negotiatingKey: - tunnelManager.reconnectTunnel(selectNewRelay: true) - #endif - case .disconnecting, .disconnected: tunnelManager.startTunnel() diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift index e7bf690f87..ea5260b8af 100644 --- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift +++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift @@ -176,14 +176,16 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { guard let selectedRelay = selectedRelay else { return } do { + let settings = try SettingsManager.readSettings() observedState = .connected( ObservedConnectionState( selectedRelay: selectedRelay, - relayConstraints: try SettingsManager.readSettings().relayConstraints, + relayConstraints: settings.relayConstraints, networkReachability: .reachable, connectionAttemptCount: 0, transportLayer: .udp, - remotePort: selectedRelay.endpoint.ipv4Relay.port + remotePort: selectedRelay.endpoint.ipv4Relay.port, + isPostQuantum: settings.tunnelQuantumResistance.isEnabled ) ) } catch { diff --git a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift index e2a87e882a..4957af7c52 100644 --- a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift +++ b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift @@ -51,21 +51,19 @@ class MapConnectionStatusOperation: AsyncOperation { switch observedState { case let .connected(connectionState): return connectionState.isNetworkReachable - ? .connected(connectionState.selectedRelay) + ? .connected(connectionState.selectedRelay, isPostQuantum: connectionState.isPostQuantum) : .waitingForConnectivity(.noConnection) case let .connecting(connectionState): return connectionState.isNetworkReachable - ? .connecting(connectionState.selectedRelay) + ? .connecting(connectionState.selectedRelay, isPostQuantum: connectionState.isPostQuantum) : .waitingForConnectivity(.noConnection) - #if DEBUG - case let .negotiatingKey(connectionState): + case let .negotiatingPostQuantumKey(connectionState, privateKey): return connectionState.isNetworkReachable - ? .negotiatingKey(connectionState.selectedRelay) + ? .negotiatingPostQuantumKey(connectionState.selectedRelay, privateKey) : .waitingForConnectivity(.noConnection) - #endif case let .reconnecting(connectionState): return connectionState.isNetworkReachable - ? .reconnecting(connectionState.selectedRelay) + ? .reconnecting(connectionState.selectedRelay, isPostQuantum: connectionState.isPostQuantum) : .waitingForConnectivity(.noConnection) case let .error(blockedState): return .error(blockedState.reason) diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift index 9474a0a481..cd9e8b7a88 100644 --- a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift @@ -90,7 +90,10 @@ class StartTunnelOperation: ResultOperation<Void> { interactor.updateTunnelStatus { tunnelStatus in tunnelStatus = TunnelStatus() - tunnelStatus.state = .connecting(selectedRelay) + tunnelStatus.state = .connecting( + selectedRelay, + isPostQuantum: interactor.settings.tunnelQuantumResistance.isEnabled + ) } try tunnel.start(options: tunnelOptions.rawOptions()) diff --git a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift index 4701a4238c..1bd7a4f9c7 100644 --- a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift @@ -35,14 +35,10 @@ class StopTunnelOperation: ResultOperation<Void> { finish(result: .success(())) - case .connected, .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .error: + case .connected, .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .error, + .negotiatingPostQuantumKey: doShutDownTunnel() - #if DEBUG - case .negotiatingKey: - doShutDownTunnel() - #endif - case .disconnected, .disconnecting, .pendingReconnect, .waitingForConnectivity(.noNetwork): finish(result: .success(())) } diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index 8f3b69112c..897f0074ac 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -675,10 +675,9 @@ final class TunnelManager: StorePaymentObserver { // while the tunnel process is trying to connect. startPollingTunnelStatus(interval: establishingTunnelStatusPollInterval) - #if DEBUG - case .negotiatingKey: - startPollingTunnelStatus(interval: establishingTunnelStatusPollInterval) - #endif + case .negotiatingPostQuantumKey: + // No need to poll the tunnel while negotiating post quantum keys, assume the connection will work + break case .connected, .waitingForConnectivity(.noConnection): // Start polling tunnel status to keep connectivity status up to date. diff --git a/ios/MullvadVPN/TunnelManager/TunnelState+UI.swift b/ios/MullvadVPN/TunnelManager/TunnelState+UI.swift new file mode 100644 index 0000000000..eefb1db415 --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/TunnelState+UI.swift @@ -0,0 +1,259 @@ +// +// TunnelState+UI.swift +// MullvadVPN +// +// Created by Andrew Bulhak on 2024-05-03. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +extension TunnelState { + var textColorForSecureLabel: UIColor { + switch self { + case .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .negotiatingPostQuantumKey: + .white + + case .connected: + .successColor + + case .disconnecting, .disconnected, .pendingReconnect, .waitingForConnectivity(.noNetwork), .error: + .dangerColor + } + } + + var shouldEnableButtons: Bool { + if case .waitingForConnectivity(.noNetwork) = self { + return false + } + + return true + } + + var localizedTitleForSecureLabel: String { + switch self { + case let .connecting(_, isPostQuantum), let .reconnecting(_, isPostQuantum): + if isPostQuantum { + NSLocalizedString( + "TUNNEL_STATE_PQ_CONNECTING", + tableName: "Main", + value: "Creating quantum secure connection", + comment: "" + ) + } else { + NSLocalizedString( + "TUNNEL_STATE_CONNECTING", + tableName: "Main", + value: "Creating secure connection", + comment: "" + ) + } + + case .negotiatingPostQuantumKey: + NSLocalizedString( + "TUNNEL_STATE_NEGOTIATING_KEY", + tableName: "Main", + value: "Creating quantum secure connection", + comment: "" + ) + + case let .connected(_, isPostQuantum): + if isPostQuantum { + NSLocalizedString( + "TUNNEL_STATE_PQ_CONNECTED", + tableName: "Main", + value: "Quantum secure connection", + comment: "" + ) + } else { + NSLocalizedString( + "TUNNEL_STATE_CONNECTED", + tableName: "Main", + value: "Secure connection", + comment: "" + ) + } + + case .disconnecting(.nothing): + NSLocalizedString( + "TUNNEL_STATE_DISCONNECTING", + tableName: "Main", + value: "Disconnecting", + comment: "" + ) + case .disconnecting(.reconnect), .pendingReconnect: + NSLocalizedString( + "TUNNEL_STATE_PENDING_RECONNECT", + tableName: "Main", + value: "Reconnecting", + comment: "" + ) + + case .disconnected: + NSLocalizedString( + "TUNNEL_STATE_DISCONNECTED", + tableName: "Main", + value: "Unsecured connection", + comment: "" + ) + + case .waitingForConnectivity(.noConnection), .error: + NSLocalizedString( + "TUNNEL_STATE_WAITING_FOR_CONNECTIVITY", + tableName: "Main", + value: "Blocked connection", + comment: "" + ) + + case .waitingForConnectivity(.noNetwork): + NSLocalizedString( + "TUNNEL_STATE_NO_NETWORK", + tableName: "Main", + value: "No network", + comment: "" + ) + } + } + + var localizedTitleForSelectLocationButton: String? { + switch self { + case .disconnecting(.reconnect), .pendingReconnect: + NSLocalizedString( + "SWITCH_LOCATION_BUTTON_TITLE", + tableName: "Main", + value: "Select location", + comment: "" + ) + + case .disconnected, .disconnecting(.nothing): + NSLocalizedString( + "SELECT_LOCATION_BUTTON_TITLE", + tableName: "Main", + value: "Select location", + comment: "" + ) + + case .connecting, .connected, .reconnecting, .waitingForConnectivity, .error: + NSLocalizedString( + "SWITCH_LOCATION_BUTTON_TITLE", + tableName: "Main", + value: "Switch location", + comment: "" + ) + + case .negotiatingPostQuantumKey: + NSLocalizedString( + "SWITCH_LOCATION_BUTTON_TITLE", + tableName: "Main", + value: "Switch location", + comment: "" + ) + } + } + + var localizedAccessibilityLabel: String { + switch self { + case let .connecting(_, isPostQuantum): + if isPostQuantum { + NSLocalizedString( + "TUNNEL_STATE_PQ_CONNECTING_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Creating quantum secure connection", + comment: "" + ) + } else { + NSLocalizedString( + "TUNNEL_STATE_CONNECTING_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Creating secure connection", + comment: "" + ) + } + + case .negotiatingPostQuantumKey: + NSLocalizedString( + "TUNNEL_STATE_CONNECTING_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Creating quantum secure connection", + comment: "" + ) + + case let .connected(tunnelInfo, isPostQuantum): + if isPostQuantum { + String( + format: NSLocalizedString( + "TUNNEL_STATE_PQ_CONNECTED_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Quantum secure connection. Connected to %@, %@", + comment: "" + ), + tunnelInfo.location.city, + tunnelInfo.location.country + ) + } else { + String( + format: NSLocalizedString( + "TUNNEL_STATE_CONNECTED_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Secure connection. Connected to %@, %@", + comment: "" + ), + tunnelInfo.location.city, + tunnelInfo.location.country + ) + } + + case .disconnected: + NSLocalizedString( + "TUNNEL_STATE_DISCONNECTED_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Unsecured connection", + comment: "" + ) + + case let .reconnecting(tunnelInfo, _): + String( + format: NSLocalizedString( + "TUNNEL_STATE_RECONNECTING_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Reconnecting to %@, %@", + comment: "" + ), + tunnelInfo.location.city, + tunnelInfo.location.country + ) + + case .waitingForConnectivity(.noConnection), .error: + NSLocalizedString( + "TUNNEL_STATE_WAITING_FOR_CONNECTIVITY_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Blocked connection", + comment: "" + ) + + case .waitingForConnectivity(.noNetwork): + NSLocalizedString( + "TUNNEL_STATE_NO_NETWORK_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "No network", + comment: "" + ) + + case .disconnecting(.nothing): + NSLocalizedString( + "TUNNEL_STATE_DISCONNECTING_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Disconnecting", + comment: "" + ) + + case .disconnecting(.reconnect), .pendingReconnect: + NSLocalizedString( + "TUNNEL_STATE_PENDING_RECONNECT_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Reconnecting", + comment: "" + ) + } + } +} diff --git a/ios/MullvadVPN/TunnelManager/TunnelState.swift b/ios/MullvadVPN/TunnelManager/TunnelState.swift index 08b889899e..76148bdbb8 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelState.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelState.swift @@ -9,6 +9,7 @@ import Foundation import MullvadTypes import PacketTunnelCore +import WireGuardKitTypes /// A struct describing the tunnel status. struct TunnelStatus: Equatable, CustomStringConvertible { @@ -48,15 +49,13 @@ enum TunnelState: Equatable, CustomStringConvertible { case pendingReconnect /// Connecting the tunnel. - case connecting(SelectedRelay?) + case connecting(SelectedRelay?, isPostQuantum: Bool) - #if DEBUG /// Negotiating a key for post-quantum resistance - case negotiatingKey(SelectedRelay) - #endif + case negotiatingPostQuantumKey(SelectedRelay, PrivateKey) /// Connected the tunnel - case connected(SelectedRelay) + case connected(SelectedRelay, isPostQuantum: Bool) /// Disconnecting the tunnel case disconnecting(ActionAfterDisconnect) @@ -69,7 +68,7 @@ enum TunnelState: Equatable, CustomStringConvertible { /// 1. Asking the running tunnel to reconnect to new relay via IPC. /// 2. Tunnel attempts to reconnect to new relay as the current relay appears to be /// dysfunctional. - case reconnecting(SelectedRelay) + case reconnecting(SelectedRelay, isPostQuantum: Bool) /// Waiting for connectivity to come back up. case waitingForConnectivity(WaitingForConnectionReason) @@ -81,55 +80,45 @@ enum TunnelState: Equatable, CustomStringConvertible { switch self { case .pendingReconnect: "pending reconnect after disconnect" - case let .connecting(tunnelRelay): + case let .connecting(tunnelRelay, isPostQuantum): if let tunnelRelay { - "connecting to \(tunnelRelay.hostname)" + "connecting \(isPostQuantum ? "(PQ) " : "")to \(tunnelRelay.hostname)" } else { - "connecting, fetching relay" + "connecting\(isPostQuantum ? " (PQ)" : ""), fetching relay" } - case let .connected(tunnelRelay): - "connected to \(tunnelRelay.hostname)" + case let .connected(tunnelRelay, isPostQuantum): + "connected \(isPostQuantum ? "(PQ) " : "")to \(tunnelRelay.hostname)" case let .disconnecting(actionAfterDisconnect): "disconnecting and then \(actionAfterDisconnect)" case .disconnected: "disconnected" - case let .reconnecting(tunnelRelay): - "reconnecting to \(tunnelRelay.hostname)" + case let .reconnecting(tunnelRelay, isPostQuantum): + "reconnecting \(isPostQuantum ? "(PQ) " : "")to \(tunnelRelay.hostname)" case .waitingForConnectivity: "waiting for connectivity" case let .error(blockedStateReason): "error state: \(blockedStateReason)" - #if DEBUG - case let .negotiatingKey(tunnelRelay): + case let .negotiatingPostQuantumKey(tunnelRelay, _): "negotiating key with \(tunnelRelay.hostname)" - #endif } } var isSecured: Bool { switch self { case .reconnecting, .connecting, .connected, .waitingForConnectivity(.noConnection), .error(.accountExpired), - .error(.deviceRevoked): + .error(.deviceRevoked), .negotiatingPostQuantumKey: true case .pendingReconnect, .disconnecting, .disconnected, .waitingForConnectivity(.noNetwork), .error: false - #if DEBUG - case .negotiatingKey: - false - #endif } } var relay: SelectedRelay? { switch self { - case let .connected(relay), let .reconnecting(relay): - relay - case let .connecting(relay): + case let .connected(relay, _), let .reconnecting(relay, _), let .negotiatingPostQuantumKey(relay, _): relay - #if DEBUG - case let .negotiatingKey(relay): + case let .connecting(relay, _): relay - #endif case .disconnecting, .disconnected, .waitingForConnectivity, .pendingReconnect, .error: nil } diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift index 8635bc1b9d..92acdb7a55 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift @@ -27,7 +27,7 @@ private enum TunnelControlActionButton { } final class TunnelControlView: UIView { - private let secureLabel = makeBoldTextLabel(ofSize: 20) + private let secureLabel = makeBoldTextLabel(ofSize: 20, numberOfLines: 0) private let cityLabel = makeBoldTextLabel(ofSize: 34) private let countryLabel = makeBoldTextLabel(ofSize: 34) @@ -420,11 +420,12 @@ final class TunnelControlView: UIView { ) } - private class func makeBoldTextLabel(ofSize fontSize: CGFloat) -> UILabel { + private class func makeBoldTextLabel(ofSize fontSize: CGFloat, numberOfLines: Int = 1) -> UILabel { let textLabel = UILabel() textLabel.translatesAutoresizingMaskIntoConstraints = false textLabel.font = UIFont.boldSystemFont(ofSize: fontSize) textLabel.textColor = .white + textLabel.numberOfLines = numberOfLines return textLabel } @@ -452,225 +453,6 @@ final class TunnelControlView: UIView { } private extension TunnelState { - var textColorForSecureLabel: UIColor { - switch self { - case .connecting, .reconnecting, .waitingForConnectivity(.noConnection): - .white - - #if DEBUG - case .negotiatingKey: - .white - #endif - - case .connected: - .successColor - - case .disconnecting, .disconnected, .pendingReconnect, .waitingForConnectivity(.noNetwork), .error: - .dangerColor - } - } - - var shouldEnableButtons: Bool { - if case .waitingForConnectivity(.noNetwork) = self { - return false - } - - return true - } - - var localizedTitleForSecureLabel: String { - switch self { - case .connecting, .reconnecting: - NSLocalizedString( - "TUNNEL_STATE_CONNECTING", - tableName: "Main", - value: "Creating secure connection", - comment: "" - ) - - #if DEBUG - case .negotiatingKey: - NSLocalizedString( - "TUNNEL_STATE_NEGOTIATING_KEY", - tableName: "Main", - value: "Negotiating key", - comment: "" - ) - #endif - - case .connected: - NSLocalizedString( - "TUNNEL_STATE_CONNECTED", - tableName: "Main", - value: "Secure connection", - comment: "" - ) - - case .disconnecting(.nothing): - NSLocalizedString( - "TUNNEL_STATE_DISCONNECTING", - tableName: "Main", - value: "Disconnecting", - comment: "" - ) - case .disconnecting(.reconnect), .pendingReconnect: - NSLocalizedString( - "TUNNEL_STATE_PENDING_RECONNECT", - tableName: "Main", - value: "Reconnecting", - comment: "" - ) - - case .disconnected: - NSLocalizedString( - "TUNNEL_STATE_DISCONNECTED", - tableName: "Main", - value: "Unsecured connection", - comment: "" - ) - - case .waitingForConnectivity(.noConnection), .error: - NSLocalizedString( - "TUNNEL_STATE_WAITING_FOR_CONNECTIVITY", - tableName: "Main", - value: "Blocked connection", - comment: "" - ) - - case .waitingForConnectivity(.noNetwork): - NSLocalizedString( - "TUNNEL_STATE_NO_NETWORK", - tableName: "Main", - value: "No network", - comment: "" - ) - } - } - - var localizedTitleForSelectLocationButton: String? { - switch self { - case .disconnecting(.reconnect), .pendingReconnect: - NSLocalizedString( - "SWITCH_LOCATION_BUTTON_TITLE", - tableName: "Main", - value: "Select location", - comment: "" - ) - - case .disconnected, .disconnecting(.nothing): - NSLocalizedString( - "SELECT_LOCATION_BUTTON_TITLE", - tableName: "Main", - value: "Select location", - comment: "" - ) - - case .connecting, .connected, .reconnecting, .waitingForConnectivity, .error: - NSLocalizedString( - "SWITCH_LOCATION_BUTTON_TITLE", - tableName: "Main", - value: "Switch location", - comment: "" - ) - - #if DEBUG - case .negotiatingKey: - NSLocalizedString( - "SWITCH_LOCATION_BUTTON_TITLE", - tableName: "Main", - value: "Switch location", - comment: "" - ) - #endif - } - } - - var localizedAccessibilityLabel: String { - switch self { - case .connecting: - NSLocalizedString( - "TUNNEL_STATE_CONNECTING_ACCESSIBILITY_LABEL", - tableName: "Main", - value: "Creating secure connection", - comment: "" - ) - - #if DEBUG - case .negotiatingKey: - NSLocalizedString( - "TUNNEL_STATE_CONNECTING_ACCESSIBILITY_LABEL", - tableName: "Main", - value: "Creating secure connection", - comment: "" - ) - #endif - - case let .connected(tunnelInfo): - String( - format: NSLocalizedString( - "TUNNEL_STATE_CONNECTED_ACCESSIBILITY_LABEL", - tableName: "Main", - value: "Secure connection. Connected to %@, %@", - comment: "" - ), - tunnelInfo.location.city, - tunnelInfo.location.country - ) - - case .disconnected: - NSLocalizedString( - "TUNNEL_STATE_DISCONNECTED_ACCESSIBILITY_LABEL", - tableName: "Main", - value: "Unsecured connection", - comment: "" - ) - - case let .reconnecting(tunnelInfo): - String( - format: NSLocalizedString( - "TUNNEL_STATE_RECONNECTING_ACCESSIBILITY_LABEL", - tableName: "Main", - value: "Reconnecting to %@, %@", - comment: "" - ), - tunnelInfo.location.city, - tunnelInfo.location.country - ) - - case .waitingForConnectivity(.noConnection), .error: - NSLocalizedString( - "TUNNEL_STATE_WAITING_FOR_CONNECTIVITY_ACCESSIBILITY_LABEL", - tableName: "Main", - value: "Blocked connection", - comment: "" - ) - - case .waitingForConnectivity(.noNetwork): - NSLocalizedString( - "TUNNEL_STATE_NO_NETWORK_ACCESSIBILITY_LABEL", - tableName: "Main", - value: "No network", - comment: "" - ) - - case .disconnecting(.nothing): - NSLocalizedString( - "TUNNEL_STATE_DISCONNECTING_ACCESSIBILITY_LABEL", - tableName: "Main", - value: "Disconnecting", - comment: "" - ) - - case .disconnecting(.reconnect), .pendingReconnect: - NSLocalizedString( - "TUNNEL_STATE_PENDING_RECONNECT_ACCESSIBILITY_LABEL", - tableName: "Main", - value: "Reconnecting", - comment: "" - ) - } - } - func actionButtons(traitCollection: UITraitCollection) -> [TunnelControlActionButton] { switch (traitCollection.userInterfaceIdiom, traitCollection.horizontalSizeClass) { case (.phone, _), (.pad, .compact): @@ -682,10 +464,8 @@ private extension TunnelState { .waitingForConnectivity(.noConnection): [.selectLocation, .cancel] - #if DEBUG - case .negotiatingKey: + case .negotiatingPostQuantumKey: [.selectLocation, .cancel] - #endif case .connected, .reconnecting, .error: [.selectLocation, .disconnect] @@ -700,10 +480,8 @@ private extension TunnelState { .waitingForConnectivity(.noConnection): [.cancel] - #if DEBUG - case .negotiatingKey: + case .negotiatingPostQuantumKey: [.cancel] - #endif case .connected, .reconnecting, .error: [.disconnect] } diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift index 7d68b3dd41..36f8535047 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift @@ -147,24 +147,17 @@ class TunnelViewController: UIViewController, RootContainment { private func updateMap(animated: Bool) { switch tunnelState { - case let .connecting(tunnelRelay): + case let .connecting(tunnelRelay, _): mapViewController.removeLocationMarker() contentView.setAnimatingActivity(true) mapViewController.setCenter(tunnelRelay?.location.geoCoordinate, animated: animated) - case let .reconnecting(tunnelRelay): + case let .reconnecting(tunnelRelay, _), let .negotiatingPostQuantumKey(tunnelRelay, _): mapViewController.removeLocationMarker() contentView.setAnimatingActivity(true) mapViewController.setCenter(tunnelRelay.location.geoCoordinate, animated: animated) - #if DEBUG - case let .negotiatingKey(tunnelRelay): - mapViewController.removeLocationMarker() - contentView.setAnimatingActivity(true) - mapViewController.setCenter(tunnelRelay.location.geoCoordinate, animated: animated) - #endif - - case let .connected(tunnelRelay): + case let .connected(tunnelRelay, _): let center = tunnelRelay.location.geoCoordinate mapViewController.setCenter(center, animated: animated) { self.contentView.setAnimatingActivity(false) diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift index bd3b42b76a..2934e7d457 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift @@ -170,7 +170,6 @@ final class VPNSettingsCellFactory: CellFactoryProtocol { cell.accessibilityIdentifier = "\(item.accessibilityIdentifier.rawValue)\(portString)" cell.applySubCellStyling() - #if DEBUG case .quantumResistanceAutomatic: guard let cell = cell as? SelectableSettingsCell else { return } @@ -205,7 +204,6 @@ final class VPNSettingsCellFactory: CellFactoryProtocol { ) cell.accessibilityIdentifier = item.accessibilityIdentifier cell.applySubCellStyling() - #endif } } } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift index 4525b7d886..2891fc4d5f 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift @@ -57,9 +57,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case wireGuardPorts case wireGuardObfuscation case wireGuardObfuscationPort - #if DEBUG case quantumResistance - #endif } enum Item: Hashable { @@ -71,11 +69,9 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case wireGuardObfuscationOn case wireGuardObfuscationOff case wireGuardObfuscationPort(_ port: UInt16) - #if DEBUG case quantumResistanceAutomatic case quantumResistanceOn case quantumResistanceOff - #endif static var wireGuardPorts: [Item] { let defaultPorts = VPNSettingsViewModel.defaultWireGuardPorts.map { @@ -92,11 +88,9 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< [.wireGuardObfuscationPort(0), wireGuardObfuscationPort(80), wireGuardObfuscationPort(5001)] } - #if DEBUG static var quantumResistance: [Item] { [.quantumResistanceAutomatic, .quantumResistanceOn, .quantumResistanceOff] } - #endif var accessibilityIdentifier: AccessibilityIdentifier { switch self { @@ -116,14 +110,12 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< return .wireGuardObfuscationOff case .wireGuardObfuscationPort: return .wireGuardObfuscationPort - #if DEBUG case .quantumResistanceAutomatic: return .quantumResistanceAutomatic case .quantumResistanceOn: return .quantumResistanceOn case .quantumResistanceOff: return .quantumResistanceOff - #endif } } @@ -141,10 +133,8 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< return .wireGuardObfuscation case .wireGuardObfuscationPort: return .wireGuardObfuscationPort - #if DEBUG case .quantumResistanceAutomatic, .quantumResistanceOn, .quantumResistanceOff: return .quantumResistance - #endif } } } @@ -167,30 +157,20 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case .off: .wireGuardObfuscationOff case .on: .wireGuardObfuscationOn } - #if DEBUG let quantumResistanceItem: Item = switch viewModel.quantumResistance { case .automatic: .quantumResistanceAutomatic case .off: .quantumResistanceOff case .on: .quantumResistanceOn } - #endif let obfuscationPortItem: Item = .wireGuardObfuscationPort(viewModel.obfuscationPort.portValue) - #if DEBUG return [ wireGuardPortItem, obfuscationStateItem, obfuscationPortItem, quantumResistanceItem, ].compactMap { indexPath(for: $0) } - #else - return [ - wireGuardPortItem, - obfuscationStateItem, - obfuscationPortItem, - ].compactMap { indexPath(for: $0) } - #endif } init(tableView: UITableView) { @@ -299,7 +279,6 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< selectObfuscationPort(port) delegate?.didChangeViewModel(viewModel) - #if DEBUG case .quantumResistanceAutomatic: selectQuantumResistance(.automatic) delegate?.didChangeViewModel(viewModel) @@ -309,7 +288,6 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case .quantumResistanceOff: selectQuantumResistance(.off) delegate?.didChangeViewModel(viewModel) - #endif default: break } @@ -347,11 +325,9 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case .wireGuardObfuscationPort: configureObfuscationPortHeader(view) return view - #if DEBUG case .quantumResistance: configureQuantumResistanceHeader(view) return view - #endif default: return nil @@ -548,7 +524,6 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< } } - #if DEBUG private func configureQuantumResistanceHeader(_ header: SettingsHeaderView) { let title = NSLocalizedString( "QUANTUM_RESISTANCE_HEADER_LABEL", @@ -577,7 +552,6 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< self.map { $0.delegate?.showInfo(for: .quantumResistance) } } } - #endif private func selectRow(at indexPath: IndexPath?, animated: Bool = false) { tableView?.selectRow(at: indexPath, animated: animated, scrollPosition: .none) diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelStateTests.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelStateTests.swift new file mode 100644 index 0000000000..9a707fb30d --- /dev/null +++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelStateTests.swift @@ -0,0 +1,139 @@ +// +// TunnelStateTests.swift +// MullvadVPNTests +// +// Created by Andrew Bulhak on 2024-05-03. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import MullvadTypes +import PacketTunnelCore +import XCTest + +final class TunnelStateTests: XCTestCase { + let arbitrarySelectedRelay = SelectedRelay( + endpoint: MullvadEndpoint( + ipv4Relay: IPv4Endpoint(ip: .any, port: 0), + ipv4Gateway: .any, + ipv6Gateway: .any, + publicKey: Data() + ), + hostname: "hostname-goes-here", + location: Location(country: "country", countryCode: "", city: "city", cityCode: "", latitude: 0, longitude: 0), + retryAttempts: 0 + ) + + // MARK: description + + func testDescription_Connecting_NoRelay() { + XCTAssertEqual( + TunnelState.connecting(nil, isPostQuantum: false).description, + "connecting, fetching relay" + ) + + XCTAssertEqual( + TunnelState.connecting(nil, isPostQuantum: true).description, + "connecting (PQ), fetching relay" + ) + } + + func testDescription_Connecting_WithRelay() { + XCTAssertEqual( + TunnelState.connecting(arbitrarySelectedRelay, isPostQuantum: false).description, + "connecting to hostname-goes-here" + ) + + XCTAssertEqual( + TunnelState.connecting(arbitrarySelectedRelay, isPostQuantum: true).description, + "connecting (PQ) to hostname-goes-here" + ) + } + + func testDescription_Connected() { + XCTAssertEqual( + TunnelState.connected(arbitrarySelectedRelay, isPostQuantum: false).description, + "connected to hostname-goes-here" + ) + + XCTAssertEqual( + TunnelState.connected(arbitrarySelectedRelay, isPostQuantum: true).description, + "connected (PQ) to hostname-goes-here" + ) + } + + // MARK: localizedTitleForSecureLabel + + func testLocalizedTitleForSecureLabel_Connecting() { + XCTAssertEqual( + TunnelState.connecting(nil, isPostQuantum: false).localizedTitleForSecureLabel, + "Creating secure connection" + ) + + XCTAssertEqual( + TunnelState.connecting(nil, isPostQuantum: true).localizedTitleForSecureLabel, + "Creating quantum secure connection" + ) + } + + func testLocalizedTitleForSecureLabel_Reconnecting() { + XCTAssertEqual( + TunnelState.reconnecting(arbitrarySelectedRelay, isPostQuantum: false).localizedTitleForSecureLabel, + "Creating secure connection" + ) + + XCTAssertEqual( + TunnelState.reconnecting(arbitrarySelectedRelay, isPostQuantum: true).localizedTitleForSecureLabel, + "Creating quantum secure connection" + ) + } + + func testLocalizedTitleForSecureLabel_Connected() { + XCTAssertEqual( + TunnelState.connected(arbitrarySelectedRelay, isPostQuantum: false).localizedTitleForSecureLabel, + "Secure connection" + ) + + XCTAssertEqual( + TunnelState.connected(arbitrarySelectedRelay, isPostQuantum: true).localizedTitleForSecureLabel, + "Quantum secure connection" + ) + } + + // MARK: localizedAccessibilityLabel + + func testLocalizedAccessibilityLabel_Connecting() { + XCTAssertEqual( + TunnelState.connecting(nil, isPostQuantum: false).localizedAccessibilityLabel, + "Creating secure connection" + ) + + XCTAssertEqual( + TunnelState.connecting(nil, isPostQuantum: true).localizedAccessibilityLabel, + "Creating quantum secure connection" + ) + } + + func testLocalizedAccessibilityLabel_Reconnecting() { + XCTAssertEqual( + TunnelState.reconnecting(arbitrarySelectedRelay, isPostQuantum: false).localizedAccessibilityLabel, + "Reconnecting to city, country" + ) + + XCTAssertEqual( + TunnelState.reconnecting(arbitrarySelectedRelay, isPostQuantum: true).localizedAccessibilityLabel, + "Reconnecting to city, country" + ) + } + + func testLocalizedAccessibilityLabel_Connected() { + XCTAssertEqual( + TunnelState.connected(arbitrarySelectedRelay, isPostQuantum: false).localizedAccessibilityLabel, + "Secure connection. Connected to city, country" + ) + + XCTAssertEqual( + TunnelState.connected(arbitrarySelectedRelay, isPostQuantum: true).localizedAccessibilityLabel, + "Quantum secure connection. Connected to city, country" + ) + } +} diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift index a635348773..ab64040f0a 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift @@ -8,6 +8,7 @@ import Foundation import MullvadLogging +import MullvadPostQuantum import MullvadREST import MullvadSettings import MullvadTypes @@ -22,9 +23,12 @@ class PacketTunnelProvider: NEPacketTunnelProvider { private let constraintsUpdater = RelayConstraintsUpdater() private var actor: PacketTunnelActor! + private var postQuantumActor: PostQuantumKeyExchangeActor! private var appMessageHandler: AppMessageHandler! private var stateObserverTask: AnyTask? private var deviceChecker: DeviceChecker! + private var adapter: WgAdapter! + private var relaySelector: RelaySelectorWrapper! override init() { Self.configureLogging() @@ -48,7 +52,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { addressCache: addressCache ) - let adapter = WgAdapter(packetTunnelProvider: self) + adapter = WgAdapter(packetTunnelProvider: self) let tunnelMonitor = TunnelMonitor( eventQueue: internalQueue, @@ -65,6 +69,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { let devicesProxy = proxyFactory.createDevicesProxy() deviceChecker = DeviceChecker(accountsProxy: accountsProxy, devicesProxy: devicesProxy) + relaySelector = RelaySelectorWrapper(relayCache: ipOverrideWrapper) actor = PacketTunnelActor( timings: PacketTunnelActorTimings(), @@ -72,11 +77,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider { tunnelMonitor: tunnelMonitor, defaultPathObserver: PacketTunnelPathObserver(packetTunnelProvider: self, eventQueue: internalQueue), blockedStateErrorMapper: BlockedStateErrorMapper(), - relaySelector: RelaySelectorWrapper(relayCache: ipOverrideWrapper), + relaySelector: relaySelector, settingsReader: SettingsReader(), protocolObfuscator: ProtocolObfuscator<UDPOverTCPObfuscator>() ) + postQuantumActor = PostQuantumKeyExchangeActor(packetTunnel: self) + let urlRequestProxy = URLRequestProxy(dispatchQueue: internalQueue, transportProvider: transportProvider) appMessageHandler = AppMessageHandler(packetTunnelActor: actor, urlRequestProxy: urlRequestProxy) @@ -104,6 +111,12 @@ class PacketTunnelProvider: NEPacketTunnelProvider { if connectionState.connectionAttemptCount > 1 { return } + case .negotiatingPostQuantumKey: + // When negotiating post quantum keys, allow the connection to go through immediately. + // Otherwise, the in-tunnel TCP connection will never become ready as the OS doesn't let + // any traffic through until this function returns, which would prevent negotiating keys + // from an unconnected state. + return default: break } @@ -235,10 +248,8 @@ extension PacketTunnelProvider { // Cache last connection attempt to filter out repeating calls. lastConnectionAttempt = connectionAttempt - #if DEBUG - case .negotiatingKey: - break - #endif + case let .negotiatingPostQuantumKey(_, privateKey): + postQuantumActor.startNegotiation(with: privateKey) case .initial, .connected, .disconnecting, .disconnected, .error: break @@ -247,6 +258,16 @@ extension PacketTunnelProvider { } } + func createTCPConnectionForPQPSK(_ gatewayAddress: String) -> NWTCPConnection { + let gatewayEndpoint = NWHostEndpoint(hostname: gatewayAddress, port: "1337") + return createTCPConnectionThroughTunnel( + to: gatewayEndpoint, + enableTLS: false, + tlsParameters: nil, + delegate: nil + ) + } + private func stopObservingActorState() { stateObserverTask?.cancel() stateObserverTask = nil @@ -283,8 +304,13 @@ extension PacketTunnelProvider { } extension PacketTunnelProvider: PostQuantumKeyReceiving { - func receivePostQuantumKey(_ key: PreSharedKey) { - // TODO: send the key to the actor - actor.replacePreSharedKey(key) + func receivePostQuantumKey(_ key: PreSharedKey, ephemeralKey: PrivateKey) { + actor.replacePreSharedKey(key, ephemeralKey: ephemeralKey) + postQuantumActor.acknowledgeNegotiationConcluded() + } + + func keyExchangeFailed() { + postQuantumActor.acknowledgeNegotiationConcluded() + actor.reconnect(to: .current) } } diff --git a/ios/PacketTunnel/PacketTunnelProvider/SettingsReader.swift b/ios/PacketTunnel/PacketTunnelProvider/SettingsReader.swift index 10282b5ff3..ade3b5a3af 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/SettingsReader.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/SettingsReader.swift @@ -21,7 +21,8 @@ struct SettingsReader: SettingsReaderProtocol { interfaceAddresses: [deviceData.ipv4Address, deviceData.ipv6Address], relayConstraints: settings.relayConstraints, dnsServers: settings.dnsSettings.selectedDNSServers, - obfuscation: settings.wireGuardObfuscation + obfuscation: settings.wireGuardObfuscation, + quantumResistance: settings.tunnelQuantumResistance ) } } diff --git a/ios/PacketTunnel/PostQuantumKeyExchangeActor.swift b/ios/PacketTunnel/PostQuantumKeyExchangeActor.swift new file mode 100644 index 0000000000..e9189d947c --- /dev/null +++ b/ios/PacketTunnel/PostQuantumKeyExchangeActor.swift @@ -0,0 +1,78 @@ +// +// PostQuantumKeyExchangeActor.swift +// PacketTunnel +// +// Created by Marco Nikic on 2024-04-12. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadPostQuantum +import NetworkExtension +import WireGuardKitTypes + +class PostQuantumKeyExchangeActor { + struct Negotiation { + var negotiator: PostQuantumKeyNegotiator + var inTunnelTCPConnection: NWTCPConnection + var tcpConnectionObserver: NSKeyValueObservation + + func cancel() { + negotiator.cancelKeyNegotiation() + tcpConnectionObserver.invalidate() + inTunnelTCPConnection.cancel() + } + } + + unowned let packetTunnel: PacketTunnelProvider + private var negotiation: Negotiation? + + init(packetTunnel: PacketTunnelProvider) { + self.packetTunnel = packetTunnel + } + + private func createTCPConnection(_ gatewayEndpoint: NWHostEndpoint) -> NWTCPConnection { + self.packetTunnel.createTCPConnectionThroughTunnel( + to: gatewayEndpoint, + enableTLS: false, + tlsParameters: nil, + delegate: nil + ) + } + + func startNegotiation(with privateKey: PrivateKey) { + let negotiator = PostQuantumKeyNegotiator() + + let gatewayAddress = "10.64.0.1" + let IPv4Gateway = IPv4Address(gatewayAddress)! + let endpoint = NWHostEndpoint(hostname: gatewayAddress, port: "1337") + let inTunnelTCPConnection = createTCPConnection(endpoint) + + let ephemeralSharedKey = PrivateKey() // This will become the new private key of the device + + let tcpConnectionObserver = inTunnelTCPConnection.observe(\.isViable, options: [ + .initial, + .new, + ]) { [weak self] observedConnection, _ in + guard let self, observedConnection.isViable else { return } + negotiator.negotiateKey( + gatewayIP: IPv4Gateway, + devicePublicKey: privateKey.publicKey, + presharedKey: ephemeralSharedKey, + packetTunnel: packetTunnel, + tcpConnection: inTunnelTCPConnection + ) + self.negotiation?.tcpConnectionObserver.invalidate() + } + negotiation = Negotiation( + negotiator: negotiator, + inTunnelTCPConnection: inTunnelTCPConnection, + tcpConnectionObserver: tcpConnectionObserver + ) + } + + func acknowledgeNegotiationConcluded() { + negotiation?.cancel() + negotiation = nil + } +} diff --git a/ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift b/ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift index 06b970a223..79833d16c2 100644 --- a/ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift +++ b/ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift @@ -21,14 +21,32 @@ public struct PublicKeyError: LocalizedError { } /// Struct building tunnel adapter configuration. -struct ConfigurationBuilder { +public struct ConfigurationBuilder { var privateKey: PrivateKey var interfaceAddresses: [IPAddressRange] var dns: SelectedDNSServers? var endpoint: MullvadEndpoint? var allowedIPs: [IPAddressRange] + // or should this go in MullvadEndpoint? + var preSharedKey: PreSharedKey? - func makeConfiguration() throws -> TunnelAdapterConfiguration { + public init( + privateKey: PrivateKey, + interfaceAddresses: [IPAddressRange], + dns: SelectedDNSServers? = nil, + endpoint: MullvadEndpoint? = nil, + allowedIPs: [IPAddressRange], + preSharedKey: PreSharedKey? = nil + ) { + self.privateKey = privateKey + self.interfaceAddresses = interfaceAddresses + self.dns = dns + self.endpoint = endpoint + self.allowedIPs = allowedIPs + self.preSharedKey = preSharedKey + } + + public func makeConfiguration() throws -> TunnelAdapterConfiguration { return TunnelAdapterConfiguration( privateKey: privateKey, interfaceAddresses: interfaceAddresses, @@ -48,7 +66,8 @@ struct ConfigurationBuilder { return TunnelPeer( endpoint: .ipv4(endpoint.ipv4Relay), - publicKey: publicKey + publicKey: publicKey, + preSharedKey: preSharedKey ) } } diff --git a/ios/PacketTunnelCore/Actor/ObservedState+Extensions.swift b/ios/PacketTunnelCore/Actor/ObservedState+Extensions.swift index 1504f3e47d..08f023a2d8 100644 --- a/ios/PacketTunnelCore/Actor/ObservedState+Extensions.swift +++ b/ios/PacketTunnelCore/Actor/ObservedState+Extensions.swift @@ -12,13 +12,10 @@ import MullvadTypes extension ObservedState { public var relayConstraints: RelayConstraints? { switch self { - case let .connecting(connState), let .connected(connState), let .reconnecting(connState): + case let .connecting(connState), let .connected(connState), let .reconnecting(connState), + let .negotiatingPostQuantumKey(connState, _): connState.relayConstraints - #if DEBUG - case let .negotiatingKey(connState): - connState.relayConstraints - #endif case let .error(blockedState): blockedState.relayConstraints @@ -33,10 +30,8 @@ extension ObservedState { "Connected" case .connecting: "Connecting" - #if DEBUG - case .negotiatingKey: - "Negotiating key" - #endif + case .negotiatingPostQuantumKey: + "Negotiating Post Quantum Secure Key" case .reconnecting: "Reconnecting" case .disconnecting: @@ -56,6 +51,7 @@ extension ObservedState { let .connecting(connectionState), let .reconnecting(connectionState), let .connected(connectionState), + let .negotiatingPostQuantumKey(connectionState, _), let .disconnecting(connectionState): connectionState default: diff --git a/ios/PacketTunnelCore/Actor/ObservedState.swift b/ios/PacketTunnelCore/Actor/ObservedState.swift index 3f92b300e5..bdb85a8e51 100644 --- a/ios/PacketTunnelCore/Actor/ObservedState.swift +++ b/ios/PacketTunnelCore/Actor/ObservedState.swift @@ -10,15 +10,14 @@ import Combine import Foundation import MullvadTypes import Network +import WireGuardKitTypes /// A serializable representation of internal state. public enum ObservedState: Equatable, Codable { case initial case connecting(ObservedConnectionState) case reconnecting(ObservedConnectionState) - #if DEBUG - case negotiatingKey(ObservedConnectionState) - #endif + case negotiatingPostQuantumKey(ObservedConnectionState, PrivateKey) case connected(ObservedConnectionState) case disconnecting(ObservedConnectionState) case disconnected @@ -34,6 +33,7 @@ public struct ObservedConnectionState: Equatable, Codable { public var transportLayer: TransportLayer public var remotePort: UInt16 public var lastKeyRotation: Date? + public let isPostQuantum: Bool public var isNetworkReachable: Bool { networkReachability != .unreachable @@ -46,7 +46,8 @@ public struct ObservedConnectionState: Equatable, Codable { connectionAttemptCount: UInt, transportLayer: TransportLayer, remotePort: UInt16, - lastKeyRotation: Date? = nil + lastKeyRotation: Date? = nil, + isPostQuantum: Bool ) { self.selectedRelay = selectedRelay self.relayConstraints = relayConstraints @@ -55,6 +56,7 @@ public struct ObservedConnectionState: Equatable, Codable { self.transportLayer = transportLayer self.remotePort = remotePort self.lastKeyRotation = lastKeyRotation + self.isPostQuantum = isPostQuantum } } @@ -78,6 +80,8 @@ extension State { return .reconnecting(connState.observedConnectionState) case let .disconnecting(connState): return .disconnecting(connState.observedConnectionState) + case let .negotiatingPostQuantumKey(connState, privateKey): + return .negotiatingPostQuantumKey(connState.observedConnectionState, privateKey) case .disconnected: return .disconnected case let .error(blockedState): @@ -96,7 +100,8 @@ extension State.ConnectionData { connectionAttemptCount: connectionAttemptCount, transportLayer: transportLayer, remotePort: remotePort, - lastKeyRotation: lastKeyRotation + lastKeyRotation: lastKeyRotation, + isPostQuantum: isPostQuantum ) } } diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor+ConnectionMonitoring.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+ConnectionMonitoring.swift index c2bc921b05..84d34d2650 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor+ConnectionMonitoring.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+ConnectionMonitoring.swift @@ -42,7 +42,7 @@ extension PacketTunnelActor { connState.connectionAttemptCount = 0 state = .connected(connState) - case .initial, .connected, .disconnecting, .disconnected, .error: + case .initial, .connected, .disconnecting, .disconnected, .error, .negotiatingPostQuantumKey: break } } @@ -53,7 +53,7 @@ extension PacketTunnelActor { case .connecting, .reconnecting, .connected: commandChannel.send(.reconnect(.random, reason: .connectionLoss)) - case .initial, .disconnected, .disconnecting, .error: + case .initial, .disconnected, .disconnecting, .error, .negotiatingPostQuantumKey: break } } diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor+ErrorState.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+ErrorState.swift index b450f44819..fd88ea94e4 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor+ErrorState.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+ErrorState.swift @@ -84,7 +84,8 @@ extension PacketTunnelActor { return nil } - case .disconnecting, .disconnected: + // Post quantum key exchange cannot enter the blocked state + case .disconnecting, .disconnected, .negotiatingPostQuantumKey: return nil } } diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift index 0d80fb6d58..209a6041d5 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift @@ -56,8 +56,8 @@ extension PacketTunnelActor { - Parameter key: the new key */ - nonisolated public func replacePreSharedKey(_ key: PreSharedKey) { - commandChannel.send(.replaceDevicePrivateKey(key)) + nonisolated public func replacePreSharedKey(_ key: PreSharedKey, ephemeralKey: PrivateKey) { + commandChannel.send(.replaceDevicePrivateKey(key, ephemeralKey: ephemeralKey)) } /** diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift index 81b3bfaff1..552624d504 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift @@ -43,7 +43,7 @@ public actor PacketTunnelActor { let tunnelMonitor: TunnelMonitorProtocol let defaultPathObserver: DefaultPathObserverProtocol let blockedStateErrorMapper: BlockedStateErrorMapperProtocol - let relaySelector: RelaySelectorProtocol + public let relaySelector: RelaySelectorProtocol let settingsReader: SettingsReaderProtocol let protocolObfuscator: ProtocolObfuscation @@ -113,8 +113,8 @@ public actor PacketTunnelActor { case let .networkReachability(defaultPath): await handleDefaultPathChange(defaultPath) - case .replaceDevicePrivateKey: - self.logger.warning("Not yet implemented") + case let .replaceDevicePrivateKey(preSharedKey, ephemeralKey): + await postQuantumConnect(with: preSharedKey, privateKey: ephemeralKey) } } } @@ -164,7 +164,8 @@ extension PacketTunnelActor { /// Stop the tunnel. private func stop() async { switch state { - case let .connected(connState), let .connecting(connState), let .reconnecting(connState): + case let .connected(connState), let .connecting(connState), let .reconnecting(connState), + let .negotiatingPostQuantumKey(connState, _): state = .disconnecting(connState) tunnelMonitor.stop() @@ -199,7 +200,9 @@ extension PacketTunnelActor { private func reconnect(to nextRelay: NextRelay, reason: ReconnectReason) async { do { switch state { - case .connecting, .connected, .reconnecting, .error: + // There is no connection monitoring going on when exchanging keys. + // The procedure starts from scratch for each reconnection attempts. + case .connecting, .connected, .reconnecting, .error, .negotiatingPostQuantumKey: switch reason { case .connectionLoss: // Tunnel monitor is already paused at this point. Avoid calling stop() to prevent the reset of @@ -221,6 +224,44 @@ extension PacketTunnelActor { } } + private func postQuantumConnect(with key: PreSharedKey, privateKey: PrivateKey) async { + guard + // It is important to select the same relay that was saved in the connection state as the key negotiation happened with this specific relay. + let selectedRelay = state.connectionData?.selectedRelay, + let settings: Settings = try? settingsReader.read(), + let connectionState = try? obfuscateConnection( + nextRelay: .preSelected(selectedRelay), + settings: settings, + reason: .userInitiated + ) + else { return } + + let configurationBuilder = ConfigurationBuilder( + privateKey: privateKey, + interfaceAddresses: settings.interfaceAddresses, + dns: settings.dnsServers, + endpoint: connectionState.connectedEndpoint, + allowedIPs: [ + IPAddressRange(from: "0.0.0.0/0")!, + IPAddressRange(from: "::/0")!, + ], + preSharedKey: key + ) + stopDefaultPathObserver() + + state = .connecting(connectionState) + + defer { + // Restart default path observer and notify the observer with the current path that might have changed while + // path observer was paused. + startDefaultPathObserver(notifyObserverWithCurrentPath: false) + } + + try? await tunnelAdapter.start(configuration: configurationBuilder.makeConfiguration()) + // Resume tunnel monitoring and use IPv4 gateway as a probe address. + tunnelMonitor.start(probeAddress: connectionState.selectedRelay.endpoint.ipv4Gateway) + } + /** Attempt to start the tunnel by performing the following steps: @@ -237,21 +278,36 @@ extension PacketTunnelActor { - reason: reason for reconnect */ private func tryStart( - nextRelay: NextRelay = .random, + nextRelay: NextRelay, reason: ReconnectReason = .userInitiated ) async throws { let settings: Settings = try settingsReader.read() + if settings.quantumResistance.isEnabled { + if let connectionState = try makeConnectionState(nextRelay: nextRelay, settings: settings, reason: reason) { + let selectedEndpoint = connectionState.selectedRelay.endpoint + let activeKey = activeKey(from: connectionState, in: settings) + + let configurationBuilder = ConfigurationBuilder( + privateKey: activeKey, + interfaceAddresses: settings.interfaceAddresses, + dns: settings.dnsServers, + endpoint: selectedEndpoint, + allowedIPs: [ + IPAddressRange(from: "10.64.0.1/32")!, + ] + ) + + try await tunnelAdapter.start(configuration: configurationBuilder.makeConfiguration()) + state = .negotiatingPostQuantumKey(connectionState, activeKey) + } + return + } + guard let connectionState = try obfuscateConnection(nextRelay: nextRelay, settings: settings, reason: reason), let targetState = state.targetStateForReconnect else { return } - let activeKey: PrivateKey - switch connectionState.keyPolicy { - case .useCurrent: - activeKey = settings.privateKey - case let .usePrior(priorKey, _): - activeKey = priorKey - } + let activeKey = activeKey(from: connectionState, in: settings) switch targetState { case .connecting: @@ -326,6 +382,8 @@ extension PacketTunnelActor { connectionState.incrementAttemptCount() } fallthrough + case let .negotiatingPostQuantumKey(connectionState, _): + return connectionState case var .connected(connectionState): let selectedRelay = try callRelaySelector( connectionState.selectedRelay, @@ -353,10 +411,20 @@ extension PacketTunnelActor { lastKeyRotation: lastKeyRotation, connectedEndpoint: selectedRelay.endpoint, transportLayer: .udp, - remotePort: selectedRelay.endpoint.ipv4Relay.port + remotePort: selectedRelay.endpoint.ipv4Relay.port, + isPostQuantum: settings.quantumResistance.isEnabled ) } + private func activeKey(from state: State.ConnectionData, in settings: Settings) -> PrivateKey { + switch state.keyPolicy { + case .useCurrent: + settings.privateKey + case let .usePrior(priorKey, _): + priorKey + } + } + private func obfuscateConnection( nextRelay: NextRelay, settings: Settings, @@ -382,7 +450,8 @@ extension PacketTunnelActor { lastKeyRotation: connectionState.lastKeyRotation, connectedEndpoint: obfuscatedEndpoint, transportLayer: transportLayer, - remotePort: protocolObfuscator.remotePort + remotePort: protocolObfuscator.remotePort, + isPostQuantum: connectionState.isPostQuantum ) } diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift index be325fc7b6..c4bd2c314b 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift @@ -37,7 +37,7 @@ extension PacketTunnelActor { case networkReachability(NetworkPath) /// Update the device private key, as per post-quantum protocols - case replaceDevicePrivateKey(PreSharedKey) + case replaceDevicePrivateKey(PreSharedKey, ephemeralKey: PrivateKey) /// Format command for log output. func logFormat() -> String { diff --git a/ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift b/ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift index 4224233f70..a4408392e3 100644 --- a/ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift +++ b/ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift @@ -36,3 +36,9 @@ public struct SelectedRelay: Equatable, Codable { self.retryAttempts = retryAttempts } } + +extension SelectedRelay: CustomDebugStringConvertible { + public var debugDescription: String { + "\(hostname) -> \(endpoint.ipv4Relay.description)" + } +} diff --git a/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift b/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift index 9d75149b45..ffe7cdcc2f 100644 --- a/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift +++ b/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift @@ -40,18 +40,22 @@ public struct Settings { /// Obfuscation settings public var obfuscation: WireGuardObfuscationSettings + public var quantumResistance: TunnelQuantumResistance + public init( privateKey: PrivateKey, interfaceAddresses: [IPAddressRange], relayConstraints: RelayConstraints, dnsServers: SelectedDNSServers, - obfuscation: WireGuardObfuscationSettings + obfuscation: WireGuardObfuscationSettings, + quantumResistance: TunnelQuantumResistance ) { self.privateKey = privateKey self.interfaceAddresses = interfaceAddresses self.relayConstraints = relayConstraints self.dnsServers = dnsServers self.obfuscation = obfuscation + self.quantumResistance = quantumResistance } } diff --git a/ios/PacketTunnelCore/Actor/State+Extensions.swift b/ios/PacketTunnelCore/Actor/State+Extensions.swift index 45b28a0c9c..be1f05d52d 100644 --- a/ios/PacketTunnelCore/Actor/State+Extensions.swift +++ b/ios/PacketTunnelCore/Actor/State+Extensions.swift @@ -23,7 +23,7 @@ extension State { case .initial: return .connecting - case .connecting: + case .connecting, .negotiatingPostQuantumKey: return .connecting case .connected, .reconnecting: @@ -59,13 +59,15 @@ extension State { case let .error(blockedState): return "\(name): \(blockedState.reason)" - case .initial, .disconnecting, .disconnected: + case .initial, .disconnecting, .disconnected, .negotiatingPostQuantumKey: return name } } var name: String { switch self { + case .negotiatingPostQuantumKey: + "Negotiating Post Quantum Key" case .connected: "Connected" case .connecting: @@ -89,6 +91,7 @@ extension State { let .connecting(connState), let .connected(connState), let .reconnecting(connState), + let .negotiatingPostQuantumKey(connState, _), let .disconnecting(connState): connState default: nil } @@ -118,6 +121,7 @@ extension State { case .connected: .connected(newValue) case .reconnecting: .reconnecting(newValue) case .disconnecting: .disconnecting(newValue) + case let .negotiatingPostQuantumKey(_, privateKey): .negotiatingPostQuantumKey(newValue, privateKey) default: self } } @@ -130,6 +134,7 @@ extension State { case let .connecting(connState), let .connected(connState), let .reconnecting(connState), + let .negotiatingPostQuantumKey(connState, _), let .disconnecting(connState): var associatedData: StateAssociatedData = connState modifier(&associatedData) diff --git a/ios/PacketTunnelCore/Actor/State.swift b/ios/PacketTunnelCore/Actor/State.swift index ab3fe767c9..f99799201c 100644 --- a/ios/PacketTunnelCore/Actor/State.swift +++ b/ios/PacketTunnelCore/Actor/State.swift @@ -58,6 +58,9 @@ enum State: Equatable { /// Initial state at the time when actor is initialized but before the first connection attempt. case initial + /// Establish a connection to the gateway, and exchange a post quantum key with the GRPC service that resides there. + case negotiatingPostQuantumKey(ConnectionData, PrivateKey) + /// Tunnel is attempting to connect. /// The actor should remain in this state until the very first connection is established, i.e determined by tunnel monitor. case connecting(ConnectionData) @@ -143,6 +146,9 @@ extension State { /// The remote port that was chosen to connect to `connectedEndpoint` public let remotePort: UInt16 + + /// True if post-quantum key exchange is enabled + public let isPostQuantum: Bool } /// Data associated with error state. diff --git a/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift b/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift index c95133b091..edb9e99e6d 100644 --- a/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift +++ b/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift @@ -29,7 +29,8 @@ extension SettingsReaderStub { interfaceAddresses: [IPAddressRange(from: "127.0.0.1/32")!], relayConstraints: RelayConstraints(), dnsServers: .gateway, - obfuscation: WireGuardObfuscationSettings(state: .off, port: .automatic) + obfuscation: WireGuardObfuscationSettings(state: .off, port: .automatic), + quantumResistance: .automatic ) return SettingsReaderStub { diff --git a/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift b/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift index fb37ef6b0d..9fa8b90258 100644 --- a/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift +++ b/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift @@ -208,7 +208,8 @@ final class PacketTunnelActorTests: XCTestCase { interfaceAddresses: [IPAddressRange(from: "127.0.0.1/32")!], relayConstraints: RelayConstraints(), dnsServers: .gateway, - obfuscation: WireGuardObfuscationSettings(state: .off, port: .automatic) + obfuscation: WireGuardObfuscationSettings(state: .off, port: .automatic), + quantumResistance: .automatic ) } } diff --git a/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift b/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift index dd644a74e1..21f30991bb 100644 --- a/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift +++ b/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift @@ -34,14 +34,14 @@ final class ProtocolObfuscatorTests: XCTestCase { } func testObfuscateOffDoesNotChangeEndpoint() { - let settings = settings(.off, obfuscationPort: .automatic) + let settings = settings(.off, obfuscationPort: .automatic, quantumResistance: .automatic) let nonObfuscatedEndpoint = obfuscator.obfuscate(endpoint, settings: settings) XCTAssertEqual(endpoint, nonObfuscatedEndpoint) } func testObfuscateOnPort80() throws { - let settings = settings(.on, obfuscationPort: .port80) + let settings = settings(.on, obfuscationPort: .port80, quantumResistance: .automatic) let obfuscatedEndpoint = obfuscator.obfuscate(endpoint, settings: settings) let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub) @@ -49,7 +49,7 @@ final class ProtocolObfuscatorTests: XCTestCase { } func testObfuscateOnPort5001() throws { - let settings = settings(.on, obfuscationPort: .port5001) + let settings = settings(.on, obfuscationPort: .port5001, quantumResistance: .automatic) let obfuscatedEndpoint = obfuscator.obfuscate(endpoint, settings: settings) let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub) @@ -57,7 +57,7 @@ final class ProtocolObfuscatorTests: XCTestCase { } func testObfuscateOnPortAutomaticIsPort80OnEvenRetryAttempts() throws { - let settings = settings(.on, obfuscationPort: .automatic) + let settings = settings(.on, obfuscationPort: .automatic, quantumResistance: .automatic) let obfuscatedEndpoint = obfuscator.obfuscate(endpoint, settings: settings, retryAttempts: 2) let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub) @@ -65,7 +65,7 @@ final class ProtocolObfuscatorTests: XCTestCase { } func testObfuscateOnPortAutomaticIsPort5001OnOddRetryAttempts() throws { - let settings = settings(.on, obfuscationPort: .automatic) + let settings = settings(.on, obfuscationPort: .automatic, quantumResistance: .automatic) let obfuscatedEndpoint = obfuscator.obfuscate(endpoint, settings: settings, retryAttempts: 3) let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub) @@ -73,7 +73,7 @@ final class ProtocolObfuscatorTests: XCTestCase { } func testObfuscateAutomaticIsPort80EveryThirdAttempts() throws { - let settings = settings(.automatic, obfuscationPort: .automatic) + let settings = settings(.automatic, obfuscationPort: .automatic, quantumResistance: .automatic) let obfuscatedEndpoint = obfuscator.obfuscate(endpoint, settings: settings, retryAttempts: 6) let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub) @@ -81,7 +81,7 @@ final class ProtocolObfuscatorTests: XCTestCase { } func testObfuscateAutomaticIsPort5001EveryFourthAttempts() throws { - let settings = settings(.automatic, obfuscationPort: .automatic) + let settings = settings(.automatic, obfuscationPort: .automatic, quantumResistance: .automatic) let obfuscatedEndpoint = obfuscator.obfuscate(endpoint, settings: settings, retryAttempts: 7) let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub) @@ -100,7 +100,8 @@ final class ProtocolObfuscatorTests: XCTestCase { private func settings( _ obfuscationState: WireGuardObfuscationState, - obfuscationPort: WireGuardObfuscationPort + obfuscationPort: WireGuardObfuscationPort, + quantumResistance: TunnelQuantumResistance ) -> Settings { Settings( privateKey: PrivateKey(), @@ -110,7 +111,7 @@ final class ProtocolObfuscatorTests: XCTestCase { obfuscation: WireGuardObfuscationSettings( state: obfuscationState, port: obfuscationPort - ) + ), quantumResistance: quantumResistance ) } } diff --git a/ios/build-rust-library.sh b/ios/build-rust-library.sh index 5de58559b0..94b8571a78 100644 --- a/ios/build-rust-library.sh +++ b/ios/build-rust-library.sh @@ -9,6 +9,8 @@ then exit 1 fi + + # what to pass to cargo build -p, e.g. your_lib_ffi FFI_TARGET=$1 @@ -30,13 +32,17 @@ if [[ "$CONFIGURATION" == "MockRelease" ]]; then RELFLAG=--release fi -if [[ -n "${DEVELOPER_SDK_DIR:-}" ]]; then - # Assume we're in Xcode, which means we're probably cross-compiling. - # In this case, we need to add an extra library search path for build scripts and proc-macros, - # which run on the host instead of the target. - # (macOS Big Sur does not have linkable libraries in /usr/lib/.) - export LIBRARY_PATH="${DEVELOPER_SDK_DIR}/MacOSX.sdk/usr/lib:${LIBRARY_PATH:-}" -fi +# For whatever reason, Xcode includes its toolchain paths in the PATH variable such as +# +# /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin +# /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/appleinternal/bin +# /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/local/bin +# /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/libexec +# When this happens, cargo will be tricked into building for the wrong architecture, which will lead to linker issues down the line. +# cargo does not need to know about all this, therefore, set the path to the bare minimum +export PATH="${HOME}/.cargo/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:" +# Since some of the dependencies come from homebrew, add it manually as well +export PATH="${PATH}:/opt/homebrew/bin:" IS_SIMULATOR=0 if [ "${LLVM_TARGET_TRIPLE_SUFFIX-}" = "-simulator" ]; then diff --git a/talpid-routing/src/unix/mod.rs b/talpid-routing/src/unix/mod.rs index 5f14d88d67..ca3f440935 100644 --- a/talpid-routing/src/unix/mod.rs +++ b/talpid-routing/src/unix/mod.rs @@ -18,7 +18,7 @@ use futures::stream::Stream; use std::net::IpAddr; #[allow(clippy::module_inception)] -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "ios"))] #[path = "macos/mod.rs"] pub mod imp; diff --git a/talpid-tunnel-config-client/Cargo.toml b/talpid-tunnel-config-client/Cargo.toml index 650f2ba566..18eb9342d0 100644 --- a/talpid-tunnel-config-client/Cargo.toml +++ b/talpid-tunnel-config-client/Cargo.toml @@ -17,20 +17,28 @@ talpid-types = { path = "../talpid-types" } tonic = { workspace = true } tower = { workspace = true } prost = { workspace = true } -tokio = { workspace = true, features = ["macros"] } -classic-mceliece-rust = { version = "2.0.0", features = ["mceliece460896f", "zeroize"] } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +classic-mceliece-rust = { version = "2.0.0", features = [ + "mceliece460896f", + "zeroize", +] } pqc_kyber = { version = "0.4.0", features = ["std", "kyber1024", "zeroize"] } zeroize = "1.5.7" libc = "0.2" [target.'cfg(windows)'.dependencies.windows-sys] workspace = true -features = [ - "Win32_Networking_WinSock" -] - -[dev-dependencies] -tokio = { workspace = true, features = ["rt-multi-thread"] } +features = ["Win32_Networking_WinSock"] [build-dependencies] -tonic-build = { workspace = true, default-features = false, features = ["transport", "prost"] } +tonic-build = { workspace = true, default-features = false, features = [ + "transport", + "prost", +] } +cbindgen = { version = "0.24.3", default-features = false } + +[target.'cfg(target_os = "ios")'.dependencies] +oslog = "0.2" + +[lib] +crate-type = ["staticlib"] diff --git a/talpid-tunnel-config-client/build.rs b/talpid-tunnel-config-client/build.rs index aeb21fe009..50c9bfd1df 100644 --- a/talpid-tunnel-config-client/build.rs +++ b/talpid-tunnel-config-client/build.rs @@ -1,3 +1,15 @@ fn main() { tonic_build::compile_protos("proto/ephemeralpeer.proto").unwrap(); + match std::env::var("TARGET").unwrap().as_str() { + "aarch64-apple-ios" | "aarch64-apple-ios-sim" => { + let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + cbindgen::Builder::new() + .with_crate(crate_dir) + .with_language(cbindgen::Language::C) + .generate() + .expect("failed to generate bindings") + .write_to_file("../ios/MullvadPostQuantum/talpid-tunnel-config-client/include/talpid_tunnel_config_client.h"); + } + &_ => (), + } } diff --git a/talpid-tunnel-config-client/src/ios_ffi/ios_runtime.rs b/talpid-tunnel-config-client/src/ios_ffi/ios_runtime.rs new file mode 100644 index 0000000000..ce0faf4ddb --- /dev/null +++ b/talpid-tunnel-config-client/src/ios_ffi/ios_runtime.rs @@ -0,0 +1,167 @@ +use super::{ios_tcp_connection::*, PostQuantumCancelToken}; +use crate::{request_ephemeral_peer, Error, RelayConfigService}; +use libc::c_void; +use std::{future::Future, io, pin::Pin, ptr, sync::Arc}; +use talpid_types::net::wireguard::{PrivateKey, PublicKey}; +use tokio::{runtime::Builder, sync::mpsc}; +use tonic::transport::channel::Endpoint; +use tower::util::service_fn; + +/// # Safety +/// packet_tunnel and tcp_connection must be valid pointers to a packet tunnel and a TCP connection +/// instances. +pub unsafe fn run_ios_runtime( + pub_key: [u8; 32], + ephemeral_key: [u8; 32], + packet_tunnel: *const c_void, + tcp_connection: *const c_void, +) -> Result<PostQuantumCancelToken, i32> { + match unsafe { IOSRuntime::new(pub_key, ephemeral_key, packet_tunnel, tcp_connection) } { + Ok(runtime) => { + let token = runtime.cancel_token_tx.clone(); + + runtime.run(); + Ok(PostQuantumCancelToken { + context: Arc::into_raw(token) as *mut _, + }) + } + Err(err) => { + log::error!("Failed to create runtime {}", err); + Err(-1) + } + } +} + +#[derive(Clone)] +pub struct SwiftContext { + pub packet_tunnel: *const c_void, + pub tcp_connection: *const c_void, +} + +unsafe impl Send for SwiftContext {} +unsafe impl Sync for SwiftContext {} + +struct IOSRuntime { + runtime: tokio::runtime::Runtime, + pub_key: [u8; 32], + ephemeral_key: [u8; 32], + packet_tunnel: SwiftContext, + cancel_token_tx: Arc<mpsc::UnboundedSender<()>>, + cancel_token_rx: mpsc::UnboundedReceiver<()>, +} + +impl IOSRuntime { + pub unsafe fn new( + pub_key: [u8; 32], + ephemeral_key: [u8; 32], + packet_tunnel: *const libc::c_void, + tcp_connection: *const c_void, + ) -> io::Result<Self> { + let runtime = Builder::new_multi_thread() + .enable_all() + .worker_threads(2) + .build()?; + + let context = SwiftContext { + packet_tunnel, + tcp_connection, + }; + + let (tx, rx) = mpsc::unbounded_channel(); + + Ok(Self { + runtime, + pub_key, + ephemeral_key, + packet_tunnel: context, + cancel_token_tx: Arc::new(tx), + cancel_token_rx: rx, + }) + } + + pub fn run(self) { + std::thread::spawn(move || { + self.run_service_inner(); + }); + } + + pub async fn ios_tcp_client( + ctx: SwiftContext, + ) -> Result<(RelayConfigService, IosTcpShutdownHandle), Error> { + let endpoint = Endpoint::from_static("tcp://0.0.0.0:0"); + + let (tcp_provider, conn_handle) = unsafe { IosTcpProvider::new(ctx.tcp_connection) }; + // One (1) TCP connection + let mut one_tcp_connection = Some(tcp_provider); + let conn = endpoint + .connect_with_connector(service_fn( + move |_| -> Pin<Box<dyn Future<Output = _> + Send>> { + if let Some(connection) = one_tcp_connection.take() { + return Box::pin(async move { Ok::<_, Error>(connection) }); + } + Box::pin(async { Err(Error::TcpConnectionExpired) }) + }, + )) + .await + .map_err(Error::GrpcConnectError)?; + + Ok((RelayConfigService::new(conn), conn_handle)) + } + + fn run_service_inner(self) { + let Self { + runtime, + mut cancel_token_rx, + .. + } = self; + + let packet_tunnel_ptr = self.packet_tunnel.packet_tunnel; + runtime.block_on(async move { + let (async_provider, shutdown_handle) = match Self::ios_tcp_client(self.packet_tunnel).await { + Ok(result) => result, + Err(error) => { + log::error!("Failed to create iOS TCP client: {error}"); + unsafe { + swift_post_quantum_key_ready(packet_tunnel_ptr, ptr::null(), ptr::null()); + } + return; + } + }; + let ephemeral_pub_key = PrivateKey::from(self.ephemeral_key).public_key(); + + tokio::select! { + ephemeral_peer = request_ephemeral_peer( + PublicKey::from(self.pub_key), + ephemeral_pub_key, + true, + false, + async_provider, + ) => { + shutdown_handle.shutdown(); + match ephemeral_peer { + Ok(peer) => { + match peer.psk { + Some(preshared_key) => unsafe { + let preshared_key_bytes = preshared_key.as_bytes(); + swift_post_quantum_key_ready(packet_tunnel_ptr, preshared_key_bytes.as_ptr(), self.ephemeral_key.as_ptr()); + }, + None => unsafe { + swift_post_quantum_key_ready(packet_tunnel_ptr, ptr::null(), ptr::null()); + } + + } + }, + Err(_) => unsafe { + swift_post_quantum_key_ready(packet_tunnel_ptr, ptr::null(), ptr::null()); + } + } + } + + _ = cancel_token_rx.recv() => { + shutdown_handle.shutdown() + // The swift runtime pre emptively cancelled the key exchange, nothing to do here. + } + } + }); + } +} diff --git a/talpid-tunnel-config-client/src/ios_ffi/ios_tcp_connection.rs b/talpid-tunnel-config-client/src/ios_ffi/ios_tcp_connection.rs new file mode 100644 index 0000000000..7609511b53 --- /dev/null +++ b/talpid-tunnel-config-client/src/ios_ffi/ios_tcp_connection.rs @@ -0,0 +1,184 @@ +use libc::c_void; +use std::{ + io::{self, Result}, + sync::{ + atomic::{self, AtomicBool}, + Arc, + }, + task::Poll, +}; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + sync::mpsc, +}; + +fn connection_closed_err() -> io::Error { + io::Error::new(io::ErrorKind::BrokenPipe, "TCP connection closed") +} + +extern "C" { + /// Called when there is data to send on the TCP connection. + /// The TCP connection must write data on the wire, then call the `handle_sent` function. + pub fn swift_nw_tcp_connection_send( + connection: *const libc::c_void, + data: *const libc::c_void, + data_len: usize, + sender: *const libc::c_void, + ); + + /// Called when there is data to read on the TCP connection. + /// The TCP connection must read data from the wire, then call the `handle_read` function. + pub fn swift_nw_tcp_connection_read( + connection: *const libc::c_void, + sender: *const libc::c_void, + ); + + /// Called when the preshared post quantum key is ready. + /// `raw_preshared_key` might be NULL if the key negotiation failed. + pub fn swift_post_quantum_key_ready( + raw_packet_tunnel: *const c_void, + raw_preshared_key: *const u8, + raw_ephemeral_private_key: *const u8, + ); +} + +unsafe impl Send for IosTcpProvider {} + +pub struct IosTcpProvider { + write_tx: mpsc::UnboundedSender<usize>, + write_rx: mpsc::UnboundedReceiver<usize>, + read_tx: mpsc::UnboundedSender<Box<[u8]>>, + read_rx: mpsc::UnboundedReceiver<Box<[u8]>>, + tcp_connection: *const c_void, + read_in_progress: bool, + write_in_progress: bool, + shutdown: Arc<AtomicBool>, +} + +pub struct IosTcpShutdownHandle { + shutdown: Arc<AtomicBool>, +} + +impl IosTcpProvider { + /** + * # Safety + * `tcp_connection` must be pointing to a valid instance of a `NWTCPConnection`, created by the `PacketTunnelProvider` + */ + pub unsafe fn new(tcp_connection: *const c_void) -> (Self, IosTcpShutdownHandle) { + let (tx, rx) = mpsc::unbounded_channel(); + let (recv_tx, recv_rx) = mpsc::unbounded_channel(); + let shutdown = Arc::new(AtomicBool::new(false)); + + ( + Self { + write_tx: tx, + write_rx: rx, + read_tx: recv_tx, + read_rx: recv_rx, + tcp_connection, + read_in_progress: false, + write_in_progress: false, + shutdown: shutdown.clone(), + }, + IosTcpShutdownHandle { shutdown }, + ) + } + + fn is_shutdown(&self) -> bool { + self.shutdown.load(atomic::Ordering::SeqCst) + } +} + +impl IosTcpShutdownHandle { + pub fn shutdown(&self) { + self.shutdown.store(true, atomic::Ordering::SeqCst); + } +} + +impl AsyncWrite for IosTcpProvider { + fn poll_write( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll<Result<usize>> { + let raw_sender = Box::into_raw(Box::new(self.write_tx.clone())); + + match self.write_rx.poll_recv(cx) { + std::task::Poll::Ready(Some(bytes_sent)) => { + self.write_in_progress = false; + Poll::Ready(Ok(bytes_sent)) + } + std::task::Poll::Ready(None) => { + self.write_in_progress = false; + Poll::Ready(Err(connection_closed_err())) + } + std::task::Poll::Pending => { + if self.is_shutdown() { + return Poll::Ready(Err(connection_closed_err())); + } + if self.write_in_progress { + return std::task::Poll::Pending; + } + self.write_in_progress = true; + unsafe { + swift_nw_tcp_connection_send( + self.tcp_connection, + buf.as_ptr() as _, + buf.len(), + raw_sender as _, + ); + } + std::task::Poll::Pending + } + } + } + + fn poll_flush( + self: std::pin::Pin<&mut Self>, + _: &mut std::task::Context<'_>, + ) -> std::task::Poll<Result<()>> { + std::task::Poll::Ready(Ok(())) + } + + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + _: &mut std::task::Context<'_>, + ) -> std::task::Poll<Result<()>> { + std::task::Poll::Ready(Ok(())) + } +} +impl AsyncRead for IosTcpProvider { + fn poll_read( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll<std::io::Result<()>> { + let raw_sender = Box::into_raw(Box::new(self.read_tx.clone())); + if self.is_shutdown() { + return Poll::Ready(Err(connection_closed_err())); + } + + match self.read_rx.poll_recv(cx) { + std::task::Poll::Ready(Some(data)) => { + buf.put_slice(&data); + self.read_in_progress = false; + Poll::Ready(Ok(())) + } + std::task::Poll::Ready(None) => { + self.read_in_progress = false; + Poll::Ready(Err(connection_closed_err())) + } + std::task::Poll::Pending => { + if self.read_in_progress { + return std::task::Poll::Pending; + } + self.read_in_progress = true; + unsafe { + swift_nw_tcp_connection_read(self.tcp_connection, raw_sender as _); + } + + std::task::Poll::Pending + } + } + } +} diff --git a/talpid-tunnel-config-client/src/ios_ffi/mod.rs b/talpid-tunnel-config-client/src/ios_ffi/mod.rs new file mode 100644 index 0000000000..9941bf4340 --- /dev/null +++ b/talpid-tunnel-config-client/src/ios_ffi/mod.rs @@ -0,0 +1,113 @@ +pub mod ios_runtime; +pub mod ios_tcp_connection; + +use crate::ios_ffi::ios_runtime::run_ios_runtime; +use libc::c_void; +use std::sync::Arc; +use tokio::sync::mpsc; + +use std::sync::Once; +static INIT_LOGGING: Once = Once::new(); + +#[repr(C)] +pub struct PostQuantumCancelToken { + // Must keep a pointer to a valid std::sync::Arc<tokio::mpsc::UnboundedSender> + pub context: *mut c_void, +} + +impl PostQuantumCancelToken { + /// #Safety + /// This function can only be called when the context pointer is valid. + unsafe fn cancel(&self) { + // Try to take the value, if there is a value, we can safely send the message, otherwise, assume it has been dropped and nothing happens + let send_tx: Arc<mpsc::UnboundedSender<()>> = unsafe { Arc::from_raw(self.context as _) }; + let _ = send_tx.send(()); + std::mem::forget(send_tx); + } +} + +impl Drop for PostQuantumCancelToken { + fn drop(&mut self) { + let _: Arc<mpsc::UnboundedSender<()>> = unsafe { Arc::from_raw(self.context as _) }; + } +} +unsafe impl Send for PostQuantumCancelToken {} + +#[no_mangle] +/** + * # Safety + * `sender` must be pointing to a valid instance of a `PostQuantumCancelToken` created by the `PacketTunnelProvider` + */ +pub unsafe extern "C" fn cancel_post_quantum_key_exchange(sender: *const PostQuantumCancelToken) { + let sender = unsafe { &*sender }; + sender.cancel(); +} + +/** + * # Safety + * `sender` must be pointing to a valid instance of a `PostQuantumCancelToken` created by the `PacketTunnelProvider`. + */ +#[no_mangle] +pub unsafe extern "C" fn drop_post_quantum_key_exchange_token( + sender: *const PostQuantumCancelToken, +) { + let _sender = unsafe { std::ptr::read(sender) }; +} + +/** + * # Safety + * `sender` must be pointing to a valid instance of a `write_tx` created by the `IosTcpProvider` + * + * Callback to call when the TCP connection has written data. + */ +#[no_mangle] +pub unsafe extern "C" fn handle_sent(bytes_sent: usize, sender: *const c_void) { + let send_tx: Box<mpsc::UnboundedSender<usize>> = unsafe { Box::from_raw(sender as _) }; + _ = send_tx.send(bytes_sent); +} + +/** + * # Safety + * `sender` must be pointing to a valid instance of a `read_tx` created by the `IosTcpProvider` + * + * Callback to call when the TCP connection has received data. + */ +#[no_mangle] +pub unsafe extern "C" fn handle_recv(data: *const u8, data_len: usize, sender: *const c_void) { + let read_tx: Box<mpsc::UnboundedSender<Box<[u8]>>> = unsafe { Box::from_raw(sender as _) }; + let mut bytes = vec![0u8; data_len]; + if !data.is_null() { + std::ptr::copy_nonoverlapping(data, bytes.as_mut_ptr(), data_len); + } + _ = read_tx.send(bytes.into_boxed_slice()); +} + +/// Entry point for exchanging post quantum keys on iOS. +/// The TCP connection must be created to go through the tunnel. +/// # Safety +/// This function is safe to call +#[no_mangle] +pub unsafe extern "C" fn negotiate_post_quantum_key( + public_key: *const u8, + ephemeral_key: *const u8, + packet_tunnel: *const c_void, + tcp_connection: *const c_void, + cancel_token: *mut PostQuantumCancelToken, +) -> i32 { + INIT_LOGGING.call_once(|| { + let _ = oslog::OsLogger::new("net.mullvad.MullvadVPN.TTCC") + .level_filter(log::LevelFilter::Trace) + .init(); + }); + + let pub_key_copy: [u8; 32] = unsafe { std::ptr::read(public_key as *const [u8; 32]) }; + let eph_key_copy: [u8; 32] = unsafe { std::ptr::read(ephemeral_key as *const [u8; 32]) }; + + match unsafe { run_ios_runtime(pub_key_copy, eph_key_copy, packet_tunnel, tcp_connection) } { + Ok(token) => { + unsafe { std::ptr::write(cancel_token, token) }; + 0 + } + Err(err) => err, + } +} diff --git a/talpid-tunnel-config-client/src/lib.rs b/talpid-tunnel-config-client/src/lib.rs index e272e794c7..04a81d924b 100644 --- a/talpid-tunnel-config-client/src/lib.rs +++ b/talpid-tunnel-config-client/src/lib.rs @@ -1,10 +1,16 @@ -use std::{ - fmt, - net::{IpAddr, SocketAddr}, -}; +use proto::PostQuantumRequestV1; +use std::fmt; +#[cfg(not(target_os = "ios"))] +use std::net::IpAddr; +#[cfg(not(target_os = "ios"))] +use std::net::SocketAddr; use talpid_types::net::wireguard::{PresharedKey, PublicKey}; +#[cfg(not(target_os = "ios"))] use tokio::net::TcpSocket; -use tonic::transport::{Channel, Endpoint}; +use tonic::transport::Channel; +#[cfg(not(target_os = "ios"))] +use tonic::transport::Endpoint; +#[cfg(not(target_os = "ios"))] use tower::service_fn; use zeroize::Zeroize; @@ -16,18 +22,27 @@ mod proto { tonic::include_proto!("ephemeralpeer"); } +#[cfg(target_os = "ios")] +pub mod ios_ffi; +#[cfg(target_os = "ios")] +use proto::ephemeral_peer_client::EphemeralPeerClient; + +#[cfg(not(target_os = "ios"))] use libc::setsockopt; -#[cfg(not(target_os = "windows"))] +#[cfg(not(any(target_os = "windows", target_os = "ios")))] mod sys { pub use libc::{socklen_t, IPPROTO_TCP, TCP_MAXSEG}; - pub use std::os::fd::{AsRawFd, RawFd}; + pub use std::os::fd::RawFd; } +#[cfg(not(target_os = "ios"))] +pub use std::os::fd::AsRawFd; #[cfg(target_os = "windows")] mod sys { pub use std::os::windows::io::{AsRawSocket, RawSocket}; pub use windows_sys::Win32::Networking::WinSock::{IPPROTO_IP, IP_USER_MTU}; } +#[cfg(not(target_os = "ios"))] use sys::*; #[derive(Debug)] @@ -44,6 +59,8 @@ pub enum Error { actual: usize, }, FailedDecapsulateKyber(kyber::KyberError), + #[cfg(target_os = "ios")] + TcpConnectionExpired, } impl std::fmt::Display for Error { @@ -65,6 +82,8 @@ impl std::fmt::Display for Error { write!(f, "Expected 2 ciphertext in the response, got {actual}") } FailedDecapsulateKyber(_) => "Failed to decapsulate Kyber1024 ciphertext".fmt(f), + #[cfg(target_os = "ios")] + TcpConnectionExpired => "TCP connection is already shut down".fmt(f), } } } @@ -79,7 +98,7 @@ impl std::error::Error for Error { } } -type RelayConfigService = proto::ephemeral_peer_client::EphemeralPeerClient<Channel>; +pub type RelayConfigService = proto::ephemeral_peer_client::EphemeralPeerClient<Channel>; /// Port used by the tunnel config service. pub const CONFIG_SERVICE_PORT: u16 = 1337; @@ -93,6 +112,7 @@ pub const CONFIG_SERVICE_PORT: u16 = 1337; /// 2. MH + PQ on macOS has connection issues during the handshake due to PF blocking packet /// fragments for not having a port. In the longer term this might be fixed by allowing the /// handshake to work even if there is fragmentation. +#[cfg(not(target_os = "ios"))] const CONFIG_CLIENT_MTU: u16 = 576; pub struct EphemeralPeer { @@ -101,40 +121,22 @@ pub struct EphemeralPeer { /// Negotiate a short-lived peer with a PQ-safe PSK or with DAITA enabled. pub async fn request_ephemeral_peer( - service_address: IpAddr, + #[cfg(not(target_os = "ios"))] service_address: IpAddr, parent_pubkey: PublicKey, ephemeral_pubkey: PublicKey, enable_post_quantum: bool, enable_daita: bool, + #[cfg(target_os = "ios")] mut client: EphemeralPeerClient<Channel>, ) -> Result<EphemeralPeer, Error> { - let (pq_request, kem_secrets) = if enable_post_quantum { - let (cme_kem_pubkey, cme_kem_secret) = classic_mceliece::generate_keys().await; - let kyber_keypair = kyber::keypair(&mut rand::thread_rng()); - - ( - Some(proto::PostQuantumRequestV1 { - kem_pubkeys: vec![ - proto::KemPubkeyV1 { - algorithm_name: classic_mceliece::ALGORITHM_NAME.to_owned(), - key_data: cme_kem_pubkey.as_array().to_vec(), - }, - proto::KemPubkeyV1 { - algorithm_name: kyber::ALGORITHM_NAME.to_owned(), - key_data: kyber_keypair.public.to_vec(), - }, - ], - }), - Some((cme_kem_secret, kyber_keypair.secret)), - ) - } else { - (None, None) - }; + let (pq_request, kem_secrets) = post_quantum_secrets(enable_post_quantum).await; let daita = Some(proto::DaitaRequestV1 { activate_daita: enable_daita, }); + #[cfg(not(target_os = "ios"))] let mut client = new_client(service_address).await?; + let response = client .register_peer_v1(proto::EphemeralPeerRequestV1 { wg_parent_pubkey: parent_pubkey.as_bytes().to_vec(), @@ -192,6 +194,37 @@ pub async fn request_ephemeral_peer( Ok(EphemeralPeer { psk }) } +async fn post_quantum_secrets( + enable_post_quantum: bool, +) -> ( + Option<PostQuantumRequestV1>, + Option<(classic_mceliece_rust::SecretKey<'static>, [u8; 3168])>, +) { + let (pq_request, kem_secrets) = if enable_post_quantum { + let (cme_kem_pubkey, cme_kem_secret) = classic_mceliece::generate_keys().await; + let kyber_keypair = kyber::keypair(&mut rand::thread_rng()); + + ( + Some(proto::PostQuantumRequestV1 { + kem_pubkeys: vec![ + proto::KemPubkeyV1 { + algorithm_name: classic_mceliece::ALGORITHM_NAME.to_owned(), + key_data: cme_kem_pubkey.as_array().to_vec(), + }, + proto::KemPubkeyV1 { + algorithm_name: kyber::ALGORITHM_NAME.to_owned(), + key_data: kyber_keypair.public.to_vec(), + }, + ], + }), + Some((cme_kem_secret, kyber_keypair.secret)), + ) + } else { + (None, None) + }; + (pq_request, kem_secrets) +} + /// Performs `dst = dst ^ src`. fn xor_assign(dst: &mut [u8; 32], src: &[u8; 32]) { for (dst_byte, src_byte) in dst.iter_mut().zip(src.iter()) { @@ -199,6 +232,7 @@ fn xor_assign(dst: &mut [u8; 32], src: &[u8; 32]) { } } +#[cfg(not(target_os = "ios"))] async fn new_client(addr: IpAddr) -> Result<RelayConfigService, Error> { let endpoint = Endpoint::from_static("tcp://0.0.0.0:0"); @@ -245,7 +279,7 @@ fn try_set_tcp_sock_mtu(sock: RawSocket, mtu: u16) { } } -#[cfg(not(windows))] +#[cfg(not(any(target_os = "windows", target_os = "ios")))] fn try_set_tcp_sock_mtu(dest: &IpAddr, sock: RawFd, mut mtu: u16) { const IPV4_HEADER_SIZE: u16 = 20; const IPV6_HEADER_SIZE: u16 = 40; |
