swift-nio/Sources/NIOPosix/GetaddrinfoResolver.swift

225 lines
8.3 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import NIOCore
/// A DNS resolver built on top of the libc `getaddrinfo` function.
///
/// This is the lowest-common-denominator resolver available to NIO. It's not really a very good
/// solution because the `getaddrinfo` call blocks during the DNS resolution, meaning that this resolver
/// will block a thread for as long as it takes to perform the getaddrinfo call. To prevent it from blocking `EventLoop`
/// threads, it will offload the blocking `getaddrinfo` calls to a `DispatchQueue`.
/// One advantage from leveraging `getaddrinfo` is the automatic conformance to RFC 6724, which removes some of the work
/// needed to implement it.
///
/// This resolver is a single-use object: it can only be used to perform a single host resolution.
import Dispatch
#if os(Linux) || os(FreeBSD) || os(Android)
import CNIOLinux
#endif
#if os(Windows)
import let WinSDK.AF_INET
import let WinSDK.AF_INET6
import func WinSDK.FreeAddrInfoW
import func WinSDK.GetAddrInfoW
import func WinSDK.gai_strerrorA
import struct WinSDK.ADDRESS_FAMILY
import struct WinSDK.ADDRINFOW
import struct WinSDK.SOCKADDR_IN
import struct WinSDK.SOCKADDR_IN6
#endif
// A thread-specific variable where we store the offload queue if we're on an `SelectableEventLoop`.
let offloadQueueTSV = ThreadSpecificVariable<DispatchQueue>()
internal class GetaddrinfoResolver: Resolver {
private let v4Future: EventLoopPromise<[SocketAddress]>
private let v6Future: EventLoopPromise<[SocketAddress]>
private let aiSocktype: NIOBSDSocket.SocketType
private let aiProtocol: CInt
/// Create a new resolver.
///
/// - parameters:
/// - loop: The `EventLoop` whose thread this resolver will block.
/// - aiSocktype: The sock type to use as hint when calling getaddrinfo.
/// - aiProtocol: the protocol to use as hint when calling getaddrinfo.
init(loop: EventLoop, aiSocktype: NIOBSDSocket.SocketType, aiProtocol: CInt) {
self.v4Future = loop.makePromise()
self.v6Future = loop.makePromise()
self.aiSocktype = aiSocktype
self.aiProtocol = aiProtocol
}
/// Initiate a DNS A query for a given host.
///
/// Due to the nature of `getaddrinfo`, we only actually call the function once, in the AAAA query.
/// That means this just returns the future for the A results, which in practice will always have been
/// satisfied by the time this function is called.
///
/// - parameters:
/// - host: The hostname to do an A lookup on.
/// - port: The port we'll be connecting to.
/// - returns: An `EventLoopFuture` that fires with the result of the lookup.
func initiateAQuery(host: String, port: Int) -> EventLoopFuture<[SocketAddress]> {
return v4Future.futureResult
}
/// Initiate a DNS AAAA query for a given host.
///
/// Due to the nature of `getaddrinfo`, we only actually call the function once, in this function.
/// That means this function call actually blocks: sorry!
///
/// - parameters:
/// - host: The hostname to do an AAAA lookup on.
/// - port: The port we'll be connecting to.
/// - returns: An `EventLoopFuture` that fires with the result of the lookup.
func initiateAAAAQuery(host: String, port: Int) -> EventLoopFuture<[SocketAddress]> {
self.offloadQueue().async {
self.resolveBlocking(host: host, port: port)
}
return v6Future.futureResult
}
private func offloadQueue() -> DispatchQueue {
if let offloadQueue = offloadQueueTSV.currentValue {
return offloadQueue
} else {
if MultiThreadedEventLoopGroup.currentEventLoop != nil {
// Okay, we're on an SelectableEL thread. Let's stuff our queue into the thread local.
let offloadQueue = DispatchQueue(label: "io.swiftnio.GetaddrinfoResolver.offloadQueue")
offloadQueueTSV.currentValue = offloadQueue
return offloadQueue
} else {
return DispatchQueue.global()
}
}
}
/// Cancel all outstanding DNS queries.
///
/// This method is called whenever queries that have not completed no longer have their
/// results needed. The resolver should, if possible, abort any outstanding queries and
/// clean up their state.
///
/// In the getaddrinfo case this is a no-op, as the resolver blocks.
func cancelQueries() { }
/// Perform the DNS queries and record the result.
///
/// - parameters:
/// - host: The hostname to do the DNS queries on.
/// - port: The port we'll be connecting to.
private func resolveBlocking(host: String, port: Int) {
#if os(Windows)
host.withCString(encodedAs: UTF16.self) { wszHost in
String(port).withCString(encodedAs: UTF16.self) { wszPort in
var pResult: UnsafeMutablePointer<ADDRINFOW>?
var aiHints: ADDRINFOW = ADDRINFOW()
aiHints.ai_socktype = self.aiSocktype.rawValue
aiHints.ai_protocol = self.aiProtocol
let iResult = GetAddrInfoW(wszHost, wszPort, &aiHints, &pResult)
guard iResult == 0 else {
self.fail(SocketAddressError.unknown(host: host, port: port))
return
}
if let pResult = pResult {
self.parseAndPublishResults(pResult, host: host)
FreeAddrInfoW(pResult)
} else {
self.fail(SocketAddressError.unsupported)
}
}
}
#else
var info: UnsafeMutablePointer<addrinfo>?
var hint = addrinfo()
hint.ai_socktype = self.aiSocktype.rawValue
hint.ai_protocol = self.aiProtocol
guard getaddrinfo(host, String(port), &hint, &info) == 0 else {
self.fail(SocketAddressError.unknown(host: host, port: port))
return
}
if let info = info {
self.parseAndPublishResults(info, host: host)
freeaddrinfo(info)
} else {
/* this is odd, getaddrinfo returned NULL */
self.fail(SocketAddressError.unsupported)
}
#endif
}
/// Parses the DNS results from the `addrinfo` linked list.
///
/// - parameters:
/// - info: The pointer to the first of the `addrinfo` structures in the list.
/// - host: The hostname we resolved.
#if os(Windows)
internal typealias CAddrInfo = ADDRINFOW
#else
internal typealias CAddrInfo = addrinfo
#endif
private func parseAndPublishResults(_ info: UnsafeMutablePointer<CAddrInfo>, host: String) {
var v4Results: [SocketAddress] = []
var v6Results: [SocketAddress] = []
var info: UnsafeMutablePointer<CAddrInfo> = info
while true {
switch NIOBSDSocket.AddressFamily(rawValue: info.pointee.ai_family) {
case .inet:
info.pointee.ai_addr.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { ptr in
v4Results.append(.init(ptr.pointee, host: host))
}
case .inet6:
info.pointee.ai_addr.withMemoryRebound(to: sockaddr_in6.self, capacity: 1) { ptr in
v6Results.append(.init(ptr.pointee, host: host))
}
default:
self.fail(SocketAddressError.unsupported)
return
}
guard let nextInfo = info.pointee.ai_next else {
break
}
info = nextInfo
}
v6Future.succeed(v6Results)
v4Future.succeed(v4Results)
}
/// Record an error and fail the lookup process.
///
/// - parameters:
/// - error: The error encountered during lookup.
private func fail(_ error: Error) {
self.v6Future.fail(error)
self.v4Future.fail(error)
}
}