Skip to content

Commit 1f0d07c

Browse files
committed
Add support for starting a server from accepted conns
Motivation: Some applications receive a file descriptor for an already accepted TCP connection. There's currently no API to create a server from an fd like this. Modifications: - Add an API to the server builder allowing a server to be created from an accepted socket - Add tests Result: Can create a server from an accepted socket fd
1 parent bdff45d commit 1f0d07c

File tree

3 files changed

+475
-68
lines changed

3 files changed

+475
-68
lines changed

Sources/GRPC/Server.swift

Lines changed: 157 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -98,25 +98,7 @@ public final class Server: @unchecked Sendable {
9898
}
9999

100100
#if canImport(NIOSSL)
101-
// Making a `NIOSSLContext` is expensive, we should only do it once per TLS configuration so
102-
// we'll do it now, before accepting connections. Unfortunately our API isn't throwing so we'll
103-
// only surface any error when initializing a child channel.
104-
//
105-
// 'nil' means we're not using TLS, or we're using the Network.framework TLS backend. If we're
106-
// using the Network.framework TLS backend we'll apply the settings just below.
107-
let sslContext: Result<NIOSSLContext, Error>?
108-
109-
if let tlsConfiguration = configuration.tlsConfiguration {
110-
do {
111-
sslContext = try tlsConfiguration.makeNIOSSLContext().map { .success($0) }
112-
} catch {
113-
sslContext = .failure(error)
114-
}
115-
116-
} else {
117-
// No TLS configuration, no SSL context.
118-
sslContext = nil
119-
}
101+
let sslContext = Self.makeNIOSSLContext(configuration: configuration)
120102
#endif // canImport(NIOSSL)
121103

122104
#if canImport(Network)
@@ -152,53 +134,10 @@ public final class Server: @unchecked Sendable {
152134
)
153135
// Set the handlers that are applied to the accepted Channels
154136
.childChannelInitializer { channel in
155-
var configuration = configuration
156-
configuration.logger[metadataKey: MetadataKey.connectionID] = "\(UUID().uuidString)"
157-
configuration.logger.addIPAddressMetadata(
158-
local: channel.localAddress,
159-
remote: channel.remoteAddress
160-
)
161-
162-
do {
163-
let sync = channel.pipeline.syncOperations
137+
Self.configureAcceptedChannel(channel, configuration: configuration) { sync in
164138
#if canImport(NIOSSL)
165-
if let sslContext = try sslContext?.get() {
166-
let sslHandler: NIOSSLServerHandler
167-
if let verify = configuration.tlsConfiguration?.nioSSLCustomVerificationCallback {
168-
sslHandler = NIOSSLServerHandler(
169-
context: sslContext,
170-
customVerificationCallback: verify
171-
)
172-
} else {
173-
sslHandler = NIOSSLServerHandler(context: sslContext)
174-
}
175-
176-
try sync.addHandler(sslHandler)
177-
}
139+
try Self.addNIOSSLHandler(sslContext, configuration: configuration, sync: sync)
178140
#endif // canImport(NIOSSL)
179-
180-
// Configures the pipeline based on whether the connection uses TLS or not.
181-
try sync.addHandler(GRPCServerPipelineConfigurator(configuration: configuration))
182-
183-
// Work around the zero length write issue, if needed.
184-
let requiresZeroLengthWorkaround = PlatformSupport.requiresZeroLengthWriteWorkaround(
185-
group: configuration.eventLoopGroup,
186-
hasTLS: configuration.tlsConfiguration != nil
187-
)
188-
if requiresZeroLengthWorkaround,
189-
#available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
190-
{
191-
try sync.addHandler(NIOFilterEmptyWritesHandler())
192-
}
193-
} catch {
194-
return channel.eventLoop.makeFailedFuture(error)
195-
}
196-
197-
// Run the debug initializer, if there is one.
198-
if let debugAcceptedChannelInitializer = configuration.debugChannelInitializer {
199-
return debugAcceptedChannelInitializer(channel)
200-
} else {
201-
return channel.eventLoop.makeSucceededVoidFuture()
202141
}
203142
}
204143

@@ -210,11 +149,108 @@ public final class Server: @unchecked Sendable {
210149
)
211150
}
212151

