summaryrefslogtreecommitdiffhomepage
path: root/ios/PacketTunnelCore/Pinger/ICMP.swift
blob: 38bdbb2fbc5a4241604d08178d374f141e3a2dd6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//
//  ICMP.swift
//  PacketTunnelCore
//
//  Created by Andrew Bulhak on 2024-07-03.
//  Copyright © 2025 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
    }
}