Skip to content

Commit

Permalink
Updated ByteBuffer.readRecord() to support case .ptr
Browse files Browse the repository at this point in the history
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<PTRRecord>)

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.
  • Loading branch information
camunro committed Aug 23, 2023
1 parent a656d2d commit 3c4f035
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 2 deletions.
8 changes: 7 additions & 1 deletion Sources/DNSClient/Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
7 changes: 6 additions & 1 deletion Sources/DNSClient/Messages/Message.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -154,6 +156,9 @@ public enum Record {
/// Mail exchange record. This is used for mail servers.
case mx(ResourceRecord<MXRecord>)

/// A domain name pointer (ie. in-addr.arpa)
case ptr(ResourceRecord<PTRRecord>)

/// Any other record. This is used for records that are not yet supported through convenience methods.
case other(ResourceRecord<ByteBuffer>)
}
Expand Down
121 changes: 121 additions & 0 deletions Sources/DNSClient/PTR.swift
Original file line number Diff line number Diff line change
@@ -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<PTRRecord>]> {
// 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<PTRRecord>]> {
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
}
}
45 changes: 45 additions & 0 deletions Tests/DNSClientTests/DNSUDPClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}

0 comments on commit 3c4f035

Please sign in to comment.