diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 2b82f011843e..6775c52f513d 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 = ""; }; 06FAE67B28F83CA50033DD93 /* REST.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = REST.swift; sourceTree = ""; }; 06FAE67D28F83CA50033DD93 /* RESTTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RESTTransport.swift; sourceTree = ""; }; + 449275412C3570CA000526DE /* ICMP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ICMP.swift; sourceTree = ""; }; 449872E02B7BBC5400094DDC /* TunnelSettingsUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsUpdate.swift; sourceTree = ""; }; 449872E32B7CB96300094DDC /* TunnelSettingsUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsUpdateTests.swift; sourceTree = ""; }; 449EB9FC2B95F8AD00DFA4EB /* DeviceMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceMock.swift; sourceTree = ""; }; @@ -3419,6 +3421,7 @@ 58218E1428B65058000C624F /* IPv4Header.h */, 5838318A27C40A3900000571 /* Pinger.swift */, 58799A352A84FC9F007BE51F /* PingerProtocol.swift */, + 449275412C3570CA000526DE /* ICMP.swift */, ); path = Pinger; sourceTree = ""; @@ -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 000000000000..917ccee5bce9 --- /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) -> 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.size else { + throw Error.malformedResponse(.ipv4PacketTooSmall) + } + + // Verify IPv4 header. + let ipv4Header = bufferPointer.load(as: IPv4Header.self) + let payloadLength = length - ipv4Header.headerLength + + guard payloadLength >= MemoryLayout.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.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 1cc9ccea41e0..8f63648061e7 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.size else { - throw Error.malformedResponse(.ipv4PacketTooSmall) - } - - // Verify IPv4 header. - let ipv4Header = bufferPointer.load(as: IPv4Header.self) - let payloadLength = length - ipv4Header.headerLength - - guard payloadLength >= MemoryLayout.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) -> 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.size - } - - /// Returns `true` if version header indicates IPv4. - var isIPv4Version: Bool { - (versionAndHeaderLength & 0xF0) == 0x40 - } - - // swiftlint:disable:next file_length }