152+
#if canImport(NIOSSL)
153+
private static func makeNIOSSLContext(
154+
configuration: Configuration
155+
) -> Result<NIOSSLContext, Error>? {
156+
// Making a `NIOSSLContext` is expensive, we should only do it once per TLS configuration so
157+
// we'll do it now, before accepting connections. Unfortunately our API isn't throwing so we'll
158+
// only surface any error when initializing a child channel.
159+
//
160+
// 'nil' means we're not using TLS, or we're using the Network.framework TLS backend. If we're
161+
// using the Network.framework TLS backend we'll apply the settings just below.
162+
let sslContext: Result<NIOSSLContext, Error>?
163+
164+
if let tlsConfiguration = configuration.tlsConfiguration {
165+
do {
166+
sslContext = try tlsConfiguration.makeNIOSSLContext().map { .success($0) }
167+
} catch {
168+
sslContext = .failure(error)
169+
}
170+
171+
} else {
172+
// No TLS configuration, no SSL context.
173+
sslContext = nil
174+
}
175+
176+
return sslContext
177+
}
178+
179+
private static func addNIOSSLHandler(
180+
_ sslContext: Result<NIOSSLContext, Error>?,
181+
configuration: Configuration,
182+
sync: ChannelPipeline.SynchronousOperations
183+
) throws {
184+
if let sslContext = try sslContext?.get() {
185+
let sslHandler: NIOSSLServerHandler
186+
if let verify = configuration.tlsConfiguration?.nioSSLCustomVerificationCallback {
187+
sslHandler = NIOSSLServerHandler(
188+
context: sslContext,
189+
customVerificationCallback: verify
190+
)
191+
} else {
192+
sslHandler = NIOSSLServerHandler(context: sslContext)
193+
}
194+
195+
try sync.addHandler(sslHandler)
196+
}
197+
}
198+
#endif // canImport(NIOSSL)
199+
200+
private static func configureAcceptedChannel(
201+
_ channel: Channel,
202+
configuration: Configuration,
203+
addNIOSSLIfNecessary: (ChannelPipeline.SynchronousOperations) throws -> Void
204+
) -> EventLoopFuture<Void> {
205+
var configuration = configuration
206+
configuration.logger[metadataKey: MetadataKey.connectionID] = "\(UUID().uuidString)"
207+
configuration.logger.addIPAddressMetadata(
208+
local: channel.localAddress,
209+
remote: channel.remoteAddress
210+
)
211+
212+
do {
213+
let sync = channel.pipeline.syncOperations
214+
try addNIOSSLIfNecessary(sync)
215+
216+
// Configures the pipeline based on whether the connection uses TLS or not.
217+
try sync.addHandler(GRPCServerPipelineConfigurator(configuration: configuration))
218+
219+
// Work around the zero length write issue, if needed.
220+
let requiresZeroLengthWorkaround = PlatformSupport.requiresZeroLengthWriteWorkaround(
221+
group: configuration.eventLoopGroup,
222+
hasTLS: configuration.tlsConfiguration != nil
223+
)
224+
if requiresZeroLengthWorkaround,
225+
#available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
226+
{
227+
try sync.addHandler(NIOFilterEmptyWritesHandler())
228+
}
229+
} catch {
230+
return channel.eventLoop.makeFailedFuture(error)
231+
}
232+
233+
// Run the debug initializer, if there is one.
234+
if let debugAcceptedChannelInitializer = configuration.debugChannelInitializer {
235+
return debugAcceptedChannelInitializer(channel)
236+
} else {
237+
return channel.eventLoop.makeSucceededVoidFuture()
238+
}
239+
}
240+
213241
/// Starts a server with the given configuration. See `Server.Configuration` for the options
214242
/// available to configure the server.
215243
public static func start(configuration: Configuration) -> EventLoopFuture<Server> {
216-
let quiescingHelper = ServerQuiescingHelper(group: configuration.eventLoopGroup)
244+
switch configuration.target.wrapped {
245+
case .connectedSocket(let handle) where configuration.connectedSocketTargetIsAcceptedConnection:
246+
return Self.startServerFromAcceptedConnection(handle: handle, configuration: configuration)
247+
case .connectedSocket, .hostAndPort, .unixDomainSocket, .socketAddress, .vsockAddress:
248+
return Self.startServer(configuration: configuration)
249+
}
250+
}
217251

