From f94a4a0502ba89c94c0363cd1438566fac9cb0c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Gro=C3=9Fmann?= Date: Sun, 23 Aug 2026 11:16:57 +0200 Subject: [PATCH] Wait 15 seconds for the listener port, not 100 BINDTRIES and BINDWAIT shipped with HTTPD's defaults of 10 and 10, which was consistency taken as its own justification. HTTPD's number predates a stale port sweep: there, a repeated bind is the only defence against a port a dead instance still holds. Here close_stale_port() has already cleared that case before the retry loop is reached (#109), so what still answers EADDRINUSE afterwards is most likely a LIVE foreign listener -- and waiting on one does not help, because it does not go away. What waiting costs changed too. Before #111 a failed bind left the STC sitting there and the budget was spent by nobody; now it ends the address space, so the operator who typed /S and is watching the console waits out the whole thing for a verdict that was decided in the first few seconds. 3 x 5 covers TIME_WAIT and a tight restart, and a failed start is recognisable as one while someone is still looking at it. The case that genuinely wants more patience is EADDRNOTAVAIL early in an IPL -- a different order of magnitude from EADDRINUSE, and what raising BINDTRIES is for. Sample member and installation guide say so. Measured on mvsdev from the built-in defaults, with no BINDTRIES or BINDWAIT in the config member at all: 04.15.26 FTPD051E BIND() FAILED ON 10.99.99.99 PORT 2122, ERRNO=49 04.15.26 FTPD051I RETRYING BIND IN 5S (1 OF 3) 04.15.31 FTPD051I RETRYING BIND IN 5S (2 OF 3) 04.15.36 FTPD051I RETRYING BIND IN 5S (3 OF 3) 04.15.41 FTPD051E BIND() STILL FAILING AFTER 3 TRIES, ERRNO=49 04.15.41 FTPD056E FTPD IS NOT LISTENING ON PORT 2122, THIS INSTANCE ENDS IEF142I FTPDT FTPDT - STEP WAS EXECUTED - COND CODE 0008 Claude-Session: https://claude.ai/code/session_01A6q4hLaokSJBeifJDkNJtH --- doc/installation.md | 2 +- samplib/ftpdprm0 | 8 ++++---- src/ftpd#cfg.c | 20 +++++++++++++++++--- src/ftpd.c | 10 ++++++---- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/doc/installation.md b/doc/installation.md index 65be32b..8fabc45 100644 --- a/doc/installation.md +++ b/doc/installation.md @@ -371,7 +371,7 @@ DEFVOLUME=PUB001 - `SSLPROXY=YES` is only for a TLS-terminating proxy in front of FTPD. Run bare it promises clients a confidentiality it does not deliver; the member's own comments spell this out. -- `BINDTRIES`/`BINDWAIT` (both 1–100, both default 10) decide how long FTPD +- `BINDTRIES`/`BINDWAIT` (both 1–100, defaulting to 3 and 5) decide how long FTPD keeps trying a listener port it cannot have yet — a port still held, or a stack that is not up. Giving up ends the started task, so raise `BINDTRIES` if `/S FTPD` runs early in an IPL. diff --git a/samplib/ftpdprm0 b/samplib/ftpdprm0 index 29405eb..47dc3d0 100644 --- a/samplib/ftpdprm0 +++ b/samplib/ftpdprm0 @@ -25,16 +25,16 @@ PASVPORTS=22000-22200 # # A bind() that fails for a reason waiting can fix -- the port still held # by something, or the stack not up yet at IPL -- is retried BINDTRIES -# times, BINDWAIT seconds apart. Both 1-100, both default 10, so FTPD -# waits up to 100 seconds before giving up. +# times, BINDWAIT seconds apart. Both 1-100; the defaults 3 and 5 give +# FTPD 15 seconds before it gives up. # # Giving up ends the started task (FTPD056E): an FTPD that cannot listen # has nothing left to do, and no console command re-tries the bind. So # these two decide how much transient trouble a start survives. Raise # BINDTRIES where FTPD is started before TCP/IP is reliably up. # -BINDTRIES=10 -BINDWAIT=10 +BINDTRIES=3 +BINDWAIT=5 # # PASVBIND is where the passive listener binds, PASVADR is what the # client is told to connect to -- they are only the same by default. diff --git a/src/ftpd#cfg.c b/src/ftpd#cfg.c index 5d12458..c46e5e4 100644 --- a/src/ftpd#cfg.c +++ b/src/ftpd#cfg.c @@ -30,9 +30,23 @@ ftpdcfg_defaults(ftpd_config_t *cfg) strcpy(cfg->pasv_bind, "ANY"); /* bind every address, as before */ cfg->pasv_lo = 22000; cfg->pasv_hi = 22200; - cfg->bind_tries = 10; /* HTTPD's defaults: up to 100s of - ** patience before the STC ends */ - cfg->bind_wait = 10; + /* 15 seconds of patience, not HTTPD's 100. HTTPD's number predates a + ** stale port sweep: there, a repeated bind was the only defence against + ** a port a dead instance still held. Here close_stale_port() has + ** already cleared that case before the retry loop is reached (#109), so + ** what still answers EADDRINUSE afterwards is most likely a LIVE foreign + ** listener -- and waiting on one does not help, it does not go away. + ** + ** The cost of waiting also changed: since #111 a bind that never + ** succeeds ends the STC, so the operator who typed /S and is watching + ** the console waits out the whole budget for a verdict. 15 seconds + ** covers TIME_WAIT and a tight restart; a failed start is recognisable + ** as one while someone is still looking at it. + ** + ** The one case that wants far more patience is EADDRNOTAVAIL early in an + ** IPL, and that is what raising BINDTRIES is for. */ + cfg->bind_tries = 3; + cfg->bind_wait = 5; /* Limits */ cfg->max_sessions = 10; cfg->idle_timeout = 300; diff --git a/src/ftpd.c b/src/ftpd.c index c919ac6..0150306 100644 --- a/src/ftpd.c +++ b/src/ftpd.c @@ -459,10 +459,12 @@ bind_retryable(int err) ** give up early when the server is being stopped. ** ** The step is what makes /P work during a retry run. BINDTRIES x -** BINDWAIT can now be 100 seconds; terminate() waits 10 for the socket -** thread and then says FTPD095W SOCKET THREAD DID NOT TERMINATE, so a -** thread asleep in one long STIMER would make every longer retry setting -** worse for the operator than the short one it replaced. +** BINDWAIT is 15 seconds by default but goes to 100 x 100 -- and the +** sites that raise it are exactly the ones that will need to interrupt +** it. terminate() waits 10 seconds for the socket thread and then says +** FTPD095W SOCKET THREAD DID NOT TERMINATE, so a thread asleep in one +** long STIMER would make every longer retry setting worse for the +** operator than the short one it replaced. ** ** Returns 0 when the wait completed, -1 when shutdown was signalled. ** ----------------------------------------------------------------- */