Files
container/Sources/SocketForwarder/UDPForwarder.swift
Dmitry Kovba 9e9d056339 Transition to Mutex (#364)
Due to the reduced use of the macro, we can now fully transition to
`Mutex`.
2025-07-31 13:36:18 -07:00

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)) }
}
}