Skip to content

Commit ebedeb4

Browse files
committed
Reuse the connection attempts count logic for PQ PSK negotiation
1 parent 489e812 commit ebedeb4

12 files changed

+79
-30
lines changed

ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,6 @@ extension PacketTunnelProvider: PostQuantumKeyReceiving {
307307
postQuantumActor.endCurrentNegotiation()
308308
// Do not try reconnecting to the `.current` relay, else the actor's `State` equality check will fail
309309
// and it will not try to reconnect
310-
actor.reconnect(to: .random)
310+
actor.reconnect(to: .random, reconnectReason: .connectionLoss)
311311
}
312312
}

ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ extension PacketTunnelActor {
1717
internal func tryStartPostQuantumNegotiation(
1818
withSettings settings: Settings,
1919
nextRelay: NextRelay,
20-
reason: ReconnectReason
20+
reason: ActorReconnectReason
2121
) async throws {
22-
if let connectionState = try makeConnectionState(nextRelay: nextRelay, settings: settings, reason: reason) {
23-
let selectedEndpoint = connectionState.selectedRelay.endpoint
22+
if let connectionState = try obfuscateConnection(nextRelay: nextRelay, settings: settings, reason: reason) {
23+
let selectedEndpoint = connectionState.connectedEndpoint
2424
let activeKey = activeKey(from: connectionState, in: settings)
2525

2626
let configurationBuilder = ConfigurationBuilder(

ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ extension PacketTunnelActor {
3838

3939
- Parameter nextRelay: next relay to connect to.
4040
*/
41-
public nonisolated func reconnect(to nextRelay: NextRelay) {
42-
eventChannel.send(.reconnect(nextRelay))
41+
public nonisolated func reconnect(to nextRelay: NextRelay, reconnectReason: ActorReconnectReason = .userInitiated) {
42+
eventChannel.send(.reconnect(nextRelay, reason: reconnectReason))
4343
}
4444

4545
/**

ios/PacketTunnelCore/Actor/PacketTunnelActor.swift

+12-18
Original file line numberDiff line numberDiff line change
@@ -148,16 +148,6 @@ public actor PacketTunnelActor {
148148
// MARK: -
149149

150150
extension PacketTunnelActor {
151-
/// Describes the reason for reconnection request.
152-
enum ReconnectReason: Equatable {
153-
/// Initiated by user.
154-
case userInitiated
155-
156-
/// Initiated by tunnel monitor due to loss of connectivity.
157-
/// Actor will increment the connection attempt counter before picking next relay.
158-
case connectionLoss
159-
}
160-
161151
/**
162152
Start the tunnel.
163153

@@ -221,7 +211,7 @@ extension PacketTunnelActor {
221211
- nextRelay: next relay to connect to
222212
- reason: reason for reconnect
223213
*/
224-
private func reconnect(to nextRelay: NextRelay, reason: ReconnectReason) async {
214+
private func reconnect(to nextRelay: NextRelay, reason: ActorReconnectReason) async {
225215
do {
226216
switch state {
227217
// There is no connection monitoring going on when exchanging keys.
@@ -256,7 +246,7 @@ extension PacketTunnelActor {
256246
*/
257247
private func tryStart(
258248
nextRelay: NextRelay,
259-
reason: ReconnectReason = .userInitiated
249+
reason: ActorReconnectReason = .userInitiated
260250
) async throws {
261251
let settings: Settings = try settingsReader.read()
262252

@@ -284,7 +274,7 @@ extension PacketTunnelActor {
284274
private func tryStartConnection(
285275
withSettings settings: Settings,
286276
nextRelay: NextRelay,
287-
reason: ReconnectReason
277+
reason: ActorReconnectReason
288278
) async throws {
289279
guard let connectionState = try obfuscateConnection(nextRelay: nextRelay, settings: settings, reason: reason),
290280
let targetState = state.targetStateForReconnect else { return }
@@ -341,7 +331,7 @@ extension PacketTunnelActor {
341331
internal func makeConnectionState(
342332
nextRelay: NextRelay,
343333
settings: Settings,
344-
reason: ReconnectReason
334+
reason: ActorReconnectReason
345335
) throws -> State.ConnectionData? {
346336
var keyPolicy: State.KeyPolicy = .useCurrent
347337
var networkReachability = defaultPathObserver.defaultPath?.networkReachability ?? .undetermined
@@ -359,19 +349,23 @@ extension PacketTunnelActor {
359349
switch state {
360350
case .initial:
361351
break
362-
case var .connecting(connectionState), var .reconnecting(connectionState):
352+
// Handle PQ PSK separately as it doesn't interfere with either the `.connecting` or `.reconnecting` states.
353+
case var .negotiatingPostQuantumKey(connectionState, _):
363354
if reason == .connectionLoss {
364355
connectionState.incrementAttemptCount()
365356
}
366-
fallthrough
367-
case var .negotiatingPostQuantumKey(connectionState, _):
368357
let selectedRelay = try callRelaySelector(
369358
connectionState.selectedRelay,
370359
connectionState.connectionAttemptCount
371360
)
372361
connectionState.selectedRelay = selectedRelay
373362
connectionState.relayConstraints = settings.relayConstraints
374363
return connectionState
364+
case var .connecting(connectionState), var .reconnecting(connectionState):
365+
if reason == .connectionLoss {
366+
connectionState.incrementAttemptCount()
367+
}
368+
fallthrough
375369
case var .connected(connectionState):
376370
let selectedRelay = try callRelaySelector(
377371
connectionState.selectedRelay,
@@ -416,7 +410,7 @@ extension PacketTunnelActor {
416410
internal func obfuscateConnection(
417411
nextRelay: NextRelay,
418412
settings: Settings,
419-
reason: ReconnectReason
413+
reason: ActorReconnectReason
420414
) throws -> State.ConnectionData? {
421415
guard let connectionState = try makeConnectionState(nextRelay: nextRelay, settings: settings, reason: reason)
422416
else { return nil }

ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ extension PacketTunnelActor {
1919
case stop
2020

2121
/// Reconnect tunnel.
22-
case reconnect(NextRelay, reason: ReconnectReason = .userInitiated)
22+
case reconnect(NextRelay, reason: ActorReconnectReason = .userInitiated)
2323

2424
/// Enter blocked state.
2525
case error(BlockedStateReason)

ios/PacketTunnelCore/Actor/PacketTunnelActorProtocol.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ import Foundation
1111
public protocol PacketTunnelActorProtocol {
1212
var observedState: ObservedState { get async }
1313

14-
func reconnect(to nextRelay: NextRelay)
14+
func reconnect(to nextRelay: NextRelay, reconnectReason: ActorReconnectReason)
1515
func notifyKeyRotation(date: Date?)
1616
}

ios/PacketTunnelCore/Actor/PacketTunnelActorReducer.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ extension PacketTunnelActor {
1818
case stopTunnelMonitor
1919
case updateTunnelMonitorPath(NetworkPath)
2020
case startConnection(NextRelay)
21-
case restartConnection(NextRelay, ReconnectReason)
21+
case restartConnection(NextRelay, ActorReconnectReason)
2222
// trigger a reconnect, which becomes several effects depending on the state
2323
case reconnect(NextRelay)
2424
case stopTunnelAdapter
@@ -123,7 +123,7 @@ extension PacketTunnelActor {
123123

124124
fileprivate static func subreducerForReconnect(
125125
_ state: State,
126-
_ reason: PacketTunnelActor.ReconnectReason,
126+
_ reason: ActorReconnectReason,
127127
_ nextRelay: NextRelay
128128
) -> [PacketTunnelActor.Effect] {
129129
switch state {

ios/PacketTunnelCore/Actor/State.swift

+10
Original file line numberDiff line numberDiff line change
@@ -238,3 +238,13 @@ public enum NextRelay: Equatable, Codable {
238238
/// Use pre-selected relay.
239239
case preSelected(SelectedRelay)
240240
}
241+
242+
/// Describes the reason for reconnection request.
243+
public enum ActorReconnectReason: Equatable {
244+
/// Initiated by user.
245+
case userInitiated
246+
247+
/// Initiated by tunnel monitor due to loss of connectivity, or if post quantum key negotiation times out.
248+
/// Actor will increment the connection attempt counter before picking next relay.
249+
case connectionLoss
250+
}

ios/PacketTunnelCore/IPC/AppMessageHandler.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public struct AppMessageHandler {
5454
return nil
5555

5656
case let .reconnectTunnel(nextRelay):
57-
packetTunnelActor.reconnect(to: nextRelay)
57+
packetTunnelActor.reconnect(to: nextRelay, reconnectReason: ActorReconnectReason.userInitiated)
5858
return nil
5959
}
6060
}

ios/PacketTunnelCoreTests/Mocks/PacketTunnelActorStub.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ struct PacketTunnelActorStub: PacketTunnelActorProtocol {
2323
}
2424
}
2525

26-
func reconnect(to nextRelay: NextRelay) {
26+
func reconnect(to nextRelay: PacketTunnelCore.NextRelay, reconnectReason: ActorReconnectReason) {
2727
reconnectExpectation?.fulfill()
2828
}
2929

ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift

+14
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,18 @@ extension SettingsReaderStub {
3737
return staticSettings
3838
}
3939
}
40+
41+
static func postQuantumConfiguration() -> SettingsReaderStub {
42+
let staticSettings = Settings(
43+
privateKey: PrivateKey(),
44+
interfaceAddresses: [IPAddressRange(from: "127.0.0.1/32")!],
45+
relayConstraints: RelayConstraints(),
46+
dnsServers: .gateway,
47+
obfuscation: WireGuardObfuscationSettings(state: .off, port: .automatic),
48+
quantumResistance: .on
49+
)
50+
return SettingsReaderStub {
51+
return staticSettings
52+
}
53+
}
4054
}

ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift

+31
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,37 @@ final class PacketTunnelActorTests: XCTestCase {
123123
await fulfillment(of: [connectingStateExpectation], timeout: 1)
124124
}
125125

126+
func testPostQuantumReconnectionTransition() async throws {
127+
let tunnelMonitor = TunnelMonitorStub { _, _ in }
128+
let actor = PacketTunnelActor.mock(
129+
tunnelMonitor: tunnelMonitor,
130+
settingsReader: SettingsReaderStub.postQuantumConfiguration()
131+
)
132+
let negotiatingPostQuantumKeyStateExpectation = expectation(description: "Expect post quantum state")
133+
negotiatingPostQuantumKeyStateExpectation.expectedFulfillmentCount = 5
134+
var nextAttemptCount: UInt = 0
135+
stateSink = await actor.$observedState
136+
.receive(on: DispatchQueue.main)
137+
.sink { newState in
138+
switch newState {
139+
case .initial:
140+
break
141+
case let .negotiatingPostQuantumKey(connState, _):
142+
XCTAssertEqual(connState.connectionAttemptCount, nextAttemptCount)
143+
nextAttemptCount += 1
144+
negotiatingPostQuantumKeyStateExpectation.fulfill()
145+
if nextAttemptCount < negotiatingPostQuantumKeyStateExpectation.expectedFulfillmentCount {
146+
actor.reconnect(to: .random, reconnectReason: .connectionLoss)
147+
}
148+
default:
149+
XCTFail("Received invalid state: \(newState.name).")
150+
}
151+
}
152+
153+
actor.start(options: StartOptions(launchSource: .app))
154+
await fulfillment(of: [negotiatingPostQuantumKeyStateExpectation], timeout: 1)
155+
}
156+
126157
/**
127158
Each subsequent re-connection attempt should produce a single change to `state` containing the incremented attempt counter and new relay.
128159
.reconnecting (attempt: 0) → .reconnecting (attempt: 1) → .reconnecting (attempt: 2) → ...

0 commit comments

Comments
 (0)