Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IAN-based TunnelPinger, refactoring the pinger protocol accordingly. #6802

Merged
merged 2 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading