Skip to content

Commit f8184b8

Browse files
authored
Add support for TLS on H2 NIOTS server transport (grpc#2040)
## Motivation We currently have a NIOTS server transport implementation in gRPC v2, but it doesn't support TLS. ## Modifications This PR adds support for TLS in the NIOTS-backed HTTP/2 implementation of the server transport for gRPC v2. It also adds support for ALPN, to validate that the negotiated protocol, if required, is HTTP2 or `grpc-exp`. If it's not, an error will be fired/the channel will be closed, since we don't support H1. ## Result We now support TLS/ALPN when using the NIOTS server transport in gRPC V2.
1 parent 62b7f85 commit f8184b8

File tree

4 files changed

+205
-17
lines changed

4 files changed

+205
-17
lines changed

Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ public import GRPCCore
1919
public import NIOTransportServices // has to be public because of default argument value in init
2020
public import GRPCHTTP2Core
2121

22-
internal import NIOCore
23-
internal import NIOExtras
24-
internal import NIOHTTP2
22+
private import NIOCore
23+
private import NIOExtras
24+
private import NIOHTTP2
25+
private import Network
2526

2627
private import Synchronization
2728

@@ -171,7 +172,25 @@ extension HTTP2ServerTransport {
171172
}
172173
}
173174

174-
let serverChannel = try await NIOTSListenerBootstrap(group: self.eventLoopGroup)
175+
let bootstrap: NIOTSListenerBootstrap
176+
177+
let requireALPN: Bool
178+
let scheme: Scheme
179+
switch self.config.transportSecurity.wrapped {
180+
case .plaintext:
181+
requireALPN = false
182+
scheme = .http
183+
bootstrap = NIOTSListenerBootstrap(group: self.eventLoopGroup)
184+
185+
case .tls(let tlsConfig):
186+
requireALPN = tlsConfig.requireALPN
187+
scheme = .https
188+
bootstrap = NIOTSListenerBootstrap(group: self.eventLoopGroup)
189+
.tlsOptions(try NWProtocolTLS.Options(tlsConfig))
190+
}
191+
192+
let serverChannel =
193+
try await bootstrap
175194
.serverChannelOption(
176195
ChannelOptions.socketOption(.so_reuseaddr),
177196
value: 1
@@ -190,8 +209,8 @@ extension HTTP2ServerTransport {
190209
connectionConfig: self.config.connection,
191210
http2Config: self.config.http2,
192211
rpcConfig: self.config.rpc,
193-
requireALPN: false,
194-
scheme: .http
212+
requireALPN: requireALPN,
213+
scheme: scheme
195214
)
196215
}
197216
}
@@ -292,41 +311,55 @@ extension HTTP2ServerTransport.TransportServices {
292311
public struct Config: Sendable {
293312
/// Compression configuration.
294313
public var compression: HTTP2ServerTransport.Config.Compression
314+
295315
/// Connection configuration.
296316
public var connection: HTTP2ServerTransport.Config.Connection
317+
297318
/// HTTP2 configuration.
298319
public var http2: HTTP2ServerTransport.Config.HTTP2
320+
299321
/// RPC configuration.
300322
public var rpc: HTTP2ServerTransport.Config.RPC
301323

324+
/// The transport's security.
325+
public var transportSecurity: TransportSecurity
326+
302327
/// Construct a new `Config`.
303328
/// - Parameters:
304329
/// - compression: Compression configuration.
305330
/// - connection: Connection configuration.
306331
/// - http2: HTTP2 configuration.
307332
/// - rpc: RPC configuration.
333+
/// - transportSecurity: The transport's security configuration.
308334
public init(
309335
compression: HTTP2ServerTransport.Config.Compression,
310336
connection: HTTP2ServerTransport.Config.Connection,
311337
http2: HTTP2ServerTransport.Config.HTTP2,
312-
rpc: HTTP2ServerTransport.Config.RPC
338+
rpc: HTTP2ServerTransport.Config.RPC,
339+
transportSecurity: TransportSecurity
313340
) {
314341
self.compression = compression
315342
self.connection = connection
316343
self.http2 = http2
317344
self.rpc = rpc
345+
self.transportSecurity = transportSecurity
318346
}
319347

320348
/// Default values for the different configurations.
321349
///
322-
/// - Parameter configure: A closure which allows you to modify the defaults before
323-
/// returning them.
324-
public static func defaults(configure: (_ config: inout Self) -> Void = { _ in }) -> Self {
350+
/// - Parameters:
351+
/// - transportSecurity: The transport's security configuration.
352+
/// - configure: A closure which allows you to modify the defaults before returning them.
353+
public static func defaults(
354+
transportSecurity: TransportSecurity,
355+
configure: (_ config: inout Self) -> Void = { _ in }
356+
) -> Self {
325357
var config = Self(
326358
compression: .defaults,
327359
connection: .defaults,
328360
http2: .defaults,
329-
rpc: .defaults
361+
rpc: .defaults,
362+
transportSecurity: transportSecurity
330363
)
331364
configure(&config)
332365
return config
@@ -396,4 +429,38 @@ extension ServerTransport where Self == HTTP2ServerTransport.TransportServices {
396429
)
397430
}
398431
}
432+
433+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
434+
extension NWProtocolTLS.Options {
435+
convenience init(_ tlsConfig: HTTP2ServerTransport.TransportServices.Config.TLS) throws {
436+
self.init()
437+
438+
guard let sec_identity = sec_identity_create(try tlsConfig.identityProvider()) else {
439+
throw RuntimeError(
440+
code: .transportError,
441+
message: """
442+
There was an issue creating the SecIdentity required to set up TLS. \
443+
Please check your TLS configuration.
444+
"""
445+
)
446+
}
447+
448+
sec_protocol_options_set_local_identity(
449+
self.securityProtocolOptions,
450+
sec_identity
451+
)
452+
453+
sec_protocol_options_set_min_tls_protocol_version(
454+
self.securityProtocolOptions,
455+
.TLSv12
456+
)
457+
458+
for `protocol` in ["grpc-exp", "h2"] {
459+
sec_protocol_options_add_tls_application_protocol(
460+
self.securityProtocolOptions,
461+
`protocol`
462+
)
463+
}
464+
}
465+
}
399466
#endif
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
#if canImport(Network)
18+
public import Network
19+
20+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
21+
extension HTTP2ServerTransport.TransportServices.Config {
22+
/// The security configuration for this connection.
23+
public struct TransportSecurity: Sendable {
24+
package enum Wrapped: Sendable {
25+
case plaintext
26+
case tls(TLS)
27+
}
28+
29+
package let wrapped: Wrapped
30+
31+
/// This connection is plaintext: no encryption will take place.
32+
public static let plaintext = Self(wrapped: .plaintext)
33+
34+
/// This connection will use TLS.
35+
public static func tls(_ tls: TLS) -> Self {
36+
Self(wrapped: .tls(tls))
37+
}
38+
}
39+
40+
public struct TLS: Sendable {
41+
/// A provider for the `SecIdentity` to be used when setting up TLS.
42+
public var identityProvider: @Sendable () throws -> SecIdentity
43+
44+
/// Whether ALPN is required.
45+
///
46+
/// If this is set to `true` but the client does not support ALPN, then the connection will be rejected.
47+
public var requireALPN: Bool
48+
49+
/// Create a new HTTP2 NIO Transport Services transport TLS config, with some values defaulted:
50+
/// - `requireALPN` equals `false`
51+
///
52+
/// - Returns: A new HTTP2 NIO Transport Services transport TLS config.
53+
public static func defaults(
54+
identityProvider: @Sendable @escaping () throws -> SecIdentity
55+
) -> Self {
56+
Self(
57+
identityProvider: identityProvider,
58+
requireALPN: false
59+
)
60+
}
61+
}
62+
}
63+
#endif

Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,59 @@ import XCTest
2222

2323
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
2424
final class HTTP2TransportNIOTransportServicesTests: XCTestCase {
25+
private static let p12bundleURL = URL(fileURLWithPath: #filePath)
26+
.deletingLastPathComponent() // (this file)
27+
.deletingLastPathComponent() // GRPCHTTP2TransportTests
28+
.deletingLastPathComponent() // Tests
29+
.appendingPathComponent("Sources")
30+
.appendingPathComponent("GRPCSampleData")
31+
.appendingPathComponent("bundle")
32+
.appendingPathExtension("p12")
33+
34+
@Sendable private static func loadIdentity() throws -> SecIdentity {
35+
let data = try Data(contentsOf: Self.p12bundleURL)
36+
37+
var externalFormat = SecExternalFormat.formatUnknown
38+
var externalItemType = SecExternalItemType.itemTypeUnknown
39+
let passphrase = "password" as CFTypeRef
40+
var exportKeyParams = SecItemImportExportKeyParameters()
41+
exportKeyParams.passphrase = Unmanaged.passUnretained(passphrase)
42+
var items: CFArray?
43+
44+
let status = SecItemImport(
45+
data as CFData,
46+
"bundle.p12" as CFString,
47+
&externalFormat,
48+
&externalItemType,
49+
SecItemImportExportFlags(rawValue: 0),
50+
&exportKeyParams,
51+
nil,
52+
&items
53+
)
54+
55+
if status != errSecSuccess {
56+
XCTFail(
57+
"""
58+
Unable to load identity from '\(Self.p12bundleURL)'. \
59+
SecItemImport failed with status \(status)
60+
"""
61+
)
62+
} else if items == nil {
63+
XCTFail(
64+
"""
65+
Unable to load identity from '\(Self.p12bundleURL)'. \
66+
SecItemImport failed.
67+
"""
68+
)
69+
}
70+
71+
return ((items! as NSArray)[0] as! SecIdentity)
72+
}
73+
2574
func testGetListeningAddress_IPv4() async throws {
2675
let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices(
2776
address: .ipv4(host: "0.0.0.0", port: 0),
28-
config: .defaults()
77+
config: .defaults(transportSecurity: .plaintext)
2978
)
3079

3180
try await withThrowingDiscardingTaskGroup { group in
@@ -45,7 +94,7 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase {
4594
func testGetListeningAddress_IPv6() async throws {
4695
let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices(
4796
address: .ipv6(host: "::1", port: 0),
48-
config: .defaults()
97+
config: .defaults(transportSecurity: .plaintext)
4998
)
5099

51100
try await withThrowingDiscardingTaskGroup { group in
@@ -65,7 +114,7 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase {
65114
func testGetListeningAddress_UnixDomainSocket() async throws {
66115
let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices(
67116
address: .unixDomainSocket(path: "/tmp/niots-uds-test"),
68-
config: .defaults()
117+
config: .defaults(transportSecurity: .plaintext)
69118
)
70119
defer {
71120
// NIOTS does not unlink the UDS on close.
@@ -91,7 +140,7 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase {
91140
func testGetListeningAddress_InvalidAddress() async {
92141
let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices(
93142
address: .unixDomainSocket(path: "/this/should/be/an/invalid/path"),
94-
config: .defaults()
143+
config: .defaults(transportSecurity: .plaintext)
95144
)
96145

97146
try? await withThrowingDiscardingTaskGroup { group in
@@ -120,7 +169,7 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase {
120169
func testGetListeningAddress_StoppedListening() async throws {
121170
let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices(
122171
address: .ipv4(host: "0.0.0.0", port: 0),
123-
config: .defaults()
172+
config: .defaults(transportSecurity: .plaintext)
124173
)
125174

126175
try? await withThrowingDiscardingTaskGroup { group in
@@ -149,5 +198,14 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase {
149198
}
150199
}
151200
}
201+
202+
func testTLSConfig_Defaults() throws {
203+
let identityProvider = Self.loadIdentity
204+
let grpcTLSConfig = HTTP2ServerTransport.TransportServices.Config.TLS.defaults(
205+
identityProvider: identityProvider
206+
)
207+
XCTAssertEqual(try grpcTLSConfig.identityProvider(), try identityProvider())
208+
XCTAssertEqual(grpcTLSConfig.requireALPN, false)
209+
}
152210
}
153211
#endif

Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ final class HTTP2TransportTests: XCTestCase {
166166
let server = GRPCServer(
167167
transport: .http2NIOTS(
168168
address: .ipv4(host: "127.0.0.1", port: 0),
169-
config: .defaults {
169+
config: .defaults(transportSecurity: .plaintext) {
170170
$0.compression.enabledAlgorithms = compression
171171
}
172172
),

0 commit comments

Comments
 (0)