diff options
50 files changed, 2198 insertions, 467 deletions
diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index f0c12323c3..e1ab5c4b74 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -93,6 +93,11 @@ jobs: brew update brew install xcbeautify + - name: Install protobuf + run: | + brew update + brew install protobuf + - name: Run unit tests run: | set -o pipefail && env NSUnbufferedIO=YES xcodebuild \ 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/ios/MullvadPostQuantum/MullvadPostQuantum.h b/ios/MullvadPostQuantum/MullvadPostQuantum.h new file mode 100644 index 0000000000..c196b0527b --- /dev/null +++ b/ios/MullvadPostQuantum/MullvadPostQuantum.h @@ -0,0 +1,10 @@ +// +// MullvadPostQuantum.h +// MullvadPostQuantum +// +// Created by Marco Nikic on 2024-02-27. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +#import <Foundation/Foundation.h> +#import "talpid_tunnel_config_client.h" diff --git a/ios/MullvadPostQuantum/PacketTunnelProvider+TCPConnection.swift b/ios/MullvadPostQuantum/PacketTunnelProvider+TCPConnection.swift new file mode 100644 index 0000000000..b3bf815b1b --- /dev/null +++ b/ios/MullvadPostQuantum/PacketTunnelProvider+TCPConnection.swift @@ -0,0 +1,114 @@ +// +// 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 + +/// Writes data to the in-tunnel TCP connection +/// +/// This FFI function is called by Rust whenever there is data to be written to the in-tunnel TCP connection when exchanging +/// quantum-resistant pre shared keys. +/// +/// Whenever the flow control is given back from the connection, acknowledge that data was written using `rawWriteAcknowledgement`. +/// - Parameters: +/// - rawConnection: A raw pointer to the in-tunnel TCP connection +/// - rawData: A raw pointer to the data to write in the connection +/// - dataLength: The length of data to write in the connection +/// - rawWriteAcknowledgement: An opaque pointer needed for write acknowledgement +@_cdecl("swift_nw_tcp_connection_send") +func tcpConnectionSend( + rawConnection: UnsafeMutableRawPointer?, + rawData: UnsafeMutableRawPointer, + dataLength: UInt, + rawWriteAcknowledgement: UnsafeMutableRawPointer? +) { + guard let rawConnection, let rawWriteAcknowledgement else { + handle_sent(0, rawWriteAcknowledgement) + return + } + let tcpConnection = Unmanaged<NWTCPConnection>.fromOpaque(rawConnection).takeUnretainedValue() + let data = Data(bytes: rawData, count: Int(dataLength)) + + // The guarantee that all writes are sequential is done by virtue of not returning the execution context + // to Rust before this closure is done executing. + tcpConnection.write(data, completionHandler: { maybeError in + if maybeError != nil { + handle_sent(0, rawWriteAcknowledgement) + } else { + handle_sent(dataLength, rawWriteAcknowledgement) + } + }) +} + +/// Reads data to the in-tunnel TCP connection +/// +/// This FFI function is called by Rust whenever there is data to be read from the in-tunnel TCP connection when exchanging +/// quantum-resistant pre shared keys. +/// +/// Whenever the flow control is given back from the connection, acknowledge that data was read using `rawReadAcknowledgement`. +/// - Parameters: +/// - rawConnection: A raw pointer to the in-tunnel TCP connection +/// - rawReadAcknowledgement: An opaque pointer needed for read acknowledgement +@_cdecl("swift_nw_tcp_connection_read") +func tcpConnectionReceive( + rawConnection: UnsafeMutableRawPointer?, + rawReadAcknowledgement: UnsafeMutableRawPointer? +) { + guard let rawConnection, let rawReadAcknowledgement else { + handle_recv(nil, 0, rawReadAcknowledgement) + return + } + let tcpConnection = Unmanaged<NWTCPConnection>.fromOpaque(rawConnection).takeUnretainedValue() + tcpConnection.readMinimumLength(0, maximumLength: Int(UInt16.max)) { data, maybeError in + if let data { + if maybeError != nil { + handle_recv(nil, 0, rawReadAcknowledgement) + } else { + handle_recv(data.map { $0 }, UInt(data.count), rawReadAcknowledgement) + } + } + } +} + +/// End sequence of a quantum-secure pre shared key exchange. +/// +/// This FFI function is called by Rust when the quantum-secure pre shared key exchange has either failed, or succeeded. +/// When both the `rawPresharedKey` and the `rawEphemeralKey` are raw pointers to 32 bytes data arrays, +/// the quantum-secure key exchange is considered successful. In any other case, the exchange is considered failed. +/// +/// - Parameters: +/// - rawPacketTunnel: A raw pointer to the running instance of `NEPacketTunnelProvider` +/// - rawPresharedKey: A raw pointer to the quantum-secure pre shared key +/// - rawEphemeralKey: A raw pointer to the ephemeral private key of the device +@_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..16f7a24931 --- /dev/null +++ b/ios/MullvadPostQuantum/PostQuantumKeyNegotiator.swift @@ -0,0 +1,56 @@ +// +// 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 + +/** + Attempt to start the asynchronous process of key negotiation. Returns true if successfully started, false if failed. + */ +public class PostQuantumKeyNegotiator { + public init() {} + + var cancelToken: PostQuantumCancelToken? + + public func startNegotiation( + gatewayIP: IPv4Address, + devicePublicKey: PublicKey, + presharedKey: PrivateKey, + packetTunnel: NEPacketTunnelProvider, + tcpConnection: NWTCPConnection + ) -> Bool { + let packetTunnelPointer = Unmanaged.passUnretained(packetTunnel).toOpaque() + let opaqueConnection = Unmanaged.passUnretained(tcpConnection).toOpaque() + var cancelToken = PostQuantumCancelToken() + + let result = negotiate_post_quantum_key( + devicePublicKey.rawValue.map { $0 }, + presharedKey.rawValue.map { $0 }, + packetTunnelPointer, + opaqueConnection, + &cancelToken + ) + guard result == 0 else { + return false + } + self.cancelToken = cancelToken + return true + } + + 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..04180db289 --- /dev/null +++ b/ios/MullvadPostQuantum/talpid-tunnel-config-client/include/talpid_tunnel_config_client.h @@ -0,0 +1,96 @@ +#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; + +/** + * Called by the Swift side to signal that the quantum-secure key exchange should be cancelled. + * + * # 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); + +/** + * Called by the Swift side to signal that the Rust `PostQuantumCancelToken` can be safely dropped from memory. + * + * # 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); + +/** + * Called by Swift whenever data has been written to the in-tunnel TCP connection when exchanging + * quantum-resistant pre shared keys. + * + * If `bytes_sent` is 0, this indicates that the connection was closed or that an error occurred. + * + * # 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); + +/** + * Called by Swift whenever data has been read from the in-tunnel TCP connection when exchanging + * quantum-resistant pre shared keys. + * + * If `data` is null or empty, this indicates that the connection was closed or that an error occurred. + * An empty buffer is sent to the underlying reader to signal EOF. + * + * # 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 + * `public_key` and `ephemeral_key` must be valid respective `PublicKey` and `PrivateKey` types. + * They will not be valid after this function is called, and thus must be copied here. + * `packet_tunnel` and `tcp_connection` must be valid pointers to a packet tunnel and a TCP connection + * instances. + * `cancel_token` should be owned by the caller of this function. + */ +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..7b2bc1a714 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -44,10 +44,13 @@ 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 */; }; 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 */; }; 44DD7D2D2B74E44A0005F67F /* QuantumResistanceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D2C2B74E44A0005F67F /* QuantumResistanceSettings.swift */; }; + 44DF8AC42BF20BD200869CA4 /* PacketTunnelActor+PostQuantum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DF8AC32BF20BD200869CA4 /* PacketTunnelActor+PostQuantum.swift */; }; 5803B4B02940A47300C23744 /* TunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4AF2940A47300C23744 /* TunnelConfiguration.swift */; }; 5803B4B22940A48700C23744 /* TunnelStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4B12940A48700C23744 /* TunnelStore.swift */; }; 5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.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 */; }; @@ -814,6 +828,7 @@ A9BFB0012BD00B7F00F2BCA1 /* CustomListPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BFB0002BD00B7F00F2BCA1 /* CustomListPage.swift */; }; A9C342C12ACC37E30045F00E /* TunnelStatusBlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E0317D2ACC32920095D843 /* TunnelStatusBlockObserver.swift */; }; A9C342C32ACC3EE90045F00E /* RelayCacheTracker+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C342C22ACC3EE90045F00E /* RelayCacheTracker+Stubs.swift */; }; + A9D7E43C2BFCE43200213D55 /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = A906F9492BA1E09A002BF22E /* WireGuardKitTypes */; }; A9D99B9A2A1F7C3200DE27D3 /* RESTTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67D28F83CA50033DD93 /* RESTTransport.swift */; }; A9DF789B2B7D1DF10094E4AD /* mullvad-api.h in Headers */ = {isa = PBXBuildFile; fileRef = 01EF6F2D2B6A51B100125696 /* mullvad-api.h */; settings = {ATTRIBUTES = (Private, ); }; }; A9DF789D2B7D1E8B0094E4AD /* LoggedInWithTimeUITestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859089692B61763B003AF5F5 /* LoggedInWithTimeUITestCase.swift */; }; @@ -1195,19 +1210,26 @@ remoteGlobalIDString = 5840231E2A406BF5007B27AC; remoteInfo = TunnelObfuscation; }; - A9EC20F12A5D79ED0040D56E /* PBXContainerItemProxy */ = { + A944F2602B8DEFDB00473F4C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 58CE5E58224146200008646E /* Project object */; proxyType = 1; - remoteGlobalIDString = 5840231E2A406BF5007B27AC; - remoteInfo = TunnelObfuscation; + remoteGlobalIDString = A944F25B2B8DEFDB00473F4C; + remoteInfo = MullvadPostQuantum; }; - F04F959E2B21D02700431E08 /* PBXContainerItemProxy */ = { + A9630E412B8E10F700A65999 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 58CE5E58224146200008646E /* Project object */; proxyType = 1; - remoteGlobalIDString = 06799ABB28F98E1D00ACD94E; - remoteInfo = MullvadREST; + remoteGlobalIDString = 58D223D4294C8E5E0029F5F8; + remoteInfo = MullvadTypes; + }; + A9EC20F12A5D79ED0040D56E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 58CE5E58224146200008646E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5840231E2A406BF5007B27AC; + remoteInfo = TunnelObfuscation; }; F0ACE30E2BE4E478006D5333 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -1228,6 +1250,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 */, @@ -1309,6 +1332,27 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + A906F94B2BA1E09A002BF22E /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + 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; + }; F0ACE3292BE4E712006D5333 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -1371,10 +1415,13 @@ 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>"; }; 44DD7D2C2B74E44A0005F67F /* QuantumResistanceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuantumResistanceSettings.swift; sourceTree = "<group>"; }; + 44DF8AC32BF20BD200869CA4 /* PacketTunnelActor+PostQuantum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+PostQuantum.swift"; sourceTree = "<group>"; }; 5802EBC42A8E44AC00E5CE4C /* AppRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRoutes.swift; sourceTree = "<group>"; }; 5802EBC62A8E457A00E5CE4C /* AppRouteProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteProtocol.swift; sourceTree = "<group>"; }; 5802EBC82A8E45BA00E5CE4C /* ApplicationRouterDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationRouterDelegate.swift; sourceTree = "<group>"; }; @@ -1998,9 +2045,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 +2068,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 +2083,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 +2250,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 +2264,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 */, @@ -2284,6 +2340,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A944F2592B8DEFDB00473F4C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A9D7E43C2BFCE43200213D55 /* WireGuardKitTypes in Frameworks */, + A9630E432B8E10FB00A65999 /* MullvadTypes.framework in Frameworks */, + A944F2722B8E02F600473F4C /* libtalpid_tunnel_config_client.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; F0ACE3052BE4E478006D5333 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -2468,6 +2534,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 +2649,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 +3050,7 @@ 584F991F2902CBDD001F858D /* Frameworks */ = { isa = PBXGroup; children = ( + A944F2712B8E02E800473F4C /* libtalpid_tunnel_config_client.a */, 01EF6F332B6A590700125696 /* libmullvad_api.a */, 01EF6F312B6A58F000125696 /* debug */, 01EF6F2F2B6A588300125696 /* aarch64-apple-ios */, @@ -3046,6 +3115,7 @@ 58342C032AAB61FB003BA12D /* State+Extensions.swift */, 586E8DB72AAF4AC4007BF3DA /* Task+Duration.swift */, 58DDA18E2ABC32380039C360 /* Timings.swift */, + 44DF8AC32BF20BD200869CA4 /* PacketTunnelActor+PostQuantum.swift */, ); path = Actor; sourceTree = "<group>"; @@ -3367,7 +3437,6 @@ children = ( 58F3C0A824A50C0E003E76BE /* Assets */, 58ECD29023F178FD004298B6 /* Configurations */, - 584F991F2902CBDD001F858D /* Frameworks */, 01EF6F2D2B6A51B100125696 /* mullvad-api.h */, 8556EB512B9A1C6900D26DD4 /* MullvadApi.swift */, 58D223F4294C8FF00029F5F8 /* MullvadLogging */, @@ -3385,13 +3454,15 @@ 58CE5E7A224146470008646E /* PacketTunnel */, 58C7A4372A863F450060C66F /* PacketTunnelCore */, 58C7A4432A863F490060C66F /* PacketTunnelCoreTests */, - 58CE5E61224146200008646E /* Products */, 7A88DCCF2A8FABBE00D2FF0E /* Routing */, 7A88DCDD2A8FABBE00D2FF0E /* RoutingTests */, 589A454A28DDF59B00565204 /* Shared */, 7A83C3FC2A55B39500DFB83A /* TestPlans */, 584023202A406BF5007B27AC /* TunnelObfuscation */, 58695A9E2A4ADA9200328DB3 /* TunnelObfuscationTests */, + A944F25D2B8DEFDB00473F4C /* MullvadPostQuantum */, + 58CE5E61224146200008646E /* Products */, + 584F991F2902CBDD001F858D /* Frameworks */, ); sourceTree = "<group>"; }; @@ -3417,6 +3488,7 @@ 58B2FDD32AA71D2A003EB5C6 /* MullvadSettings.framework */, 852969252B4D9C1F007EAD4C /* MullvadVPNUITests.xctest */, F0ACE3082BE4E478006D5333 /* MullvadMockData.framework */, + A944F25C2B8DEFDB00473F4C /* MullvadPostQuantum.framework */, ); name = Products; sourceTree = "<group>"; @@ -3459,6 +3531,7 @@ 58F3F3682AA08E2200D3B0A4 /* PacketTunnelProvider */, 58915D662A25F9F20066445B /* DeviceCheck */, 588395612A9DF497008B63F6 /* WireGuardAdapter */, + A948809A2BC9308D0090A44C /* PostQuantumKeyExchangeActor.swift */, ); path = PacketTunnel; sourceTree = "<group>"; @@ -3910,6 +3983,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 = ( @@ -4186,6 +4270,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; + }; F0ACE3032BE4E478006D5333 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -4255,7 +4348,6 @@ buildRules = ( ); dependencies = ( - F04F959F2B21D02700431E08 /* PBXTargetDependency */, A91614D32B108F4D00F416EB /* PBXTargetDependency */, ); name = TunnelObfuscation; @@ -4413,6 +4505,7 @@ 7ABCA5B62A9349F20044A708 /* PBXTargetDependency */, 58B2FDD82AA71D2A003EB5C6 /* PBXTargetDependency */, F0ACE30F2BE4E478006D5333 /* PBXTargetDependency */, + A944F2612B8DEFDB00473F4C /* PBXTargetDependency */, ); name = MullvadVPN; packageProductDependencies = ( @@ -4430,6 +4523,7 @@ 58CE5E75224146470008646E /* Sources */, 58CE5E76224146470008646E /* Frameworks */, 58CE5E77224146470008646E /* Resources */, + A9259FD52B8E06E90032C82B /* Embed Frameworks */, ); buildRules = ( ); @@ -4612,6 +4706,30 @@ productReference = 852969252B4D9C1F007EAD4C /* MullvadVPNUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; + 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"; + }; F0ACE3072BE4E478006D5333 /* MullvadMockData */ = { isa = PBXNativeTarget; buildConfigurationList = F0ACE3162BE4E479006D5333 /* Build configuration list for PBXNativeTarget "MullvadMockData" */; @@ -4720,8 +4838,11 @@ CreatedOnToolsVersion = 15.1; TestTargetID = 58CE5E5F224146200008646E; }; + A944F25B2B8DEFDB00473F4C = { + CreatedOnToolsVersion = 15.2; + }; F0ACE3072BE4E478006D5333 = { - CreatedOnToolsVersion = 15.3; + CreatedOnToolsVersion = 15.2; }; }; }; @@ -4762,6 +4883,7 @@ 7A88DCCD2A8FABBE00D2FF0E /* Routing */, 7A88DCD62A8FABBE00D2FF0E /* RoutingTests */, F0ACE3072BE4E478006D5333 /* MullvadMockData */, + A944F25B2B8DEFDB00473F4C /* MullvadPostQuantum */, ); }; /* End PBXProject section */ @@ -4893,6 +5015,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A944F25A2B8DEFDB00473F4C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; F0ACE3062BE4E478006D5333 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -4977,6 +5106,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 +5395,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 */, @@ -5326,6 +5475,7 @@ 58FE25DA2AA72A8F003D1918 /* PacketTunnelActor.swift in Sources */, 587A5E522ADD7569003A70F1 /* ObservedState+Extensions.swift in Sources */, 58FE25E62AA738E8003D1918 /* TunnelAdapterProtocol.swift in Sources */, + 44DF8AC42BF20BD200869CA4 /* PacketTunnelActor+PostQuantum.swift in Sources */, 583832252AC318A100EA2071 /* PacketTunnelActor+ConnectionMonitoring.swift in Sources */, 58C7A4552A863FB90060C66F /* TunnelMonitor.swift in Sources */, 58C7AF182ABD84AB007EDD7A /* ProxyURLResponse.swift in Sources */, @@ -5424,6 +5574,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 +5927,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 */, @@ -5964,6 +6116,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A944F2582B8DEFDB00473F4C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A9630E492B921E6D00A65999 /* PacketTunnelProvider+TCPConnection.swift in Sources */, + A944F26A2B8DF32900473F4C /* PostQuantumKeyNegotiator.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; F0ACE3042BE4E478006D5333 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -6179,16 +6340,21 @@ 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 */; targetProxy = A9EC20F12A5D79ED0040D56E /* PBXContainerItemProxy */; }; - F04F959F2B21D02700431E08 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 06799ABB28F98E1D00ACD94E /* MullvadREST */; - targetProxy = F04F959E2B21D02700431E08 /* PBXContainerItemProxy */; - }; F0ACE30F2BE4E478006D5333 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F0ACE3072BE4E478006D5333 /* MullvadMockData */; @@ -6804,6 +6970,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 +6991,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_STRICT_CONCURRENCY = minimal; SWIFT_VERSION = 5.0; }; name = Release; @@ -6844,6 +7012,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 +7032,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).PacketTunnel"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_STRICT_CONCURRENCY = minimal; SWIFT_VERSION = 5.0; }; name = Release; @@ -7473,6 +7643,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 +7682,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 +8182,211 @@ }; 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_IDENTITY[sdk=iphoneos*]" = "Apple Distribution"; + 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 +8462,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 +8497,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; @@ -8673,6 +9052,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; @@ -8721,7 +9101,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; @@ -8987,6 +9367,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; + }; F0ACE3162BE4E479006D5333 /* Build configuration list for PBXNativeTarget "MullvadMockData" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -9070,6 +9461,11 @@ package = 58F097482A20C30000DA2DAD /* XCRemoteSwiftPackageReference "wireguard-apple" */; productName = WireGuardKitTypes; }; + A906F9492BA1E09A002BF22E /* WireGuardKitTypes */ = { + isa = XCSwiftPackageProductDependency; + package = 58F097482A20C30000DA2DAD /* XCRemoteSwiftPackageReference "wireguard-apple" */; + productName = WireGuardKitTypes; + }; F0ACE3272BE4E712006D5333 /* WireGuardKitTypes */ = { isa = XCSwiftPackageProductDependency; package = 58F097482A20C30000DA2DAD /* XCRemoteSwiftPackageReference "wireguard-apple" */; 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..f162810199 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -670,16 +670,11 @@ final class TunnelManager: StorePaymentObserver { refreshDeviceState() } switch newTunnelStatus.state { - case .connecting, .reconnecting: + case .connecting, .reconnecting, .negotiatingPostQuantumKey: // Start polling tunnel status to keep the relay information up to date // while the tunnel process is trying to connect. startPollingTunnelStatus(interval: establishingTunnelStatusPollInterval) - #if DEBUG - case .negotiatingKey: - startPollingTunnelStatus(interval: establishingTunnelStatusPollInterval) - #endif - case .connected, .waitingForConnectivity(.noConnection): // Start polling tunnel status to keep connectivity status up to date. startPollingTunnelStatus(interval: establishedTunnelStatusPollInterval) 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..6b27aa59f8 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,16 @@ 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, + onFailure: self.keyExchangeFailed + ) + let urlRequestProxy = URLRequestProxy(dispatchQueue: internalQueue, transportProvider: transportProvider) appMessageHandler = AppMessageHandler(packetTunnelActor: actor, urlRequestProxy: urlRequestProxy) @@ -104,6 +114,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 +251,9 @@ extension PacketTunnelProvider { // Cache last connection attempt to filter out repeating calls. lastConnectionAttempt = connectionAttempt - #if DEBUG - case .negotiatingKey: - break - #endif + case let .negotiatingPostQuantumKey(_, privateKey): + postQuantumActor.endCurrentNegotiation() + postQuantumActor.startNegotiation(with: privateKey) case .initial, .connected, .disconnecting, .disconnected, .error: break @@ -283,8 +298,15 @@ 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.endCurrentNegotiation() + } + + func keyExchangeFailed() { + postQuantumActor.endCurrentNegotiation() + // Do not try reconnecting to the `.current` relay, else the actor's `State` equality check will fail + // and it will not try to reconnect + actor.reconnect(to: .random) } } 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..33291d8ea5 --- /dev/null +++ b/ios/PacketTunnel/PostQuantumKeyExchangeActor.swift @@ -0,0 +1,107 @@ +// +// 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? + private var timer: DispatchSourceTimer? + + // Callback in the event of the negotiation failing on startup + var onFailure: () -> Void + + init(packetTunnel: PacketTunnelProvider, onFailure: @escaping (() -> Void)) { + self.packetTunnel = packetTunnel + self.onFailure = onFailure + } + + 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: "\(CONFIG_SERVICE_PORT)") + let inTunnelTCPConnection = createTCPConnection(endpoint) + + // This will become the new private key of the device + let ephemeralSharedKey = PrivateKey() + + // If the connection never becomes viable, force a reconnection after 10 seconds + scheduleInTunnelConnectionTimeout(startTime: .now() + 10) + + let tcpConnectionObserver = inTunnelTCPConnection.observe(\.isViable, options: [ + .initial, + .new, + ]) { [weak self] observedConnection, _ in + guard let self, observedConnection.isViable else { return } + self.negotiation?.tcpConnectionObserver.invalidate() + self.timer?.cancel() + + if !negotiator.startNegotiation( + gatewayIP: IPv4Gateway, + devicePublicKey: privateKey.publicKey, + presharedKey: ephemeralSharedKey, + packetTunnel: packetTunnel, + tcpConnection: inTunnelTCPConnection + ) { + self.negotiation = nil + self.onFailure() + } + } + negotiation = Negotiation( + negotiator: negotiator, + inTunnelTCPConnection: inTunnelTCPConnection, + tcpConnectionObserver: tcpConnectionObserver + ) + } + + func endCurrentNegotiation() { + negotiation?.cancel() + negotiation = nil + } + + private func scheduleInTunnelConnectionTimeout(startTime: DispatchWallTime) { + let newTimer = DispatchSource.makeTimerSource() + + newTimer.setEventHandler { [weak self] in + self?.onFailure() + self?.timer?.cancel() + } + + newTimer.schedule(wallDeadline: startTime) + newTimer.activate() + + timer?.cancel() + timer = newTimer + } +} diff --git a/ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift b/ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift index 06b970a223..226b8b05fe 100644 --- a/ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift +++ b/ios/PacketTunnelCore/Actor/ConfigurationBuilder.swift @@ -21,14 +21,31 @@ 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] + 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 +65,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+PostQuantum.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift new file mode 100644 index 0000000000..53066ce437 --- /dev/null +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift @@ -0,0 +1,84 @@ +// +// PacketTunnelActor+PostQuantum.swift +// PacketTunnelCore +// +// Created by Andrew Bulhak on 2024-05-13. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import WireGuardKitTypes + +extension PacketTunnelActor { + /** + Attempt to start the process of negotiating a post-quantum secure key, setting up an initial + connection restricted to the negotiation host and entering the negotiating state. + */ + internal func tryStartPostQuantumNegotiation( + withSettings settings: Settings, + nextRelay: NextRelay, + reason: ReconnectReason + ) async throws { + 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) + } + } + + /** + Called on receipt of the new PQ-negotiated key, to reconnect to the relay, in PQ-secure mode. + */ + internal 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 { + logger.error("Could not create connection state in PostQuantumConnect") + + let nextRelay: NextRelay = (state.connectionData?.selectedRelay).map { .preSelected($0) } ?? .current + commandChannel.send(.reconnect(nextRelay)) + 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) + + 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) + // Restart default path observer and notify the observer with the current path that might have changed while + // path observer was paused. + startDefaultPathObserver(notifyObserverWithCurrentPath: false) + } +} 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..153c1c0430 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 @@ -222,9 +225,27 @@ extension PacketTunnelActor { } /** - Attempt to start the tunnel by performing the following steps: + Entry point for attempting to start the tunnel by performing the following steps: + + - Read settings + - Start either a direct connection or the post-quantum key negotiation process, depending on settings. + */ + private func tryStart( + nextRelay: NextRelay, + reason: ReconnectReason = .userInitiated + ) async throws { + let settings: Settings = try settingsReader.read() + + if settings.quantumResistance.isEnabled { + try await tryStartPostQuantumNegotiation(withSettings: settings, nextRelay: nextRelay, reason: reason) + } else { + try await tryStartConnection(withSettings: settings, nextRelay: nextRelay, reason: reason) + } + } + + /** + Attempt to start a direct (non-quantum) connection to the tunnel by performing the following steps: - - Read settings. - Determine target state, it can either be `.connecting` or `.reconnecting`. (See `TargetStateForReconnect`) - Bail if target state cannot be determined. That means that the actor is past the point when it could logically connect or reconnect, i.e it can already be in `.disconnecting` state. @@ -236,22 +257,15 @@ extension PacketTunnelActor { - nextRelay: which relay should be selected next. - reason: reason for reconnect */ - private func tryStart( - nextRelay: NextRelay = .random, - reason: ReconnectReason = .userInitiated + private func tryStartConnection( + withSettings settings: Settings, + nextRelay: NextRelay, + reason: ReconnectReason ) async throws { - let settings: Settings = try settingsReader.read() - 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: @@ -300,7 +314,7 @@ extension PacketTunnelActor { - Returns: New connection state or `nil` if current state is at or past `.disconnecting` phase. */ - private func makeConnectionState( + internal func makeConnectionState( nextRelay: NextRelay, settings: Settings, reason: ReconnectReason @@ -326,6 +340,14 @@ extension PacketTunnelActor { connectionState.incrementAttemptCount() } fallthrough + case var .negotiatingPostQuantumKey(connectionState, _): + let selectedRelay = try callRelaySelector( + connectionState.selectedRelay, + connectionState.connectionAttemptCount + ) + connectionState.selectedRelay = selectedRelay + connectionState.relayConstraints = settings.relayConstraints + return connectionState case var .connected(connectionState): let selectedRelay = try callRelaySelector( connectionState.selectedRelay, @@ -353,11 +375,21 @@ 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 obfuscateConnection( + internal func activeKey(from state: State.ConnectionData, in settings: Settings) -> PrivateKey { + switch state.keyPolicy { + case .useCurrent: + settings.privateKey + case let .usePrior(priorKey, _): + priorKey + } + } + + internal func obfuscateConnection( nextRelay: NextRelay, settings: Settings, reason: ReconnectReason @@ -382,7 +414,8 @@ extension PacketTunnelActor { lastKeyRotation: connectionState.lastKeyRotation, connectedEndpoint: obfuscatedEndpoint, transportLayer: transportLayer, - remotePort: protocolObfuscator.remotePort + remotePort: protocolObfuscator.remotePort, + isPostQuantum: settings.quantumResistance.isEnabled ) } 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-tunnel-config-client/Cargo.toml b/talpid-tunnel-config-client/Cargo.toml index 650f2ba566..cce1aa86b0 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", "rlib"] 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..37fdb0d16d --- /dev/null +++ b/talpid-tunnel-config-client/src/ios_ffi/ios_runtime.rs @@ -0,0 +1,168 @@ +use super::{ios_tcp_connection::*, PostQuantumCancelToken}; +use crate::{request_ephemeral_peer_with, 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_post_quantum_psk_exchange( + pub_key: [u8; 32], + ephemeral_key: [u8; 32], + packet_tunnel: *const c_void, + tcp_connection: *const c_void, +) -> Result<PostQuantumCancelToken, Error> { + 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(Error::UnableToCreateRuntime) + } + } +} + +#[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(); + }); + } + + /// Creates a `RelayConfigService` using the in-tunnel TCP Connection provided by the Packet Tunnel Provider + /// # Safety + /// It is unsafe to call this with an already used `SwiftContext` + async unsafe 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) = unsafe { match Self::ios_tcp_client(self.packet_tunnel).await { + Ok(result) => result, + Err(error) => { + log::error!("Failed to create iOS TCP client: {error}"); + 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_with( + async_provider, + PublicKey::from(self.pub_key), + ephemeral_pub_key, + true, + false, + ) => { + 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..86a3114edf --- /dev/null +++ b/talpid-tunnel-config-client/src/ios_ffi/ios_tcp_connection.rs @@ -0,0 +1,194 @@ +use libc::c_void; +use std::{ + io::{self, Result}, + sync::{ + atomic::{self, AtomicBool}, + Arc, Mutex, Weak, + }, + task::{Poll, Waker}, +}; +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: Arc<mpsc::UnboundedSender<usize>>, + write_rx: mpsc::UnboundedReceiver<usize>, + read_tx: Arc<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>, + waker: Arc<Mutex<Option<Waker>>>, +} + +pub struct IosTcpShutdownHandle { + shutdown: Arc<AtomicBool>, + waker: Arc<Mutex<Option<Waker>>>, +} + +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)); + let waker = Arc::new(Mutex::new(None)); + + ( + Self { + write_tx: Arc::new(tx), + write_rx: rx, + read_tx: Arc::new(recv_tx), + read_rx: recv_rx, + tcp_connection, + read_in_progress: false, + write_in_progress: false, + shutdown: shutdown.clone(), + waker: waker.clone(), + }, + IosTcpShutdownHandle { shutdown, waker }, + ) + } + + fn is_shutdown(&self) -> bool { + self.shutdown.load(atomic::Ordering::SeqCst) + } + + fn maybe_set_waker(&self, new_waker: Waker) { + if let Ok(mut waker) = self.waker.lock() { + *waker = Some(new_waker); + } + } +} + +impl IosTcpShutdownHandle { + pub fn shutdown(&self) { + self.shutdown.store(true, atomic::Ordering::SeqCst); + if let Some(waker) = self.waker.lock().ok().and_then(|mut waker| waker.take()) { + waker.wake(); + } + } +} + +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>> { + self.maybe_set_waker(cx.waker().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 { + let raw_sender = Weak::into_raw(Arc::downgrade(&self.write_tx)); + unsafe { + swift_nw_tcp_connection_send( + self.tcp_connection, + buf.as_ptr() as _, + buf.len(), + raw_sender as _, + ); + } + self.write_in_progress = true; + } + 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<()>> { + 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 { + let raw_sender = Weak::into_raw(Arc::downgrade(&self.read_tx)); + unsafe { + swift_nw_tcp_connection_read(self.tcp_connection, raw_sender as _); + } + self.read_in_progress = true; + } + 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..94dcf95b6f --- /dev/null +++ b/talpid-tunnel-config-client/src/ios_ffi/mod.rs @@ -0,0 +1,134 @@ +pub mod ios_runtime; +pub mod ios_tcp_connection; + +use crate::ios_ffi::ios_runtime::run_post_quantum_psk_exchange; +use libc::c_void; +use std::sync::{Arc, Weak}; +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) { + // # Safety + // 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(()); + // Call std::mem::forget here to avoid dropping the channel. + 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 {} + +/// Called by the Swift side to signal that the quantum-secure key exchange should be cancelled. +/// +/// # Safety +/// `sender` must be pointing to a valid instance of a `PostQuantumCancelToken` created by the `PacketTunnelProvider`. +#[no_mangle] +pub unsafe extern "C" fn cancel_post_quantum_key_exchange(sender: *const PostQuantumCancelToken) { + let sender = unsafe { &*sender }; + sender.cancel(); +} +/// Called by the Swift side to signal that the Rust `PostQuantumCancelToken` can be safely dropped from memory. +/// +/// # 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) }; +} + +/// Called by Swift whenever data has been written to the in-tunnel TCP connection when exchanging +/// quantum-resistant pre shared keys. +/// +/// If `bytes_sent` is 0, this indicates that the connection was closed or that an error occurred. +/// +/// # 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 weak_tx: Weak<mpsc::UnboundedSender<usize>> = unsafe { Weak::from_raw(sender as _) }; + if let Some(send_tx) = weak_tx.upgrade() { + _ = send_tx.send(bytes_sent); + } +} + +/// Called by Swift whenever data has been read from the in-tunnel TCP connection when exchanging +/// quantum-resistant pre shared keys. +/// +/// If `data` is null or empty, this indicates that the connection was closed or that an error occurred. +/// An empty buffer is sent to the underlying reader to signal EOF. +/// +/// # 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, mut data_len: usize, sender: *const c_void) { + let weak_tx: Weak<mpsc::UnboundedSender<Box<[u8]>>> = unsafe { Weak::from_raw(sender as _) }; + + if data.is_null() { + data_len = 0; + } + let mut bytes = vec![0u8; data_len]; + if !data.is_null() { + std::ptr::copy_nonoverlapping(data, bytes.as_mut_ptr(), data_len); + } + if let Some(read_tx) = weak_tx.upgrade() { + _ = 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 +/// `public_key` and `ephemeral_key` must be valid respective `PublicKey` and `PrivateKey` types. +/// They will not be valid after this function is called, and thus must be copied here. +/// `packet_tunnel` and `tcp_connection` must be valid pointers to a packet tunnel and a TCP connection +/// instances. +/// `cancel_token` should be owned by the caller of this function. +#[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_post_quantum_psk_exchange(pub_key_copy, eph_key_copy, packet_tunnel, tcp_connection) + } { + Ok(token) => { + unsafe { std::ptr::write(cancel_token, token) }; + 0 + } + Err(_) => -1, + } +} diff --git a/talpid-tunnel-config-client/src/lib.rs b/talpid-tunnel-config-client/src/lib.rs index e272e794c7..62c124dd44 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,24 @@ mod proto { tonic::include_proto!("ephemeralpeer"); } +#[cfg(target_os = "ios")] +pub mod ios_ffi; + +#[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}; } + #[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 +56,10 @@ pub enum Error { actual: usize, }, FailedDecapsulateKyber(kyber::KyberError), + #[cfg(target_os = "ios")] + TcpConnectionExpired, + #[cfg(target_os = "ios")] + UnableToCreateRuntime, } impl std::fmt::Display for Error { @@ -65,6 +81,10 @@ 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), + #[cfg(target_os = "ios")] + UnableToCreateRuntime => "Unable to create iOS PQ PSK runtime".fmt(f), } } } @@ -79,7 +99,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,48 +113,25 @@ 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 { pub psk: Option<PresharedKey>, } -/// Negotiate a short-lived peer with a PQ-safe PSK or with DAITA enabled. -pub async fn request_ephemeral_peer( - service_address: IpAddr, +pub async fn request_ephemeral_peer_with( + mut client: RelayConfigService, parent_pubkey: PublicKey, ephemeral_pubkey: PublicKey, enable_post_quantum: bool, enable_daita: bool, ) -> 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, }); - 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 +189,57 @@ pub async fn request_ephemeral_peer( Ok(EphemeralPeer { psk }) } +/// Negotiate a short-lived peer with a PQ-safe PSK or with DAITA enabled. +#[cfg(not(target_os = "ios"))] +pub async fn request_ephemeral_peer( + service_address: IpAddr, + parent_pubkey: PublicKey, + ephemeral_pubkey: PublicKey, + enable_post_quantum: bool, + enable_daita: bool, +) -> Result<EphemeralPeer, Error> { + let client = new_client(service_address).await?; + + request_ephemeral_peer_with( + client, + parent_pubkey, + ephemeral_pubkey, + enable_post_quantum, + enable_daita, + ) + .await +} + +async fn post_quantum_secrets( + enable_post_quantum: bool, +) -> ( + Option<PostQuantumRequestV1>, + Option<(classic_mceliece_rust::SecretKey<'static>, [u8; 3168])>, +) { + 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) + } +} + /// 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 +247,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 +294,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; |
