Skip to content

Commit 3a91bdb

Browse files
committed
Add NAT64 support in Utilities
1 parent 5fed088 commit 3a91bdb

4 files changed

Lines changed: 1087 additions & 1 deletion

File tree

Sources/SwiftNetwork/Endpoint/IPv4Address.swift

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,48 @@ public struct IPv4Address: IPAddress, Hashable, CustomDebugStringConvertible {
6363
return v4WireAddress & mask == subnet
6464
}
6565

66+
var isInLoopbackRange: Bool {
67+
let v4WireAddress = self.address
68+
let mask = (0xFF00_0000 as UInt32).bigEndian
69+
let subnet = (0x7F00_0000 as UInt32).bigEndian
70+
return v4WireAddress & mask == subnet // 127.0.0.0/8
71+
}
72+
73+
var isDSLite: Bool {
74+
let v4WireAddress = self.address
75+
let mask = (0xFFFF_FFF8 as UInt32).bigEndian
76+
let subnet = (0xC000_0000 as UInt32).bigEndian
77+
return v4WireAddress & mask == subnet // 192.0.0.0/29
78+
}
79+
80+
var is6to4RelayAnycast: Bool {
81+
let v4WireAddress = self.address
82+
let mask = (0xFFFF_FF00 as UInt32).bigEndian
83+
let subnet = (0xC058_6300 as UInt32).bigEndian
84+
return v4WireAddress & mask == subnet // 192.88.99.0/24
85+
}
86+
87+
var isPrivateUse: Bool {
88+
let v4WireAddress = self.address
89+
let mask8 = (0xFF00_0000 as UInt32).bigEndian
90+
let mask12 = (0xFFF0_0000 as UInt32).bigEndian
91+
let mask16 = (0xFFFF_0000 as UInt32).bigEndian
92+
return v4WireAddress & mask8 == (0x0A00_0000 as UInt32).bigEndian // 10.0.0.0/8
93+
|| v4WireAddress & mask12 == (0xAC10_0000 as UInt32).bigEndian // 172.16.0.0/12
94+
|| v4WireAddress & mask16 == (0xC0A8_0000 as UInt32).bigEndian // 192.168.0.0/16
95+
}
96+
97+
var isSharedAddressSpace: Bool {
98+
let v4WireAddress = self.address
99+
let mask = (0xFFC0_0000 as UInt32).bigEndian
100+
let subnet = (0x6440_0000 as UInt32).bigEndian
101+
return v4WireAddress & mask == subnet // 100.64.0.0/10
102+
}
103+
104+
var isBroadcast: Bool {
105+
self == .broadcast // 255.255.255.255
106+
}
107+
66108
// Stored in network byte order
67109
let address: UInt32
68110

Sources/SwiftNetwork/Path/PathProperties.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ public struct PathProperties: CustomStringConvertible {
202202

203203
var customEthertype: UInt16 = 0
204204
var customIPProtocol: UInt8 = 0
205-
var nat64Prefixes: [UInt8]? = nil
205+
var nat64Prefixes: [NAT64Prefix]? = nil
206206
var recommendedMSS: UInt8 = 0
207207
var linkQualityInternal: Int8 = 0
208208
var clientNECPFD: Int = -1
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
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

Comments
 (0)