Repository navigation
Conversation
A reservation's Close deleted whatever reservation the port held for that IP, so a transfer ending after its data connection was dispatched removed the next session's reservation. Only delete the reservation being closed. deliver could send to connCh after Close drained it, leaving the connection with nobody to read or close it. Decide under stateMu, which Close takes before draining. Sessions from different IPs using a port for the first time raced to bind it, and the losers moved on to the next port or got a 421. Hold the lock across the bind. released was only ever written, so closing replaces it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #692 +/- ##
==========================================
+ Coverage 91.26% 91.67% +0.40%
==========================================
Files 13 13
Lines 2164 2149 -15
==========================================
- Hits 1975 1970 -5
+ Misses 176 166 -10
Partials 13 13 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Same warnings as drakkan#2253, on ftpserverlib v0.32.6 plus fclairamb/ftpserverlib#692. Drop the replace once that is released. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
I am not convinced that I may be wrong here, I just did a quick look at the code |
|
Thank you. I agree that I believe the proxy protocol can be supported, try to use |
Fixes the two issues @drakkan raised in #623, plus a third one I found while reproducing them. All three are in
passive_multiplexer.goand only apply withPassiveTransferPortMultiplexingon.Fixes
A late
Close()deleted another session's reservation.releasedeleted by IP, without checking whose reservation it was. If A's data connection was dispatched, B reserved the same port from the same IP, and then A's transfer ended, A'sClose()removed B's reservation. B's connection was dropped and B got a 425 afterConnectionTimeout.releasenow takes the reservation and only deletes that one.deliverracingClose()could strand a connection.delivercould pass itsclosedChcheck, lose the CPU, and send toconnChafterClose()had drained it, leaving the connection with nobody to read or close it.delivernow decides understateMu, andClose()setsclosingunder that lock before draining. The send can't block:connChhas room for the one connection a reservation ever gets.Concurrent first use of a port gave spurious failures.
getOrCreatebound outside the lock, so when sessions from different IPs hit an unused port at once, the loser's bind failed with "address already in use" and that session skipped a port it could have shared. With a single port, that's a 421. The lock is now held across the bind. That serializes binds within a server, but a bind is quick and each port is only bound once.releasedwas only ever written, soclosingreplaces it.Measurements
12 s of
PASV+LISTin a loop on loopback, with 5 ms betweenPASVand the data connection, andConnectionTimeoutset to 2 s. "Lost" means the data connection never paired, "refused" means a 421 onPASV. "Before" was measured on v0.32.6, whosepassive_multiplexer.gois identical tomain.With 8 sessions from one IP on 2 ports, there are still 421s (3,486 in the run above): that's the expected limit of one pending reservation per IP and port, and those sessions are now refused up front instead of losing transfers.
Testing
go test -parallel 20 -race ./...passes.golangci-lintv2.15.0 reports 0 issues.TestPassiveReservationDeliverCloseRaceis a stress test, so a run on broken code isn't guaranteed to catch the race.Not addressed
Connections are matched by raw source IP before
WrapPassiveListenerruns, and this is inherent to multiplexing:IPMatchDisabled, a data connection from an IP other than the control connection's is still closed. Tested.🤖 Generated with Claude Code