Skip to content

Commit 9d1f281

Browse files
authored
add support to bind to a specific device (#1)
1 parent 7a72794 commit 9d1f281

File tree

4 files changed

+86
-2
lines changed

4 files changed

+86
-2
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Foundation
2+
import NIOCore
3+
4+
extension ChannelOption where Self == ChannelOptions.Types.SocketOption {
5+
public static func ipv6Option(_ name: NIOBSDSocket.Option) -> Self {
6+
.init(level: .ipv6, name: name)
7+
}
8+
}

Sources/WebSocketKit/WebSocket+Connect.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ extension WebSocket {
3737
/// - Parameters:
3838
/// - url: URL for the WebSocket server.
3939
/// - headers: Headers to send to the WebSocket server.
40+
/// - queueSize: the size of the buffer queue.
41+
/// - deviceName: the device to which the data will be sent.
4042
/// - configuration: Configuration for the WebSocket client.
4143
/// - eventLoopGroup: Event loop group to be used by the WebSocket client.
4244
/// - onUpgrade: An escaping closure to be executed after the upgrade is completed by `NIOWebSocketClientUpgrader`.
@@ -46,6 +48,7 @@ extension WebSocket {
4648
to url: URL,
4749
headers: HTTPHeaders = [:],
4850
queueSize: Int? = nil,
51+
deviceName: String? = nil,
4952
configuration: WebSocketClient.Configuration = .init(),
5053
on eventLoopGroup: EventLoopGroup,
5154
onUpgrade: @Sendable @escaping (WebSocket) -> ()
@@ -59,6 +62,7 @@ extension WebSocket {
5962
query: url.query,
6063
queueSize: queueSize,
6164
headers: headers,
65+
deviceName: deviceName,
6266
configuration: configuration,
6367
on: eventLoopGroup,
6468
onUpgrade: onUpgrade
@@ -74,6 +78,7 @@ extension WebSocket {
7478
/// - path: Path component of the URI for the WebSocket server.
7579
/// - query: Query component of the URI for the WebSocket server.
7680
/// - headers: Headers to send to the WebSocket server.
81+
/// - deviceName: the device to which the data will be sent.
7782
/// - configuration: Configuration for the WebSocket client.
7883
/// - eventLoopGroup: Event loop group to be used by the WebSocket client.
7984
/// - onUpgrade: An escaping closure to be executed after the upgrade is completed by `NIOWebSocketClientUpgrader`.
@@ -87,6 +92,7 @@ extension WebSocket {
8792
query: String? = nil,
8893
queueSize: Int? = nil,
8994
headers: HTTPHeaders = [:],
95+
deviceName: String? = nil,
9096
configuration: WebSocketClient.Configuration = .init(),
9197
on eventLoopGroup: EventLoopGroup,
9298
onUpgrade: @Sendable @escaping (WebSocket) -> ()
@@ -102,6 +108,7 @@ extension WebSocket {
102108
query: query,
103109
headers: headers,
104110
maxQueueSize: queueSize,
111+
deviceName: deviceName,
105112
onUpgrade: onUpgrade
106113
)
107114
}
@@ -119,6 +126,7 @@ extension WebSocket {
119126
/// - proxyPort: Port on which to connect to the proxy server.
120127
/// - proxyHeaders: Headers to send to the proxy server.
121128
/// - proxyConnectDeadline: Deadline for establishing the proxy connection.
129+
/// - deviceName: the device to which the data will be sent.
122130
/// - configuration: Configuration for the WebSocket client.
123131
/// - eventLoopGroup: Event loop group to be used by the WebSocket client.
124132
/// - onUpgrade: An escaping closure to be executed after the upgrade is completed by `NIOWebSocketClientUpgrader`.
@@ -135,6 +143,7 @@ extension WebSocket {
135143
proxyPort: Int? = nil,
136144
proxyHeaders: HTTPHeaders = [:],
137145
proxyConnectDeadline: NIODeadline = NIODeadline.distantFuture,
146+
deviceName: String? = nil,
138147
configuration: WebSocketClient.Configuration = .init(),
139148
on eventLoopGroup: EventLoopGroup,
140149
onUpgrade: @Sendable @escaping (WebSocket) -> ()
@@ -153,6 +162,7 @@ extension WebSocket {
153162
proxyPort: proxyPort,
154163
proxyHeaders: proxyHeaders,
155164
proxyConnectDeadline: proxyConnectDeadline,
165+
deviceName: deviceName,
156166
onUpgrade: onUpgrade
157167
)
158168
}
@@ -166,6 +176,7 @@ extension WebSocket {
166176
/// - proxyPort: Port on which to connect to the proxy server.
167177
/// - proxyHeaders: Headers to send to the proxy server.
168178
/// - proxyConnectDeadline: Deadline for establishing the proxy connection.
179+
/// - deviceName: the device to which the data will be sent.
169180
/// - configuration: Configuration for the WebSocket client.
170181
/// - eventLoopGroup: Event loop group to be used by the WebSocket client.
171182
/// - onUpgrade: An escaping closure to be executed after the upgrade is completed by `NIOWebSocketClientUpgrader`.
@@ -178,6 +189,7 @@ extension WebSocket {
178189
proxyPort: Int? = nil,
179190
proxyHeaders: HTTPHeaders = [:],
180191
proxyConnectDeadline: NIODeadline = NIODeadline.distantFuture,
192+
deviceName: String? = nil,
181193
configuration: WebSocketClient.Configuration = .init(),
182194
on eventLoopGroup: EventLoopGroup,
183195
onUpgrade: @Sendable @escaping (WebSocket) -> ()
@@ -197,6 +209,7 @@ extension WebSocket {
197209
proxyPort: proxyPort,
198210
proxyHeaders: proxyHeaders,
199211
proxyConnectDeadline: proxyConnectDeadline,
212+
deviceName: deviceName,
200213
on: eventLoopGroup,
201214
onUpgrade: onUpgrade
202215
)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Foundation
2+
import NIOCore
3+
4+
extension NIOBSDSocket.Option {
5+
#if canImport(Darwin)
6+
public static let ip_bound_if: NIOBSDSocket.Option = Self(rawValue: IP_BOUND_IF)
7+
public static let ipv6_bound_if: NIOBSDSocket.Option = Self(rawValue: IPV6_BOUND_IF)
8+
#elseif canImport(Glibc)
9+
public static let so_bindtodevice = Self(rawValue: SO_BINDTODEVICE)
10+
#endif
11+
}

Sources/WebSocketKit/WebSocketClient.swift

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public final class WebSocketClient: Sendable {
1414
case invalidURL
1515
case invalidResponseStatus(HTTPResponseHead)
1616
case alreadyShutdown
17+
case invalidAddress
1718
public var errorDescription: String? {
1819
return "\(self)"
1920
}
@@ -85,9 +86,10 @@ public final class WebSocketClient: Sendable {
8586
query: String? = nil,
8687
headers: HTTPHeaders = [:],
8788
maxQueueSize: Int? = nil,
89+
deviceName: String? = nil,
8890
onUpgrade: @Sendable @escaping (WebSocket) -> Void
8991
) -> EventLoopFuture<Void> {
90-
self.connect(scheme: scheme, host: host, port: port, path: path, query: query, maxQueueSize: maxQueueSize, headers: headers, proxy: nil, onUpgrade: onUpgrade)
92+
self.connect(scheme: scheme, host: host, port: port, path: path, query: query, maxQueueSize: maxQueueSize, headers: headers, proxy: nil, deviceName: deviceName, onUpgrade: onUpgrade)
9193
}
9294

9395
/// Establish a WebSocket connection via a proxy server.
@@ -103,6 +105,7 @@ public final class WebSocketClient: Sendable {
103105
/// - proxyPort: Port on which to connect to the proxy server.
104106
/// - proxyHeaders: Headers to send to the proxy server.
105107
/// - proxyConnectDeadline: Deadline for establishing the proxy connection.
108+
/// - deviceName: the device to which the data will be sent.
106109
/// - onUpgrade: An escaping closure to be executed after the upgrade is completed by `NIOWebSocketClientUpgrader`.
107110
/// - Returns: A future which completes when the connection to the origin server is established.
108111
@preconcurrency
@@ -118,6 +121,7 @@ public final class WebSocketClient: Sendable {
118121
proxyPort: Int? = nil,
119122
proxyHeaders: HTTPHeaders = [:],
120123
proxyConnectDeadline: NIODeadline = NIODeadline.distantFuture,
124+
deviceName: String? = nil,
121125
onUpgrade: @Sendable @escaping (WebSocket) -> Void
122126
) -> EventLoopFuture<Void> {
123127
assert(["ws", "wss"].contains(scheme))
@@ -140,6 +144,51 @@ public final class WebSocketClient: Sendable {
140144
}
141145
}
142146

147+
let resolvedAddress: SocketAddress
148+
do {
149+
resolvedAddress = try SocketAddress.makeAddressResolvingHost(host, port: port)
150+
} catch {
151+
return channel.eventLoop.makeFailedFuture(error)
152+
}
153+
154+
var bindDevice: NIONetworkDevice?
155+
do {
156+
for device in try System.enumerateDevices() {
157+
if device.name == deviceName, let address = device.address {
158+
switch (address.protocol, resolvedAddress.protocol) {
159+
case (.inet, .inet), (.inet6, .inet6):
160+
bindDevice = device
161+
default:
162+
continue
163+
}
164+
}
165+
if bindDevice != nil {
166+
break
167+
}
168+
}
169+
} catch {
170+
return channel.eventLoop.makeFailedFuture(error)
171+
}
172+
173+
func bindToDevice() -> EventLoopFuture<Void> {
174+
if let device = bindDevice {
175+
#if canImport(Darwin)
176+
switch device.address {
177+
case .v4:
178+
return channel.setOption(.ipOption(.ip_bound_if), value: CInt(device.interfaceIndex))
179+
case .v6:
180+
return channel.setOption(.ipv6Option(.ipv6_bound_if), value: CInt(device.interfaceIndex))
181+
default:
182+
return channel.eventLoop.makeFailedFuture(WebSocketClient.Error.invalidAddress)
183+
}
184+
#elseif canImport(Glibc)
185+
return channel.setOption(.socketOption(.so_bindtodevice), value: device.interfaceIndex)
186+
#endif
187+
} else {
188+
return channel.eventLoop.makeSucceededVoidFuture()
189+
}
190+
}
191+
143192
let httpUpgradeRequestHandler = HTTPUpgradeRequestHandler(
144193
host: host,
145194
path: uri,
@@ -177,11 +226,12 @@ public final class WebSocketClient: Sendable {
177226
return channel.pipeline.close(mode: .all)
178227
}
179228
}
180-
181229
return channel.pipeline.addHTTPClientHandlers(
182230
leftOverBytesStrategy: .forwardBytes,
183231
withClientUpgrade: config
184232
).flatMap {
233+
return bindToDevice()
234+
}.flatMap {
185235
if let maxQueueSize = maxQueueSize {
186236
return channel.setOption(ChannelOptions.writeBufferWaterMark, value: .init(low: maxQueueSize, high: maxQueueSize))
187237
}
@@ -228,6 +278,8 @@ public final class WebSocketClient: Sendable {
228278
return channel.setOption(ChannelOptions.writeBufferWaterMark, value: .init(low: maxQueueSize, high: maxQueueSize))
229279
}
230280
return channel.eventLoop.makeSucceededVoidFuture()
281+
}.flatMap {
282+
return bindToDevice()
231283
}.whenComplete { result in
232284
switch result {
233285
case .success:

0 commit comments

Comments
 (0)