/src/swift-nio/Sources/NIOPosix/GetaddrinfoResolver.swift
Line | Count | Source (jump to first uncovered line) |
1 | | //===----------------------------------------------------------------------===// |
2 | | // |
3 | | // This source file is part of the SwiftNIO open source project |
4 | | // |
5 | | // Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors |
6 | | // Licensed under Apache License v2.0 |
7 | | // |
8 | | // See LICENSE.txt for license information |
9 | | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors |
10 | | // |
11 | | // SPDX-License-Identifier: Apache-2.0 |
12 | | // |
13 | | //===----------------------------------------------------------------------===// |
14 | | |
15 | | import NIOCore |
16 | | |
17 | | #if canImport(Dispatch) |
18 | | import Dispatch |
19 | | #endif |
20 | | |
21 | | /// A DNS resolver built on top of the libc `getaddrinfo` function. |
22 | | /// |
23 | | /// This is the lowest-common-denominator resolver available to NIO. It's not really a very good |
24 | | /// solution because the `getaddrinfo` call blocks during the DNS resolution, meaning that this resolver |
25 | | /// will block a thread for as long as it takes to perform the getaddrinfo call. To prevent it from blocking `EventLoop` |
26 | | /// threads, it will offload the blocking `getaddrinfo` calls to a `DispatchQueue`. |
27 | | /// One advantage from leveraging `getaddrinfo` is the automatic conformance to RFC 6724, which removes some of the work |
28 | | /// needed to implement it. |
29 | | /// |
30 | | /// This resolver is a single-use object: it can only be used to perform a single host resolution. |
31 | | |
32 | | #if os(Linux) || os(FreeBSD) || os(Android) |
33 | | import CNIOLinux |
34 | | #endif |
35 | | |
36 | | #if os(Windows) |
37 | | import let WinSDK.AF_INET |
38 | | import let WinSDK.AF_INET6 |
39 | | |
40 | | import func WinSDK.FreeAddrInfoW |
41 | | import func WinSDK.GetAddrInfoW |
42 | | import func WinSDK.gai_strerrorA |
43 | | |
44 | | import struct WinSDK.ADDRESS_FAMILY |
45 | | import struct WinSDK.ADDRINFOW |
46 | | import struct WinSDK.SOCKADDR_IN |
47 | | import struct WinSDK.SOCKADDR_IN6 |
48 | | #endif |
49 | | |
50 | | // A thread-specific variable where we store the offload queue if we're on an `SelectableEventLoop`. |
51 | | let offloadQueueTSV = ThreadSpecificVariable<DispatchQueue>() |
52 | | |
53 | | internal final class GetaddrinfoResolver: Resolver, Sendable { |
54 | | private let loop: EventLoop |
55 | | private let v4Future: EventLoopPromise<[SocketAddress]> |
56 | | private let v6Future: EventLoopPromise<[SocketAddress]> |
57 | | private let aiSocktype: NIOBSDSocket.SocketType |
58 | | private let aiProtocol: NIOBSDSocket.OptionLevel |
59 | | |
60 | | /// Create a new resolver. |
61 | | /// |
62 | | /// - Parameters: |
63 | | /// - loop: The `EventLoop` whose thread this resolver will block. |
64 | | /// - aiSocktype: The sock type to use as hint when calling getaddrinfo. |
65 | | /// - aiProtocol: the protocol to use as hint when calling getaddrinfo. |
66 | | init( |
67 | | loop: EventLoop, |
68 | | aiSocktype: NIOBSDSocket.SocketType, |
69 | | aiProtocol: NIOBSDSocket.OptionLevel |
70 | 0 | ) { |
71 | 0 | self.loop = loop |
72 | 0 | self.v4Future = loop.makePromise() |
73 | 0 | self.v6Future = loop.makePromise() |
74 | 0 | self.aiSocktype = aiSocktype |
75 | 0 | self.aiProtocol = aiProtocol |
76 | 0 | } |
77 | | |
78 | | /// Initiate a DNS A query for a given host. |
79 | | /// |
80 | | /// Due to the nature of `getaddrinfo`, we only actually call the function once, in the AAAA query. |
81 | | /// That means this just returns the future for the A results, which in practice will always have been |
82 | | /// satisfied by the time this function is called. |
83 | | /// |
84 | | /// - Parameters: |
85 | | /// - host: The hostname to do an A lookup on. |
86 | | /// - port: The port we'll be connecting to. |
87 | | /// - Returns: An `EventLoopFuture` that fires with the result of the lookup. |
88 | 0 | func initiateAQuery(host: String, port: Int) -> EventLoopFuture<[SocketAddress]> { |
89 | 0 | v4Future.futureResult |
90 | 0 | } |
91 | | |
92 | | /// Initiate a DNS AAAA query for a given host. |
93 | | /// |
94 | | /// Due to the nature of `getaddrinfo`, we only actually call the function once, in this function. |
95 | | /// |
96 | | /// - Parameters: |
97 | | /// - host: The hostname to do an AAAA lookup on. |
98 | | /// - port: The port we'll be connecting to. |
99 | | /// - Returns: An `EventLoopFuture` that fires with the result of the lookup. |
100 | 0 | func initiateAAAAQuery(host: String, port: Int) -> EventLoopFuture<[SocketAddress]> { |
101 | 0 | self.offloadQueue().async { |
102 | 0 | self.resolveBlocking(host: host, port: port) |
103 | 0 | } |
104 | 0 | return v6Future.futureResult |
105 | 0 | } |
106 | | |
107 | 0 | private func offloadQueue() -> DispatchQueue { |
108 | 0 | if let offloadQueue = offloadQueueTSV.currentValue { |
109 | 0 | return offloadQueue |
110 | 0 | } else { |
111 | 0 | if MultiThreadedEventLoopGroup.currentEventLoop != nil { |
112 | 0 | // Okay, we're on an SelectableEL thread. Let's stuff our queue into the thread local. |
113 | 0 | let offloadQueue = DispatchQueue(label: "io.swiftnio.GetaddrinfoResolver.offloadQueue") |
114 | 0 | offloadQueueTSV.currentValue = offloadQueue |
115 | 0 | return offloadQueue |
116 | 0 | } else { |
117 | 0 | return DispatchQueue.global() |
118 | 0 | } |
119 | 0 | } |
120 | 0 | } |
121 | | |
122 | | /// Cancel all outstanding DNS queries. |
123 | | /// |
124 | | /// This method is called whenever queries that have not completed no longer have their |
125 | | /// results needed. The resolver should, if possible, abort any outstanding queries and |
126 | | /// clean up their state. |
127 | | /// |
128 | | /// In the getaddrinfo case this is a no-op, as the resolver blocks. |
129 | 0 | func cancelQueries() {} |
130 | | |
131 | | /// Perform the DNS queries and record the result. |
132 | | /// |
133 | | /// - Parameters: |
134 | | /// - host: The hostname to do the DNS queries on. |
135 | | /// - port: The port we'll be connecting to. |
136 | 0 | private func resolveBlocking(host: String, port: Int) { |
137 | 0 | #if os(Windows) |
138 | 0 | host.withCString(encodedAs: UTF16.self) { wszHost in |
139 | 0 | String(port).withCString(encodedAs: UTF16.self) { wszPort in |
140 | 0 | var pResult: UnsafeMutablePointer<ADDRINFOW>? |
141 | 0 |
|
142 | 0 | var aiHints: ADDRINFOW = ADDRINFOW() |
143 | 0 | aiHints.ai_socktype = self.aiSocktype.rawValue |
144 | 0 | aiHints.ai_protocol = self.aiProtocol.rawValue |
145 | 0 |
|
146 | 0 | let iResult = GetAddrInfoW(wszHost, wszPort, &aiHints, &pResult) |
147 | 0 | guard iResult == 0 else { |
148 | 0 | self.fail(SocketAddressError.unknown(host: host, port: port)) |
149 | 0 | return |
150 | 0 | } |
151 | 0 |
|
152 | 0 | if let pResult = pResult { |
153 | 0 | self.parseAndPublishResults(pResult, host: host) |
154 | 0 | FreeAddrInfoW(pResult) |
155 | 0 | } else { |
156 | 0 | self.fail(SocketAddressError.unsupported) |
157 | 0 | } |
158 | 0 | } |
159 | 0 | } |
160 | 0 | #else |
161 | 0 | var info: UnsafeMutablePointer<addrinfo>? |
162 | 0 |
|
163 | 0 | var hint = addrinfo() |
164 | 0 | hint.ai_socktype = self.aiSocktype.rawValue |
165 | 0 | hint.ai_protocol = self.aiProtocol.rawValue |
166 | 0 | guard getaddrinfo(host, String(port), &hint, &info) == 0 else { |
167 | 0 | self.fail(SocketAddressError.unknown(host: host, port: port)) |
168 | 0 | return |
169 | 0 | } |
170 | 0 |
|
171 | 0 | if let info = info { |
172 | 0 | self.parseAndPublishResults(info, host: host) |
173 | 0 | freeaddrinfo(info) |
174 | 0 | } else { |
175 | 0 | // this is odd, getaddrinfo returned NULL |
176 | 0 | self.fail(SocketAddressError.unsupported) |
177 | 0 | } |
178 | 0 | #endif |
179 | 0 | } |
180 | | |
181 | | /// Parses the DNS results from the `addrinfo` linked list. |
182 | | /// |
183 | | /// - Parameters: |
184 | | /// - info: The pointer to the first of the `addrinfo` structures in the list. |
185 | | /// - host: The hostname we resolved. |
186 | | #if os(Windows) |
187 | | internal typealias CAddrInfo = ADDRINFOW |
188 | | #else |
189 | | internal typealias CAddrInfo = addrinfo |
190 | | #endif |
191 | | |
192 | 0 | private func parseAndPublishResults(_ info: UnsafeMutablePointer<CAddrInfo>, host: String) { |
193 | 0 | var v4Results: [SocketAddress] = [] |
194 | 0 | var v6Results: [SocketAddress] = [] |
195 | 0 |
|
196 | 0 | var info: UnsafeMutablePointer<CAddrInfo> = info |
197 | 0 | while true { |
198 | 0 | let addressBytes = UnsafeRawPointer(info.pointee.ai_addr) |
199 | 0 | switch NIOBSDSocket.AddressFamily(rawValue: info.pointee.ai_family) { |
200 | 0 | case .inet: |
201 | 0 | // Force-unwrap must be safe, or libc did the wrong thing. |
202 | 0 | v4Results.append(.init(addressBytes!.load(as: sockaddr_in.self), host: host)) |
203 | 0 | case .inet6: |
204 | 0 | // Force-unwrap must be safe, or libc did the wrong thing. |
205 | 0 | v6Results.append(.init(addressBytes!.load(as: sockaddr_in6.self), host: host)) |
206 | 0 | default: |
207 | 0 | self.fail(SocketAddressError.unsupported) |
208 | 0 | return |
209 | 0 | } |
210 | 0 |
|
211 | 0 | guard let nextInfo = info.pointee.ai_next else { |
212 | 0 | break |
213 | 0 | } |
214 | 0 |
|
215 | 0 | info = nextInfo |
216 | 0 | } |
217 | 0 |
|
218 | 0 | // Ensure that both futures are succeeded in the same tick |
219 | 0 | // to avoid racing and potentially leaking a promise |
220 | 0 | self.loop.execute { [v4Results, v6Results] in |
221 | 0 | self.v6Future.succeed(v6Results) |
222 | 0 | self.v4Future.succeed(v4Results) |
223 | 0 | } |
224 | 0 | } |
225 | | |
226 | | /// Record an error and fail the lookup process. |
227 | | /// |
228 | | /// - Parameters: |
229 | | /// - error: The error encountered during lookup. |
230 | 0 | private func fail(_ error: Error) { |
231 | 0 | // Ensure that both futures are succeeded in the same tick |
232 | 0 | // to avoid racing and potentially leaking a promise |
233 | 0 | self.loop.execute { |
234 | 0 | self.v6Future.fail(error) |
235 | 0 | self.v4Future.fail(error) |
236 | 0 | } |
237 | 0 | } |
238 | | } |