Skip to content

Conversation

@robklg
Copy link
Contributor

@robklg robklg commented Dec 29, 2025

Implements #552

The pooled listener mode adds a new mode for the way in which passive data channels are handled, next to legacy and proxy protocol. It reuses a lot of the code for the proxy protocol, particularly the lookup table (switchboard) to associate a passive listening port to a particular FTP control session.

Recall that the legacy way of allocating ports is done within PASV/EPSV: try_port_range binds to a free port, and starts listening. A task is started to await for the incoming connection, upon receiving the connection, spawn_processing is called for the data channel.

Benefits of pooled mode

This new mode offers several advantages over the legacy listener, especially for high-traffic servers:

  • Higher performance: Reduces the latency of PASV commands, as a port is already available and doesn't need to be bound on-demand.
  • Improved security: Prevents "port stealing" race conditions inherent in the legacy model where an attacker could connect to a newly opened port before the legitimate client. Despite the source ip check preventing stealing the stream, it still is a DoS risk.
  • Reduced congestion: In legacy mode, a passive port is exclusively reserved for a single session system-wide for the duration of PASV issuing & incoming connection. In pooled mode, the reservation is keyed to the client's IP address, allowing different clients to be assigned the same port number simultaneously, which significantly increases the availability of the passive port pool, allowing way higher connection rates even with a small number of listening ports.

The pooled mode works similar to proxy protocol mode: The listening for passive data connections is delegated to a separate listener (listen_pooled to replace listen). In pooled mode, the server listens on all ports in the passive port range continuously, accepting all incoming connections. When a PASV command is received, a port is assigned to the session, it is registered in the switchboard under source ip + passive port. When a subsequent incoming connection is received, the pooled listener can resolve the session that belongs to this incoming data channel, by looking it up in the switchboard baesd on source ip + port. There it dispatches the data channel.

For PASV, the way the data port allocation is handled is delegated to the pooled or proxy mode listener via a channel that takes the command to assign a port, plus a oneshot return channel, so that PASV knows the assigned port and can reply it to the client.

The difference between pooled & proxy mode now is: First of all, most obviously, proxy protocol mode extracts information from the proxy header, and listens on only one port. The pooled mode listens on both the control port (same way as listen.rs does, so it just uses the unftp bind_address) and the full range of passive ports. The shared code for pooled and proxy mode is bundled in listen_prebound.

Improvement in switchboard

A scavenger task was added to the switchboard to prevent a memory leak. Previously, if a client disconnected after a PASV command without using the data connection, the port reservation could remain in the switchboard forever. The new scavenger periodically cleans up these "zombie" entries by using Weak pointers to detect when a session has been deallocated.

Other changes in this MR

Some related changes have been made:

  • Deduplicated code in PASV/EPSV, by extracting the common code into passive_common
  • PORT refactored to be independent of the listening mode, it was erroneously disabled for non-legacy passive listening modes, while active mode is completely unaffected by it.
  • Decoupled switchboard logic from proxy protocol mode into a "prebound" listener which houses the common logic for proxy protocol and pooled mode
  • Disabled proxy and pooled mode for EPSV as currently the switchboard code doesn't yet support IPv6. When IPv6 support is added in the future (for proxy protocol + switchboard) we can reenable EPSV.

@robklg robklg requested a review from hannesdejager December 29, 2025 20:52
@robklg robklg marked this pull request as draft December 29, 2025 20:56
@robklg robklg marked this pull request as ready for review December 30, 2025 09:58
@hannesdejager
Copy link
Collaborator

This is really cool Rob. Thank you for the elaborate MR description too.

Copy link
Collaborator

@hannesdejager hannesdejager left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

I'd say we just need to add docs too for unftp.rs e.g. like https://unftp.rs/server/proxy-protocol

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants