summaryrefslogtreecommitdiffhomepage
path: root/ios/PacketTunnelCore/Pinger/TunnelPinger.swift
blob: 20defd7a90358c2a4b8041cf65aa3c85aed81b11 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
//
//  TunnelPinger.swift
//  PacketTunnelCore
//
//  Created by Andrew Bulhak on 2024-07-08.
//  Copyright © 2025 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadLogging
import Network
import PacketTunnelCore
import WireGuardKit

public final class TunnelPinger: PingerProtocol, @unchecked Sendable {
    private var sequenceNumber: UInt16 = 0
    private let stateLock = NSLock()
    private let pingReceiveQueue: DispatchQueue
    private let replyQueue: DispatchQueue
    private var destAddress: IPv4Address?
    /// Always accessed from the `replyQueue` and is assigned once, on the main thread of the PacketTunnel. It is thread safe.
    public var onReply: ((PingerReply) -> Void)?
    private var pingProvider: ICMPPingProvider

    private let logger: Logger

    init(pingProvider: ICMPPingProvider, replyQueue: DispatchQueue) {
        self.pingProvider = pingProvider
        self.replyQueue = replyQueue
        self.pingReceiveQueue = DispatchQueue(label: "PacketTunnel.Receive.icmp")
        self.logger = Logger(label: "TunnelPinger")
    }

    public func startPinging(destAddress: IPv4Address) throws {
        stateLock.withLock {
            self.destAddress = destAddress
        }
        pingReceiveQueue.async { [weak self] in
            while let self {
                do {
                    let seq = try pingProvider.receiveICMP()

                    replyQueue.async { [weak self] in
                        self?.stateLock.withLock {
                            self?.onReply?(PingerReply.success(destAddress, UInt16(seq)))
                        }
                    }
                } catch {
                    replyQueue.async { [weak self] in
                        self?.stateLock.withLock {
                            if self?.destAddress != nil {
                                self?.onReply?(PingerReply.parseError(error))
                            }
                        }
                    }
                    return
                }
            }
        }
    }

    public func stopPinging() {
        stateLock.withLock {
            self.destAddress = nil
        }
    }

    public func send() throws -> PingerSendResult {
        let sequenceNumber = nextSequenceNumber()

        stateLock.lock()
        defer { stateLock.unlock() }
        guard destAddress != nil else { throw WireGuardAdapterError.invalidState }
        // NOTE: we cheat here by returning the destination address we were passed, rather than parsing it from the packet on the other side of the FFI boundary.
        try pingProvider.sendICMPPing(seqNumber: sequenceNumber)

        return PingerSendResult(sequenceNumber: UInt16(sequenceNumber))
    }

    private func nextSequenceNumber() -> UInt16 {
        stateLock.lock()
        let (nextValue, _) = sequenceNumber.addingReportingOverflow(1)
        sequenceNumber = nextValue
        stateLock.unlock()

        return nextValue
    }
}