[Bug]: UDP proxy socket leaks when a backend is evicted before its channel becomes active
- Dominant language
- Swift
- Stars
- 49.9k
- Forks
- 1.8k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 22
Description
### I have done the following
- [x] I have searched the existing issues
- [x] If possible, I've reproduced the issue using the 'main' branch of this project
### 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
# 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 | 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
```swift
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
```swift
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
- [x] I agree to follow this project's Code of Conduct
Contributor guide
Research direction
Start in Sources/SocketForwarder/UDPForwarder.swift at UDPProxyFrontend's eviction logic and UDPProxyBackend.close(), then reproduce the issue with the provided UDP spray command and lsof check. Trace the period before channelActive and verify that an evicted backend's channel is closed once it becomes active, with UDP socket counts returning to their earlier level.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- networking
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100