Coverage Report

Created: 2026-09-14 06:47

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