Skip to content

Commit 069a3e2

Browse files
author
jean
committed
Fix for MobileNativeFoundation#94 (private IP querying)
Changes from DataDog/dd-sdk-ios#830 Also added 'GENERATE_INFOPLIST_FILE = YES;' to Tests target in order for tests to run. Signed-off-by: jean <[email protected]>
1 parent 04af0b2 commit 069a3e2

File tree

4 files changed

+178
-1
lines changed

4 files changed

+178
-1
lines changed

Kronos.xcodeproj/project.pbxproj

+6
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
930B39DD2051E6D300360BA2 /* TimeStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 930B39DC2051E6D300360BA2 /* TimeStorage.swift */; };
2424
930B39E02051F26500360BA2 /* TimeStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 930B39DE2051F25300360BA2 /* TimeStorageTests.swift */; };
2525
C201748E1BD5509D00E4FE18 /* Kronos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C20174831BD5509D00E4FE18 /* Kronos.framework */; };
26+
F3D6F95F28B3BA590093BFA9 /* InternetAddressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3D6F95E28B3BA590093BFA9 /* InternetAddressTests.swift */; };
2627
/* End PBXBuildFile section */
2728

2829
/* Begin PBXContainerItemProxy section */
@@ -57,6 +58,7 @@
5758
C2C036D41C2B180D003FB853 /* UniversalFramework_Base.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework_Base.xcconfig; sourceTree = "<group>"; };
5859
C2C036D51C2B180D003FB853 /* UniversalFramework_Framework.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework_Framework.xcconfig; sourceTree = "<group>"; };
5960
C2C036D61C2B180D003FB853 /* UniversalFramework_Test.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework_Test.xcconfig; sourceTree = "<group>"; };
61+
F3D6F95E28B3BA590093BFA9 /* InternetAddressTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternetAddressTests.swift; sourceTree = "<group>"; };
6062
/* End PBXFileReference section */
6163

