Skip to content

Commit ba557f2

Browse files
authored
Add a DNS NameResolver (grpc#5)
Motivation: Many users will rely on DNS to resolve the IP addresses of servers to connect to, we should therefore provide a DNS name resolver. Modifications: - Add a DNS name resolver factory capable of resolving IP addresses - Add the resolver to the registry defaults Result: Can resolve DNS targets
1 parent 2978160 commit ba557f2

File tree

3 files changed

+163
-2
lines changed

3 files changed

+163
-2
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright 2024, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
private import GRPCCore
18+
19+
extension ResolvableTargets {
20+
/// A resolvable target for addresses which can be resolved via DNS.
21+
///
22+
/// If you already have an IPv4 or IPv6 address use ``ResolvableTargets/IPv4`` and
23+
/// ``ResolvableTargets/IPv6`` respectively.
24+
public struct DNS: ResolvableTarget, Sendable {
25+
/// The host to resolve via DNS.
26+
public var host: String
27+
28+
/// The port to use with resolved addresses.
29+
public var port: Int
30+
31+
/// Create a new DNS target.
32+
/// - Parameters:
33+
/// - host: The host to resolve via DNS.
34+
/// - port: The port to use with resolved addresses.
35+
public init(host: String, port: Int) {
36+
self.host = host
37+
self.port = port
38+
}
39+
}
40+
}
41+
42+
extension ResolvableTarget where Self == ResolvableTargets.DNS {
43+
/// Creates a new resolvable DNS target.
44+
/// - Parameters:
45+
/// - host: The host address to resolve.
46+
/// - port: The port to use for each resolved address.
47+
/// - Returns: A ``ResolvableTarget``.
48+
public static func dns(host: String, port: Int = 443) -> Self {
49+
return Self(host: host, port: port)
50+
}
51+
}
52+
53+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
54+
extension NameResolvers {
55+
/// A ``NameResolverFactory`` for ``ResolvableTargets/DNS`` targets.
56+
public struct DNS: NameResolverFactory {
57+
public typealias Target = ResolvableTargets.DNS
58+
59+
/// Create a new DNS name resolver factory.
60+
public init() {}
61+
62+
public func resolver(for target: Target) -> NameResolver {
63+
let resolver = Self.Resolver(target: target)
64+
return NameResolver(names: RPCAsyncSequence(wrapping: resolver), updateMode: .pull)
65+
}
66+
}
67+
}
68+
69+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
70+
extension NameResolvers.DNS {
71+
struct Resolver: Sendable {
72+
var target: ResolvableTargets.DNS
73+
74+
init(target: ResolvableTargets.DNS) {
75+
self.target = target
76+
}
77+
78+
func resolve(
79+
isolation actor: isolated (any Actor)? = nil
80+
) async throws -> NameResolutionResult {
81+
let addresses: [SocketAddress]
82+
83+
do {
84+
addresses = try await DNSResolver.resolve(host: self.target.host, port: self.target.port)
85+
} catch let error as CancellationError {
86+
throw error
87+
} catch {
88+
throw RPCError(
89+
code: .internalError,
90+
message: "Couldn't resolve address for \(self.target.host):\(self.target.port)",
91+
cause: error
92+
)
93+
}
94+
95+
return NameResolutionResult(endpoints: [Endpoint(addresses: addresses)], serviceConfig: nil)
96+
}
97+
}
98+
}
99+
100+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
101+
extension NameResolvers.DNS.Resolver: AsyncSequence {
102+
typealias Element = NameResolutionResult
103+
104+
func makeAsyncIterator() -> AsyncIterator {
105+
return AsyncIterator(resolver: self)
106+
}
107+
108+
struct AsyncIterator: AsyncIteratorProtocol {
109+
typealias Element = NameResolutionResult
110+
111+
private let resolver: NameResolvers.DNS.Resolver
112+
113+
init(resolver: NameResolvers.DNS.Resolver) {
114+
self.resolver = resolver
115+
}
116+
117+
func next() async throws -> NameResolutionResult? {
118+
return try await self.next(isolation: nil)
119+
}
120+
121+
func next(
122+
isolation actor: isolated (any Actor)?
123+
) async throws(any Error) -> NameResolutionResult? {
124+
return try await self.resolver.resolve(isolation: actor)
125+
}
126+
}
127+
}

Sources/GRPCNIOTransportCore/Client/Resolver/NameResolverRegistry.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,17 @@
4040
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
4141
public struct NameResolverRegistry {
4242
private enum Factory {
43+
case dns(NameResolvers.DNS)
4344
case ipv4(NameResolvers.IPv4)
4445
case ipv6(NameResolvers.IPv6)
4546
case unix(NameResolvers.UnixDomainSocket)
4647
case vsock(NameResolvers.VirtualSocket)
4748
case other(any NameResolverFactory)
4849

4950
init(_ factory: some NameResolverFactory) {
50-
if let ipv4 = factory as? NameResolvers.IPv4 {
51+
if let dns = factory as? NameResolvers.DNS {
52+
self = .dns(dns)
53+
} else if let ipv4 = factory as? NameResolvers.IPv4 {
5154
self = .ipv4(ipv4)
5255
} else if let ipv6 = factory as? NameResolvers.IPv6 {
5356
self = .ipv6(ipv6)
@@ -62,6 +65,8 @@ public struct NameResolverRegistry {
6265

6366
func makeResolverIfCompatible<Target: ResolvableTarget>(_ target: Target) -> NameResolver? {
6467
switch self {
68+
case .dns(let factory):
69+
return factory.makeResolverIfCompatible(target)
6570
case .ipv4(let factory):
6671
return factory.makeResolverIfCompatible(target)
6772
case .ipv6(let factory):
@@ -77,6 +82,8 @@ public struct NameResolverRegistry {
7782

7883
func hasTarget<Target: ResolvableTarget>(_ target: Target) -> Bool {
7984
switch self {
85+
case .dns(let factory):
86+
return factory.isCompatible(withTarget: target)
8087
case .ipv4(let factory):
8188
return factory.isCompatible(withTarget: target)
8289
case .ipv6(let factory):
@@ -92,6 +99,8 @@ public struct NameResolverRegistry {
9299

93100
func `is`<Factory: NameResolverFactory>(ofType factoryType: Factory.Type) -> Bool {
94101
switch self {
102+
case .dns:
103+
return NameResolvers.DNS.self == factoryType
95104
case .ipv4:
96105
return NameResolvers.IPv4.self == factoryType
97106
case .ipv6:
@@ -116,12 +125,14 @@ public struct NameResolverRegistry {
116125
/// Returns a new name resolver registry with the default factories registered.
117126
///
118127
/// The default resolvers include:
128+
/// - ``NameResolvers/DNS``,
119129
/// - ``NameResolvers/IPv4``,
120130
/// - ``NameResolvers/IPv6``,
121131
/// - ``NameResolvers/UnixDomainSocket``,
122132
/// - ``NameResolvers/VirtualSocket``.
123133
public static var defaults: Self {
124134
var resolvers = NameResolverRegistry()
135+
resolvers.registerFactory(NameResolvers.DNS())
125136
resolvers.registerFactory(NameResolvers.IPv4())
126137
resolvers.registerFactory(NameResolvers.IPv6())
127138
resolvers.registerFactory(NameResolvers.UnixDomainSocket())

Tests/GRPCNIOTransportCoreTests/Client/Resolver/NameResolverRegistryTests.swift

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,12 @@ final class NameResolverRegistryTests: XCTestCase {
132132

133133
func testDefaultResolvers() {
134134
let resolvers = NameResolverRegistry.defaults
135+
XCTAssert(resolvers.containsFactory(ofType: NameResolvers.DNS.self))
135136
XCTAssert(resolvers.containsFactory(ofType: NameResolvers.IPv4.self))
136137
XCTAssert(resolvers.containsFactory(ofType: NameResolvers.IPv6.self))
137138
XCTAssert(resolvers.containsFactory(ofType: NameResolvers.UnixDomainSocket.self))
138139
XCTAssert(resolvers.containsFactory(ofType: NameResolvers.VirtualSocket.self))
139-
XCTAssertEqual(resolvers.count, 4)
140+
XCTAssertEqual(resolvers.count, 5)
140141
}
141142

142143
func testMakeResolver() {
@@ -167,6 +168,28 @@ final class NameResolverRegistryTests: XCTestCase {
167168
}
168169
}
169170

171+
func testDNSResolverForIPv4() async throws {
172+
let factory = NameResolvers.DNS()
173+
let resolver = factory.resolver(for: .dns(host: "127.0.0.1", port: 1234))
174+
XCTAssertEqual(resolver.updateMode, .pull)
175+
176+
var iterator = resolver.names.makeAsyncIterator()
177+
let result = try await XCTUnwrapAsync { try await iterator.next() }
178+
XCTAssertEqual(result.endpoints, [Endpoint(.ipv4(host: "127.0.0.1", port: 1234))])
179+
XCTAssertNil(result.serviceConfig)
180+
}
181+
182+
func testDNSResolverForIPv6() async throws {
183+
let factory = NameResolvers.DNS()
184+
let resolver = factory.resolver(for: .dns(host: "::1", port: 1234))
185+
XCTAssertEqual(resolver.updateMode, .pull)
186+
187+
var iterator = resolver.names.makeAsyncIterator()
188+
let result = try await XCTUnwrapAsync { try await iterator.next() }
189+
XCTAssertEqual(result.endpoints, [Endpoint(.ipv6(host: "::1", port: 1234))])
190+
XCTAssertNil(result.serviceConfig)
191+
}
192+
170193
func testIPv4ResolverForSingleHost() async throws {
171194
let factory = NameResolvers.IPv4()
172195
let resolver = factory.resolver(for: .ipv4(host: "foo", port: 1234))

0 commit comments

Comments
 (0)