chore: import upstream snapshot with attribution
container project - merge build / Invoke build (push) Successful in 1s
container project - merge build / Invoke build (push) Successful in 1s
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Logging
|
||||
import NIOCore
|
||||
import NIOPosix
|
||||
|
||||
final class ConnectHandler {
|
||||
private var pendingBytes: [NIOAny]
|
||||
private let serverAddress: SocketAddress
|
||||
private var log: Logger? = nil
|
||||
|
||||
init(serverAddress: SocketAddress, log: Logger?) {
|
||||
self.pendingBytes = []
|
||||
self.serverAddress = serverAddress
|
||||
self.log = log
|
||||
}
|
||||
}
|
||||
|
||||
extension ConnectHandler: ChannelInboundHandler {
|
||||
typealias InboundIn = ByteBuffer
|
||||
typealias OutboundOut = ByteBuffer
|
||||
|
||||
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
||||
self.pendingBytes.append(data)
|
||||
}
|
||||
|
||||
func handlerAdded(context: ChannelHandlerContext) {
|
||||
// Add logger metadata.
|
||||
self.log?[metadataKey: "proxy"] = "\(context.channel.localAddress?.description ?? "none")"
|
||||
self.log?[metadataKey: "server"] = "\(context.channel.remoteAddress?.description ?? "none")"
|
||||
}
|
||||
|
||||
func channelActive(context: ChannelHandlerContext) {
|
||||
self.log?.trace("frontend - channel active, connecting to backend")
|
||||
self.connectToServer(context: context)
|
||||
context.fireChannelActive()
|
||||
}
|
||||
}
|
||||
|
||||
extension ConnectHandler: RemovableChannelHandler {
|
||||
func removeHandler(context: ChannelHandlerContext, removalToken: ChannelHandlerContext.RemovalToken) {
|
||||
var didRead = false
|
||||
|
||||
// We are being removed, and need to deliver any pending bytes we may have if we're upgrading.
|
||||
while self.pendingBytes.count > 0 {
|
||||
let data = self.pendingBytes.removeFirst()
|
||||
context.fireChannelRead(data)
|
||||
didRead = true
|
||||
}
|
||||
|
||||
if didRead {
|
||||
context.fireChannelReadComplete()
|
||||
}
|
||||
|
||||
self.log?.trace("backend - removing connect handler from pipeline")
|
||||
context.leavePipeline(removalToken: removalToken)
|
||||
}
|
||||
}
|
||||
|
||||
extension ConnectHandler {
|
||||
private func connectToServer(context: ChannelHandlerContext) {
|
||||
self.log?.trace("backend - connecting")
|
||||
|
||||
ClientBootstrap(group: context.eventLoop)
|
||||
.connect(to: serverAddress)
|
||||
.assumeIsolatedUnsafeUnchecked()
|
||||
.whenComplete { result in
|
||||
switch result {
|
||||
case .success(let channel):
|
||||
guard context.channel.isActive else {
|
||||
self.log?.trace("backend - frontend channel closed, closing backend connection")
|
||||
context.channel.close(promise: nil)
|
||||
return
|
||||
}
|
||||
self.log?.trace("backend - connected")
|
||||
self.glue(channel, context: context)
|
||||
case .failure(let error):
|
||||
self.log?.error("backend - connect failed: \(error)")
|
||||
context.close(promise: nil)
|
||||
context.fireErrorCaught(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func glue(_ peerChannel: Channel, context: ChannelHandlerContext) {
|
||||
self.log?.trace("backend - gluing channels")
|
||||
|
||||
// Now we need to glue our channel and the peer channel together.
|
||||
let (localGlue, peerGlue) = GlueHandler.matchedPair()
|
||||
do {
|
||||
try context.channel.pipeline.syncOperations.addHandler(localGlue)
|
||||
try peerChannel.pipeline.syncOperations.addHandler(peerGlue)
|
||||
context.pipeline.syncOperations.removeHandler(self, promise: nil)
|
||||
} catch {
|
||||
// Close connected peer channel before closing our channel.
|
||||
peerChannel.close(mode: .all, promise: nil)
|
||||
context.close(promise: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import NIOCore
|
||||
|
||||
final class GlueHandler {
|
||||
|
||||
private var partner: GlueHandler?
|
||||
|
||||
private var context: ChannelHandlerContext?
|
||||
|
||||
private var pendingRead: Bool = false
|
||||
|
||||
private init() {}
|
||||
}
|
||||
|
||||
extension GlueHandler {
|
||||
static func matchedPair() -> (GlueHandler, GlueHandler) {
|
||||
let first = GlueHandler()
|
||||
let second = GlueHandler()
|
||||
|
||||
first.partner = second
|
||||
second.partner = first
|
||||
|
||||
return (first, second)
|
||||
}
|
||||
}
|
||||
|
||||
extension GlueHandler {
|
||||
private func partnerWrite(_ data: NIOAny) {
|
||||
self.context?.write(data, promise: nil)
|
||||
}
|
||||
|
||||
private func partnerFlush() {
|
||||
self.context?.flush()
|
||||
}
|
||||
|
||||
private func partnerWriteEOF() {
|
||||
self.context?.close(mode: .output, promise: nil)
|
||||
}
|
||||
|
||||
private func partnerCloseFull() {
|
||||
self.context?.close(promise: nil)
|
||||
}
|
||||
|
||||
private func partnerBecameWritable() {
|
||||
if self.pendingRead {
|
||||
self.pendingRead = false
|
||||
self.context?.read()
|
||||
}
|
||||
}
|
||||
|
||||
private var partnerWritable: Bool {
|
||||
self.context?.channel.isWritable ?? false
|
||||
}
|
||||
}
|
||||
|
||||
extension GlueHandler: ChannelDuplexHandler {
|
||||
typealias InboundIn = NIOAny
|
||||
typealias OutboundIn = NIOAny
|
||||
typealias OutboundOut = NIOAny
|
||||
|
||||
func handlerAdded(context: ChannelHandlerContext) {
|
||||
self.context = context
|
||||
}
|
||||
|
||||
func handlerRemoved(context: ChannelHandlerContext) {
|
||||
self.context = nil
|
||||
self.partner = nil
|
||||
}
|
||||
|
||||
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
||||
self.partner?.partnerWrite(data)
|
||||
}
|
||||
|
||||
func channelReadComplete(context: ChannelHandlerContext) {
|
||||
self.partner?.partnerFlush()
|
||||
}
|
||||
|
||||
func channelInactive(context: ChannelHandlerContext) {
|
||||
self.partner?.partnerCloseFull()
|
||||
}
|
||||
|
||||
func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
|
||||
if let event = event as? ChannelEvent, case .inputClosed = event {
|
||||
// We have read EOF.
|
||||
self.partner?.partnerWriteEOF()
|
||||
}
|
||||
}
|
||||
|
||||
func errorCaught(context: ChannelHandlerContext, error: Error) {
|
||||
self.partner?.partnerCloseFull()
|
||||
}
|
||||
|
||||
func channelWritabilityChanged(context: ChannelHandlerContext) {
|
||||
if context.channel.isWritable {
|
||||
self.partner?.partnerBecameWritable()
|
||||
}
|
||||
}
|
||||
|
||||
func read(context: ChannelHandlerContext) {
|
||||
if let partner = self.partner, partner.partnerWritable {
|
||||
context.read()
|
||||
} else {
|
||||
self.pendingRead = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
struct KeyExistsError: Error {}
|
||||
|
||||
class LRUCache<K: Hashable, V> {
|
||||
private class Node {
|
||||
fileprivate var prev: Node?
|
||||
fileprivate var next: Node?
|
||||
fileprivate let key: K
|
||||
fileprivate let value: V
|
||||
|
||||
init(key: K, value: V) {
|
||||
self.prev = nil
|
||||
self.next = nil
|
||||
self.key = key
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
private let size: UInt
|
||||
private var head: Node?
|
||||
private var tail: Node?
|
||||
private var members: [K: Node]
|
||||
|
||||
init(size: UInt) {
|
||||
self.size = size
|
||||
self.head = nil
|
||||
self.tail = nil
|
||||
self.members = [:]
|
||||
}
|
||||
|
||||
var count: Int { members.count }
|
||||
|
||||
func get(_ key: K) -> V? {
|
||||
guard let node = members[key] else {
|
||||
return nil
|
||||
}
|
||||
listRemove(node: node)
|
||||
listInsert(node: node, after: tail)
|
||||
return node.value
|
||||
}
|
||||
|
||||
func put(key: K, value: V) -> (K, V)? {
|
||||
let node = Node(key: key, value: value)
|
||||
var evicted: (K, V)? = nil
|
||||
|
||||
if let existingNode = members[key] {
|
||||
// evict the replaced node
|
||||
listRemove(node: existingNode)
|
||||
evicted = (existingNode.key, existingNode.value)
|
||||
} else if self.count >= self.size {
|
||||
// evict the least recently used node
|
||||
evicted = evict()
|
||||
}
|
||||
|
||||
// insert the new node and return any evicted node
|
||||
members[key] = node
|
||||
listInsert(node: node, after: tail)
|
||||
return evicted
|
||||
}
|
||||
|
||||
private func evict() -> (K, V)? {
|
||||
guard let head else {
|
||||
return nil
|
||||
}
|
||||
let ret = (head.key, head.value)
|
||||
listRemove(node: head)
|
||||
members.removeValue(forKey: head.key)
|
||||
return ret
|
||||
}
|
||||
|
||||
private func listRemove(node: Node) {
|
||||
if let prev = node.prev {
|
||||
prev.next = node.next
|
||||
} else {
|
||||
head = node.next
|
||||
}
|
||||
if let next = node.next {
|
||||
next.prev = node.prev
|
||||
} else {
|
||||
tail = node.prev
|
||||
}
|
||||
}
|
||||
|
||||
private func listInsert(node: Node, after: Node?) {
|
||||
let before: Node?
|
||||
if let after {
|
||||
before = after.next
|
||||
after.next = node
|
||||
} else {
|
||||
before = head
|
||||
head = node
|
||||
}
|
||||
|
||||
if let before {
|
||||
before.prev = node
|
||||
} else {
|
||||
tail = node
|
||||
}
|
||||
|
||||
node.prev = after
|
||||
node.next = before
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import NIO
|
||||
|
||||
public protocol SocketForwarder: Sendable {
|
||||
func run() throws -> EventLoopFuture<SocketForwarderResult>
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import NIO
|
||||
|
||||
public struct SocketForwarderResult: Sendable {
|
||||
private let channel: any Channel
|
||||
|
||||
public init(channel: Channel) {
|
||||
self.channel = channel
|
||||
}
|
||||
|
||||
public var proxyAddress: SocketAddress? { self.channel.localAddress }
|
||||
|
||||
public func close() {
|
||||
self.channel.eventLoop.execute {
|
||||
_ = channel.close()
|
||||
}
|
||||
}
|
||||
|
||||
public func wait() async throws {
|
||||
try await self.channel.closeFuture.get()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
import Logging
|
||||
import NIO
|
||||
import NIOFoundationCompat
|
||||
|
||||
public struct TCPForwarder: SocketForwarder {
|
||||
private let proxyAddress: SocketAddress
|
||||
|
||||
private let serverAddress: SocketAddress
|
||||
|
||||
private let eventLoopGroup: any EventLoopGroup
|
||||
|
||||
private let log: Logger?
|
||||
|
||||
public init(
|
||||
proxyAddress: SocketAddress,
|
||||
serverAddress: SocketAddress,
|
||||
eventLoopGroup: any EventLoopGroup,
|
||||
log: Logger? = nil
|
||||
) throws {
|
||||
self.proxyAddress = proxyAddress
|
||||
self.serverAddress = serverAddress
|
||||
self.eventLoopGroup = eventLoopGroup
|
||||
self.log = log
|
||||
}
|
||||
|
||||
public func run() throws -> EventLoopFuture<SocketForwarderResult> {
|
||||
self.log?.trace("frontend - creating listener")
|
||||
|
||||
let bootstrap = ServerBootstrap(group: self.eventLoopGroup)
|
||||
.serverChannelOption(ChannelOptions.socket(.init(SOL_SOCKET), .init(SO_REUSEADDR)), value: 1)
|
||||
.childChannelOption(ChannelOptions.socket(.init(SOL_SOCKET), .init(SO_REUSEADDR)), value: 1)
|
||||
.childChannelInitializer { channel in
|
||||
channel.eventLoop.makeCompletedFuture {
|
||||
try channel.pipeline.syncOperations.addHandler(
|
||||
ConnectHandler(serverAddress: self.serverAddress, log: log)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
bootstrap
|
||||
.bind(to: self.proxyAddress)
|
||||
.map { SocketForwarderResult(channel: $0) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Collections
|
||||
import Foundation
|
||||
import Logging
|
||||
import NIO
|
||||
import NIOFoundationCompat
|
||||
import Synchronization
|
||||
|
||||
// Proxy backend for a single client address (clientIP, clientPort).
|
||||
private final class UDPProxyBackend: ChannelInboundHandler {
|
||||
typealias InboundIn = AddressedEnvelope<ByteBuffer>
|
||||
typealias OutboundOut = AddressedEnvelope<ByteBuffer>
|
||||
|
||||
private struct State {
|
||||
var queuedPayloads: Deque<ByteBuffer>
|
||||
var channel: (any Channel)?
|
||||
}
|
||||
|
||||
private let clientAddress: SocketAddress
|
||||
private let serverAddress: SocketAddress
|
||||
private let frontendChannel: any Channel
|
||||
private let log: Logger?
|
||||
private var state: State
|
||||
|
||||
init(clientAddress: SocketAddress, serverAddress: SocketAddress, frontendChannel: any Channel, log: Logger? = nil) {
|
||||
self.clientAddress = clientAddress
|
||||
self.serverAddress = serverAddress
|
||||
self.frontendChannel = frontendChannel
|
||||
self.log = log
|
||||
let initialState = State(queuedPayloads: Deque(), channel: nil)
|
||||
self.state = initialState
|
||||
}
|
||||
|
||||
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
||||
// relay data from server to client.
|
||||
let inbound = self.unwrapInboundIn(data)
|
||||
let outbound = OutboundOut(remoteAddress: self.clientAddress, data: inbound.data)
|
||||
self.log?.trace("backend - writing datagram to client")
|
||||
self.frontendChannel.writeAndFlush(outbound, promise: nil)
|
||||
}
|
||||
|
||||
func channelActive(context: ChannelHandlerContext) {
|
||||
if !state.queuedPayloads.isEmpty {
|
||||
self.log?.trace("backend - writing \(state.queuedPayloads.count) queued datagrams to server")
|
||||
while let queuedData = state.queuedPayloads.popFirst() {
|
||||
let outbound: UDPProxyBackend.OutboundOut = OutboundOut(remoteAddress: self.serverAddress, data: queuedData)
|
||||
context.channel.writeAndFlush(outbound, promise: nil)
|
||||
}
|
||||
}
|
||||
state.channel = context.channel
|
||||
}
|
||||
|
||||
func write(data: ByteBuffer) {
|
||||
// change package remote address from proxy server to real server
|
||||
if let channel = state.channel {
|
||||
// channel has been initialized, so relay any queued packets, along with this one to outbound
|
||||
self.log?.trace("backend - writing datagram to server")
|
||||
let outbound: UDPProxyBackend.OutboundOut = OutboundOut(remoteAddress: self.serverAddress, data: data)
|
||||
channel.writeAndFlush(outbound, promise: nil)
|
||||
} else {
|
||||
// channel is initializing, queue
|
||||
self.log?.trace("backend - queuing datagram")
|
||||
state.queuedPayloads.append(data)
|
||||
}
|
||||
}
|
||||
|
||||
func close() {
|
||||
guard let channel = state.channel else {
|
||||
self.log?.warning("backend - close on inactive channel")
|
||||
return
|
||||
}
|
||||
_ = channel.close()
|
||||
}
|
||||
}
|
||||
|
||||
private struct ProxyContext {
|
||||
public let proxy: UDPProxyBackend
|
||||
public let closeFuture: EventLoopFuture<Void>
|
||||
}
|
||||
|
||||
private final class UDPProxyFrontend: ChannelInboundHandler {
|
||||
typealias InboundIn = AddressedEnvelope<ByteBuffer>
|
||||
typealias OutboundOut = AddressedEnvelope<ByteBuffer>
|
||||
private let maxProxies = UInt(256)
|
||||
|
||||
private let proxyAddress: SocketAddress
|
||||
private let serverAddress: SocketAddress
|
||||
private let log: Logger?
|
||||
|
||||
private var proxies: LRUCache<String, ProxyContext>
|
||||
|
||||
init(proxyAddress: SocketAddress, serverAddress: SocketAddress, log: Logger? = nil) {
|
||||
self.proxyAddress = proxyAddress
|
||||
self.serverAddress = serverAddress
|
||||
self.proxies = LRUCache(size: maxProxies)
|
||||
self.log = log
|
||||
}
|
||||
|
||||
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
||||
let inbound = self.unwrapInboundIn(data)
|
||||
|
||||
guard let clientIP = inbound.remoteAddress.ipAddress else {
|
||||
log?.error("frontend - no client IP address in inbound payload")
|
||||
return
|
||||
}
|
||||
|
||||
guard let clientPort = inbound.remoteAddress.port else {
|
||||
log?.error("frontend - no client port in inbound payload")
|
||||
return
|
||||
}
|
||||
|
||||
let key = "\(clientIP):\(clientPort)"
|
||||
do {
|
||||
if let context = proxies.get(key) {
|
||||
context.proxy.write(data: inbound.data)
|
||||
} else {
|
||||
self.log?.trace("frontend - creating backend")
|
||||
let proxy = UDPProxyBackend(
|
||||
clientAddress: inbound.remoteAddress,
|
||||
serverAddress: self.serverAddress,
|
||||
frontendChannel: context.channel,
|
||||
log: log
|
||||
)
|
||||
let proxyAddress = try SocketAddress(ipAddress: "0.0.0.0", port: 0)
|
||||
let loopBoundProxy = NIOLoopBound(proxy, eventLoop: context.eventLoop)
|
||||
let proxyToServerFuture = DatagramBootstrap(group: context.eventLoop)
|
||||
.channelInitializer { [log] channel in
|
||||
log?.trace("frontend - initializing backend")
|
||||
return channel.eventLoop.makeCompletedFuture {
|
||||
try channel.pipeline.syncOperations.addHandler(loopBoundProxy.value)
|
||||
}
|
||||
}
|
||||
.bind(to: proxyAddress)
|
||||
.flatMap { $0.closeFuture }
|
||||
let context = ProxyContext(proxy: proxy, closeFuture: proxyToServerFuture)
|
||||
if let (_, evictedContext) = proxies.put(key: key, value: context) {
|
||||
self.log?.trace("frontend - closing evicted backend")
|
||||
evictedContext.proxy.close()
|
||||
}
|
||||
|
||||
proxy.write(data: inbound.data)
|
||||
}
|
||||
} catch {
|
||||
log?.error("server handler - backend channel creation failed with error: \(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct UDPForwarder: SocketForwarder {
|
||||
private let proxyAddress: SocketAddress
|
||||
|
||||
private let serverAddress: SocketAddress
|
||||
|
||||
private let eventLoopGroup: any EventLoopGroup
|
||||
|
||||
private let log: Logger?
|
||||
|
||||
public init(
|
||||
proxyAddress: SocketAddress,
|
||||
serverAddress: SocketAddress,
|
||||
eventLoopGroup: any EventLoopGroup,
|
||||
log: Logger? = nil
|
||||
) throws {
|
||||
self.proxyAddress = proxyAddress
|
||||
self.serverAddress = serverAddress
|
||||
self.eventLoopGroup = eventLoopGroup
|
||||
self.log = log
|
||||
}
|
||||
|
||||
public func run() throws -> EventLoopFuture<SocketForwarderResult> {
|
||||
self.log?.trace("frontend - creating channel")
|
||||
let bootstrap = DatagramBootstrap(group: self.eventLoopGroup)
|
||||
.channelInitializer { serverChannel in
|
||||
self.log?.trace("frontend - initializing channel")
|
||||
let proxyToServerHandler = UDPProxyFrontend(
|
||||
proxyAddress: proxyAddress,
|
||||
serverAddress: serverAddress,
|
||||
log: log
|
||||
)
|
||||
return serverChannel.eventLoop.makeCompletedFuture {
|
||||
try serverChannel.pipeline.syncOperations.addHandler(proxyToServerHandler)
|
||||
}
|
||||
}
|
||||
return
|
||||
bootstrap
|
||||
.bind(to: proxyAddress)
|
||||
.map { SocketForwarderResult(channel: $0) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user