Skip to content

Commit 61e723e

Browse files
acb-mvpinkisemils
authored andcommitted
Move ICMP packet creation and parsing out of Pinger implementation
1 parent 966a56c commit 61e723e

File tree

3 files changed

+136
-117
lines changed

3 files changed

+136
-117
lines changed

ios/MullvadVPN.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
06799AFC28F98EE300ACD94E /* AddressCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AC114128F8413A0037AF9A /* AddressCache.swift */; };
4040
0697D6E728F01513007A9E99 /* TransportMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0697D6E628F01513007A9E99 /* TransportMonitor.swift */; };
4141
06AC116228F94C450037AF9A /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; };
42+
449275422C3570CA000526DE /* ICMP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449275412C3570CA000526DE /* ICMP.swift */; };
4243
449872E12B7BBC5400094DDC /* TunnelSettingsUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449872E02B7BBC5400094DDC /* TunnelSettingsUpdate.swift */; };
4344
449872E42B7CB96300094DDC /* TunnelSettingsUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449872E32B7CB96300094DDC /* TunnelSettingsUpdateTests.swift */; };
4445
449EBA262B975B9700DFA4EB /* PostQuantumKeyReceiving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449EBA252B975B9700DFA4EB /* PostQuantumKeyReceiving.swift */; };
@@ -1451,6 +1452,7 @@
14511452
06FAE67A28F83CA50033DD93 /* RESTDevicesProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RESTDevicesProxy.swift; sourceTree = "<group>"; };
14521453
06FAE67B28F83CA50033DD93 /* REST.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = REST.swift; sourceTree = "<group>"; };
14531454
06FAE67D28F83CA50033DD93 /* RESTTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RESTTransport.swift; sourceTree = "<group>"; };
1455+
449275412C3570CA000526DE /* ICMP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ICMP.swift; sourceTree = "<group>"; };
14541456
449872E02B7BBC5400094DDC /* TunnelSettingsUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsUpdate.swift; sourceTree = "<group>"; };
14551457
449872E32B7CB96300094DDC /* TunnelSettingsUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsUpdateTests.swift; sourceTree = "<group>"; };
14561458
449EB9FC2B95F8AD00DFA4EB /* DeviceMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceMock.swift; sourceTree = "<group>"; };
@@ -3419,6 +3421,7 @@
34193421
58218E1428B65058000C624F /* IPv4Header.h */,
34203422
5838318A27C40A3900000571 /* Pinger.swift */,
34213423
58799A352A84FC9F007BE51F /* PingerProtocol.swift */,
3424+
449275412C3570CA000526DE /* ICMP.swift */,
34223425
);
34233426
path = Pinger;
34243427
sourceTree = "<group>";
@@ -5615,6 +5618,7 @@
56155618
58C7A4512A863FB50060C66F /* PingerProtocol.swift in Sources */,
56165619
583832292AC3DF1300EA2071 /* PacketTunnelActorCommand.swift in Sources */,
56175620
58CF95A22AD6F35800B59F5D /* ObservedState.swift in Sources */,
5621+
449275422C3570CA000526DE /* ICMP.swift in Sources */,
56185622
583832232AC3181400EA2071 /* PacketTunnelActor+ErrorState.swift in Sources */,
56195623
58C7AF112ABD8480007EDD7A /* TunnelProviderMessage.swift in Sources */,
56205624
58C7AF162ABD84A8007EDD7A /* URLRequestProxy.swift in Sources */,
+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
//
2+
// ICMP.swift
3+
// PacketTunnelCore
4+
//
5+
// Created by Andrew Bulhak on 2024-07-03.
6+
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
struct ICMP {
12+
public enum Error: LocalizedError {
13+
case malformedResponse(MalformedResponseReason)
14+
15+
public var errorDescription: String? {
16+
switch self {
17+
case let .malformedResponse(reason):
18+
return "Malformed response: \(reason)."
19+
}
20+
}
21+
}
22+
23+
public enum MalformedResponseReason {
24+
case ipv4PacketTooSmall
25+
case icmpHeaderTooSmall
26+
case invalidIPVersion
27+
case checksumMismatch(UInt16, UInt16)
28+
}
29+
30+
private static func in_chksum(_ data: some Sequence<UInt8>) -> UInt16 {
31+
var iterator = data.makeIterator()
32+
var words = [UInt16]()
33+
34+
while let byte = iterator.next() {
35+
let nextByte = iterator.next() ?? 0
36+
let word = UInt16(byte) << 8 | UInt16(nextByte)
37+
38+
words.append(word)
39+
}
40+
41+
let sum = words.reduce(0, &+)
42+
43+
return ~sum
44+
}
45+
46+
static func createICMPPacket(identifier: UInt16, sequenceNumber: UInt16) -> Data {
47+
var header = ICMPHeader(
48+
type: UInt8(ICMP_ECHO),
49+
code: 0,
50+
checksum: 0,
51+
identifier: identifier.bigEndian,
52+
sequenceNumber: sequenceNumber.bigEndian
53+
)
54+
header.checksum = withUnsafeBytes(of: &header) { in_chksum($0).bigEndian }
55+
56+
return withUnsafeBytes(of: &header) { Data($0) }
57+
}
58+
59+
static func parseICMPResponse(buffer: inout [UInt8], length: Int) throws -> ICMPHeader {
60+
try buffer.withUnsafeMutableBytes { bufferPointer in
61+
// Check IP packet size.
62+
guard length >= MemoryLayout<IPv4Header>.size else {
63+
throw Error.malformedResponse(.ipv4PacketTooSmall)
64+
}
65+
66+
// Verify IPv4 header.
67+
let ipv4Header = bufferPointer.load(as: IPv4Header.self)
68+
let payloadLength = length - ipv4Header.headerLength
69+
70+
guard payloadLength >= MemoryLayout<ICMPHeader>.size else {
71+
throw Error.malformedResponse(.icmpHeaderTooSmall)
72+
}
73+
74+
guard ipv4Header.isIPv4Version else {
75+
throw Error.malformedResponse(.invalidIPVersion)
76+
}
77+
78+
// Parse ICMP header.
79+
let icmpHeaderPointer = bufferPointer.baseAddress!
80+
.advanced(by: ipv4Header.headerLength)
81+
.assumingMemoryBound(to: ICMPHeader.self)
82+
83+
// Copy server checksum.
84+
let serverChecksum = icmpHeaderPointer.pointee.checksum.bigEndian
85+
86+
// Reset checksum field before calculating checksum.
87+
icmpHeaderPointer.pointee.checksum = 0
88+
89+
// Verify ICMP checksum.
90+
let payloadPointer = UnsafeRawBufferPointer(
91+
start: icmpHeaderPointer,
92+
count: payloadLength
93+
)
94+
let clientChecksum = ICMP.in_chksum(payloadPointer)
95+
if clientChecksum != serverChecksum {
96+
throw Error.malformedResponse(.checksumMismatch(clientChecksum, serverChecksum))
97+
}
98+
99+
// Ensure endianness before returning ICMP packet to delegate.
100+
var icmpHeader = icmpHeaderPointer.pointee
101+
icmpHeader.identifier = icmpHeader.identifier.bigEndian
102+
icmpHeader.sequenceNumber = icmpHeader.sequenceNumber.bigEndian
103+
icmpHeader.checksum = serverChecksum
104+
return icmpHeader
105+
}
106+
}
107+
}
108+
109+
private extension IPv4Header {
110+
/// Returns IPv4 header length.
111+
var headerLength: Int {
112+
Int(versionAndHeaderLength & 0x0F) * MemoryLayout<UInt32>.size
113+
}
114+
115+
/// Returns `true` if version header indicates IPv4.
116+
var isIPv4Version: Bool {
117+
(versionAndHeaderLength & 0xF0) == 0x40
118+
}
119+
}

