Coverage Report

Created: 2025-09-08 06:42

/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
}