mirror of
https://github.com/apple/container.git
synced 2026-07-14 21:47:01 +00:00
9e9d056339
Due to the reduced use of the macro, we can now fully transition to `Mutex`.
212 lines
8.0 KiB
Swift
212 lines
8.0 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
|
|
//
|
|
// 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, Sendable {
|
|
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 let state: Mutex<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 = Mutex(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)
|
|
}
|
|
|
|
func channelActive(context: ChannelHandlerContext) {
|
|
state.withLock {
|
|
if !$0.queuedPayloads.isEmpty {
|
|
self.log?.trace("backend - writing \($0.queuedPayloads.count) queued datagrams to server")
|
|
while let queuedData = $0.queuedPayloads.popFirst() {
|
|
let outbound: UDPProxyBackend.OutboundOut = OutboundOut(remoteAddress: self.serverAddress, data: queuedData)
|
|
_ = context.channel.writeAndFlush(outbound)
|
|
}
|
|
}
|
|
$0.channel = context.channel
|
|
}
|
|
}
|
|
|
|
func write(data: ByteBuffer) {
|
|
// change package remote address from proxy server to real server
|
|
state.withLock {
|
|
if let channel = $0.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)
|
|
} else {
|
|
// channel is initializing, queue
|
|
self.log?.trace("backend - queuing datagram")
|
|
$0.queuedPayloads.append(data)
|
|
}
|
|
}
|
|
}
|
|
|
|
func close() {
|
|
state.withLock {
|
|
guard let channel = $0.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, Sendable {
|
|
typealias InboundIn = AddressedEnvelope<ByteBuffer>
|
|
typealias OutboundOut = AddressedEnvelope<ByteBuffer>
|
|
private let maxProxies = UInt(256)
|
|
|
|
private let proxyAddress: SocketAddress
|
|
private let serverAddress: SocketAddress
|
|
private let eventLoopGroup: any EventLoopGroup
|
|
private let log: Logger?
|
|
|
|
private let proxies: Mutex<LRUCache<String, ProxyContext>>
|
|
|
|
init(proxyAddress: SocketAddress, serverAddress: SocketAddress, eventLoopGroup: any EventLoopGroup, log: Logger? = nil) {
|
|
self.proxyAddress = proxyAddress
|
|
self.serverAddress = serverAddress
|
|
self.eventLoopGroup = eventLoopGroup
|
|
self.proxies = Mutex(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 {
|
|
try proxies.withLock {
|
|
if let context = $0.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 proxyToServerFuture = DatagramBootstrap(group: self.eventLoopGroup)
|
|
.channelInitializer {
|
|
self.log?.trace("frontend - initializing backend")
|
|
return $0.pipeline.addHandler(proxy)
|
|
}
|
|
.bind(to: proxyAddress)
|
|
.flatMap { $0.closeFuture }
|
|
let context = ProxyContext(proxy: proxy, closeFuture: proxyToServerFuture)
|
|
if let (_, evictedContext) = $0.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 proxyToServerHandler = UDPProxyFrontend(
|
|
proxyAddress: proxyAddress,
|
|
serverAddress: serverAddress,
|
|
eventLoopGroup: self.eventLoopGroup,
|
|
log: log
|
|
)
|
|
let bootstrap = DatagramBootstrap(group: self.eventLoopGroup)
|
|
.channelInitializer { serverChannel in
|
|
self.log?.trace("frontend - initializing channel")
|
|
return serverChannel.pipeline.addHandler(proxyToServerHandler)
|
|
}
|
|
return
|
|
bootstrap
|
|
.bind(to: proxyAddress)
|
|
.flatMap { $0.eventLoop.makeSucceededFuture(SocketForwarderResult(channel: $0)) }
|
|
}
|
|
}
|