Skip to content

Commit

Permalink
Fix for MobileNativeFoundation#94 (private IP querying)
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
jean committed Aug 22, 2022
1 parent 04af0b2 commit 069a3e2
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 1 deletion.
6 changes: 6 additions & 0 deletions Kronos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
930B39DD2051E6D300360BA2 /* TimeStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 930B39DC2051E6D300360BA2 /* TimeStorage.swift */; };
930B39E02051F26500360BA2 /* TimeStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 930B39DE2051F25300360BA2 /* TimeStorageTests.swift */; };
C201748E1BD5509D00E4FE18 /* Kronos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C20174831BD5509D00E4FE18 /* Kronos.framework */; };
F3D6F95F28B3BA590093BFA9 /* InternetAddressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3D6F95E28B3BA590093BFA9 /* InternetAddressTests.swift */; };
/* End PBXBuildFile section */

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

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -139,6 +141,7 @@
children = (
26447D851D6E54FF00159BEE /* ClockTests.swift */,
26447D861D6E54FF00159BEE /* DNSResolverTests.swift */,
F3D6F95E28B3BA590093BFA9 /* InternetAddressTests.swift */,
26447D871D6E54FF00159BEE /* NTPClientTests.swift */,
26447D881D6E54FF00159BEE /* NTPPacketTests.swift */,
930B39DE2051F25300360BA2 /* TimeStorageTests.swift */,
Expand Down Expand Up @@ -247,6 +250,7 @@
26447D8B1D6E54FF00159BEE /* NTPClientTests.swift in Sources */,
26447D891D6E54FF00159BEE /* ClockTests.swift in Sources */,
26447D8A1D6E54FF00159BEE /* DNSResolverTests.swift in Sources */,
F3D6F95F28B3BA590093BFA9 /* InternetAddressTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -431,6 +435,7 @@
buildSettings = {
CLANG_ENABLE_MODULES = YES;
COMBINE_HIDPI_IMAGES = YES;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand All @@ -449,6 +454,7 @@
buildSettings = {
CLANG_ENABLE_MODULES = YES;
COMBINE_HIDPI_IMAGES = YES;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down
1 change: 1 addition & 0 deletions Sources/DNSResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ final class DNSResolver {
let IPs = (addresses.takeUnretainedValue() as NSArray)
.compactMap { $0 as? NSData }
.compactMap(InternetAddress.init)
.filter { ip in !ip.isPrivate } // to avoid querying private IPs, see: https://github.com/MobileNativeFoundation/Kronos/issues/94

resolver.completion?(IPs)
retainedSelf.release()
Expand Down
48 changes: 47 additions & 1 deletion Sources/InternetAddress.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,53 @@ enum InternetAddress: Hashable {
return PF_INET6
}
}


/// If the address is reserved for private internets (local / private IP).
var isPrivate: Bool {
guard let host = host else {
return false
}

switch self {
case .ipv6:
// Ref.: https://datatracker.ietf.org/doc/html/rfc4193#section-3
// +--------+-+------------+-----------+----------------------------+
// | 7 bits |1| 40 bits | 16 bits | 64 bits |
// +--------+-+------------+-----------+----------------------------+
// | Prefix |L| Global ID | Subnet ID | Interface ID |
// +--------+-+------------+-----------+----------------------------+
//
// Local IP is expected to have FC00::/7 prefix (7 bits) and L byte set to 1,
// which effectively means `fd` prefix for local IPs.
let localPrefix = "fd"

// Ref.: https://datatracker.ietf.org/doc/html/rfc4291#section-2.4
let multicastPrefix = "ff"

let hostLowercased = host.lowercased()
return hostLowercased.starts(with: localPrefix)
|| hostLowercased.starts(with: multicastPrefix)
case .ipv4:
// Ref.: https://datatracker.ietf.org/doc/html/rfc1918#section-3
// Local IPs have predefined ranges:
// - class A: 10.0.0.0 — 10.255.255.255
// - class B: 172.16.0.0 — 172.31.255.255
// - class C: 192.168.0.0 — 192.168.255.255
let classABCregex = #"^((10\.)|(172\.1[6-9]\.)|(172\.2[0-9]\.)|(172\.3[0-1]\.)|(192\.168\.))"#

// Ref.: https://datatracker.ietf.org/doc/html/rfc5771#section-3
// Multicast address (range 224.0.0.0 - 239.255.255.255) are considered to be local network
// addresses too (https://developer.apple.com/forums/thread/663848)
//
let multicastRegex = #"^((22[4-9]\.)|(23[0-9]\.))"#
let broadcastIP = "255.255.255.255"

return host.range(of: classABCregex, options: .regularExpression) != nil
|| host.range(of: multicastRegex, options: .regularExpression) != nil
|| host == broadcastIP
}
}

func hash(into hasher: inout Hasher) {
hasher.combine(self.host)
}
Expand Down
124 changes: 124 additions & 0 deletions Tests/KronosTests/InternetAddressTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import XCTest
@testable import Kronos

/// Extension used to generate random UInt8 values within the given bounds and excluding the given value set.
extension UInt8 {
static func mockRandom(min: Self = .min, max: Self = .max, otherThan values: Set<Self> = []) -> Self {
var random: Self = .random(in: min...max)
while values.contains(random) { random = .random(in: min...max) }
return random
}
}

class InternetAddressTests: XCTestCase {
func testIfIPv4AddressIsPrivate() throws {
let privateIPs: [InternetAddress] = try (0..<50).flatMap { _ in
return [
// random private IPs of class A: 10.0.0.0 — 10.255.255.255
try .mockIPv4([10, .mockRandom(), .mockRandom(), .mockRandom()]),
// random private IPs of class B: 172.16.0.0 — 172.31.255.255
try .mockIPv4([172, .mockRandom(min: 16, max: 31), .mockRandom(), .mockRandom()]),
// random private IPs of class C: 192.168.0.0 — 192.168.255.255
try .mockIPv4([192, 168, .mockRandom(), .mockRandom()]),
// multicast IPs 224.0.0.0 - 239.255.255.255
try .mockIPv4([.mockRandom(min: 224, max: 239), .mockRandom(), .mockRandom(), .mockRandom()]),
// broadcast IP 255.255.255.255
try .mockIPv4([255, 255, 255, 255]),
]
}
let publicIPs: [InternetAddress] = try (0..<50).flatMap { _ in
return [
try .mockIPv4([.mockRandom(otherThan: Set<UInt8>([10, 172, 192, 255] + (224...239))), .mockRandom(), .mockRandom(), .mockRandom()]),
try .mockIPv4([172, .mockRandom(min: 0, max: 15), .mockRandom(), .mockRandom()]),
try .mockIPv4([172, .mockRandom(min: 32, max: 255), .mockRandom(), .mockRandom()]),
try .mockIPv4([192, .mockRandom(otherThan: [168]), .mockRandom(), .mockRandom()]),
try .mockIPv4([255, .mockRandom(max: 254), .mockRandom(), .mockRandom()]),
try .mockIPv4([255, .mockRandom(), .mockRandom(max: 254), .mockRandom()]),
try .mockIPv4([255, .mockRandom(), .mockRandom(), .mockRandom(max: 254)]),
]
}

privateIPs.forEach { ip in
XCTAssertTrue(ip.isPrivate, "\(ip.host ?? "nil") should be private IP")
}
publicIPs.forEach { ip in
XCTAssertFalse(ip.isPrivate, "\(ip.host ?? "nil") should not be private IP")
}
}

func testIfIPv6AddressIsPrivate() throws {
let privateIPs: [InternetAddress] = try (0..<50).flatMap { _ in
return [
// random private IP starting with `fd` prefix
try .mockIPv6([0xfd] + (0..<15).map({ _ in .mockRandom() })),
// random multicast IP starting with `ff` prefix
try .mockIPv6([0xff] + (0..<15).map({ _ in .mockRandom() })),
]
}
let publicIPs: [InternetAddress] = try (0..<50).flatMap { _ in
return [
// first byte is mocked to avoid having `fd` or `ff` prefix
try .mockIPv6([.mockRandom(min: 0xf0, otherThan: [0xfd, 0xff])] + (0..<15).map({ _ in .mockRandom() })),
try .mockIPv6([.mockRandom(max: 0xfc, otherThan: [0xf])] + (0..<15).map({ _ in .mockRandom() })),
]
}

privateIPs.forEach { ip in
XCTAssertTrue(ip.isPrivate, "\(ip.host ?? "nil") should be private IP")
}
publicIPs.forEach { ip in
XCTAssertFalse(ip.isPrivate, "\(ip.host ?? "nil") should not be private IP")
}
}
}

// MARK: - Mocks

private extension InternetAddress {
static func mockIPv4(_ bytes: [UInt8]) throws -> InternetAddress {
precondition(bytes.count == 4, "Expected 4 bytes")
let numbers = bytes.map { String($0) }
let ipv4String = numbers.joined(separator: ".") // e.g. '192.168.1.1'
let address: InternetAddress? = .mockWith(ipv4String: ipv4String)
return try XCTUnwrap(address, "\(ipv4String) is not a valid IPv4 string")
}

static func mockIPv6(_ bytes: [UInt8]) throws -> InternetAddress {
precondition(bytes.count == 16, "Expected 16 bytes")
let groups: [String] = (0..<8).map { idx in
let hexA = String(bytes[idx * 2], radix: 16)
let hexB = String(bytes[idx * 2 + 1], radix: 16)
return hexA + hexB
}
let ipv6String = groups.joined(separator: ":") // e.g. 'ab:ab:ab:ab:ab:ab:ab:ab'
let randomcasedIpv6String = Bool.random() ? ipv6String.lowercased() : ipv6String.uppercased()
let address: InternetAddress? = .mockWith(ipv6String: randomcasedIpv6String)
return try XCTUnwrap(address, "\(ipv6String) is not a valid IPv6 string")
}

static func mockWith(ipv4String: String) -> InternetAddress? {
var inaddr = in_addr()
guard ipv4String.withCString({ inet_pton(AF_INET, $0, &inaddr) }) == 1 else {
return nil // likely, not an IPv4 string
}

var addr = sockaddr_in()
addr.sin_len = UInt8(MemoryLayout.size(ofValue: addr))
addr.sin_family = sa_family_t(AF_INET)
addr.sin_addr = inaddr
return .ipv4(addr)
}

static func mockWith(ipv6String: String) -> InternetAddress? {
var inaddr = in6_addr()
guard ipv6String.withCString({ inet_pton(AF_INET6, $0, &inaddr) }) == 1 else {
return nil // likely, not an IPv6 string
}

var addr = sockaddr_in6()
addr.sin6_len = UInt8(MemoryLayout.size(ofValue: addr))
addr.sin6_family = sa_family_t(AF_INET6)
addr.sin6_addr = inaddr
return .ipv6(addr)
}
}

0 comments on commit 069a3e2

Please sign in to comment.