From 3c4f0351b30828ddaac6ccea9beef7b34ebf1b95 Mon Sep 17 00:00:00 2001 From: Craig Munro Date: Wed, 23 Aug 2023 17:34:26 -0400 Subject: [PATCH 1/2] Updated ByteBuffer.readRecord() to support case .ptr Note - Original .ptr comment stated PTR was obsolete, but that must have been mistaken with Obsoleting IQUERY [https://www.rfc-editor.org/rfc/rfc3425]. Updated enum Record to support case ptr(ResourceRecord) Added PTR.swift for PTRRecord to support inverse addressing queries for IPv4 and IPv6. Added inverse addressing tests for IPv4 and IPv6 that use PTRRecord. --- Sources/DNSClient/Helpers.swift | 8 +- Sources/DNSClient/Messages/Message.swift | 7 +- Sources/DNSClient/PTR.swift | 121 +++++++++++++++++++ Tests/DNSClientTests/DNSUDPClientTests.swift | 45 +++++++ 4 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 Sources/DNSClient/PTR.swift diff --git a/Sources/DNSClient/Helpers.swift b/Sources/DNSClient/Helpers.swift index e7e0ba9..aa4af88 100644 --- a/Sources/DNSClient/Helpers.swift +++ b/Sources/DNSClient/Helpers.swift @@ -175,7 +175,13 @@ extension ByteBuffer { } return .cname(cname) - default: + case .ptr: + guard let ptr = make(PTRRecord.self) else { + return nil + } + + return .ptr(ptr) + default: break } diff --git a/Sources/DNSClient/Messages/Message.swift b/Sources/DNSClient/Messages/Message.swift index da5f7a4..40dca2b 100644 --- a/Sources/DNSClient/Messages/Message.swift +++ b/Sources/DNSClient/Messages/Message.swift @@ -45,6 +45,8 @@ public struct DNSLabel: ExpressibleByStringLiteral { } /// The type of resource record. This is used to determine the format of the record. +/// +/// The official standard list of all Resource Record (RR) Types. [IANA](https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4) public enum DNSResourceType: UInt16 { /// A request for an IPv4 address case a = 1 @@ -78,7 +80,7 @@ public enum DNSResourceType: UInt16 { /// A request for a well known service description. case wks - /// A request for a well known service description (Obsolete - see SRV). + /// A domain name pointer (ie. in-addr.arpa) for address to name case ptr /// A request for a canonical name for an alias @@ -154,6 +156,9 @@ public enum Record { /// Mail exchange record. This is used for mail servers. case mx(ResourceRecord) + /// A domain name pointer (ie. in-addr.arpa) + case ptr(ResourceRecord) + /// Any other record. This is used for records that are not yet supported through convenience methods. case other(ResourceRecord) } diff --git a/Sources/DNSClient/PTR.swift b/Sources/DNSClient/PTR.swift new file mode 100644 index 0000000..5007496 --- /dev/null +++ b/Sources/DNSClient/PTR.swift @@ -0,0 +1,121 @@ +// +// PTR.swift +// +// +// Created by Craig A. Munro on 8/18/23. +// + +import Foundation +import NIO + +/// A DNS PTR record. This is used for address to name mapping. +public struct PTRRecord: DNSResource { + /// A domain-name which points to some location in the domain name space. + public let domainName: [DNSLabel] + + public static func read(from buffer: inout ByteBuffer, length: Int) -> PTRRecord? { + guard let domainName = buffer.readLabels() else { + return nil + } + return PTRRecord(domainName: domainName) + } + + public init(domainName: [DNSLabel]) { + self.domainName = domainName + } +} + +extension DNSClient { + /// Request IPv4 inverse address (PTR records) from nameserver + /// + /// PTR Records are for mapping IP addresses to Internet domain names + /// Reverse DNS is also used for functions such as: + /// - Network troubleshooting and testing + /// - Checking domain names for suspicious information, such as overly generic reverse DNS names, dialup users or dynamically-assigned addresses in an attempt to limit email spam + /// - Screening spam/phishing groups who forge domain information + /// - Data logging and analysis within web servers + /// + /// Background references: + /// - Management Guidelines & Operational Requirements for the Address and Routing Parameter Area Domain ("arpa") [IETF RFC 3172](https://www.rfc-editor.org/rfc/rfc3172.html) + /// - IANA [.ARPA Zone Management](https://www.iana.org/domains/arpa) + /// - About reverse DNS at [ARIN](https://www.arin.net/resources/manage/reverse/) + /// + /// - Parameter address: IPv4 Address with four dotted decial unsigned integers between the values of 0...255 + /// - Returns: A future with the resource record containing a domain name associated with the IPv4 Address. + public func ipv4InverseAddress(_ address: String) -> EventLoopFuture<[ResourceRecord]> { + // A.B.C.D -> D.C.B.A.IN-ADDR.ARPA. + let inAddrArpaDomain = address + .split(separator: ".") + .map(String.init) + .reversed() + .joined(separator: ".") + .appending(".in-addr.arpa.") + + return self.sendQuery(forHost: inAddrArpaDomain, type: .ptr).map { message in + return message.answers.compactMap { answer in + guard case .ptr(let record) = answer else { return nil } + return record + } + } + } + + /// Request IPv6 inverse address (PTR records) from nameserver + /// + /// Inverse addressing queries use DNS PTR Records. + /// An IPv6 address "2001:503:c27::2:30" is transformed into an inverse domain, then DNS query performed to get associated domain name. + /// 0.3.0.0.2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.7.2.c.0.3.0.5.0.1.0.0.2.ip6.arpa domainname = j.root-servers.net. + /// + /// - Parameter address: IPv6 Address in long or compressed zero format + /// - Returns: A future with the resource record containing a domain name associated with the IPv6 Address. + /// - Throws: IOError(errnoCode: EINVAL, reason: #function) , IOError(errnoCode: errno, reason: #function) + public func ipv6InverseAddress(_ address: String) throws -> EventLoopFuture<[ResourceRecord]> { + var ipv6Addr = in6_addr() + + let retval = withUnsafeMutablePointer(to: &ipv6Addr) { + inet_pton(AF_INET6, address, UnsafeMutablePointer($0)) + } + + if retval == 0 { + // print("Invalid address: \(address)") + throw IOError(errnoCode: EINVAL, reason: #function) + } else if retval == -1 { + // print("Failed:", String(cString: strerror(errno))) + throw IOError(errnoCode: errno, reason: #function) + } + + let inAddrArpaDomain = String(format: "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + ipv6Addr.__u6_addr.__u6_addr8.0, + ipv6Addr.__u6_addr.__u6_addr8.1, + ipv6Addr.__u6_addr.__u6_addr8.2, + ipv6Addr.__u6_addr.__u6_addr8.3, + ipv6Addr.__u6_addr.__u6_addr8.4, + ipv6Addr.__u6_addr.__u6_addr8.5, + ipv6Addr.__u6_addr.__u6_addr8.6, + ipv6Addr.__u6_addr.__u6_addr8.7, + ipv6Addr.__u6_addr.__u6_addr8.8, + ipv6Addr.__u6_addr.__u6_addr8.9, + ipv6Addr.__u6_addr.__u6_addr8.10, + ipv6Addr.__u6_addr.__u6_addr8.11, + ipv6Addr.__u6_addr.__u6_addr8.12, + ipv6Addr.__u6_addr.__u6_addr8.13, + ipv6Addr.__u6_addr.__u6_addr8.14, + ipv6Addr.__u6_addr.__u6_addr8.15 + ).reversed() + .map { "\($0)" } + .joined(separator: ".") + .appending(".ip6.arpa.") + + return self.sendQuery(forHost: inAddrArpaDomain, type: .ptr).map { message in + return message.answers.compactMap { answer in + guard case .ptr(let record) = answer else { return nil } + return record + } + } + } +} + +extension PTRRecord: CustomStringConvertible { + public var description: String { + "\(Self.self): " + domainName.string + } +} diff --git a/Tests/DNSClientTests/DNSUDPClientTests.swift b/Tests/DNSClientTests/DNSUDPClientTests.swift index 8a08579..14c4bd3 100644 --- a/Tests/DNSClientTests/DNSUDPClientTests.swift +++ b/Tests/DNSClientTests/DNSUDPClientTests.swift @@ -97,4 +97,49 @@ final class DNSUDPClientTests: XCTestCase { } } } + + // 4.4.8.8.in-addr.arpa domain points to dns.google. + func testipv4InverseAddress() throws { + let answers = try dnsClient.ipv4InverseAddress("8.8.4.4").wait() + // print("getIPv4PTRRecords: ", answers[0].resource.domainName.string) + + XCTAssertGreaterThanOrEqual(answers.count, 1, "The returned answers should be greater than or equal to 1") + } + + // 'nslookup 208.67.222.222' has multiple (3) PTR records for opendns.com + func testipv4InverseAddressMultipleResponses() throws { + let answers = try dnsClient.ipv4InverseAddress("208.67.222.222").wait() + + // for answer in answers { + // print("testPTRRecords2", answer.domainName.string) + // print("testPTRRecords2", answer.resource.domainName.string) + // } + + XCTAssertEqual(answers.count, 3, "The returned answers should be equal to 3") + } + + func testipv6InverseAddress() throws { + // dns.google. + // let answers = try dnsClient.ipv6InverseAddress("2001:4860:4860::8844").wait() + + // j.root-servers.net operated by Verisign, Inc. + let answers = try dnsClient.ipv6InverseAddress("2001:503:c27::2:30").wait() + // print("getIPv6PTRRecords: ", answers[0].resource.domainName.string) + + XCTAssertGreaterThanOrEqual(answers.count, 1, "The returned answers should be greater than or equal to 1") + } + + func testipv6InverseAddressInvalidInput() throws { + XCTAssertThrowsError(try dnsClient.ipv6InverseAddress(":::0").wait()) { error in + XCTAssertEqual(error.localizedDescription , "The operation couldn’t be completed. (NIOCore.IOError error 1.)") + } + } + + func testPTRRecordDescription() throws { + let domainname = PTRRecord(domainName: [DNSLabel(stringLiteral: "dns"), + DNSLabel(stringLiteral: "google"), + DNSLabel(stringLiteral: "")]) + + XCTAssertEqual(domainname.description, "PTRRecord: dns.google") + } } From f84febb23029dc1f2c2ab94da16d9ebd007cf1d0 Mon Sep 17 00:00:00 2001 From: Craig Munro Date: Wed, 23 Aug 2023 18:13:36 -0400 Subject: [PATCH 2/2] updated README.md with examples using IETF RFC 3849 IPv6 documentation address and RFC 5737 IPv4 documentation addresses --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index a100886..51c4409 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,14 @@ Resolve TXT Records: let records = try client.sendQuery(forHost: "example.com", type: .txt).wait() ``` +Resolve PTR Records: + +```swift +let records = try client.ipv4InverseAddress("198.51.100.1").wait() + +let records = try client.ipv6InverseAddress("2001:DB8::").wait() +``` + Need I say more? ### Notes