252+
private static func startServer(configuration: Configuration) -> EventLoopFuture<Server> {
253+
let quiescingHelper = ServerQuiescingHelper(group: configuration.eventLoopGroup)
218254
return self.makeBootstrap(configuration: configuration)
219255
.serverChannelInitializer { channel in
220256
channel.pipeline.addHandler(quiescingHelper.makeServerChannelHandler(channel: channel))
@@ -229,13 +265,53 @@ public final class Server: @unchecked Sendable {
229265
}
230266
}
231267

268+
private static func startServerFromAcceptedConnection(
269+
handle: NIOBSDSocket.Handle,
270+
configuration: Configuration
271+
) -> EventLoopFuture<Server> {
272+
guard let bootstrap = ClientBootstrap(validatingGroup: configuration.eventLoopGroup) else {
273+
let status = GRPCStatus(
274+
code: .unimplemented,
275+
message: """
276+
You must use a NIOPosix EventLoopGroup to create a server from an already accepted \
277+
socket.
278+
"""
279+
)
280+
return configuration.eventLoopGroup.any().makeFailedFuture(status)
281+
}
282+
283+
#if canImport(NIOSSL)
284+
let sslContext = Self.makeNIOSSLContext(configuration: configuration)
285+
#endif // canImport(NIOSSL)
286+
287+
return bootstrap.channelInitializer { channel in
288+
Self.configureAcceptedChannel(channel, configuration: configuration) { sync in
289+
#if canImport(NIOSSL)
290+
try Self.addNIOSSLHandler(sslContext, configuration: configuration, sync: sync)
291+
#endif // canImport(NIOSSL)
292+
}
293+
}.withConnectedSocket(handle).map { channel in
294+
Server(
295+
channel: channel,
296+
quiescingHelper: nil,
297+
errorDelegate: configuration.errorDelegate
298+
)
299+
}
300+
}
301+
302+
/// The listening server channel.
303+
///
304+
/// If the server was created from an already accepted connection then this channel will
305+
/// be for the accepted connection.
232306
public let channel: Channel
233-
private let quiescingHelper: ServerQuiescingHelper
307+
308+
/// Quiescing helper. `nil` if `channel` is for an accepted connection.
309+
private let quiescingHelper: ServerQuiescingHelper?
234310
private var errorDelegate: ServerErrorDelegate?
235311

236312
private init(
237313
channel: Channel,
238-
quiescingHelper: ServerQuiescingHelper,
314+
quiescingHelper: ServerQuiescingHelper?,
239315
errorDelegate: ServerErrorDelegate?
240316
) {
241317
self.channel = channel
@@ -264,7 +340,13 @@ public final class Server: @unchecked Sendable {
264340
/// Initiates a graceful shutdown. Existing RPCs may run to completion, any new RPCs or
265341
/// connections will be rejected.
266342
public func initiateGracefulShutdown(promise: EventLoopPromise<Void>?) {
267-
self.quiescingHelper.initiateShutdown(promise: promise)
343+
if let quiescingHelper = self.quiescingHelper {
344+
quiescingHelper.initiateShutdown(promise: promise)
345+
} else {
346+
// No quiescing helper: the channel must be for an already accepted connection.
347+
self.channel.closeFuture.cascade(to: promise)
348+
self.channel.pipeline.fireUserInboundEventTriggered(ChannelShouldQuiesceEvent())
349+
}
268350
}
269351

270352
/// Initiates a graceful shutdown. Existing RPCs may run to completion, any new RPCs or
@@ -436,6 +518,13 @@ extension Server {
436518
/// CORS configuration for gRPC-Web support.
437519
public var webCORS = Configuration.CORS()
438520

521+
/// Indicates whether a `connectedSocket` ``target`` is treated as an accepted connection.
522+
///
523+
/// If ``target`` is a `connectedSocket` then this flag indicates whether that socket is for
524+
/// an already accepted connection. If the value is `false` then the socket is treated as a
525+
/// listener. This value is ignored if ``target`` is any value other than `connectedSocket`.
526+
public var connectedSocketTargetIsAcceptedConnection: Bool = false
527+
439528
#if canImport(NIOSSL)
440529
/// Create a `Configuration` with some pre-defined defaults.
441530
///

Sources/GRPC/ServerBuilder.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,24 @@ extension Server {
7979
self.configuration.tlsConfiguration = self.maybeTLS
8080
return Server.start(configuration: self.configuration)
8181
}
82+
83+
/// Create a gRPC server from the file descriptor of an already accepted TCP connection.
84+
///
85+
/// - Parameter handle: The handle to the accepted socket.
86+
/// - Important: This is only supported with `NIOPosix` (i.e. when using a
87+
/// `MultiThreadedEventLoopGroup` or one of its loops and TLS configured via `NIOSSL`).
88+
/// - Warning: By calling this function you hand responsibility of the socket to gRPC.
89+
/// Crucially you must **not** close the socket directly after calling this function, gRPC
90+
/// will do it for you.
91+
/// - Returns: A configured gRPC server.
92+
public func fromAcceptedConnection(
93+
takingOwnershipOf handle: NIOBSDSocket.Handle
94+
) -> EventLoopFuture<Server> {
95+
self.configuration.target = .connectedSocket(handle)
96+
self.configuration.connectedSocketTargetIsAcceptedConnection = true
97+
self.configuration.tlsConfiguration = self.maybeTLS
98+
return Server.start(configuration: self.configuration)
99+
}
82100
}
83101
}
84102

0 commit comments

Comments
 (0)