Skip to content

Commit

Permalink
Merge branch 'IOS-753-TunnelPinger'
Browse files Browse the repository at this point in the history
  • Loading branch information
buggmagnet committed Sep 18, 2024
2 parents 9657779 + e90f94d commit 7d87bbb
Show file tree
Hide file tree
Showing 11 changed files with 150 additions and 23 deletions.
6 changes: 5 additions & 1 deletion ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
44B3C43D2C00CBBD0079782C /* PacketTunnelActorReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44B3C43C2C00CBBC0079782C /* PacketTunnelActorReducerTests.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 */; };
44C18DE32C74DF93009BE3E1 /* TunnelPinger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449275432C3C3029000526DE /* TunnelPinger.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 */; };
Expand Down Expand Up @@ -1371,6 +1372,7 @@
06FAE67B28F83CA50033DD93 /* REST.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = REST.swift; sourceTree = "<group>"; };
06FAE67D28F83CA50033DD93 /* RESTTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RESTTransport.swift; sourceTree = "<group>"; };
449275412C3570CA000526DE /* ICMP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ICMP.swift; sourceTree = "<group>"; };
449275432C3C3029000526DE /* TunnelPinger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelPinger.swift; sourceTree = "<group>"; };
449872E02B7BBC5400094DDC /* TunnelSettingsUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsUpdate.swift; sourceTree = "<group>"; };
449872E32B7CB96300094DDC /* TunnelSettingsUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsUpdateTests.swift; sourceTree = "<group>"; };
449EB9FC2B95F8AD00DFA4EB /* DeviceMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceMock.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3347,6 +3349,7 @@
5838318A27C40A3900000571 /* Pinger.swift */,
58799A352A84FC9F007BE51F /* PingerProtocol.swift */,
449275412C3570CA000526DE /* ICMP.swift */,
449275432C3C3029000526DE /* TunnelPinger.swift */,
);
path = Pinger;
sourceTree = "<group>";
Expand Down Expand Up @@ -5938,6 +5941,7 @@
F0570CD12C4FB8E1007BDF2D /* EphemeralPeerExchangingPipeline.swift in Sources */,
F0570CD22C4FB8E1007BDF2D /* SingleHopEphemeralPeerExchanger.swift in Sources */,
F0570CD42C4FB8E1007BDF2D /* MultiHopEphemeralPeerExchanger.swift in Sources */,
44C18DE32C74DF93009BE3E1 /* TunnelPinger.swift in Sources */,
58C7A45B2A8640030060C66F /* PacketTunnelPathObserver.swift in Sources */,
580D6B8E2AB33BBF00B2D6E0 /* BlockedStateErrorMapper.swift in Sources */,
06AC116228F94C450037AF9A /* ApplicationConfiguration.swift in Sources */,
Expand Down Expand Up @@ -9206,7 +9210,7 @@
repositoryURL = "https://github.com/mullvad/wireguard-apple.git";
requirement = {
kind = revision;
revision = 143776f946e1da2566aa3e830f95b3e75f914f35;
revision = 82ae19d03fcaa83b9636e560ce9bea8fec9dc96f;
};
};
/* End XCRemoteSwiftPackageReference section */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/mullvad/wireguard-apple.git",
"state" : {
"revision" : "143776f946e1da2566aa3e830f95b3e75f914f35"
"revision" : "82ae19d03fcaa83b9636e560ce9bea8fec9dc96f"
}
}
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider {

adapter = WgAdapter(packetTunnelProvider: self)

let pinger = TunnelPinger(pingProvider: adapter.icmpPingProvider, replyQueue: internalQueue)

let tunnelMonitor = TunnelMonitor(
eventQueue: internalQueue,
pinger: Pinger(replyQueue: internalQueue),
pinger: pinger,
tunnelDeviceInfo: adapter,
timings: TunnelMonitorTimings()
)
Expand Down
4 changes: 4 additions & 0 deletions ios/PacketTunnel/WireGuardAdapter/WgAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ struct WgAdapter: TunnelAdapterProtocol {
let isUsingSameIP = (hasIPv4SameAddress || hasIPv6SameAddress) ? "" : "NOT "
logger.debug("Same IP is \(isUsingSameIP)being used")
}

public var icmpPingProvider: ICMPPingProvider {
adapter
}
}

extension WgAdapter: TunnelDeviceInfoProtocol {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ extension WireGuardAdapterError: LocalizedError {
return "Failure to start WireGuard backend (error code: \(code))."
case .noInterfaceIp:
return "Interface has no IP address specified."
case .noSuchTunnel:
return "No such WireGuard tunnel"
case .noTunnelVirtualInterface:
return "Tunnel has no virtual (IAN) interface"
case .icmpSocketNotOpen:
return "ICMP socket not open"
case let .internalError(code):
return "Internal error \(code)"
}
}
}
17 changes: 13 additions & 4 deletions ios/PacketTunnelCore/Pinger/Pinger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import Foundation
import Network