6264
/* Begin PBXFrameworksBuildPhase section */
@@ -139,6 +141,7 @@
139141
children = (
140142
26447D851D6E54FF00159BEE /* ClockTests.swift */,
141143
26447D861D6E54FF00159BEE /* DNSResolverTests.swift */,
144+
F3D6F95E28B3BA590093BFA9 /* InternetAddressTests.swift */,
142145
26447D871D6E54FF00159BEE /* NTPClientTests.swift */,
143146
26447D881D6E54FF00159BEE /* NTPPacketTests.swift */,
144147
930B39DE2051F25300360BA2 /* TimeStorageTests.swift */,
@@ -247,6 +250,7 @@
247250
26447D8B1D6E54FF00159BEE /* NTPClientTests.swift in Sources */,
248251
26447D891D6E54FF00159BEE /* ClockTests.swift in Sources */,
249252
26447D8A1D6E54FF00159BEE /* DNSResolverTests.swift in Sources */,
253+
F3D6F95F28B3BA590093BFA9 /* InternetAddressTests.swift in Sources */,
250254
);
251255
runOnlyForDeploymentPostprocessing = 0;
252256
};
@@ -431,6 +435,7 @@
431435
buildSettings = {
432436
CLANG_ENABLE_MODULES = YES;
433437
COMBINE_HIDPI_IMAGES = YES;
438+
GENERATE_INFOPLIST_FILE = YES;
434439
LD_RUNPATH_SEARCH_PATHS = (
435440
"$(inherited)",
436441
"@executable_path/Frameworks",
@@ -449,6 +454,7 @@
449454
buildSettings = {
450455
CLANG_ENABLE_MODULES = YES;
451456
COMBINE_HIDPI_IMAGES = YES;
457+
GENERATE_INFOPLIST_FILE = YES;
452458
LD_RUNPATH_SEARCH_PATHS = (
453459
"$(inherited)",
454460
"@executable_path/Frameworks",

Sources/DNSResolver.swift

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ final class DNSResolver {
3838
let IPs = (addresses.takeUnretainedValue() as NSArray)
3939
.compactMap { $0 as? NSData }
4040
.compactMap(InternetAddress.init)
41+
.filter { ip in !ip.isPrivate } // to avoid querying private IPs, see: https://github.com/MobileNativeFoundation/Kronos/issues/94
4142

4243
resolver.completion?(IPs)
4344
retainedSelf.release()

Sources/InternetAddress.swift

+47-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,53 @@ enum InternetAddress: Hashable {
3333
return PF_INET6
3434
}
3535
}
36-
36+
37+
/// If the address is reserved for private internets (local / private IP).
38+
var isPrivate: Bool {
39+
guard let host = host else {
40+
return false
41+
}
42+
43+
switch self {
44+
case .ipv6:
45+
// Ref.: https://datatracker.ietf.org/doc/html/rfc4193#section-3
46+
// +--------+-+------------+-----------+----------------------------+
47+
// | 7 bits |1| 40 bits | 16 bits | 64 bits |
48+
// +--------+-+------------+-----------+----------------------------+
49+
// | Prefix |L| Global ID | Subnet ID | Interface ID |
50+
// +--------+-+------------+-----------+----------------------------+
51+
//
52+
// Local IP is expected to have FC00::/7 prefix (7 bits) and L byte set to 1,
53+
// which effectively means `fd` prefix for local IPs.
54+
let localPrefix = "fd"
55+
56+
// Ref.: https://datatracker.ietf.org/doc/html/rfc4291#section-2.4
57+
let multicastPrefix = "ff"
58+
59+
let hostLowercased = host.lowercased()
60+
return hostLowercased.starts(with: localPrefix)
61+
|| hostLowercased.starts(with: multicastPrefix)
62+
case .ipv4:
63+
// Ref.: https://datatracker.ietf.org/doc/html/rfc1918#section-3
64+
// Local IPs have predefined ranges:
65+
// - class A: 10.0.0.0 — 10.255.255.255
66+
// - class B: 172.16.0.0 — 172.31.255.255
67+
// - class C: 192.168.0.0 — 192.168.255.255
68+
let classABCregex = #"^((10\.)|(172\.1[6-9]\.)|(172\.2[0-9]\.)|(172\.3[0-1]\.)|(192\.168\.))"#
69+
70+
// Ref.: https://datatracker.ietf.org/doc/html/rfc5771#section-3
71+
// Multicast address (range 224.0.0.0 - 239.255.255.255) are considered to be local network
72+
// addresses too (https://developer.apple.com/forums/thread/663848)
73+
//
74+
let multicastRegex = #"^((22[4-9]\.)|(23[0-9]\.))"#
75+
let broadcastIP = "255.255.255.255"
76+
77+
return host.range(of: classABCregex, options: .regularExpression) != nil
78+
|| host.range(of: multicastRegex, options: .regularExpression) != nil
79+
|| host == broadcastIP
80+
}
81+
}
82+
3783
func hash(into hasher: inout Hasher) {
3884
hasher.combine(self.host)
3985
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import XCTest
2+
@testable import Kronos
3+
4+
/// Extension used to generate random UInt8 values within the given bounds and excluding the given value set.
5+
extension UInt8 {
6+
static func mockRandom(min: Self = .min, max: Self = .max, otherThan values: Set<Self> = []) -> Self {
7+
var random: Self = .random(in: min...max)
8+
while values.contains(random) { random = .random(in: min...max) }
9+
return random
10+
}
11+
}
12+
13+
class InternetAddressTests: XCTestCase {
14+
func testIfIPv4AddressIsPrivate() throws {
15+
let privateIPs: [InternetAddress] = try (0..<50).flatMap { _ in
16+
return [
17+
// random private IPs of class A: 10.0.0.0 — 10.255.255.255
18+
try .mockIPv4([10, .mockRandom(), .mockRandom(), .mockRandom()]),
19+
// random private IPs of class B: 172.16.0.0 — 172.31.255.255
20+
try .mockIPv4([172, .mockRandom(min: 16, max: 31), .mockRandom(), .mockRandom()]),
21+
// random private IPs of class C: 192.168.0.0 — 192.168.255.255
22+
try .mockIPv4([192, 168, .mockRandom(), .mockRandom()]),
23+
// multicast IPs 224.0.0.0 - 239.255.255.255
24+
try .mockIPv4([.mockRandom(min: 224, max: 239), .mockRandom(), .mockRandom(), .mockRandom()]),
25+
// broadcast IP 255.255.255.255
26+
try .mockIPv4([255, 255, 255, 255]),
27+
]
28+
}
29+
let publicIPs: [InternetAddress] = try (0..<50).flatMap { _ in
30+
return [
31+
try .mockIPv4([.mockRandom(otherThan: Set<UInt8>([10, 172, 192, 255] + (224...239))), .mockRandom(), .mockRandom(), .mockRandom()]),
32+
try .mockIPv4([172, .mockRandom(min: 0, max: 15), .mockRandom(), .mockRandom()]),
33+
try .mockIPv4([172, .mockRandom(min: 32, max: 255), .mockRandom(), .mockRandom()]),
34+
try .mockIPv4([192, .mockRandom(otherThan: [168]), .mockRandom(), .mockRandom()]),
35+
try .mockIPv4([255, .mockRandom(max: 254), .mockRandom(), .mockRandom()]),
36+
try .mockIPv4([255, .mockRandom(), .mockRandom(max: 254), .mockRandom()]),
37+
try .mockIPv4([255, .mockRandom(), .mockRandom(), .mockRandom(max: 254)]),
38+
]
39+
}
40+
41+
privateIPs.forEach { ip in
42+
XCTAssertTrue(ip.isPrivate, "\(ip.host ?? "nil") should be private IP")
43+
}
44+
publicIPs.forEach { ip in
45+
XCTAssertFalse(ip.isPrivate, "\(ip.host ?? "nil") should not be private IP")
46+
}
47+
}
48+
49+
func testIfIPv6AddressIsPrivate() throws {
50+
let privateIPs: [InternetAddress] = try (0..<50).flatMap { _ in
51+
return [
52+
// random private IP starting with `fd` prefix
53+
try .mockIPv6([0xfd] + (0..<15).map({ _ in .mockRandom() })),
54+
// random multicast IP starting with `ff` prefix
55+
try .mockIPv6([0xff] + (0..<15).map({ _ in .mockRandom() })),
56+
]
57+
}
58+
let publicIPs: [InternetAddress] = try (0..<50).flatMap { _ in
59+
return [
60+
// first byte is mocked to avoid having `fd` or `ff` prefix
61+
try .mockIPv6([.mockRandom(min: 0xf0, otherThan: [0xfd, 0xff])] + (0..<15).map({ _ in .mockRandom() })),
62+
try .mockIPv6([.mockRandom(max: 0xfc, otherThan: [0xf])] + (0..<15).map({ _ in .mockRandom() })),
63+
]
64+
}
65+
66+
privateIPs.forEach { ip in
67+
XCTAssertTrue(ip.isPrivate, "\(ip.host ?? "nil") should be private IP")
68+
}
69+
publicIPs.forEach { ip in
70+
XCTAssertFalse(ip.isPrivate, "\(ip.host ?? "nil") should not be private IP")
71+
}
72+
}
73+
}
74+
75+
// MARK: - Mocks
76+
77+
private extension InternetAddress {
78+
static func mockIPv4(_ bytes: [UInt8]) throws -> InternetAddress {
79+
precondition(bytes.count == 4, "Expected 4 bytes")
80+
let numbers = bytes.map { String($0) }
81+
let ipv4String = numbers.joined(separator: ".") // e.g. '192.168.1.1'
82+
let address: InternetAddress? = .mockWith(ipv4String: ipv4String)
83+
return try XCTUnwrap(address, "\(ipv4String) is not a valid IPv4 string")
84+
}
85+
86+
static func mockIPv6(_ bytes: [UInt8]) throws -> InternetAddress {
87+
precondition(bytes.count == 16, "Expected 16 bytes")
88+
let groups: [String] = (0..<8).map { idx in
89+
let hexA = String(bytes[idx * 2], radix: 16)
90+
let hexB = String(bytes[idx * 2 + 1], radix: 16)
91+
return hexA + hexB
92+
}
93+
let ipv6String = groups.joined(separator: ":") // e.g. 'ab:ab:ab:ab:ab:ab:ab:ab'
94+
let randomcasedIpv6String = Bool.random() ? ipv6String.lowercased() : ipv6String.uppercased()
95+
let address: InternetAddress? = .mockWith(ipv6String: randomcasedIpv6String)
96+
return try XCTUnwrap(address, "\(ipv6String) is not a valid IPv6 string")
97+
}
98+
99+
static func mockWith(ipv4String: String) -> InternetAddress? {
100+
var inaddr = in_addr()
101+
guard ipv4String.withCString({ inet_pton(AF_INET, $0, &inaddr) }) == 1 else {
102+
return nil // likely, not an IPv4 string
103+
}
104+
105+
var addr = sockaddr_in()
106+
addr.sin_len = UInt8(MemoryLayout.size(ofValue: addr))
107+
addr.sin_family = sa_family_t(AF_INET)
108+
addr.sin_addr = inaddr
109+
return .ipv4(addr)
110+
}
111+
112+
static func mockWith(ipv6String: String) -> InternetAddress? {
113+
var inaddr = in6_addr()
114+
guard ipv6String.withCString({ inet_pton(AF_INET6, $0, &inaddr) }) == 1 else {
115+
return nil // likely, not an IPv6 string
116+
}
117+
118+
var addr = sockaddr_in6()
119+
addr.sin6_len = UInt8(MemoryLayout.size(ofValue: addr))
120+
addr.sin6_family = sa_family_t(AF_INET6)
121+
addr.sin6_addr = inaddr
122+
return .ipv6(addr)
123+
}
124+
}

0 commit comments

Comments
 (0)