ios/PacketTunnelCore/Pinger/Pinger.swift

+13-117
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public final class Pinger: PingerProtocol {
125125
}
126126

127127
let sequenceNumber = nextSequenceNumber()
128-
let packetData = Self.createICMPPacket(
128+
let packetData = ICMP.createICMPPacket(
129129
identifier: identifier,
130130
sequenceNumber: sequenceNumber
131131
)
@@ -177,7 +177,13 @@ public final class Pinger: PingerProtocol {
177177
do {
178178
guard bytesRead > 0 else { throw Error.receivePacket(errno) }
179179

180-
let icmpHeader = try parseICMPResponse(buffer: &readBuffer, length: bytesRead)
180+
let icmpHeader = try ICMP.parseICMPResponse(buffer: &readBuffer, length: bytesRead)
181+
guard icmpHeader.identifier == identifier else {
182+
throw Error.clientIdentifierMismatch
183+
}
184+
guard icmpHeader.type == ICMP_ECHOREPLY else {
185+
throw Error.invalidICMPType(icmpHeader.type)
186+
}
181187
guard let sender = Self.makeIPAddress(from: address) else { throw Error.parseIPAddress }
182188

183189
replyQueue.async {
@@ -192,65 +198,6 @@ public final class Pinger: PingerProtocol {
192198
}
193199
}
194200

195-
private func parseICMPResponse(buffer: inout [UInt8], length: Int) throws -> ICMPHeader {
196-
try buffer.withUnsafeMutableBytes { bufferPointer in
197-
// Check IP packet size.
198-
guard length >= MemoryLayout<IPv4Header>.size else {
199-
throw Error.malformedResponse(.ipv4PacketTooSmall)
200-
}
201-
202-
// Verify IPv4 header.
203-
let ipv4Header = bufferPointer.load(as: IPv4Header.self)
204-
let payloadLength = length - ipv4Header.headerLength
205-
206-
guard payloadLength >= MemoryLayout<ICMPHeader>.size else {
207-
throw Error.malformedResponse(.icmpHeaderTooSmall)
208-
}
209-
210-
guard ipv4Header.isIPv4Version else {
211-
throw Error.malformedResponse(.invalidIPVersion)
212-
}
213-
214-
// Parse ICMP header.
215-
let icmpHeaderPointer = bufferPointer.baseAddress!
216-
.advanced(by: ipv4Header.headerLength)
217-
.assumingMemoryBound(to: ICMPHeader.self)
218-
219-
// Check if ICMP response identifier matches the one from sender.
220-
guard icmpHeaderPointer.pointee.identifier.bigEndian == identifier else {
221-
throw Error.clientIdentifierMismatch
222-
}
223-
224-
// Verify ICMP type.
225-
guard icmpHeaderPointer.pointee.type == ICMP_ECHOREPLY else {
226-
throw Error.malformedResponse(.invalidEchoReplyType)
227-
}
228-
229-
// Copy server checksum.
230-
let serverChecksum = icmpHeaderPointer.pointee.checksum.bigEndian
231-
232-
// Reset checksum field before calculating checksum.
233-
icmpHeaderPointer.pointee.checksum = 0
234-
235-
// Verify ICMP checksum.
236-
let payloadPointer = UnsafeRawBufferPointer(
237-
start: icmpHeaderPointer,
238-
count: payloadLength
239-
)
240-
let clientChecksum = in_chksum(payloadPointer)
241-
if clientChecksum != serverChecksum {
242-
throw Error.malformedResponse(.checksumMismatch(clientChecksum, serverChecksum))
243-
}
244-
245-
// Ensure endianness before returning ICMP packet to delegate.
246-
var icmpHeader = icmpHeaderPointer.pointee
247-
icmpHeader.identifier = icmpHeader.identifier.bigEndian
248-
icmpHeader.sequenceNumber = icmpHeader.sequenceNumber.bigEndian
249-
icmpHeader.checksum = serverChecksum
250-
return icmpHeader
251-
}
252-
}
253-
254201
private func bindSocket(_ socket: CFSocket, to interfaceName: String) throws {
255202
var index = if_nametoindex(interfaceName)
256203
guard index > 0 else {
@@ -270,19 +217,6 @@ public final class Pinger: PingerProtocol {
270217
}
271218
}
272219

273-
private class func createICMPPacket(identifier: UInt16, sequenceNumber: UInt16) -> Data {
274-
var header = ICMPHeader(
275-
type: UInt8(ICMP_ECHO),
276-
code: 0,
277-
checksum: 0,
278-
identifier: identifier.bigEndian,
279-
sequenceNumber: sequenceNumber.bigEndian
280-
)
281-
header.checksum = withUnsafeBytes(of: &header) { in_chksum($0).bigEndian }
282-
283-
return withUnsafeBytes(of: &header) { Data($0) }
284-
}
285-
286220
private class func makeIPAddress(from sa: sockaddr) -> IPAddress? {
287221
if sa.sa_family == AF_INET {
288222
return withUnsafeBytes(of: sa) { buffer -> IPAddress? in
@@ -337,12 +271,12 @@ extension Pinger {
337271
/// Failure to receive packet. Contains the `errno`.
338272
case receivePacket(Int32)
339273

274+
/// Unexpected ICMP reply type
275+
case invalidICMPType(UInt8)
276+
340277
/// Response identifier does not match the sender identifier.
341278
case clientIdentifierMismatch
342279

343-
/// Malformed response.
344-
case malformedResponse(MalformedResponseReason)
345-
346280
/// Failure to parse IP address.
347281
case parseIPAddress
348282

@@ -362,51 +296,13 @@ extension Pinger {
362296
return "Failure to send packet (errno: \(code))."
363297
case let .receivePacket(code):
364298
return "Failure to receive packet (errno: \(code))."
299+
case let .invalidICMPType(type):
300+
return "Unexpected ICMP reply type: \(type)"
365301
case .clientIdentifierMismatch:
366302
return "Response identifier does not match the sender identifier."
367-
case let .malformedResponse(reason):
368-
return "Malformed response: \(reason)."
369303
case .parseIPAddress:
370304
return "Failed to parse IP address."
371305
}
372306
}
373307
}
374-
375-
public enum MalformedResponseReason {
376-
case ipv4PacketTooSmall
377-
case icmpHeaderTooSmall
378-
case invalidIPVersion
379-
case invalidEchoReplyType
380-
case checksumMismatch(UInt16, UInt16)
381-
}
382-
}
383-
384-
private func in_chksum(_ data: some Sequence<UInt8>) -> UInt16 {
385-
var iterator = data.makeIterator()
386-
var words = [UInt16]()
387-
388-
while let byte = iterator.next() {
389-
let nextByte = iterator.next() ?? 0
390-
let word = UInt16(byte) << 8 | UInt16(nextByte)
391-
392-
words.append(word)
393-
}
394-
395-
let sum = words.reduce(0, &+)
396-
397-
return ~sum
398-
}
399-
400-
private extension IPv4Header {
401-
/// Returns IPv4 header length.
402-
var headerLength: Int {
403-
Int(versionAndHeaderLength & 0x0F) * MemoryLayout<UInt32>.size
404-
}
405-
406-
/// Returns `true` if version header indicates IPv4.
407-
var isIPv4Version: Bool {
408-
(versionAndHeaderLength & 0xF0) == 0x40
409-
}
410-
411-
// swiftlint:disable:next file_length
412308
}

0 commit comments

Comments
 (0)