-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
2 changed files
with
231 additions
and
0 deletions.
There are no files selected for viewing
193 changes: 193 additions & 0 deletions
193
Sources/GRPCNIOTransportCore/Client/Resolver/DNSResolver.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))) | ||
} | ||
} |
38 changes: 38 additions & 0 deletions
38
Tests/GRPCNIOTransportCoreTests/Client/Resolver/DNSResolverTests.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) | ||
} | ||
} |