Skip to content

[Bug]: UDP proxy socket leaks when a backend is evicted before its channel becomes active #2015

Description

@devops-thiago

I have done the following

  • I have searched the existing issues
  • 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 <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 agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions