Skip to content

Commit

Permalink
Implement DNS resolver (#4)
Browse files Browse the repository at this point in the history
Motivation:

The GRPCNIOTransportCore module needs a DNS resolver, as currently, IP
addresses have to be passed in manually.

Modifications:

- Add a new type `DNSResolver` with a method `resolve(host:port:)` that
calls the `getaddrinfo` `libc` function to resolve a hostname and port
number to a list of socket addresses. `resolve(host:port:)` is
non-blocking and asynchronous.

Result:

The GRPCNIOTransportCore module will have a DNS resolver.


Co-authored-by: George Barnett <gbarnett@apple.com>
  • Loading branch information
clintonpi and glbrntt authored Sep 23, 2024
1 parent a071b60 commit 1cea701
Show file tree
Hide file tree
Showing 2 changed files with 231 additions and 0 deletions.
193 changes: 193 additions & 0 deletions Sources/GRPCNIOTransportCore/Client/Resolver/DNSResolver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* Copyright 2024, gRPC Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

private import Dispatch

#if canImport(Darwin)
private import Darwin
#elseif canImport(Glibc)
private import Glibc
#elseif canImport(Musl)
private import Musl
#else
#error("The GRPCNIOTransportCore module was unable to identify your C library.")
#endif

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
/// An asynchronous non-blocking DNS resolver built on top of the libc `getaddrinfo` function.
package enum DNSResolver {
private static let dispatchQueue = DispatchQueue(
label: "io.grpc.DNSResolver"
)

/// Resolves a hostname and port number to a list of socket addresses. This method is non-blocking.
package static func resolve(host: String, port: Int) async throws -> [SocketAddress] {
try Task.checkCancellation()

return try await withCheckedThrowingContinuation { continuation in
Self.dispatchQueue.async {
do {
let result = try Self.resolveBlocking(host: host, port: port)
continuation.resume(returning: result)
} catch {
continuation.resume(throwing: error)
}
}
}
}

/// Resolves a hostname and port number to a list of socket addresses.
///
/// Calls to `getaddrinfo` are blocking and this method calls `getaddrinfo` directly. Hence, this method is also blocking.
private static func resolveBlocking(host: String, port: Int) throws -> [SocketAddress] {
var result: UnsafeMutablePointer<addrinfo>?
defer {
if let result {
// Release memory allocated by a successful call to getaddrinfo
freeaddrinfo(result)
}
}

var hints = addrinfo()
#if os(Linux)
hints.ai_socktype = CInt(SOCK_STREAM.rawValue)
#else
hints.ai_socktype = SOCK_STREAM
#endif
hints.ai_protocol = CInt(IPPROTO_TCP)

let errorCode = getaddrinfo(host, String(port), &hints, &result)

guard errorCode == 0, let result else {
throw Self.GetAddrInfoError(code: errorCode)
}

return try Self.parseResult(result)
}

/// Parses the linked list of DNS results (`addrinfo`), returning an array of socket addresses.
private static func parseResult(
_ result: UnsafeMutablePointer<addrinfo>
) throws -> [SocketAddress] {
var result = result
var socketAddresses = [SocketAddress]()

while true {
let addressBytes: UnsafeRawPointer = UnsafeRawPointer(result.pointee.ai_addr)

switch result.pointee.ai_family {
case AF_INET: // IPv4 address
let ipv4AddressStructure = addressBytes.load(as: sockaddr_in.self)
try socketAddresses.append(.ipv4(.init(ipv4AddressStructure)))
case AF_INET6: // IPv6 address
let ipv6AddressStructure = addressBytes.load(as: sockaddr_in6.self)
try socketAddresses.append(.ipv6(.init(ipv6AddressStructure)))
default:
()
}

guard let nextResult = result.pointee.ai_next else { break }
result = nextResult
}

return socketAddresses
}

/// Converts an address from a network format to a presentation format using `inet_ntop`.
fileprivate static func convertAddressFromNetworkToPresentationFormat(
addressPtr: UnsafeRawPointer,
family: CInt,
length: CInt
) throws -> String {
var presentationAddressBytes = [CChar](repeating: 0, count: Int(length))

return try presentationAddressBytes.withUnsafeMutableBufferPointer {
(presentationAddressBytesPtr: inout UnsafeMutableBufferPointer<CChar>) throws -> String in

// Convert
let presentationAddressStringPtr = inet_ntop(
family,
addressPtr,
presentationAddressBytesPtr.baseAddress!,
socklen_t(length)
)

if let presentationAddressStringPtr {
return String(cString: presentationAddressStringPtr)
} else {
throw Self.InetNetworkToPresentationError(errno: errno)
}
}
}
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension DNSResolver {
/// `Error` that may be thrown based on the error code returned by `getaddrinfo`.
package struct GetAddrInfoError: Error, Hashable, CustomStringConvertible {
package let description: String

package init(code: CInt) {
if let errorMessage = gai_strerror(code) {
self.description = String(cString: errorMessage)
} else {
self.description = "Unknown error: \(code)"
}
}
}
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension DNSResolver {
/// `Error` that may be thrown based on the system error encountered by `inet_ntop`.
package struct InetNetworkToPresentationError: Error, Hashable {
package let errno: CInt

package init(errno: CInt) {
self.errno = errno
}
}
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension SocketAddress.IPv4 {
fileprivate init(_ address: sockaddr_in) throws {
let presentationAddress = try withUnsafePointer(to: address.sin_addr) { addressPtr in
return try DNSResolver.convertAddressFromNetworkToPresentationFormat(
addressPtr: addressPtr,
family: AF_INET,
length: INET_ADDRSTRLEN
)
}

self = .init(host: presentationAddress, port: Int(in_port_t(bigEndian: address.sin_port)))
}
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension SocketAddress.IPv6 {
fileprivate init(_ address: sockaddr_in6) throws {
let presentationAddress = try withUnsafePointer(to: address.sin6_addr) { addressPtr in
return try DNSResolver.convertAddressFromNetworkToPresentationFormat(
addressPtr: addressPtr,
family: AF_INET6,
length: INET6_ADDRSTRLEN
)
}

self = .init(host: presentationAddress, port: Int(in_port_t(bigEndian: address.sin6_port)))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2024, gRPC Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import GRPCNIOTransportCore
import Testing

@Suite("DNSResolver")
struct DNSResolverTests {
@Test(
"Resolve hostname",
arguments: [
("127.0.0.1", .ipv4(host: "127.0.0.1", port: 80)),
("::1", .ipv6(host: "::1", port: 80)),
] as [(String, SocketAddress)]
)
func resolve(host: String, expected: SocketAddress) async throws {
// Note: This test checks the IPv4 and IPv6 addresses separately (instead of
// `DNSResolver.resolve(host: "localhost", port: 80)`) because the ordering of the resulting
// list containing both IP address versions can be different.

let result = try await DNSResolver.resolve(host: host, port: 80)

#expect(result == [expected])
}
}

0 comments on commit 1cea701

Please sign in to comment.