├── sonar-project.properties
├── Tests
├── LinuxMain.swift
└── DNSTests
│ ├── FuzzTests.swift
│ ├── IPTests.swift
│ └── DNSTests.swift
├── DNS.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── project.pbxproj
├── .swiftlint.yml
├── Package.swift
├── Sources
├── DNS.h
├── Info.plist
└── DNS
│ ├── Integer+Data.swift
│ ├── Data+Extensions.swift
│ ├── IP.swift
│ ├── Types.swift
│ ├── Message.swift
│ └── Bytes.swift
├── DNS.podspec
├── CHANGELOG.md
├── README.md
├── .jazzy.yaml
├── LICENSE
├── .travis.yml
└── .gitignore
/sonar-project.properties:
--------------------------------------------------------------------------------
1 | sonar.projectKey=Bouke_DNS
2 | sonar.sources=Sources
3 | sonar.tests=Tests
4 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import DNSTests
4 |
5 | XCTMain([
6 | testCase(DNSTests.allTests),
7 | testCase(IPTests.allTests),
8 | testCase(FuzzTests.allTests)
9 | ])
10 |
--------------------------------------------------------------------------------
/DNS.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/DNS.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules:
2 | - cyclomatic_complexity
3 | - file_length
4 | - force_try
5 | - function_body_length
6 | - line_length
7 | - private_over_fileprivate
8 | - todo
9 | included:
10 | - Sources
11 | - Tests
12 | identifier_name:
13 | excluded:
14 | - id
15 | - ip
16 | - qr
17 | - to
18 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:4.2
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "DNS",
7 | products: [
8 | .library(name: "DNS", targets: ["DNS"]),
9 | ],
10 | targets: [
11 | .target(name: "DNS", dependencies: []),
12 | .testTarget(name: "DNSTests", dependencies: ["DNS"])
13 | ],
14 | swiftLanguageVersions: [.v4, .v4_2]
15 | )
16 |
--------------------------------------------------------------------------------
/Sources/DNS.h:
--------------------------------------------------------------------------------
1 | //
2 | // DNS.h
3 | // DNS
4 | //
5 | // Created by Zachary Gorak on 7/23/19.
6 | // Copyright © 2019 mcfedr. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for DNS.
12 | FOUNDATION_EXPORT double DNSVersionNumber;
13 |
14 | //! Project version string for DNS.
15 | FOUNDATION_EXPORT const unsigned char DNSVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 |
20 |
--------------------------------------------------------------------------------
/DNS.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 |
3 | s.name = "DNS"
4 | s.version = "2.0.0"
5 | s.summary = "A lib for parsing and serializing DNS packets."
6 |
7 | s.homepage = "https://github.com/mcfedr/DNS"
8 | s.license = "MIT"
9 | s.authors = { "Fred Cox" => "mcfedr@gmail.com", "Bouke Haarsma" => "email@email.com"}
10 |
11 | s.ios.deployment_target = "9.3"
12 | s.osx.deployment_target = "10.10"
13 |
14 | s.source = { :git => "https://github.com/mcfedr/DNS.git", :tag => "#{s.version}" }
15 |
16 | s.source_files = "Sources/DNS/**/*.swift"
17 | s.swift_version = ["4.0", "4.2", "5.0"]
18 | end
19 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [Unreleased]
2 |
3 | ## 1.1.1 - 2018-11-18
4 | ### Added
5 | - Allow encoding of UTF-8 names
6 |
7 | ## 1.1.0 - 2018-08-03
8 | ### Added
9 | - StartOfAuthorityRecord type.
10 | - Support for Swift 4.1.
11 |
12 | ### Changed
13 | - All ResourceRecord fields are now modifable.
14 |
15 | ### Fixed
16 | - Crash when encoding records with empty names.
17 | - Crash when parsing invalid label sizes.
18 |
19 | ## 1.0.0 - 2017-10-07
20 | ### Added
21 | - Support for Swift 4.
22 | - Improved API documentation.
23 | - Support for building on iOS.
24 |
25 | ### Changed
26 | - Parser became more forgiving, replacing enums with integers where
27 | no all cases are known.
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | DNS Record Types
2 | ================
3 |
4 | A Swift implementation of DNS Record Types.
5 |
6 | [](https://travis-ci.org/mcfedr/DNS)
7 |
8 | ## Usage
9 |
10 | ```swift
11 | // Encoding a message
12 | let request = Message(
13 | type: .query,
14 | questions: [Question(name: "apple.com.", type: .pointer)]
15 | )
16 | let requestData = try request.serialize()
17 |
18 | // Not shown here: send to DNS server over UDP, receive reply.
19 |
20 | // Decoding a message
21 | let responseData = Data()
22 | let response = try Message.init(deserialize: responseData)
23 | print(response.answers.first)
24 | ```
25 |
26 | ## Credits
27 |
28 | This library was written by [Bouke Haarsma](https://twitter.com/BoukeHaarsma).
29 |
--------------------------------------------------------------------------------
/Sources/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0.2
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 |
22 |
23 |
--------------------------------------------------------------------------------
/.jazzy.yaml:
--------------------------------------------------------------------------------
1 | xcodebuild_arguments:
2 | - -scheme
3 | - DNS
4 | module: DNS
5 | author: Bouke Haarsma
6 | author_url: https://twitter.com/BoukeHaarsma
7 | output: ../DNS-Docs/master
8 | theme: fullwidth
9 | clean: true
10 | github_url: https://github.com/Bouke/DNS
11 | dash_url: http://boukehaarsma.nl/DNS/DNS.xml
12 | custom_categories:
13 | - name: Querying
14 | children:
15 | - Message
16 | - MessageType
17 | - OperationCode
18 | - Question
19 | - ReturnCode
20 | - name: Resource Record
21 | children:
22 | - ResourceRecord
23 | - ResourceRecordType
24 | - name: Resource Record Types
25 | children:
26 | - AliasRecord
27 | - HostRecord
28 | - PointerRecord
29 | - Record
30 | - ServiceRecord
31 | - TextRecord
32 | - name: Resource Data Types
33 | children:
34 | - createIP(networkBytes:)
35 | - InternetClass
36 | - IP
37 | - IPv4
38 | - IPv6
39 |
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (C) 2016 Bouke Haarsma
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/Sources/DNS/Integer+Data.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension BinaryInteger {
4 | init(bytes: [UInt8]) {
5 | precondition(bytes.count == MemoryLayout.size, "incorrect number of bytes")
6 | self = bytes.reversed().withUnsafeBufferPointer {
7 | $0.baseAddress!.withMemoryRebound(to: Self.self, capacity: 1) {
8 | return $0.pointee
9 | }
10 | }
11 | }
12 |
13 | init(bytes: S) where S.Iterator.Element == UInt8 {
14 | self.init(bytes: Array(bytes))
15 | }
16 |
17 | init(data: Data, position: inout Data.Index) throws {
18 | let start = position
19 | guard data.formIndex(&position, offsetBy: MemoryLayout.size, limitedBy: data.endIndex) else {
20 | throw DecodeError.invalidIntegerSize
21 | }
22 | let bytes = Array(Data(data[start...size).reversed())
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: generic
2 | sudo: required
3 | script: swift test
4 | matrix:
5 | include:
6 | - os: osx
7 | osx_image: xcode10
8 | install:
9 | - gem install cocoapods
10 | script:
11 | - swift test
12 | - pod lib lint --swift-version=4.0
13 | - pod lib lint --swift-version=4.2
14 | - os: osx
15 | osx_image: xcode11
16 | install:
17 | - gem install cocoapods
18 | script:
19 | - swift test
20 | - pod lib lint --swift-version=4.2
21 | - pod lib lint --swift-version=5.0
22 | - name: Swift 4.2
23 | os: linux
24 | dist: trusty
25 | install:
26 | - wget https://swift.org/builds/swift-4.2-release/ubuntu1404/swift-4.2-RELEASE/swift-4.2-RELEASE-ubuntu14.04.tar.gz
27 | - tar xzf swift-4.2-RELEASE-ubuntu14.04.tar.gz
28 | - export PATH=`pwd`/swift-4.2-RELEASE-ubuntu14.04/usr/bin:"${PATH}"
29 | - name: Swift 5.0
30 | os: linux
31 | dist: trusty
32 | install:
33 | - wget https://swift.org/builds/swift-5.0-release/ubuntu1404/swift-5.0-RELEASE/swift-5.0-RELEASE-ubuntu14.04.tar.gz
34 | - tar xzf swift-5.0-RELEASE-ubuntu14.04.tar.gz
35 | - export PATH=`pwd`/swift-5.0-RELEASE-ubuntu14.04/usr/bin:"${PATH}"
36 | - name: Code Quality Checks
37 | os: osx
38 | osx_image: xcode10
39 | install:
40 | - brew update
41 | - brew upgrade swiftlint || true
42 | script:
43 | - swiftlint --strict
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | # Xcode
4 | #
5 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
6 |
7 | ## Build generated
8 | build/
9 | DerivedData/
10 |
11 | ## Various settings
12 | *.pbxuser
13 | !default.pbxuser
14 | *.mode1v3
15 | !default.mode1v3
16 | *.mode2v3
17 | !default.mode2v3
18 | *.perspectivev3
19 | !default.perspectivev3
20 | xcuserdata/
21 |
22 | ## Other
23 | *.moved-aside
24 | *.xccheckout
25 | *.xcscmblueprint
26 |
27 | ## Obj-C/Swift specific
28 | *.hmap
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | .build/
44 |
45 | # CocoaPods
46 | #
47 | # We recommend against adding the Pods directory to your .gitignore. However
48 | # you should judge for yourself, the pros and cons are mentioned at:
49 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
50 | #
51 | # Pods/
52 | #
53 | # Add this line if you want to avoid checking in source code from the Xcode workspace
54 | # *.xcworkspace
55 |
56 | # Carthage
57 | #
58 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
59 | # Carthage/Checkouts
60 |
61 | Carthage/Build
62 |
63 | # Accio dependency management
64 | Dependencies/
65 | .accio/
66 |
67 | # fastlane
68 | #
69 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
70 | # screenshots whenever they are needed.
71 | # For more information about the recommended setup visit:
72 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
73 |
74 | fastlane/report.xml
75 | fastlane/Preview.html
76 | fastlane/screenshots/**/*.png
77 | fastlane/test_output
78 |
79 | # Code Injection
80 | #
81 | # After new code Injection tools there's a generated folder /iOSInjectionProject
82 | # https://github.com/johnno1962/injectionforxcode
83 |
84 | iOSInjectionProject/
85 |
86 |
--------------------------------------------------------------------------------
/Sources/DNS/Data+Extensions.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Data {
4 | init?(hex: String) {
5 | var result = [UInt8]()
6 | var from = hex.startIndex
7 | while from < hex.endIndex {
8 | guard let to = hex.index(from, offsetBy: 2, limitedBy: hex.endIndex) else {
9 | return nil
10 | }
11 | guard let num = UInt8(hex[from.. Int32 {
8 | return Glibc.rand() % Int32(max - 1)
9 | }
10 | #endif
11 |
12 | class FuzzTests: XCTestCase {
13 | static var allTests: [(String, (FuzzTests) -> () throws -> Void)] {
14 | return [
15 | ("testFuzzRandom", testFuzzRandom),
16 | ("testFuzzCorrupted", testFuzzCorrupted),
17 | ("testLinuxTestSuiteIncludesAllTests", testLinuxTestSuiteIncludesAllTests)
18 | ]
19 | }
20 |
21 | func testFuzzRandom() {
22 | return
23 | for _ in 0..<1_000_000 {
24 | let data = randomData()
25 | print(data.hex)
26 | do {
27 | _ = try Message(deserialize: data)
28 | } catch {
29 | }
30 | }
31 | }
32 |
33 | func testFuzzCorrupted() {
34 | return
35 | let service = "_airplay._tcp._local."
36 | let name = "example.\(service)"
37 | let server = "example.local."
38 | let message = Message(type: .response,
39 | questions: [Question(name: service, type: .pointer)],
40 | answers: [PointerRecord(name: service, ttl: 120, destination: name),
41 | ServiceRecord(name: name, ttl: 120, port: 7000, server: server)],
42 | additional: [HostRecord(name: server, ttl: 120, ip: IPv4("10.0.1.2")!),
43 | TextRecord(name: service, ttl: 120, attributes: ["hello": "world"])])
44 | let original = try! message.serialize()
45 | for _ in 0..<101_000_00000 {
46 | var corrupted = original
47 | for _ in 1..<(2 + arc4random_uniform(4)) {
48 | let index = Data.Index(arc4random_uniform(UInt32(corrupted.endIndex)))
49 | switch arc4random_uniform(3) {
50 | case 0:
51 | corrupted[index] = corrupted[index] ^ UInt8(truncatingIfNeeded: arc4random_uniform(256))
52 | case 1:
53 | corrupted.remove(at: index)
54 | case 2:
55 | corrupted.insert(UInt8(truncatingIfNeeded: arc4random_uniform(256)), at: index)
56 | default:
57 | abort()
58 | }
59 | }
60 | print(corrupted.hex)
61 | do {
62 | _ = try Message(deserialize: corrupted)
63 | } catch {
64 | }
65 | }
66 | }
67 |
68 | // from: https://oleb.net/blog/2017/03/keeping-xctest-in-sync/#appendix-code-generation-with-sourcery
69 | func testLinuxTestSuiteIncludesAllTests() {
70 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
71 | let thisClass = type(of: self)
72 | let linuxCount = thisClass.allTests.count
73 | let darwinCount = Int(thisClass
74 | .defaultTestSuite.testCaseCount)
75 | XCTAssertEqual(linuxCount, darwinCount,
76 | "\(darwinCount - linuxCount) tests are missing from allTests")
77 | #endif
78 | }
79 | }
80 |
81 | func randomData() -> Data {
82 | let size = arc4random_uniform(32)
83 | return Data(bytes: (0.. () throws -> Void)] {
6 | return [
7 | ("testIPv4Valid", testIPv4Valid),
8 | ("testIPv4Invalid", testIPv4Invalid),
9 | ("testIPv4Predefined", testIPv4Predefined),
10 | ("testIPv4Bytes", testIPv4Bytes),
11 | ("testIPv4Literal", testIPv4Literal),
12 | ("testIPv4Equality", testIPv4Equality),
13 | ("testIPv6Valid", testIPv6Valid),
14 | ("testIPv6Invalid", testIPv6Invalid),
15 | ("testIPv6Predefined", testIPv6Predefined),
16 | ("testIPv6Bytes", testIPv6Bytes),
17 | ("testLinuxTestSuiteIncludesAllTests", testLinuxTestSuiteIncludesAllTests)
18 | ]
19 | }
20 |
21 | func testIPv4Valid() {
22 | XCTAssertEqual(IPv4("0.0.0.0")?.presentation, "0.0.0.0")
23 | XCTAssertEqual(IPv4("127.0.0.1")?.presentation, "127.0.0.1")
24 | XCTAssertEqual(IPv4("1.2.3.4")?.presentation, "1.2.3.4")
25 | XCTAssertEqual(IPv4("255.255.255.255")?.presentation, "255.255.255.255")
26 | }
27 |
28 | func testIPv4Invalid() {
29 | XCTAssertNil(IPv4("127.0.0.-1"))
30 | XCTAssertNil(IPv4("256.0.0.1"))
31 | }
32 |
33 | func testIPv4Predefined() {
34 | XCTAssertEqual(IPv4(INADDR_ANY), IPv4("0.0.0.0"))
35 | XCTAssertEqual(IPv4(INADDR_BROADCAST), IPv4("255.255.255.255"))
36 | XCTAssertEqual(IPv4(INADDR_LOOPBACK), IPv4("127.0.0.1"))
37 | XCTAssertEqual(IPv4(INADDR_NONE), IPv4("255.255.255.255"))
38 | XCTAssertEqual(IPv4(INADDR_UNSPEC_GROUP), IPv4("224.0.0.0"))
39 | XCTAssertEqual(IPv4(INADDR_ALLHOSTS_GROUP), IPv4("224.0.0.1"))
40 | XCTAssertEqual(IPv4(INADDR_ALLRTRS_GROUP), IPv4("224.0.0.2"))
41 | #if os(OSX)
42 | XCTAssertEqual(IPv4(INADDR_ALLRPTS_GROUP), IPv4("224.0.0.22"))
43 | XCTAssertEqual(IPv4(INADDR_CARP_GROUP), IPv4("224.0.0.18"))
44 | XCTAssertEqual(IPv4(INADDR_PFSYNC_GROUP), IPv4("224.0.0.240"))
45 | XCTAssertEqual(IPv4(INADDR_ALLMDNS_GROUP), IPv4("224.0.0.251"))
46 | #endif
47 | XCTAssertEqual(IPv4(INADDR_MAX_LOCAL_GROUP), IPv4("224.0.0.255"))
48 | }
49 |
50 | func testIPv4Bytes() {
51 | XCTAssertEqual(IPv4(networkBytes: Data(hex: "e00000fb")!), IPv4("224.0.0.251"))
52 | XCTAssertEqual(IPv4("224.0.0.251")!.bytes.hex, "e00000fb")
53 | XCTAssertEqual(IPv4(networkBytes: IPv4("224.0.0.251")!.bytes), IPv4("224.0.0.251"))
54 | }
55 |
56 | func testIPv4Literal() {
57 | XCTAssertEqual(0, IPv4(integerLiteral: INADDR_ANY))
58 | XCTAssertEqual(0xffffffff, IPv4(integerLiteral: INADDR_BROADCAST))
59 | }
60 |
61 | func testIPv4Equality() {
62 | XCTAssertEqual(IPv4("0.0.0.0"), IPv4("0.0.0.0"))
63 | XCTAssertEqual(IPv4("127.0.0.1"), IPv4("127.0.0.1"))
64 | }
65 |
66 | func testIPv6Valid() {
67 | XCTAssertEqual(IPv6("::")?.presentation, "::")
68 | XCTAssertEqual(IPv6("::1")?.presentation, "::1")
69 | XCTAssertEqual(IPv6("ff01::1")?.presentation, "ff01::1")
70 | XCTAssertEqual(IPv6("ff02::1")?.presentation, "ff02::1")
71 | XCTAssertEqual(IPv6("ff02::2")?.presentation, "ff02::2")
72 | }
73 |
74 | func testIPv6Invalid() {
75 | XCTAssertNil(IPv6("127.0.0.1"))
76 | XCTAssertNil(IPv6("abcde::1"))
77 | XCTAssertNil(IPv6("g::1"))
78 | XCTAssertNil(IPv6("a::bb::a"))
79 | }
80 |
81 | func testIPv6Predefined() {
82 | XCTAssertEqual(IPv6(address: in6addr_any), IPv6("::"))
83 | XCTAssertEqual(IPv6(address: in6addr_loopback), IPv6("::1"))
84 | #if os(OSX)
85 | XCTAssertEqual(IPv6(address: in6addr_nodelocal_allnodes), IPv6("ff01::1"))
86 | XCTAssertEqual(IPv6(address: in6addr_linklocal_allnodes), IPv6("ff02::1"))
87 | XCTAssertEqual(IPv6(address: in6addr_linklocal_allrouters), IPv6("ff02::2"))
88 | #endif
89 | }
90 |
91 | func testIPv6Bytes() {
92 | XCTAssertEqual(IPv6(networkBytes: Data(hex: "ff010000000000000000000000000001")!), IPv6("ff01::1"))
93 | XCTAssertEqual(IPv6("ff01::1")!.bytes.hex, "ff010000000000000000000000000001")
94 | XCTAssertEqual(IPv6(networkBytes: IPv6("ff01::1")!.bytes), IPv6("ff01::1"))
95 | }
96 |
97 | // from: https://oleb.net/blog/2017/03/keeping-xctest-in-sync/#appendix-code-generation-with-sourcery
98 | func testLinuxTestSuiteIncludesAllTests() {
99 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
100 | let thisClass = type(of: self)
101 | let linuxCount = thisClass.allTests.count
102 | let darwinCount = Int(thisClass
103 | .defaultTestSuite.testCaseCount)
104 | XCTAssertEqual(linuxCount, darwinCount,
105 | "\(darwinCount - linuxCount) tests are missing from allTests")
106 | #endif
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/Sources/DNS/IP.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // TODO: replace by sockaddr_storage
4 |
5 | /// Undefined for LE
6 | func htonl(_ value: UInt32) -> UInt32 {
7 | return value.byteSwapped
8 | }
9 | let ntohl = htonl
10 |
11 | public protocol IP: CustomDebugStringConvertible {
12 | init?(networkBytes: Data)
13 | init?(_ presentation: String)
14 | var presentation: String { get }
15 |
16 | /// network-byte-order bytes
17 | var bytes: Data { get }
18 | }
19 |
20 | extension IP {
21 | public var debugDescription: String {
22 | return presentation
23 | }
24 | }
25 |
26 | // IPv4 address, wraps `in_addr`. This type is used to convert between
27 | // human-readable presentation format and bytes in both host order and
28 | // network order.
29 | public struct IPv4: IP {
30 | /// IPv4 address in network-byte-order
31 | public let address: in_addr
32 |
33 | public init(address: in_addr) {
34 | self.address = address
35 | }
36 |
37 | public init?(_ presentation: String) {
38 | var address = in_addr()
39 | guard inet_pton(AF_INET, presentation, &address) == 1 else {
40 | return nil
41 | }
42 | self.address = address
43 | }
44 |
45 | /// network order
46 | public init?(networkBytes bytes: Data) {
47 | guard bytes.count == MemoryLayout.size else {
48 | return nil
49 | }
50 | self.address = in_addr(s_addr: UInt32(bytes: bytes.reversed()))
51 | }
52 |
53 | /// host order
54 | public init(_ address: UInt32) {
55 | self.address = in_addr(s_addr: htonl(address))
56 | }
57 |
58 | /// Format this IPv4 address using common `a.b.c.d` notation.
59 | public var presentation: String {
60 | var output = Data(count: Int(INET_ADDRSTRLEN))
61 | var address = self.address
62 | #if swift(>=5.0)
63 | guard let presentationBytes = output.withUnsafeMutableBytes({ (rawBufferPointer: UnsafeMutableRawBufferPointer) -> UnsafePointer? in
64 | let unsafeBufferPointer = rawBufferPointer.bindMemory(to: Int8.self)
65 | guard let base = unsafeBufferPointer.baseAddress else { return nil }
66 | let mutableBase = UnsafeMutablePointer(mutating: base)
67 | return inet_ntop(AF_INET, &address, mutableBase, socklen_t(INET_ADDRSTRLEN))
68 | }) else {
69 | return "Invalid IPv4 address"
70 | }
71 | #else
72 | guard let presentationBytes = output.withUnsafeMutableBytes({
73 | inet_ntop(AF_INET, &address, $0, socklen_t(INET_ADDRSTRLEN))
74 | }) else {
75 | return "Invalid IPv4 address"
76 | }
77 | #endif
78 | return String(cString: presentationBytes)
79 | }
80 |
81 | public var bytes: Data {
82 | return htonl(address.s_addr).bytes
83 | }
84 | }
85 |
86 | extension IPv4: Hashable {
87 | // MARK: Conformance to `Hashable`
88 |
89 | public static func == (lhs: IPv4, rhs: IPv4) -> Bool {
90 | return lhs.address.s_addr == rhs.address.s_addr
91 | }
92 |
93 | #if swift(>=4.2)
94 | public func hash(into hasher: inout Hasher) {
95 | hasher.combine(Int(address.s_addr))
96 | }
97 | #else
98 | public var hashValue: Int {
99 | return Int(address.s_addr)
100 | }
101 | #endif
102 | }
103 |
104 | extension IPv4: ExpressibleByIntegerLiteral {
105 | // MARK: Conformance to `ExpressibleByIntegerLiteral`
106 | public init(integerLiteral value: UInt32) {
107 | self.init(value)
108 | }
109 | }
110 |
111 | public struct IPv6: IP {
112 | public let address: in6_addr
113 |
114 | public init(address: in6_addr) {
115 | self.address = address
116 | }
117 |
118 | public init?(_ presentation: String) {
119 | var address = in6_addr()
120 | guard inet_pton(AF_INET6, presentation, &address) == 1 else {
121 | return nil
122 | }
123 | self.address = address
124 | }
125 |
126 | public init?(networkBytes bytes: Data) {
127 | guard bytes.count == MemoryLayout.size else {
128 | return nil
129 | }
130 | #if swift(>=5.0)
131 | address = bytes.withUnsafeBytes({ (rawBufferPointer: UnsafeRawBufferPointer) -> in6_addr? in
132 | return rawBufferPointer.bindMemory(to: in6_addr.self).baseAddress?.pointee
133 | })!
134 | #else
135 | address = bytes.withUnsafeBytes { (bytesPointer: UnsafePointer) -> in6_addr in
136 | bytesPointer.withMemoryRebound(to: in6_addr.self, capacity: 1) { $0.pointee }
137 | }
138 | #endif
139 | }
140 |
141 | /// Format this IPv6 address using common `a:b:c:d:e:f:g:h` notation.
142 | public var presentation: String {
143 | var output = Data(count: Int(INET6_ADDRSTRLEN))
144 | var address = self.address
145 | #if swift(>=5.0)
146 | guard let presentationBytes = output.withUnsafeMutableBytes({ (rawBufferPointer: UnsafeMutableRawBufferPointer) -> UnsafePointer? in
147 | let unsafeBufferPointer = rawBufferPointer.bindMemory(to: Int8.self)
148 | guard let base = unsafeBufferPointer.baseAddress else { return nil }
149 | let mutableBase = UnsafeMutablePointer(mutating: base)
150 | return inet_ntop(AF_INET6, &address, mutableBase, socklen_t(INET6_ADDRSTRLEN))
151 | }) else {
152 | return "Invalid IPv6 address"
153 | }
154 | #else
155 | guard let presentationBytes = output.withUnsafeMutableBytes({
156 | inet_ntop(AF_INET6, &address, $0, socklen_t(INET6_ADDRSTRLEN))
157 | }) else {
158 | return "Invalid IPv6 address"
159 | }
160 | #endif
161 | return String(cString: presentationBytes)
162 | }
163 |
164 | public var bytes: Data {
165 | #if os(Linux)
166 | return
167 | htonl(address.__in6_u.__u6_addr32.0).bytes +
168 | htonl(address.__in6_u.__u6_addr32.1).bytes +
169 | htonl(address.__in6_u.__u6_addr32.2).bytes +
170 | htonl(address.__in6_u.__u6_addr32.3).bytes
171 | #else
172 | return
173 | htonl(address.__u6_addr.__u6_addr32.0).bytes +
174 | htonl(address.__u6_addr.__u6_addr32.1).bytes +
175 | htonl(address.__u6_addr.__u6_addr32.2).bytes +
176 | htonl(address.__u6_addr.__u6_addr32.3).bytes
177 | #endif
178 | }
179 | }
180 |
181 | extension IPv6: Hashable {
182 | // MARK: Conformance to `Hashable`
183 |
184 | public static func == (lhs: IPv6, rhs: IPv6) -> Bool {
185 | return lhs.presentation == rhs.presentation
186 | }
187 |
188 | #if swift(>=4.2)
189 | public func hash(into hasher: inout Hasher) {
190 | hasher.combine(presentation)
191 | }
192 | #else
193 | public var hashValue: Int {
194 | return presentation.hashValue
195 | }
196 | #endif
197 | }
198 |
--------------------------------------------------------------------------------
/Sources/DNS/Types.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public typealias InternetClass = UInt16
4 |
5 | public extension InternetClass {
6 | static let internet: InternetClass = 1 // IN
7 | static let chaos: InternetClass = 3 // CH
8 | static let hesiod: InternetClass = 4 // HS
9 | static let none: InternetClass = 254
10 | static let any: InternetClass = 255
11 | }
12 |
13 | public struct Question {
14 | public var name: String
15 | public var type: ResourceRecordType
16 | public var unique: Bool
17 | public var internetClass: InternetClass
18 |
19 | public init(name: String, type: ResourceRecordType, unique: Bool = false, internetClass: InternetClass = .internet) {
20 | self.name = name
21 | self.type = type
22 | self.unique = unique
23 | self.internetClass = internetClass
24 | }
25 |
26 | init(deserialize data: Data, position: inout Data.Index) throws {
27 | name = try deserializeName(data, &position)
28 | type = try ResourceRecordType(data: data, position: &position)
29 | unique = data[position] & 0x80 == 0x80
30 | let rawInternetClass = try UInt16(data: data, position: &position)
31 | internetClass = InternetClass(rawInternetClass & 0x7fff)
32 | }
33 | }
34 |
35 | public typealias ResourceRecordType = UInt16
36 |
37 | public extension ResourceRecordType {
38 | static let host: ResourceRecordType = 0x0001
39 | static let nameServer: ResourceRecordType = 0x0002
40 | static let alias: ResourceRecordType = 0x0005
41 | static let startOfAuthority: ResourceRecordType = 0x0006
42 | static let pointer: ResourceRecordType = 0x000c
43 | static let mailExchange: ResourceRecordType = 0x000f
44 | static let text: ResourceRecordType = 0x0010
45 | static let host6: ResourceRecordType = 0x001c
46 | static let service: ResourceRecordType = 0x0021
47 | static let incrementalZoneTransfer: ResourceRecordType = 0x00fb
48 | static let standardZoneTransfer: ResourceRecordType = 0x00fc
49 | static let all: ResourceRecordType = 0x00ff // All cached records
50 | }
51 |
52 | extension ResourceRecordType: CustomDebugStringConvertible {
53 | public var debugDescription: String {
54 | switch self {
55 | case .host: return "A"
56 | case .nameServer: return "NS"
57 | case .alias: return "CNAME"
58 | case .startOfAuthority: return "SOA"
59 | case .pointer: return "PTR"
60 | case .mailExchange: return "MX"
61 | case .text: return "TXT"
62 | case .host6: return "AAAA"
63 | case .service: return "SRV"
64 | case .incrementalZoneTransfer: return "IXFR"
65 | case .standardZoneTransfer: return "AXFR"
66 | case .all: return "*"
67 | default: return "Unknown"
68 | }
69 | }
70 | }
71 |
72 | public protocol ResourceRecord {
73 | var name: String { get set }
74 | var unique: Bool { get set }
75 | var internetClass: InternetClass { get set }
76 | var ttl: UInt32 { get set }
77 |
78 | func serialize(onto: inout Data, labels: inout Labels) throws
79 | }
80 |
81 | public struct Record {
82 | public var name: String
83 | public var type: UInt16
84 | public var internetClass: InternetClass
85 | public var unique: Bool
86 | public var ttl: UInt32
87 | public var data: Data
88 |
89 | public init(name: String, type: UInt16, internetClass: InternetClass, unique: Bool, ttl: UInt32, data: Data) {
90 | self.name = name
91 | self.type = type
92 | self.internetClass = internetClass
93 | self.unique = unique
94 | self.ttl = ttl
95 | self.data = data
96 | }
97 | }
98 |
99 | public struct HostRecord {
100 | public var name: String
101 | public var unique: Bool
102 | public var internetClass: InternetClass
103 | public var ttl: UInt32
104 | public var ip: IPType
105 |
106 | public init(name: String, unique: Bool = false, internetClass: InternetClass = .internet, ttl: UInt32, ip: IPType) {
107 | self.name = name
108 | self.unique = unique
109 | self.internetClass = internetClass
110 | self.ttl = ttl
111 | self.ip = ip
112 | }
113 | }
114 |
115 | extension HostRecord: Hashable {
116 | #if swift(>=4.2)
117 | public func hash(into hasher: inout Hasher) {
118 | hasher.combine(name)
119 | }
120 | #else
121 | public var hashValue: Int {
122 | return name.hashValue
123 | }
124 | #endif
125 |
126 | public static func == (lhs: HostRecord, rhs: HostRecord) -> Bool {
127 | return lhs.name == rhs.name
128 | // TODO: check equality of IP addresses
129 | }
130 | }
131 |
132 | public struct ServiceRecord {
133 | public var name: String
134 | public var unique: Bool
135 | public var internetClass: InternetClass
136 | public var ttl: UInt32
137 | public var priority: UInt16
138 | public var weight: UInt16
139 | public var port: UInt16
140 | public var server: String
141 |
142 | public init(name: String, unique: Bool = false, internetClass: InternetClass = .internet, ttl: UInt32, priority: UInt16 = 0, weight: UInt16 = 0, port: UInt16, server: String) {
143 | self.name = name
144 | self.unique = unique
145 | self.internetClass = internetClass
146 | self.ttl = ttl
147 | self.priority = priority
148 | self.weight = weight
149 | self.port = port
150 | self.server = server
151 | }
152 | }
153 |
154 | extension ServiceRecord: Hashable {
155 | #if swift(>=4.2)
156 | public func hash(into hasher: inout Hasher) {
157 | hasher.combine(name)
158 | }
159 | #else
160 | public var hashValue: Int {
161 | return name.hashValue
162 | }
163 | #endif
164 |
165 | public static func == (lhs: ServiceRecord, rhs: ServiceRecord) -> Bool {
166 | return lhs.name == rhs.name
167 | }
168 | }
169 |
170 | public struct TextRecord {
171 | public var name: String
172 | public var unique: Bool
173 | public var internetClass: InternetClass
174 | public var ttl: UInt32
175 | public var attributes: [String: String]
176 | public var values: [String]
177 |
178 | public init(name: String, unique: Bool = false, internetClass: InternetClass = .internet, ttl: UInt32, attributes: [String: String]) {
179 | self.name = name
180 | self.unique = unique
181 | self.internetClass = internetClass
182 | self.ttl = ttl
183 | self.attributes = attributes
184 | self.values = []
185 | }
186 | }
187 |
188 | public struct PointerRecord {
189 | public var name: String
190 | public var unique: Bool
191 | public var internetClass: InternetClass
192 | public var ttl: UInt32
193 | public var destination: String
194 |
195 | public init(name: String, unique: Bool = false, internetClass: InternetClass = .internet, ttl: UInt32, destination: String) {
196 | self.name = name
197 | self.unique = unique
198 | self.internetClass = internetClass
199 | self.ttl = ttl
200 | self.destination = destination
201 | }
202 | }
203 |
204 | extension PointerRecord: Hashable {
205 | #if swift(>=4.2)
206 | public func hash(into hasher: inout Hasher) {
207 | hasher.combine(destination)
208 | }
209 | #else
210 | public var hashValue: Int {
211 | return destination.hashValue
212 | }
213 | #endif
214 |
215 | public static func == (lhs: PointerRecord, rhs: PointerRecord) -> Bool {
216 | return lhs.name == rhs.name && lhs.destination == rhs.destination
217 | }
218 | }
219 |
220 | public struct AliasRecord {
221 | public var name: String
222 | public var unique: Bool
223 | public var internetClass: InternetClass
224 | public var ttl: UInt32
225 | public var canonicalName: String
226 | }
227 |
228 | // https://tools.ietf.org/html/rfc1035#section-3.3.13
229 | public struct StartOfAuthorityRecord {
230 | public var name: String
231 | public var unique: Bool
232 | public var internetClass: InternetClass
233 | public var ttl: UInt32
234 | public var mname: String
235 | public var rname: String
236 | public var serial: UInt32
237 | public var refresh: Int32
238 | public var retry: Int32
239 | public var expire: Int32
240 | public var minimum: UInt32
241 |
242 | public init(name: String, unique: Bool = false, internetClass: InternetClass = .internet, ttl: UInt32, mname: String, rname: String, serial: UInt32, refresh: Int32, retry: Int32, expire: Int32, minimum: UInt32) {
243 | self.name = name
244 | self.unique = unique
245 | self.internetClass = internetClass
246 | self.ttl = ttl
247 | self.mname = mname
248 | self.rname = rname
249 | self.serial = serial
250 | self.refresh = refresh
251 | self.retry = retry
252 | self.expire = expire
253 | self.minimum = minimum
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/Tests/DNSTests/DNSTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import DNS
3 |
4 | class DNSTests: XCTestCase {
5 | static var allTests: [(String, (DNSTests) -> () throws -> Void)] {
6 | return [
7 | ("testReadMe", testReadMe),
8 | ("testPointerRecord", testPointerRecord),
9 | ("testMessage1", testMessage1),
10 | ("testMessage2", testMessage2),
11 | ("testMessage3", testMessage3),
12 | ("testMessage4", testMessage4),
13 | ("testMessage5", testMessage5),
14 | ("testDeserializeName", testDeserializeName),
15 | ("testDeserializeCorruptedName", testDeserializeCorruptedName),
16 | ("testSerializeName", testSerializeName),
17 | ("testSerializeNameCondensed", testSerializeNameCondensed),
18 | ("testLinuxTestSuiteIncludesAllTests", testLinuxTestSuiteIncludesAllTests),
19 | ("testDeserializeFuzzMessages", testDeserializeFuzzMessages)
20 | ]
21 | }
22 |
23 | func testReadMe() throws {
24 | // If the code below doesn't work, also update the README.md to
25 | // demonstrate the changes in the API.
26 | let request = Message(
27 | type: .query,
28 | questions: [Question(name: "apple.com.", type: .pointer)]
29 | )
30 | let requestData = try request.serialize()
31 |
32 | // Not shown here: send to DNS server over UDP, receive reply.
33 | let responseData = requestData
34 |
35 | // Decoding a message
36 | let response = try Message(deserialize: responseData)
37 | print(response.answers.first ?? "No answers")
38 | }
39 |
40 | func testPointerRecord() {
41 | var labels0 = Labels()
42 | var serialized0 = Data()
43 | let pointer0 = PointerRecord(name: "_hap._tcp.local.", ttl: 120, destination: "Swift._hap._tcp.local.")
44 | try! pointer0.serialize(onto: &serialized0, labels: &labels0)
45 |
46 | var labels1 = Labels()
47 | var serialized1 = Data()
48 | let pointer1 = PointerRecord(name: "_hap._tcp.local.", ttl: 120, destination: "Swift._hap._tcp.local.")
49 | try! pointer1.serialize(onto: &serialized1, labels: &labels1)
50 |
51 | XCTAssertEqual(serialized0.hex, serialized1.hex)
52 |
53 | var position = serialized0.startIndex
54 | let rcf = try! deserializeRecordCommonFields(serialized0, &position)
55 | let pointer0copy = try! PointerRecord(deserialize: serialized0, position: &position, common: rcf)
56 |
57 | XCTAssertEqual(pointer0, pointer0copy)
58 | }
59 |
60 | func testMessage1() {
61 | let message0 = Message(id: 4529, type: .response, operationCode: .query, authoritativeAnswer: false, truncation: false, recursionDesired: false, recursionAvailable: false, returnCode: .nonExistentDomain)
62 | let serialized0 = try! message0.serialize()
63 | XCTAssertEqual(serialized0.hex, "11b180030000000000000000")
64 | let message1 = try! Message(deserialize: serialized0)
65 | let serialized1 = try! message1.serialize()
66 | XCTAssertEqual(serialized0.hex, serialized1.hex)
67 | }
68 |
69 | func testMessage2() {
70 | let message0 = Message(id: 18765, type: .response, operationCode: .query, authoritativeAnswer: true, truncation: true, recursionDesired: true, recursionAvailable: true, returnCode: .noError)
71 | let serialized0 = try! message0.serialize()
72 | XCTAssertEqual(serialized0.hex, "494d87800000000000000000")
73 | let message1 = try! Message(deserialize: serialized0)
74 | let serialized1 = try! message1.serialize()
75 | XCTAssertEqual(serialized0.hex, serialized1.hex)
76 | }
77 |
78 | func testMessage3() {
79 | let message0 = Message(type: .query,
80 | questions: [Question(name: "_airplay._tcp._local", type: .pointer)])
81 | let serialized0 = try! message0.serialize()
82 | let message1 = try! Message(deserialize: serialized0)
83 | let serialized1 = try! message1.serialize()
84 | XCTAssertEqual(serialized0.hex, serialized1.hex)
85 | }
86 |
87 | func testMessage4() {
88 | let service = "_airplay._tcp._local."
89 | let name = "example.\(service)"
90 | let message0 = Message(type: .response,
91 | questions: [Question(name: service, type: .pointer)],
92 | answers: [PointerRecord(name: service, ttl: 120, destination: name)])
93 | let serialized0 = try! message0.serialize()
94 | let message1 = try! Message(deserialize: serialized0)
95 | let serialized1 = try! message1.serialize()
96 | XCTAssertEqual(serialized0.hex, serialized1.hex)
97 | }
98 |
99 | func testMessage5() {
100 | let service = "_airplay._tcp._local."
101 | let name = "example.\(service)"
102 | let server = "example.local."
103 | let message0 = Message(type: .response,
104 | questions: [Question(name: service, type: .pointer)],
105 | answers: [PointerRecord(name: service, ttl: 120, destination: name),
106 | ServiceRecord(name: name, ttl: 120, port: 7000, server: server)],
107 | additional: [HostRecord(name: server, ttl: 120, ip: IPv4("10.0.1.2")!),
108 | TextRecord(name: service, ttl: 120, attributes: ["hello": "world"])])
109 | let serialized0 = try! message0.serialize()
110 | let message1 = try! Message(deserialize: serialized0)
111 | let serialized1 = try! message1.serialize()
112 | XCTAssertEqual(serialized0.hex, serialized1.hex)
113 | }
114 |
115 | func testDeserializeName() {
116 | // This is part of a record. The name can be found by following two pointers indicated by 0xc000 (mask).
117 | let data = Data(hex: "000084000000000200000006075a6974686f656b0c5f6465766963652d696e666f045f746370056c6f63616c000010000100001194000d0c6d6f64656c3d4a3432644150085f616972706c6179c021000c000100001194000a075a6974686f656bc044")!
118 | var position = 89
119 | let name = try! deserializeName(data, &position)
120 | XCTAssertEqual(name, "Zithoek._airplay._tcp.local.")
121 | XCTAssertEqual(position, 99)
122 | }
123 |
124 | func testDeserializeFuzzMessages() {
125 | let data = Data(hex: "000084000001000200000002085f61692e706c6179045f746370065f6c6f63616c00000c0001c00c000c0001000000780013076578616d706c65085f616972706c6179c015c03200210001000000780015000000001b58076578616d706c65056c6f63616c00c057000100010000007800040a000102c00c000c000100000078000c0b68656c6c6f3d776f726c64")!
126 | _ = try? Message(deserialize: data) // should either deserialize or throw, but not sigfault
127 | }
128 |
129 | func testDeserializeCorruptedName() {
130 | let data = Data(hex: "000084000001000200009302085f616972706c6179045f746370065f6c6f63616c00000c0001c00c000c0001000000480013076578616d706c65085f616972706c6179c03ac03200210001000000780015000000001b5807656a616d706c65056c6f63616c00c057000100010000007800040a000102c00c0010000143000078000c0b68656c6c6f3d7767726c64")!
131 | if (try? Message(deserialize: data)) != nil {
132 | return XCTFail("Should not have deserialized message")
133 | }
134 | }
135 |
136 | func testSerializeName() {
137 | let message = Message(type: .response,
138 | questions: [Question(name: "🤩", type: .pointer)])
139 | let size0 = try! message.serialize().count
140 | XCTAssertEqual(size0, 22)
141 | }
142 |
143 | func testSerializeNameCondensed() {
144 | var message = Message(type: .response,
145 | questions: [Question(name: "abc.def.ghi.jk.local.", type: .pointer)])
146 | let size0 = try! message.serialize().count
147 | XCTAssertEqual(size0, 38)
148 |
149 | message.questions.append(Question(name: "abc.def.ghi.jk.local.", type: .pointer))
150 | let size1 = try! message.serialize().count
151 | XCTAssertEqual(size1, size0 + 6)
152 |
153 | message.questions.append(Question(name: "def.ghi.jk.local.", type: .pointer))
154 | let size2 = try! message.serialize().count
155 | XCTAssertEqual(size2, size1 + 10)
156 |
157 | message.questions.append(Question(name: "xyz.def.ghi.jk.local.", type: .pointer))
158 | let size3 = try! message.serialize().count
159 | XCTAssertEqual(size3, size2 + 10)
160 | }
161 |
162 | // from: https://oleb.net/blog/2017/03/keeping-xctest-in-sync/#appendix-code-generation-with-sourcery
163 | func testLinuxTestSuiteIncludesAllTests() {
164 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
165 | let thisClass = type(of: self)
166 | let linuxCount = thisClass.allTests.count
167 | let darwinCount = Int(thisClass
168 | .defaultTestSuite.testCaseCount)
169 | XCTAssertEqual(linuxCount, darwinCount,
170 | "\(darwinCount - linuxCount) tests are missing from allTests")
171 | #endif
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/Sources/DNS/Message.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A one bit field that specifies whether this message is a
4 | /// query (0), or a response (1).
5 | ///
6 | /// - query: query to a name server
7 | /// - response: response from a name server
8 | public enum MessageType {
9 | case query
10 | case response
11 | }
12 |
13 | /// All communications inside of the domain protocol are carried in a single
14 | /// format called a message. The top level format of message is divided
15 | /// into 5 sections (some of which are empty in certain cases) shown below:
16 | ///
17 | /// +---------------------+
18 | /// | Header |
19 | /// +---------------------+
20 | /// | Question | the question for the name server
21 | /// +---------------------+
22 | /// | Answer | RRs answering the question
23 | /// +---------------------+
24 | /// | Authority | RRs pointing toward an authority
25 | /// +---------------------+
26 | /// | Additional | RRs holding additional information
27 | /// +---------------------+
28 | ///
29 | /// The header section is always present. The header includes fields that
30 | /// specify which of the remaining sections are present, and also specify
31 | /// whether the message is a query or a response, a standard query or some
32 | /// other opcode, etc.
33 | ///
34 | /// The names of the sections after the header are derived from their use in
35 | /// standard queries. The question section contains fields that describe a
36 | /// question to a name server. These fields are a query type (QTYPE), a
37 | /// query class (QCLASS), and a query domain name (QNAME). The last three
38 | /// sections have the same format: a possibly empty list of concatenated
39 | /// resource records (RRs). The answer section contains RRs that answer the
40 | /// question; the authority section contains RRs that point toward an
41 | /// authoritative name server; the additional records section contains RRs
42 | /// which relate to the query, but are not strictly answers for the
43 | /// question.
44 | public struct Message {
45 |
46 | // MARK: Message header section
47 |
48 | /// A 16 bit identifier assigned by the program that
49 | /// generates any kind of query. This identifier is copied
50 | /// the corresponding reply and can be used by the requester
51 | /// to match up replies to outstanding queries.
52 | public var id: UInt16
53 |
54 | /// A one bit field that specifies whether this message is a
55 | /// query (0), or a response (1).
56 | public var type: MessageType
57 |
58 | /// A four bit field that specifies kind of query in this
59 | /// message. This value is set by the originator of a query
60 | /// and copied into the response. The values are:
61 | public var operationCode: OperationCode
62 |
63 | /// Authoritative Answer - this bit is valid in responses,
64 | /// and specifies that the responding name server is an
65 | /// authority for the domain name in question section.
66 | ///
67 | /// Note that the contents of the answer section may have
68 | /// multiple owner names because of aliases. The AA bit
69 | /// corresponds to the name which matches the query name, or
70 | /// the first owner name in the answer section.
71 | public var authoritativeAnswer: Bool
72 |
73 | /// TrunCation - specifies that this message was truncated
74 | /// due to length greater than that permitted on the
75 | /// transmission channel.
76 | public var truncation: Bool
77 |
78 | /// Recursion Desired - this bit may be set in a query and
79 | /// is copied into the response. If RD is set, it directs
80 | /// the name server to pursue the query recursively.
81 | /// Recursive query support is optional.
82 | public var recursionDesired: Bool
83 |
84 | /// Recursion Available - this be is set or cleared in a
85 | /// response, and denotes whether recursive query support is
86 | /// available in the name server.
87 | public var recursionAvailable: Bool
88 |
89 | /// Response code - this 4 bit field is set as part of
90 | /// responses.
91 | public var returnCode: ReturnCode
92 |
93 | // MARK: Other sections
94 |
95 | /// Question section.
96 | public var questions: [Question]
97 |
98 | /// Answer section.
99 | public var answers: [ResourceRecord]
100 |
101 | /// Authority section.
102 | public var authorities: [ResourceRecord]
103 |
104 | /// Additional records section.
105 | public var additional: [ResourceRecord]
106 |
107 | // MARK: Initializing
108 |
109 | /// Create a `Message` instance.
110 | ///
111 | /// To create a query:
112 | ///
113 | /// ```swift
114 | /// let query = Message(
115 | /// id: UInt16(extendingOrTruncating: arc4random()),
116 | /// type: .query,
117 | /// questions: [
118 | /// Question(name: "apple.com", type: .host)
119 | /// ])
120 | /// ```
121 | ///
122 | /// To create a response for this query:
123 | ///
124 | /// ```swift
125 | /// let response = Message(
126 | /// id: query.id,
127 | /// type: .response,
128 | /// returnCode: .noError,
129 | /// questions: query.questions,
130 | /// answers: [
131 | /// HostRecord(name: "apple.com", ttl: 3600, ip: IPv4("17.172.224.47")!)
132 | /// ])
133 | /// ```
134 | ///
135 | /// - Parameters:
136 | /// - id:
137 | /// - type:
138 | /// - operationCode:
139 | /// - authoritativeAnswer:
140 | /// - truncation:
141 | /// - recursionDesired:
142 | /// - recursionAvailable:
143 | /// - returnCode:
144 | /// - questions:
145 | /// - answers:
146 | /// - authorities:
147 | /// - additional:
148 | public init(
149 | id: UInt16 = 0,
150 | type: MessageType,
151 | operationCode: OperationCode = .query,
152 | authoritativeAnswer: Bool = true,
153 | truncation: Bool = false,
154 | recursionDesired: Bool = false,
155 | recursionAvailable: Bool = false,
156 | returnCode: ReturnCode = .noError,
157 | questions: [Question] = [],
158 | answers: [ResourceRecord] = [],
159 | authorities: [ResourceRecord] = [],
160 | additional: [ResourceRecord] = []
161 | ) {
162 | self.id = id
163 | self.type = type
164 | self.operationCode = operationCode
165 | self.authoritativeAnswer = authoritativeAnswer
166 | self.truncation = truncation
167 | self.recursionDesired = recursionDesired
168 | self.recursionAvailable = recursionAvailable
169 | self.returnCode = returnCode
170 |
171 | self.questions = questions
172 | self.answers = answers
173 | self.authorities = authorities
174 | self.additional = additional
175 | }
176 | }
177 |
178 | extension Message: CustomDebugStringConvertible {
179 | /// A textual representation of this instance, suitable for debugging.
180 | public var debugDescription: String {
181 | switch type {
182 | case .query: return "DNS Request(id: \(id), authoritativeAnswer: \(authoritativeAnswer), truncation: \(truncation), recursionDesired: \(recursionDesired), recursionAvailable: \(recursionAvailable), questions: \(questions), answers: \(answers), authorities: \(authorities), additional: \(additional))"
183 | case .response: return "DNS Response(id: \(id), returnCode: \(returnCode), authoritativeAnswer: \(authoritativeAnswer), truncation: \(truncation), recursionDesired: \(recursionDesired), recursionAvailable: \(recursionAvailable), questions: \(questions), answers: \(answers), authorities: \(authorities), additional: \(additional))"
184 | }
185 | }
186 | }
187 |
188 | /// A four bit field that specifies kind of query in this
189 | /// message. This value is set by the originator of a query
190 | /// and copied into the response.
191 | ///
192 | /// - query: a standard query (QUERY)
193 | /// - inverseQuery: an inverse query (IQUERY)
194 | /// - statusRequest: a server status request (STATUS)
195 | /// - notify: (NOTIFY)
196 | /// - update: (UPDATE)
197 | public typealias OperationCode = UInt8
198 |
199 | public extension OperationCode { // 4 bits: 0-15
200 | static let query: OperationCode = 0 // QUERY
201 | static let inverseQuery: OperationCode = 1 // IQUERY
202 | static let statusRequest: OperationCode = 2 // STATUS
203 | static let notify: OperationCode = 4 // NOTIFY
204 | static let update: OperationCode = 5 // UPDATE
205 | }
206 |
207 | /// Response code - this 4 bit field is set as part of responses.
208 | ///
209 | public typealias ReturnCode = UInt8
210 |
211 | public extension ReturnCode { // 4 bits: 0-15
212 | // MARK: Basic DNS (RFC1035)
213 |
214 | /// No error condition.
215 | static let noError: ReturnCode = 0 // NOERROR
216 |
217 | /// Format error - he name server was unable to interpret the query.
218 | static let formatError: ReturnCode = 1 // FORMERR
219 |
220 | /// Server failure - The name server was unable to process this query due
221 | /// to a problem with the name server.
222 | static let serverFailure: ReturnCode = 2 // SERVFAIL
223 |
224 | /// Name Error - Meaningful only for responses from an authoritative name
225 | /// server, this code signifies that the domain name referenced in the
226 | /// query does not exist.
227 | static let nonExistentDomain: ReturnCode = 3 // NXDOMAIN
228 |
229 | /// Not Implemented - The name server does not support the requested kind
230 | /// of query.
231 | static let notImplemented: ReturnCode = 4 // NOTIMPL
232 |
233 | /// Refused - The name server refuses to perform the specified operation
234 | /// for policy reasons. For example, a name server may not wish to provide
235 | /// the information to the particular requester, or a name server may not
236 | /// wish to perform a particular operation (e.g., zone transfer) for
237 | /// particular data.
238 | static let queryRefused: ReturnCode = 5 // REFUSED
239 |
240 | // MARK: DNS UPDATE (RFC2136)
241 |
242 | /// Some name that ought not to exist, does exist.
243 | static let nameExistsWhenItShouldNot: ReturnCode = 6 // YXDOMAIN
244 |
245 | /// Some RRset that ought not to exist, does exist.
246 | static let rrSetExistsWhenItShouldNot: ReturnCode = 7 // YXRRSET
247 |
248 | /// Some RRset that ought to exist, does not exist.
249 | static let rrSetThatShouldExistDoestNot: ReturnCode = 8 // NXRRSET
250 |
251 | /// The server is not authoritative for the zone named in the Zone Section.
252 | static let serverNotAuthoritativeForZone: ReturnCode = 9 // NOTAUTH
253 |
254 | /// A name used in the Prerequisite or Update Section is not within the
255 | /// zone denoted by the Zone Section.
256 | static let nameNotContainedInZone: ReturnCode = 10 // NOTZONE
257 | }
258 |
--------------------------------------------------------------------------------
/Sources/DNS/Bytes.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | enum EncodeError: Swift.Error {
4 | }
5 |
6 | enum DecodeError: Swift.Error {
7 | case invalidMessageSize
8 | case invalidLabelSize
9 | case invalidLabelOffset
10 | case unicodeDecodingError
11 | case invalidIntegerSize
12 | case invalidIPAddress
13 | case invalidDataSize
14 | }
15 |
16 | func deserializeName(_ data: Data, _ position: inout Data.Index) throws -> String {
17 | var components = [String]()
18 | let startPosition = position
19 | while true {
20 | guard position < data.endIndex else {
21 | throw DecodeError.invalidLabelOffset
22 | }
23 | let step = data[position]
24 | if step & 0xc0 == 0xc0 {
25 | let offset = Int(try UInt16(data: data, position: &position) ^ 0xc000)
26 | guard var pointer = data.index(data.startIndex, offsetBy: offset, limitedBy: data.endIndex) else {
27 | throw DecodeError.invalidLabelOffset
28 | }
29 | // Prevent cyclic references
30 | // Its safe to assume the pointer is to an earlier label
31 | // See https://www.ietf.org/rfc/rfc1035.txt 4.1.4
32 | guard pointer < startPosition else {
33 | throw DecodeError.invalidLabelOffset
34 | }
35 | components += try deserializeName(data, &pointer).components(separatedBy: ".").filter({ $0 != "" })
36 | break
37 | }
38 |
39 | guard let start = data.index(position, offsetBy: 1, limitedBy: data.endIndex),
40 | let end = data.index(start, offsetBy: Int(step), limitedBy: data.endIndex) else {
41 | throw DecodeError.invalidLabelSize
42 | }
43 | if step > 0 {
44 | guard let component = String(bytes: Data(data[start.. 0 {
77 | try serializeName(components.joined(separator: "."), onto: &buffer, labels: &labels)
78 | } else {
79 | buffer.append(0)
80 | }
81 | }
82 |
83 | typealias RecordCommonFields = (name: String, type: UInt16, unique: Bool, internetClass: InternetClass, ttl: UInt32)
84 |
85 | func deserializeRecordCommonFields(_ data: Data, _ position: inout Data.Index) throws -> RecordCommonFields {
86 | let name = try deserializeName(data, &position)
87 | let type = try UInt16(data: data, position: &position)
88 | let rrClass = try UInt16(data: data, position: &position)
89 | let internetClass = InternetClass(rrClass & 0x7fff)
90 | let ttl = try UInt32(data: data, position: &position)
91 | return (name, type, rrClass & 0x8000 == 0x8000, internetClass, ttl)
92 | }
93 |
94 | func serializeRecordCommonFields(_ common: RecordCommonFields, onto buffer: inout Data, labels: inout Labels) throws {
95 | try serializeName(common.name, onto: &buffer, labels: &labels)
96 | buffer.append(common.type.bytes)
97 | buffer.append((common.internetClass | (common.unique ? 0x8000 : 0)).bytes)
98 | buffer.append(common.ttl.bytes)
99 | }
100 |
101 | func deserializeRecord(_ data: Data, _ position: inout Data.Index) throws -> ResourceRecord {
102 | let common = try deserializeRecordCommonFields(data, &position)
103 | switch ResourceRecordType(common.type) {
104 | case .host: return try HostRecord(deserialize: data, position: &position, common: common)
105 | case .host6: return try HostRecord(deserialize: data, position: &position, common: common)
106 | case .service: return try ServiceRecord(deserialize: data, position: &position, common: common)
107 | case .text: return try TextRecord(deserialize: data, position: &position, common: common)
108 | case .pointer: return try PointerRecord(deserialize: data, position: &position, common: common)
109 | case .alias: return try AliasRecord(deserialize: data, position: &position, common: common)
110 | case .startOfAuthority: return try StartOfAuthorityRecord(deserialize: data, position: &position, common: common)
111 | default: return try Record(deserialize: data, position: &position, common: common)
112 | }
113 | }
114 |
115 | extension Message {
116 |
117 | // MARK: Serialization
118 |
119 | /// Serialize a `Message` for sending over TCP.
120 | ///
121 | /// The DNS TCP format prepends the message size before the actual
122 | /// message.
123 | ///
124 | /// - Returns: `Data` to be send over TCP.
125 | /// - Throws:
126 | public func serializeTCP() throws -> Data {
127 | let data = try serialize()
128 | precondition(data.count <= UInt16.max)
129 | return UInt16(truncatingIfNeeded: data.count).bytes + data
130 | }
131 |
132 | /// Serialize a `Message` for sending over UDP.
133 | ///
134 | /// - Returns: `Data` to be send over UDP.
135 | /// - Throws:
136 | public func serialize() throws -> Data {
137 | var bytes = Data()
138 | var labels = Labels()
139 | let qr: UInt16 = type == .response ? 1 : 0
140 | var flags: UInt16 = qr << 15
141 | flags |= UInt16(operationCode) << 11
142 | flags |= (authoritativeAnswer ? 1 : 0) << 10
143 | flags |= (truncation ? 1 : 0) << 9
144 | flags |= (recursionDesired ? 1 : 0) << 8
145 | flags |= (recursionAvailable ? 1 : 0) << 7
146 | flags |= UInt16(returnCode)
147 |
148 | // header
149 | bytes += id.bytes
150 | bytes += flags.bytes
151 | bytes += UInt16(questions.count).bytes
152 | bytes += UInt16(answers.count).bytes
153 | bytes += UInt16(authorities.count).bytes
154 | bytes += UInt16(additional.count).bytes
155 |
156 | // questions
157 | for question in questions {
158 | try serializeName(question.name, onto: &bytes, labels: &labels)
159 | bytes += question.type.bytes
160 | bytes += question.internetClass.bytes
161 | }
162 |
163 | for answer in answers {
164 | try answer.serialize(onto: &bytes, labels: &labels)
165 | }
166 | for authority in authorities {
167 | try authority.serialize(onto: &bytes, labels: &labels)
168 | }
169 | for additional in additional {
170 | try additional.serialize(onto: &bytes, labels: &labels)
171 | }
172 |
173 | return bytes
174 | }
175 |
176 | /// Deserializes a `Message` from a TCP stream.
177 | ///
178 | /// The DNS TCP format prepends the message size before the actual
179 | /// message.
180 | ///
181 | /// - Parameter bytes: the received bytes.
182 | /// - Throws:
183 | public init(deserializeTCP bytes: Data) throws {
184 | precondition(bytes.count >= 2)
185 | var position = bytes.startIndex
186 | let size = try Int(UInt16(data: bytes, position: &position))
187 |
188 | // strip size bytes (tcp only?)
189 | let bytes = Data(bytes[2..<2+size]) // copy? :(
190 | precondition(bytes.count == Int(size))
191 |
192 | try self.init(deserialize: bytes)
193 | }
194 |
195 | /// Deserializes a `Message` from a UDP stream.
196 | ///
197 | /// - Parameter bytes: the bytes to deserialize.
198 | /// - Throws:
199 | public init(deserialize bytes: Data) throws {
200 | guard bytes.count >= 12 else {
201 | throw DecodeError.invalidMessageSize
202 | }
203 | var position = bytes.startIndex
204 | id = try UInt16(data: bytes, position: &position)
205 | let flags = try UInt16(data: bytes, position: &position)
206 |
207 | type = flags >> 15 & 1 == 1 ? .response : .query
208 | operationCode = OperationCode(flags >> 11 & 0x7)
209 | authoritativeAnswer = flags >> 10 & 0x1 == 0x1
210 | truncation = flags >> 9 & 0x1 == 0x1
211 | recursionDesired = flags >> 8 & 0x1 == 0x1
212 | recursionAvailable = flags >> 7 & 0x1 == 0x1
213 | returnCode = ReturnCode(flags & 0x7)
214 |
215 | let numQuestions = try UInt16(data: bytes, position: &position)
216 | let numAnswers = try UInt16(data: bytes, position: &position)
217 | let numAuthorities = try UInt16(data: bytes, position: &position)
218 | let numAdditional = try UInt16(data: bytes, position: &position)
219 |
220 | questions = try (0.. 0 else {
316 | break
317 | }
318 | guard data.formIndex(&position, offsetBy: size, limitedBy: data.endIndex) else {
319 | throw DecodeError.invalidLabelSize
320 | }
321 | guard let label = String(bytes: Data(data[labelStart..