|
| 1 | +//===----------------------------------------------------------------------===// |
| 2 | +// |
| 3 | +// This source file is part of the Swift open source project |
| 4 | +// |
| 5 | +// Copyright (c) 2026 Apple Inc. and the Swift project authors |
| 6 | +// Licensed under Apache License v2.0 |
| 7 | +// |
| 8 | +// See LICENSE.txt for license information |
| 9 | +// See CONTRIBUTORS.txt for the list of Swift project authors |
| 10 | +// |
| 11 | +// SPDX-License-Identifier: Apache-2.0 |
| 12 | +// |
| 13 | +//===----------------------------------------------------------------------===// |
| 14 | + |
| 15 | +/// The six prefix lengths defined by RFC 6052 for embedding IPv4 addresses into IPv6. |
| 16 | +/// |
| 17 | +/// Raw values represent the prefix length in **bytes** (not bits). Use `bitLength` |
| 18 | +/// to get the length in bits. |
| 19 | +@available(Network 0.1.0, *) |
| 20 | +public enum NAT64PrefixLength: UInt8, Sendable { |
| 21 | + case prefixLength32 = 4 |
| 22 | + case prefixLength40 = 5 |
| 23 | + case prefixLength48 = 6 |
| 24 | + case prefixLength56 = 7 |
| 25 | + case prefixLength64 = 8 |
| 26 | + case prefixLength96 = 12 |
| 27 | + |
| 28 | + var bitLength: Int { Int(rawValue) * 8 } |
| 29 | + |
| 30 | + // Byte positions in a 16-byte IPv6 buffer where each of the 4 IPv4 bytes is placed. |
| 31 | + // Byte 8 is always skipped (reserved per RFC 6052 §2.2). |
| 32 | + var ipv4ByteOffsets: [4 of Int] { |
| 33 | + switch self { |
| 34 | + case .prefixLength32: return [4, 5, 6, 7] |
| 35 | + case .prefixLength40: return [5, 6, 7, 9] |
| 36 | + case .prefixLength48: return [6, 7, 9, 10] |
| 37 | + case .prefixLength56: return [7, 9, 10, 11] |
| 38 | + case .prefixLength64: return [9, 10, 11, 12] |
| 39 | + case .prefixLength96: return [12, 13, 14, 15] |
| 40 | + } |
| 41 | + } |
| 42 | +} |
| 43 | + |
| 44 | +/// A NAT64 prefix — a base IPv6 address and length that defines the block of IPv6 address |
| 45 | +/// space used to represent IPv4 destinations on a NAT64 network. |
| 46 | +/// |
| 47 | +/// Trailing bytes past `length` are always zeroed so that two prefixes with the same |
| 48 | +/// significant bits compare equal regardless of what was in the trailing positions. |
| 49 | +@available(Network 0.1.0, *) |
| 50 | +@_spi(Essentials) |
| 51 | +public struct NAT64Prefix: Hashable, CustomDebugStringConvertible, Sendable { |
| 52 | + /// The prefix length, indicating how many leading bytes of `address` are significant. |
| 53 | + public let length: NAT64PrefixLength |
| 54 | + |
| 55 | + /// The base IPv6 address of the prefix. Bytes past `length` are always zero. |
| 56 | + public let address: IPv6Address |
| 57 | + |
| 58 | + /// The IANA well-known NAT64 prefix (`64:ff9b::/96`) defined in RFC 6052 §2.1. |
| 59 | + public static let wellKnownPrefix = NAT64Prefix( |
| 60 | + length: .prefixLength96, |
| 61 | + address: IPv6Address((UInt32(0x0064_ff9b).bigEndian, 0, 0, 0)) |
| 62 | + ) |
| 63 | + |
| 64 | + /// Creates a NAT64 prefix with the given length and base address. |
| 65 | + /// |
| 66 | + /// Bytes in `address` past `length` are zeroed so that equality and hashing |
| 67 | + /// consider only the significant prefix bytes. |
| 68 | + public init(length: NAT64PrefixLength, address: IPv6Address) { |
| 69 | + self.length = length |
| 70 | + var bytes: [16 of UInt8] = .init(repeating: 0) |
| 71 | + withUnsafeBytes(of: address.address) { src in |
| 72 | + withUnsafeMutableBytes(of: &bytes) { dst in |
| 73 | + dst.copyBytes(from: src.prefix(Int(length.rawValue))) |
| 74 | + } |
| 75 | + } |
| 76 | + self.address = withUnsafeBytes(of: &bytes) { |
| 77 | + IPv6Address($0.loadUnaligned(as: (UInt32, UInt32, UInt32, UInt32).self)) |
| 78 | + } |
| 79 | + } |
| 80 | + |
| 81 | + public var debugDescription: String { |
| 82 | + "\(address.debugDescription)/\(length.bitLength)" |
| 83 | + } |
| 84 | +} |
| 85 | + |
| 86 | +extension IPv4Address { |
| 87 | + |
| 88 | + var canBeSynthesizedNAT64: Bool { |
| 89 | + if isZeroNet { return false } // 0.0.0.0/8 source hosts on local network |
| 90 | + if isInLoopbackRange { return false } // 127.0.0.0/8 loopback |
| 91 | + if isLinkLocal { return false } // 169.254.0.0/16 link local |
| 92 | + if isDSLite { return false } // 192.0.0.0/29 DS-Lite |
| 93 | + if is6to4RelayAnycast { return false } // 192.88.99.0/24 6to4 relay anycast |
| 94 | + if isMulticast { return false } // 224.0.0.0/4 multicast |
| 95 | + if isBroadcast { return false } // 255.255.255.255 limited broadcast |
| 96 | + return true |
| 97 | + } |
| 98 | + |
| 99 | + var isBlocklistedForWellKnownNAT64Prefix: Bool { |
| 100 | + isPrivateUse || isSharedAddressSpace |
| 101 | + } |
| 102 | +} |
| 103 | + |
| 104 | +@available(Network 0.1.0, *) |
| 105 | +extension IPv6Address { |
| 106 | + // Byte 8 of the synthesized address is always zero (the "u" octet, reserved per RFC 6052 §2.2). |
| 107 | + static func synthesized(from ipv4: IPv4Address, prefix: NAT64Prefix) -> IPv6Address? { |
| 108 | + guard ipv4.canBeSynthesizedNAT64 else { return nil } |
| 109 | + if prefix == .wellKnownPrefix && ipv4.isBlocklistedForWellKnownNAT64Prefix { return nil } |
| 110 | + |
| 111 | + var v6: [16 of UInt8] = .init(repeating: 0) |
| 112 | + let offsets = prefix.length.ipv4ByteOffsets |
| 113 | + withUnsafeBytes(of: ipv4.address) { v4 in |
| 114 | + withUnsafeMutableBytes(of: &v6) { dst in |
| 115 | + dst[offsets[0]] = v4[0] |
| 116 | + dst[offsets[1]] = v4[1] |
| 117 | + dst[offsets[2]] = v4[2] |
| 118 | + dst[offsets[3]] = v4[3] |
| 119 | + // Prefix bytes overwrite v6[0..<length]; ipv4ByteOffsets always places IPv4 bytes |
| 120 | + // at positions >= length, so the prefix copy never overwrites an IPv4 byte. |
| 121 | + withUnsafeBytes(of: prefix.address.address) { src in |
| 122 | + dst.copyBytes(from: src.prefix(Int(prefix.length.rawValue))) |
| 123 | + } |
| 124 | + } |
| 125 | + } |
| 126 | + return withUnsafeBytes(of: &v6) { |
| 127 | + IPv6Address($0.loadUnaligned(as: (UInt32, UInt32, UInt32, UInt32).self)) |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + func extractedIPv4(using prefix: NAT64Prefix) -> IPv4Address? { |
| 132 | + // Byte 8 is intentionally not validated - extraction is a mechanical inverse |
| 133 | + // of synthesized(from:prefix:) and does not enforce RFC 6052 invariants on input. |
| 134 | + let byteCount = Int(prefix.length.rawValue) |
| 135 | + let prefixMatches = withUnsafeBytes(of: self.address) { v6 in |
| 136 | + withUnsafeBytes(of: prefix.address.address) { p in |
| 137 | + v6.prefix(byteCount).elementsEqual(p.prefix(byteCount)) |
| 138 | + } |
| 139 | + } |
| 140 | + guard prefixMatches else { return nil } |
| 141 | + let offsets = prefix.length.ipv4ByteOffsets |
| 142 | + var v4: [4 of UInt8] = .init(repeating: 0) |
| 143 | + withUnsafeBytes(of: self.address) { v6 in |
| 144 | + withUnsafeMutableBytes(of: &v4) { dst in |
| 145 | + dst[0] = v6[offsets[0]] |
| 146 | + dst[1] = v6[offsets[1]] |
| 147 | + dst[2] = v6[offsets[2]] |
| 148 | + dst[3] = v6[offsets[3]] |
| 149 | + } |
| 150 | + } |
| 151 | + return withUnsafeBytes(of: &v4) { |
| 152 | + IPv4Address($0.loadUnaligned(as: UInt32.self)) |
| 153 | + } |
| 154 | + } |
| 155 | +} |
0 commit comments