// This is the legacy Pinger using native TCP/IP networking.

/// ICMP client.
public final class Pinger: PingerProtocol {
// Socket read buffer size.
Expand All @@ -22,6 +24,7 @@ public final class Pinger: PingerProtocol {
private var readBuffer = [UInt8](repeating: 0, count: bufferSize)
private let stateLock = NSRecursiveLock()
private let replyQueue: DispatchQueue
private var destAddress: IPv4Address?

public var onReply: ((PingerReply) -> Void)? {
get {
Expand Down Expand Up @@ -49,12 +52,14 @@ public final class Pinger: PingerProtocol {

/// Open socket and optionally bind it to the given interface.
/// Automatically closes the previously opened socket when called multiple times in a row.
public func openSocket(bindTo interfaceName: String?) throws {
public func openSocket(bindTo interfaceName: String?, destAddress: IPv4Address) throws {
stateLock.lock()
defer { stateLock.unlock() }

closeSocket()

self.destAddress = destAddress

var context = CFSocketContext()
context.info = Unmanaged.passUnretained(self).toOpaque()

Expand Down Expand Up @@ -109,18 +114,22 @@ public final class Pinger: PingerProtocol {

/// Send ping packet to the given address.
/// Returns `PingerSendResult` on success, otherwise throws a `Pinger.Error`.
public func send(to address: IPv4Address) throws -> PingerSendResult {
public func send() throws -> PingerSendResult {
stateLock.lock()
defer { stateLock.unlock() }

guard let socket else {
throw Error.closedSocket
}

guard let destAddress else {
throw Error.parseIPAddress
}

var sa = sockaddr_in()
sa.sin_len = UInt8(MemoryLayout.size(ofValue: sa))
sa.sin_family = sa_family_t(AF_INET)
sa.sin_addr = address.rawValue.withUnsafeBytes { buffer in
sa.sin_addr = destAddress.rawValue.withUnsafeBytes { buffer in
buffer.bindMemory(to: in_addr.self).baseAddress!.pointee
}

Expand Down Expand Up @@ -149,7 +158,7 @@ public final class Pinger: PingerProtocol {
throw Error.sendPacket(errno)
}

return PingerSendResult(sequenceNumber: sequenceNumber, bytesSent: UInt(bytesSent))
return PingerSendResult(sequenceNumber: sequenceNumber)
}

private func nextSequenceNumber() -> UInt16 {
Expand Down
9 changes: 5 additions & 4 deletions ios/PacketTunnelCore/Pinger/PingerProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,16 @@ public struct PingerSendResult {
/// Sequence id.
public var sequenceNumber: UInt16

/// How many bytes were sent.
public var bytesSent: UInt
public init(sequenceNumber: UInt16) {
self.sequenceNumber = sequenceNumber
}
}

/// A type capable of sending and receving ICMP traffic.
public protocol PingerProtocol {
var onReply: ((PingerReply) -> Void)? { get set }

func openSocket(bindTo interfaceName: String?) throws
func openSocket(bindTo interfaceName: String?, destAddress: IPv4Address) throws
func closeSocket()
func send(to address: IPv4Address) throws -> PingerSendResult
func send() throws -> PingerSendResult
}
93 changes: 93 additions & 0 deletions ios/PacketTunnelCore/Pinger/TunnelPinger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//
// TunnelPinger.swift
// PacketTunnelCore
//
// Created by Andrew Bulhak on 2024-07-08.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadLogging
import Network
import PacketTunnelCore
import WireGuardKit

public final class TunnelPinger: PingerProtocol {
private var sequenceNumber: UInt16 = 0
private let stateLock = NSRecursiveLock()
private let pingQueue: DispatchQueue
private let replyQueue: DispatchQueue
private var destAddress: IPv4Address?
private var _onReply: ((PingerReply) -> Void)?
public var onReply: ((PingerReply) -> Void)? {
get {
stateLock.withLock {
return _onReply
}
}
set {
stateLock.withLock {
_onReply = newValue
}
}
}

private var pingProvider: ICMPPingProvider

private let logger: Logger

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

deinit {
pingProvider.closeICMP()
}

public func openSocket(bindTo interfaceName: String?, destAddress: IPv4Address) throws {
try pingProvider.openICMP(address: destAddress)
self.destAddress = destAddress
}

public func closeSocket() {
pingProvider.closeICMP()
self.destAddress = nil
}

public func send() throws -> PingerSendResult {
let sequenceNumber = nextSequenceNumber()
logger.debug("*** sending ping \(sequenceNumber)")

pingQueue.async { [weak self] in
guard let self, let destAddress else { return }
let reply: PingerReply
do {
try pingProvider.sendICMPPing(seqNumber: sequenceNumber)
// 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.
reply = .success(destAddress, sequenceNumber)
} catch {
reply = .parseError(error)
}
self.logger.debug("--- Pinger reply: \(reply)")

replyQueue.async { [weak self] in
guard let self else { return }
self.onReply?(reply)
}
}

return PingerSendResult(sequenceNumber: UInt16(sequenceNumber))
}

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

return nextValue
}
}
14 changes: 7 additions & 7 deletions ios/PacketTunnelCore/TunnelMonitor/TunnelMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public final class TunnelMonitor: TunnelMonitorProtocol {
nslock.lock()
defer { nslock.unlock() }

guard let probeAddress, let newStats = getStats(),
guard let newStats = getStats(),
state.connectionState == .connecting || state.connectionState == .connected
else { return }

Expand Down Expand Up @@ -222,7 +222,7 @@ public final class TunnelMonitor: TunnelMonitorProtocol {
state.isHeartbeatSuspended = false
state.timeoutReference = now
}
sendPing(to: probeAddress, now: now)
sendPing(now: now)
}
}

Expand Down Expand Up @@ -252,9 +252,9 @@ public final class TunnelMonitor: TunnelMonitorProtocol {
sendConnectionLostEvent()
}

private func sendPing(to receiver: IPv4Address, now: Date) {
private func sendPing(now: Date) {
do {
let sendResult = try pinger.send(to: receiver)
let sendResult = try pinger.send()
state.updatePingStats(sendResult: sendResult, now: now)

logger.trace("Send ping icmp_seq=\(sendResult.sequenceNumber).")
Expand Down Expand Up @@ -298,12 +298,12 @@ public final class TunnelMonitor: TunnelMonitorProtocol {

private func startMonitoring() {
do {
guard let interfaceName = tunnelDeviceInfo.interfaceName else {
logger.debug("Failed to obtain utun interface name.")
guard let interfaceName = tunnelDeviceInfo.interfaceName, let probeAddress else {
logger.debug("Failed to obtain utun interface name or probe address.")
return
}

try pinger.openSocket(bindTo: interfaceName)
try pinger.openSocket(bindTo: interfaceName, destAddress: probeAddress)

state.connectionState = .connecting
startConnectivityCheckTimer()
Expand Down
12 changes: 9 additions & 3 deletions ios/PacketTunnelCoreTests/Mocks/PingerMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ class PingerMock: PingerProtocol {
self.decideOutcome = decideOutcome
}

func openSocket(bindTo interfaceName: String?) throws {
func openSocket(bindTo interfaceName: String?, destAddress: IPv4Address) throws {
stateLock.withLock {
state.destAddress = destAddress
state.isSocketOpen = true
}
}
Expand All @@ -46,11 +47,15 @@ class PingerMock: PingerProtocol {
}
}

func send(to address: IPv4Address) throws -> PingerSendResult {
func send() throws -> PingerSendResult {
// Used for simulation. In reality can be any number.
// But for realism it is: IPv4 header (20 bytes) + ICMP header (8 bytes)
let icmpPacketSize: UInt = 28

guard let address = state.destAddress else {
fatalError("Address somehow not set when sending ping")
}

let nextSequenceId = try stateLock.withLock {
guard state.isSocketOpen else { throw POSIXError(.ENOTCONN) }

Expand Down Expand Up @@ -81,7 +86,7 @@ class PingerMock: PingerProtocol {

networkStatsReporting.reportBytesSent(UInt64(icmpPacketSize))

return PingerSendResult(sequenceNumber: nextSequenceId, bytesSent: icmpPacketSize)
return PingerSendResult(sequenceNumber: nextSequenceId)
}

// MARK: - Types
Expand All @@ -91,6 +96,7 @@ class PingerMock: PingerProtocol {
var sequenceId: UInt16 = 0
var isSocketOpen = false
var onReply: ((PingerReply) -> Void)?
var destAddress: IPv4Address?

mutating func incrementSequenceId() -> UInt16 {
sequenceId += 1
Expand Down
4 changes: 2 additions & 2 deletions ios/PacketTunnelCoreTests/PingerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ final class PingerTests: XCTestCase {
}
}

try pinger.openSocket(bindTo: "lo0")
sendResult = try pinger.send(to: .loopback)
try pinger.openSocket(bindTo: "lo0", destAddress: .loopback)
sendResult = try pinger.send()

waitForExpectations(timeout: .UnitTest.timeout)
}
Expand Down

0 comments on commit 7d87bbb

Please sign in to comment.