├── 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 | [![Build Status](https://travis-ci.org/mcfedr/DNS.svg?branch=master)](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..