summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorEmīls <emils@mullvad.net>2024-07-08 11:34:54 +0200
committerEmīls <emils@mullvad.net>2024-07-08 11:34:54 +0200
commitd726e19cf93d15d0bf6734afb28e0794bacda72c (patch)
tree64b0f407fa067f05f783bb52e83863b25570e5f0
parent966a56cb87744651f7716bdae2e590d34ecb03b0 (diff)
parent61e723efe408a3bfd3b4d3258b58f98449004a6f (diff)
downloadmullvadvpn-d726e19cf93d15d0bf6734afb28e0794bacda72c.tar.xz
mullvadvpn-d726e19cf93d15d0bf6734afb28e0794bacda72c.zip
Merge branch 'IOS-750-separate-ICMP-logic-from-Pinger'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
-rw-r--r--ios/PacketTunnelCore/Pinger/ICMP.swift119
-rw-r--r--ios/PacketTunnelCore/Pinger/Pinger.swift130
3 files changed, 136 insertions, 117 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 2b82f01184..6775c52f51 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -39,6 +39,7 @@
06799AFC28F98EE300ACD94E /* AddressCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AC114128F8413A0037AF9A /* AddressCache.swift */; };
0697D6E728F01513007A9E99 /* TransportMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0697D6E628F01513007A9E99 /* TransportMonitor.swift */; };
06AC116228F94C450037AF9A /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; };
+ 449275422C3570CA000526DE /* ICMP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449275412C3570CA000526DE /* ICMP.swift */; };
449872E12B7BBC5400094DDC /* TunnelSettingsUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449872E02B7BBC5400094DDC /* TunnelSettingsUpdate.swift */; };
449872E42B7CB96300094DDC /* TunnelSettingsUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449872E32B7CB96300094DDC /* TunnelSettingsUpdateTests.swift */; };
449EBA262B975B9700DFA4EB /* PostQuantumKeyReceiving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449EBA252B975B9700DFA4EB /* PostQuantumKeyReceiving.swift */; };
@@ -1451,6 +1452,7 @@
06FAE67A28F83CA50033DD93 /* RESTDevicesProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RESTDevicesProxy.swift; sourceTree = "<group>"; };
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>"; };
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>"; };
@@ -3419,6 +3421,7 @@
58218E1428B65058000C624F /* IPv4Header.h */,
5838318A27C40A3900000571 /* Pinger.swift */,
58799A352A84FC9F007BE51F /* PingerProtocol.swift */,
+ 449275412C3570CA000526DE /* ICMP.swift */,
);
path = Pinger;
sourceTree = "<group>";
@@ -5615,6 +5618,7 @@
58C7A4512A863FB50060C66F /* PingerProtocol.swift in Sources */,
583832292AC3DF1300EA2071 /* PacketTunnelActorCommand.swift in Sources */,
58CF95A22AD6F35800B59F5D /* ObservedState.swift in Sources */,
+ 449275422C3570CA000526DE /* ICMP.swift in Sources */,
583832232AC3181400EA2071 /* PacketTunnelActor+ErrorState.swift in Sources */,
58C7AF112ABD8480007EDD7A /* TunnelProviderMessage.swift in Sources */,
58C7AF162ABD84A8007EDD7A /* URLRequestProxy.swift in Sources */,
diff --git a/ios/PacketTunnelCore/Pinger/ICMP.swift b/ios/PacketTunnelCore/Pinger/ICMP.swift
new file mode 100644
index 0000000000..917ccee5bc
--- /dev/null
+++ b/ios/PacketTunnelCore/Pinger/ICMP.swift
@@ -0,0 +1,119 @@
+//
+// ICMP.swift
+// PacketTunnelCore
+//
+// Created by Andrew Bulhak on 2024-07-03.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+struct ICMP {
+ public enum Error: LocalizedError {
+ case malformedResponse(MalformedResponseReason)
+
+ public var errorDescription: String? {
+ switch self {
+ case let .malformedResponse(reason):
+ return "Malformed response: \(reason)."
+ }
+ }
+ }
+
+ public enum MalformedResponseReason {
+ case ipv4PacketTooSmall
+ case icmpHeaderTooSmall
+ case invalidIPVersion
+ case checksumMismatch(UInt16, UInt16)
+ }
+
+ private static func in_chksum(_ data: some Sequence<UInt8>) -> UInt16 {
+ var iterator = data.makeIterator()
+ var words = [UInt16]()
+
+ while let byte = iterator.next() {
+ let nextByte = iterator.next() ?? 0
+ let word = UInt16(byte) << 8 | UInt16(nextByte)
+
+ words.append(word)
+ }
+
+ let sum = words.reduce(0, &+)
+
+ return ~sum
+ }
+
+ static func createICMPPacket(identifier: UInt16, sequenceNumber: UInt16) -> Data {
+ var header = ICMPHeader(
+ type: UInt8(ICMP_ECHO),
+ code: 0,
+ checksum: 0,
+ identifier: identifier.bigEndian,
+ sequenceNumber: sequenceNumber.bigEndian
+ )
+ header.checksum = withUnsafeBytes(of: &header) { in_chksum($0).bigEndian }
+
+ return withUnsafeBytes(of: &header) { Data($0) }
+ }
+
+ static func parseICMPResponse(buffer: inout [UInt8], length: Int) throws -> ICMPHeader {
+ try buffer.withUnsafeMutableBytes { bufferPointer in
+ // Check IP packet size.
+ guard length >= MemoryLayout<IPv4Header>.size else {
+ throw Error.malformedResponse(.ipv4PacketTooSmall)
+ }
+
+ // Verify IPv4 header.
+ let ipv4Header = bufferPointer.load(as: IPv4Header.self)
+ let payloadLength = length - ipv4Header.headerLength
+
+ guard payloadLength >= MemoryLayout<ICMPHeader>.size else {
+ throw Error.malformedResponse(.icmpHeaderTooSmall)
+ }
+
+ guard ipv4Header.isIPv4Version else {
+ throw Error.malformedResponse(.invalidIPVersion)
+ }
+
+ // Parse ICMP header.
+ let icmpHeaderPointer = bufferPointer.baseAddress!
+ .advanced(by: ipv4Header.headerLength)
+ .assumingMemoryBound(to: ICMPHeader.self)
+
+ // Copy server checksum.
+ let serverChecksum = icmpHeaderPointer.pointee.checksum.bigEndian
+
+ // Reset checksum field before calculating checksum.
+ icmpHeaderPointer.pointee.checksum = 0
+
+ // Verify ICMP checksum.
+ let payloadPointer = UnsafeRawBufferPointer(
+ start: icmpHeaderPointer,
+ count: payloadLength
+ )
+ let clientChecksum = ICMP.in_chksum(payloadPointer)
+ if clientChecksum != serverChecksum {
+ throw Error.malformedResponse(.checksumMismatch(clientChecksum, serverChecksum))
+ }
+
+ // Ensure endianness before returning ICMP packet to delegate.
+ var icmpHeader = icmpHeaderPointer.pointee
+ icmpHeader.identifier = icmpHeader.identifier.bigEndian
+ icmpHeader.sequenceNumber = icmpHeader.sequenceNumber.bigEndian
+ icmpHeader.checksum = serverChecksum
+ return icmpHeader
+ }
+ }
+}
+
+private extension IPv4Header {
+ /// Returns IPv4 header length.
+ var headerLength: Int {
+ Int(versionAndHeaderLength & 0x0F) * MemoryLayout<UInt32>.size
+ }
+
+ /// Returns `true` if version header indicates IPv4.
+ var isIPv4Version: Bool {
+ (versionAndHeaderLength & 0xF0) == 0x40
+ }
+}
diff --git a/ios/PacketTunnelCore/Pinger/Pinger.swift b/ios/PacketTunnelCore/Pinger/Pinger.swift
index 1cc9ccea41..8f63648061 100644
--- a/ios/PacketTunnelCore/Pinger/Pinger.swift
+++ b/ios/PacketTunnelCore/Pinger/Pinger.swift
@@ -125,7 +125,7 @@ public final class Pinger: PingerProtocol {
}
let sequenceNumber = nextSequenceNumber()
- let packetData = Self.createICMPPacket(
+ let packetData = ICMP.createICMPPacket(
identifier: identifier,
sequenceNumber: sequenceNumber
)
@@ -177,7 +177,13 @@ public final class Pinger: PingerProtocol {
do {
guard bytesRead > 0 else { throw Error.receivePacket(errno) }
- let icmpHeader = try parseICMPResponse(buffer: &readBuffer, length: bytesRead)
+ let icmpHeader = try ICMP.parseICMPResponse(buffer: &readBuffer, length: bytesRead)
+ guard icmpHeader.identifier == identifier else {
+ throw Error.clientIdentifierMismatch
+ }
+ guard icmpHeader.type == ICMP_ECHOREPLY else {
+ throw Error.invalidICMPType(icmpHeader.type)
+ }
guard let sender = Self.makeIPAddress(from: address) else { throw Error.parseIPAddress }
replyQueue.async {
@@ -192,65 +198,6 @@ public final class Pinger: PingerProtocol {
}
}
- private func parseICMPResponse(buffer: inout [UInt8], length: Int) throws -> ICMPHeader {
- try buffer.withUnsafeMutableBytes { bufferPointer in
- // Check IP packet size.
- guard length >= MemoryLayout<IPv4Header>.size else {
- throw Error.malformedResponse(.ipv4PacketTooSmall)
- }
-
- // Verify IPv4 header.
- let ipv4Header = bufferPointer.load(as: IPv4Header.self)
- let payloadLength = length - ipv4Header.headerLength
-
- guard payloadLength >= MemoryLayout<ICMPHeader>.size else {
- throw Error.malformedResponse(.icmpHeaderTooSmall)
- }
-
- guard ipv4Header.isIPv4Version else {
- throw Error.malformedResponse(.invalidIPVersion)
- }
-
- // Parse ICMP header.
- let icmpHeaderPointer = bufferPointer.baseAddress!
- .advanced(by: ipv4Header.headerLength)
- .assumingMemoryBound(to: ICMPHeader.self)
-
- // Check if ICMP response identifier matches the one from sender.
- guard icmpHeaderPointer.pointee.identifier.bigEndian == identifier else {
- throw Error.clientIdentifierMismatch
- }
-
- // Verify ICMP type.
- guard icmpHeaderPointer.pointee.type == ICMP_ECHOREPLY else {
- throw Error.malformedResponse(.invalidEchoReplyType)
- }
-
- // Copy server checksum.
- let serverChecksum = icmpHeaderPointer.pointee.checksum.bigEndian
-
- // Reset checksum field before calculating checksum.
- icmpHeaderPointer.pointee.checksum = 0
-
- // Verify ICMP checksum.
- let payloadPointer = UnsafeRawBufferPointer(
- start: icmpHeaderPointer,
- count: payloadLength
- )
- let clientChecksum = in_chksum(payloadPointer)
- if clientChecksum != serverChecksum {
- throw Error.malformedResponse(.checksumMismatch(clientChecksum, serverChecksum))
- }
-
- // Ensure endianness before returning ICMP packet to delegate.
- var icmpHeader = icmpHeaderPointer.pointee
- icmpHeader.identifier = icmpHeader.identifier.bigEndian
- icmpHeader.sequenceNumber = icmpHeader.sequenceNumber.bigEndian
- icmpHeader.checksum = serverChecksum
- return icmpHeader
- }
- }
-
private func bindSocket(_ socket: CFSocket, to interfaceName: String) throws {
var index = if_nametoindex(interfaceName)
guard index > 0 else {
@@ -270,19 +217,6 @@ public final class Pinger: PingerProtocol {
}
}
- private class func createICMPPacket(identifier: UInt16, sequenceNumber: UInt16) -> Data {
- var header = ICMPHeader(
- type: UInt8(ICMP_ECHO),
- code: 0,
- checksum: 0,
- identifier: identifier.bigEndian,
- sequenceNumber: sequenceNumber.bigEndian
- )
- header.checksum = withUnsafeBytes(of: &header) { in_chksum($0).bigEndian }
-
- return withUnsafeBytes(of: &header) { Data($0) }
- }
-
private class func makeIPAddress(from sa: sockaddr) -> IPAddress? {
if sa.sa_family == AF_INET {
return withUnsafeBytes(of: sa) { buffer -> IPAddress? in
@@ -337,12 +271,12 @@ extension Pinger {
/// Failure to receive packet. Contains the `errno`.
case receivePacket(Int32)
+ /// Unexpected ICMP reply type
+ case invalidICMPType(UInt8)
+
/// Response identifier does not match the sender identifier.
case clientIdentifierMismatch
- /// Malformed response.
- case malformedResponse(MalformedResponseReason)
-
/// Failure to parse IP address.
case parseIPAddress
@@ -362,51 +296,13 @@ extension Pinger {
return "Failure to send packet (errno: \(code))."
case let .receivePacket(code):
return "Failure to receive packet (errno: \(code))."
+ case let .invalidICMPType(type):
+ return "Unexpected ICMP reply type: \(type)"
case .clientIdentifierMismatch:
return "Response identifier does not match the sender identifier."
- case let .malformedResponse(reason):
- return "Malformed response: \(reason)."
case .parseIPAddress:
return "Failed to parse IP address."
}
}
}
-
- public enum MalformedResponseReason {
- case ipv4PacketTooSmall
- case icmpHeaderTooSmall
- case invalidIPVersion
- case invalidEchoReplyType
- case checksumMismatch(UInt16, UInt16)
- }
-}
-
-private func in_chksum(_ data: some Sequence<UInt8>) -> UInt16 {
- var iterator = data.makeIterator()
- var words = [UInt16]()
-
- while let byte = iterator.next() {
- let nextByte = iterator.next() ?? 0
- let word = UInt16(byte) << 8 | UInt16(nextByte)
-
- words.append(word)
- }
-
- let sum = words.reduce(0, &+)
-
- return ~sum
-}
-
-private extension IPv4Header {
- /// Returns IPv4 header length.
- var headerLength: Int {
- Int(versionAndHeaderLength & 0x0F) * MemoryLayout<UInt32>.size
- }
-
- /// Returns `true` if version header indicates IPv4.
- var isIPv4Version: Bool {
- (versionAndHeaderLength & 0xF0) == 0x40
- }
-
- // swiftlint:disable:next file_length
}