Coverage Report

Created: 2026-07-29 06:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/swift-nio/Sources/NIOPosix/RawSocketBootstrap.swift
Line
Count
Source
1
//===----------------------------------------------------------------------===//
2
//
3
// This source file is part of the SwiftNIO open source project
4
//
5
// Copyright (c) 2022 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
/// A `RawSocketBootstrap` is an easy way to interact with IP based protocols other then TCP and UDP.
20
///
21
/// Example:
22
///
23
/// ```swift
24
///     let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
25
///     defer {
26
///         try! group.syncShutdownGracefully()
27
///     }
28
///     let bootstrap = RawSocketBootstrap(group: group)
29
///         .channelInitializer { channel in
30
///             channel.pipeline.addHandler(MyChannelHandler())
31
///         }
32
///     let channel = try! bootstrap.bind(host: "127.0.0.1", ipProtocol: .icmp).wait()
33
///     /* the Channel is now ready to send/receive IP packets */
34
///
35
///     try channel.closeFuture.wait()  // Wait until the channel un-binds.
36
/// ```
37
///
38
/// The `Channel` will operate on `AddressedEnvelope<ByteBuffer>` as inbound and outbound messages.
39
public final class NIORawSocketBootstrap {
40
41
    private let group: EventLoopGroup
42
    private var channelInitializer: Optional<ChannelInitializerCallback>
43
    @usableFromInline
44
    internal var _channelOptions: ChannelOptions.Storage
45
46
    /// Create a `RawSocketBootstrap` on the `EventLoopGroup` `group`.
47
    ///
48
    /// The `EventLoopGroup` `group` must be compatible, otherwise the program will crash. `RawSocketBootstrap` is
49
    /// compatible only with `MultiThreadedEventLoopGroup` as well as the `EventLoop`s returned by
50
    /// `MultiThreadedEventLoopGroup.next`. See `init(validatingGroup:)` for a fallible initializer for
51
    /// situations where it's impossible to tell ahead of time if the `EventLoopGroup` is compatible or not.
52
    ///
53
    /// - Parameters:
54
    ///   - group: The `EventLoopGroup` to use.
55
0
    public convenience init(group: EventLoopGroup) {
56
0
        guard NIOOnSocketsBootstraps.isCompatible(group: group) else {
57
0
            preconditionFailure(
58
0
                "RawSocketBootstrap is only compatible with MultiThreadedEventLoopGroup and "
59
0
                    + "SelectableEventLoop. You tried constructing one with \(group) which is incompatible."
60
0
            )
61
0
        }
62
0
        self.init(validatingGroup: group)!
63
0
    }
64
65
    /// Create a `RawSocketBootstrap` on the `EventLoopGroup` `group`, validating that `group` is compatible.
66
    ///
67
    /// - Parameters:
68
    ///   - group: The `EventLoopGroup` to use.
69
0
    public init?(validatingGroup group: EventLoopGroup) {
70
0
        guard NIOOnSocketsBootstraps.isCompatible(group: group) else {
71
0
            return nil
72
0
        }
73
0
        self._channelOptions = ChannelOptions.Storage()
74
0
        self.group = group
75
0
        self.channelInitializer = nil
76
0
    }
77
78
    /// Initialize the bound `Channel` with `initializer`. The most common task in initializer is to add
79
    /// `ChannelHandler`s to the `ChannelPipeline`.
80
    ///
81
    /// - Parameters:
82
    ///   - handler: A closure that initializes the provided `Channel`.
83
0
    public func channelInitializer(_ handler: @escaping @Sendable (Channel) -> EventLoopFuture<Void>) -> Self {
84
0
        self.channelInitializer = handler
85
0
        return self
86
0
    }
87
88
    /// Specifies a `ChannelOption` to be applied to the `Channel`.
89
    ///
90
    /// - Parameters:
91
    ///   - option: The option to be applied.
92
    ///   - value: The value for the option.
93
    @inlinable
94
0
    public func channelOption<Option: ChannelOption>(_ option: Option, value: Option.Value) -> Self {
95
0
        self._channelOptions.append(key: option, value: value)
96
0
        return self
97
0
    }
98
99
    /// Bind the `Channel` to `host`.
100
    /// All packets or errors matching the `ipProtocol` specified are passed to the resulting `Channel`.
101
    ///
102
    /// - Parameters:
103
    ///   - host: The host to bind on.
104
    ///   - ipProtocol: The IP protocol used in the IP protocol/nextHeader field.
105
0
    public func bind(host: String, ipProtocol: NIOIPProtocol) -> EventLoopFuture<Channel> {
106
0
        bind0(ipProtocol: ipProtocol) {
107
0
            try SocketAddress.makeAddressResolvingHost(host, port: 0)
108
0
        }
109
0
    }
110
111
    private func bind0(
112
        ipProtocol: NIOIPProtocol,
113
        _ makeSocketAddress: () throws -> SocketAddress
114
0
    ) -> EventLoopFuture<Channel> {
115
0
        let address: SocketAddress
116
0
        do {
117
0
            address = try makeSocketAddress()
118
0
        } catch {
119
0
            return group.next().makeFailedFuture(error)
120
0
        }
121
0
        precondition(address.port == nil || address.port == 0, "port must be 0 or not set")
122
0
        func makeChannel(_ eventLoop: SelectableEventLoop) throws -> DatagramChannel {
123
0
            try DatagramChannel(
124
0
                eventLoop: eventLoop,
125
0
                protocolFamily: address.protocol,
126
0
                protocolSubtype: .init(ipProtocol),
127
0
                socketType: .raw
128
0
            )
129
0
        }
130
0
        return withNewChannel(makeChannel: makeChannel) { (eventLoop, channel) in
131
0
            channel.register().flatMap {
132
0
                channel.bind(to: address)
133
0
            }
134
0
        }
135
0
    }
136
137
    /// Connect the `Channel` to `host`.
138
    ///
139
    /// - Parameters:
140
    ///   - host: The host to connect to.
141
    ///   - ipProtocol: The IP protocol used in the IP protocol/nextHeader field.
142
0
    public func connect(host: String, ipProtocol: NIOIPProtocol) -> EventLoopFuture<Channel> {
143
0
        connect0(ipProtocol: ipProtocol) {
144
0
            try SocketAddress.makeAddressResolvingHost(host, port: 0)
145
0
        }
146
0
    }
147
148
    private func connect0(
149
        ipProtocol: NIOIPProtocol,
150
        _ makeSocketAddress: () throws -> SocketAddress
151
0
    ) -> EventLoopFuture<Channel> {
152
0
        let address: SocketAddress
153
0
        do {
154
0
            address = try makeSocketAddress()
155
0
        } catch {
156
0
            return group.next().makeFailedFuture(error)
157
0
        }
158
0
        func makeChannel(_ eventLoop: SelectableEventLoop) throws -> DatagramChannel {
159
0
            try DatagramChannel(
160
0
                eventLoop: eventLoop,
161
0
                protocolFamily: address.protocol,
162
0
                protocolSubtype: .init(ipProtocol),
163
0
                socketType: .raw
164
0
            )
165
0
        }
166
0
        return withNewChannel(makeChannel: makeChannel) { (eventLoop, channel) in
167
0
            channel.register().flatMap {
168
0
                channel.connect(to: address)
169
0
            }
170
0
        }
171
0
    }
172
173
    private func withNewChannel(
174
        makeChannel: (_ eventLoop: SelectableEventLoop) throws -> DatagramChannel,
175
        _ bringup: @escaping @Sendable (EventLoop, DatagramChannel) -> EventLoopFuture<Void>
176
0
    ) -> EventLoopFuture<Channel> {
177
0
        let eventLoop = self.group.next()
178
0
        let channelInitializer = self.channelInitializer ?? { @Sendable _ in eventLoop.makeSucceededFuture(()) }
Unexecuted instantiation: $s8NIOPosix21NIORawSocketBootstrapC14withNewChannel33_05F8667EF5648375E7110E8ED8CF5F24LL04makeG0_7NIOCore15EventLoopFutureCyAG0G0_pGAA08DatagramG0CAA010SelectableqR0CKXE_AIyytGAG0qR0_p_AMtYbctFApgJ_pYbcyKXEfu_
Unexecuted instantiation: $s8NIOPosix21NIORawSocketBootstrapC14withNewChannel33_05F8667EF5648375E7110E8ED8CF5F24LL04makeG0_7NIOCore15EventLoopFutureCyAG0G0_pGAA08DatagramG0CAA010SelectableqR0CKXE_AIyytGAG0qR0_p_AMtYbctFApgJ_pYbcyKXEfu_ApgJ_pYbcfU_
179
0
        let channelOptions = self._channelOptions
180
0
181
0
        let channel: DatagramChannel
182
0
        do {
183
0
            channel = try makeChannel(eventLoop as! SelectableEventLoop)
184
0
        } catch {
185
0
            return eventLoop.makeFailedFuture(error)
186
0
        }
187
0
188
0
        @Sendable
189
0
        func setupChannel() -> EventLoopFuture<Channel> {
190
0
            eventLoop.assertInEventLoop()
191
0
            return channelOptions.applyAllChannelOptions(to: channel).flatMap {
192
0
                channelInitializer(channel)
193
0
            }.flatMap {
194
0
                eventLoop.assertInEventLoop()
195
0
                return bringup(eventLoop, channel)
196
0
            }.map {
197
0
                channel
198
0
            }.flatMapError { error in
199
0
                eventLoop.makeFailedFuture(error)
200
0
            }
201
0
        }
202
0
203
0
        if eventLoop.inEventLoop {
204
0
            return setupChannel()
205
0
        } else {
206
0
            return eventLoop.flatSubmit {
207
0
                setupChannel()
208
0
            }
209
0
        }
210
0
    }
211
}
212
213
// MARK: Async connect/bind methods
214
215
extension NIORawSocketBootstrap {
216
    /// Bind the `Channel` to `host`.
217
    /// All packets or errors matching the `ipProtocol` specified are passed to the resulting `Channel`.
218
    ///
219
    /// - Parameters:
220
    ///   - host: The host to bind on.
221
    ///   - ipProtocol: The IP protocol used in the IP protocol/nextHeader field.
222
    ///   - channelInitializer: A closure to initialize the channel. The return value of this closure is returned from the `bind`
223
    ///   method.
224
    /// - Returns: The result of the channel initializer.
225
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
226
    public func bind<Output: Sendable>(
227
        host: String,
228
        ipProtocol: NIOIPProtocol,
229
        channelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture<Output>
230
0
    ) async throws -> Output {
231
0
        try await self.bind0(
232
0
            host: host,
233
0
            ipProtocol: ipProtocol,
234
0
            channelInitializer: channelInitializer,
235
0
            postRegisterTransformation: { $1.makeSucceededFuture($0) }
236
0
        )
237
0
    }
238
239
    /// Connect the `Channel` to `host`.
240
    ///
241
    /// - Parameters:
242
    ///   - host: The host to connect to.
243
    ///   - ipProtocol: The IP protocol used in the IP protocol/nextHeader field.
244
    ///   - channelInitializer: A closure to initialize the channel. The return value of this closure is returned from the `connect`
245
    ///   method.
246
    /// - Returns: The result of the channel initializer.
247
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
248
    public func connect<Output: Sendable>(
249
        host: String,
250
        ipProtocol: NIOIPProtocol,
251
        channelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture<Output>
252
0
    ) async throws -> Output {
253
0
        try await self.connect0(
254
0
            host: host,
255
0
            ipProtocol: ipProtocol,
256
0
            channelInitializer: channelInitializer,
257
0
            postRegisterTransformation: { $1.makeSucceededFuture($0) }
258
0
        )
259
0
    }
260
261
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
262
    private func connect0<ChannelInitializerResult: Sendable, PostRegistrationTransformationResult: Sendable>(
263
        host: String,
264
        ipProtocol: NIOIPProtocol,
265
        channelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture<ChannelInitializerResult>,
266
        postRegisterTransformation:
267
            @escaping @Sendable (ChannelInitializerResult, EventLoop) -> EventLoopFuture<
268
                PostRegistrationTransformationResult
269
            >
270
0
    ) async throws -> PostRegistrationTransformationResult {
271
0
        let address = try SocketAddress.makeAddressResolvingHost(host, port: 0)
272
0
273
0
        func makeChannel(_ eventLoop: SelectableEventLoop) throws -> DatagramChannel {
274
0
            try DatagramChannel(
275
0
                eventLoop: eventLoop,
276
0
                protocolFamily: address.protocol,
277
0
                protocolSubtype: .init(ipProtocol),
278
0
                socketType: .raw
279
0
            )
280
0
        }
281
0
282
0
        return try await self.makeConfiguredChannel(
283
0
            makeChannel: makeChannel(_:),
284
0
            channelInitializer: channelInitializer,
285
0
            registration: { channel in
286
0
                channel.register().flatMap {
287
0
                    channel.connect(to: address)
288
0
                }
289
0
            },
290
0
            postRegisterTransformation: postRegisterTransformation
291
0
        ).get()
292
0
    }
293
294
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
295
    private func bind0<ChannelInitializerResult: Sendable, PostRegistrationTransformationResult: Sendable>(
296
        host: String,
297
        ipProtocol: NIOIPProtocol,
298
        channelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture<ChannelInitializerResult>,
299
        postRegisterTransformation:
300
            @escaping @Sendable (ChannelInitializerResult, EventLoop) -> EventLoopFuture<
301
                PostRegistrationTransformationResult
302
            >
303
0
    ) async throws -> PostRegistrationTransformationResult {
304
0
        let address = try SocketAddress.makeAddressResolvingHost(host, port: 0)
305
0
306
0
        precondition(address.port == nil || address.port == 0, "port must be 0 or not set")
307
0
        func makeChannel(_ eventLoop: SelectableEventLoop) throws -> DatagramChannel {
308
0
            try DatagramChannel(
309
0
                eventLoop: eventLoop,
310
0
                protocolFamily: address.protocol,
311
0
                protocolSubtype: .init(ipProtocol),
312
0
                socketType: .raw
313
0
            )
314
0
        }
315
0
316
0
        return try await self.makeConfiguredChannel(
317
0
            makeChannel: makeChannel(_:),
318
0
            channelInitializer: channelInitializer,
319
0
            registration: { channel in
320
0
                channel.register().flatMap {
321
0
                    channel.bind(to: address)
322
0
                }
323
0
            },
324
0
            postRegisterTransformation: postRegisterTransformation
325
0
        ).get()
326
0
    }
327
328
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
329
    private func makeConfiguredChannel<
330
        ChannelInitializerResult: Sendable,
331
        PostRegistrationTransformationResult: Sendable
332
    >(
333
        makeChannel: (_ eventLoop: SelectableEventLoop) throws -> DatagramChannel,
334
        channelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture<ChannelInitializerResult>,
335
        registration: @escaping @Sendable (Channel) -> EventLoopFuture<Void>,
336
        postRegisterTransformation:
337
            @escaping @Sendable (ChannelInitializerResult, EventLoop) -> EventLoopFuture<
338
                PostRegistrationTransformationResult
339
            >
340
0
    ) -> EventLoopFuture<PostRegistrationTransformationResult> {
341
0
        let eventLoop = self.group.next()
342
0
        let bootstrapInitializer = self.channelInitializer ?? { @Sendable _ in eventLoop.makeSucceededFuture(()) }
Unexecuted instantiation: $s8NIOPosix21NIORawSocketBootstrapC21makeConfiguredChannel33_05F8667EF5648375E7110E8ED8CF5F24LL0eG018channelInitializer12registration26postRegisterTransformation7NIOCore15EventLoopFutureCyq_GAA08DatagramG0CAA010SelectablevW0CKXE_ALyxGAJ0G0_pYbcALyytGAjS_pYbcAMx_AJ0vW0_ptYbcts8SendableRzsAVR_r0_lFAtjS_pYbcyKXEfu_
Unexecuted instantiation: $s8NIOPosix21NIORawSocketBootstrapC21makeConfiguredChannel33_05F8667EF5648375E7110E8ED8CF5F24LL0eG018channelInitializer12registration26postRegisterTransformation7NIOCore15EventLoopFutureCyq_GAA08DatagramG0CAA010SelectablevW0CKXE_ALyxGAJ0G0_pYbcALyytGAjS_pYbcAMx_AJ0vW0_ptYbcts8SendableRzsAVR_r0_lFAtjS_pYbcyKXEfu_AtjS_pYbcfU_
343
0
        let channelInitializer = { @Sendable (channel: Channel) -> EventLoopFuture<ChannelInitializerResult> in
344
0
            bootstrapInitializer(channel).flatMap { channelInitializer(channel) }
345
0
        }
346
0
        let channelOptions = self._channelOptions
347
0
348
0
        let channel: DatagramChannel
349
0
        do {
350
0
            channel = try makeChannel(eventLoop as! SelectableEventLoop)
351
0
        } catch {
352
0
            return eventLoop.makeFailedFuture(error)
353
0
        }
354
0
355
0
        @Sendable
356
0
        func setupChannel() -> EventLoopFuture<PostRegistrationTransformationResult> {
357
0
            eventLoop.assertInEventLoop()
358
0
            return channelOptions.applyAllChannelOptions(to: channel).flatMap {
359
0
                channelInitializer(channel)
360
0
            }.flatMap { (result: ChannelInitializerResult) in
361
0
                eventLoop.assertInEventLoop()
362
0
                return registration(channel).map {
363
0
                    result
364
0
                }
365
0
            }.flatMap { (result: ChannelInitializerResult) -> EventLoopFuture<PostRegistrationTransformationResult> in
366
0
                postRegisterTransformation(result, eventLoop)
367
0
            }.flatMapError { error in
368
0
                eventLoop.assertInEventLoop()
369
0
                channel.close0(error: error, mode: .all, promise: nil)
370
0
                return channel.eventLoop.makeFailedFuture(error)
371
0
            }
372
0
        }
373
0
374
0
        if eventLoop.inEventLoop {
375
0
            return setupChannel()
376
0
        } else {
377
0
            return eventLoop.flatSubmit {
378
0
                setupChannel()
379
0
            }
380
0
        }
381
0
    }
382
}
383
384
@available(*, unavailable)
385
extension NIORawSocketBootstrap: Sendable {}
386
#endif  // !os(WASI)