I have done the following
Steps to reproduce
Publish a UDP port and send datagrams from more than 256 distinct source ports in quick succession, faster than the backend channels finish binding:
container run --rm --detach --publish 5000:5000/udp <image running a UDP echo service>
# from the host, spray from many ephemeral source ports
for i in $(seq 1 600); do
echo probe | nc -u -w0 -p $((20000 + i)) 127.0.0.1 5000
done
Sockets held by the forwarder process grow and do not return to the earlier level once the traffic stops. lsof -p <apiserver pid> | grep UDP | wc -l stays elevated.
Problem description
UDPProxyFrontend keeps at most 256 backends in an LRU cache and closes whichever one is evicted:
https://github.com/apple/container/blob/main/Sources/SocketForwarder/UDPForwarder.swift#L151
if let (_, evictedContext) = proxies.put(key: key, value: context) {
self.log?.trace("frontend - closing evicted backend")
evictedContext.proxy.close()
}
UDPProxyBackend.close() returns early when the channel is not set yet:
https://github.com/apple/container/blob/main/Sources/SocketForwarder/UDPForwarder.swift#L82
func close() {
guard let channel = state.channel else {
self.log?.warning("backend - close on inactive channel")
return
}
_ = channel.close()
}
state.channel is nil from construction until channelActive runs, which happens after bind completes. A backend evicted inside that window is never closed. channelActive then assigns state.channel and the socket becomes active, but the ProxyContext has already been dropped from the cache, so no reference remains and nothing closes it. The socket stays open for the life of the process.
The existing log line records the condition as a warning, so the case was anticipated, but the early return leaves the socket open rather than deferring the close.
A fix would record the close request in the backend state and have channelActive honour it, closing the channel immediately instead of adopting it.
Environment
- OS: macOS 26.5.2 (25F84)
- Xcode: 26.6 (17F113)
- Container: main at 07ff3c0 (also present in 1.1.0)
Code of Conduct
I have done the following
Steps to reproduce
Publish a UDP port and send datagrams from more than 256 distinct source ports in quick succession, faster than the backend channels finish binding:
Sockets held by the forwarder process grow and do not return to the earlier level once the traffic stops.
lsof -p <apiserver pid> | grep UDP | wc -lstays elevated.Problem description
UDPProxyFrontendkeeps at most 256 backends in an LRU cache and closes whichever one is evicted:https://github.com/apple/container/blob/main/Sources/SocketForwarder/UDPForwarder.swift#L151
UDPProxyBackend.close()returns early when the channel is not set yet:https://github.com/apple/container/blob/main/Sources/SocketForwarder/UDPForwarder.swift#L82
state.channelis nil from construction untilchannelActiveruns, which happens afterbindcompletes. A backend evicted inside that window is never closed.channelActivethen assignsstate.channeland the socket becomes active, but theProxyContexthas already been dropped from the cache, so no reference remains and nothing closes it. The socket stays open for the life of the process.The existing log line records the condition as a warning, so the case was anticipated, but the early return leaves the socket open rather than deferring the close.
A fix would record the close request in the backend state and have
channelActivehonour it, closing the channel immediately instead of adopting it.Environment
Code of Conduct