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

Move ICMP packet creation and parsing out of Pinger implementation #6438

Merged
merged 1 commit into from
Jul 8, 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
4 changes: 4 additions & 0 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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>"; };
Expand Down Expand Up @@ -3419,6 +3421,7 @@
58218E1428B65058000C624F /* IPv4Header.h */,
5838318A27C40A3900000571 /* Pinger.swift */,
58799A352A84FC9F007BE51F /* PingerProtocol.swift */,
449275412C3570CA000526DE /* ICMP.swift */,
);
path = Pinger;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down
119 changes: 119 additions & 0 deletions ios/PacketTunnelCore/Pinger/ICMP.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
130 changes: 13 additions & 117 deletions ios/PacketTunnelCore/Pinger/Pinger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public final class Pinger: PingerProtocol {
}

let sequenceNumber = nextSequenceNumber()
let packetData = Self.createICMPPacket(
let packetData = ICMP.createICMPPacket(
identifier: identifier,
sequenceNumber: sequenceNumber
)
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
}
Loading