@@ -212,6 +212,11 @@ struct stratum_conn {
212212 uint64_t vd_window_start_ms ;
213213 uint32_t vd_window_shares ;
214214
215+ /* Monotonic timestamp of the most recent recv() that got any bytes.
216+ * The conn thread checks this against cfg.idle_timeout_sec after each
217+ * SO_RCVTIMEO wake-up so silent connections are reaped. */
218+ uint64_t last_activity_ms ;
219+
215220 pthread_mutex_t write_lock ;
216221
217222 struct stratum_conn * next ; /* server->conns_head linked list */
@@ -1160,6 +1165,53 @@ static int write_all(int fd, const char *buf, size_t len) {
11601165 return 0 ;
11611166}
11621167
1168+ /* Configure an accepted socket so idle miners get reaped instead of
1169+ * clogging fds/threads indefinitely. Two mechanisms, belt-and-suspenders:
1170+ *
1171+ * 1. Application-level: SO_RCVTIMEO gives recv() a bounded wake so we
1172+ * can compare last_activity_ms against cfg.idle_timeout_sec. Catches
1173+ * miners that hold the TCP open but never send anything (e.g. bad
1174+ * username, misconfigured worker).
1175+ * 2. OS-level: SO_KEEPALIVE + tightened TCP_KEEPIDLE/INTVL/CNT so Linux
1176+ * drops the socket after ~5 min of unacked probes. Catches half-open
1177+ * TCPs where the miner box vanished from the network without FIN.
1178+ *
1179+ * idle_timeout_sec <= 0 disables the read-timeout path (legacy blocking
1180+ * recv). Returns 0 on success, -1 on fatal setsockopt failure. */
1181+ static int conn_socket_setup (int fd , int idle_timeout_sec ) {
1182+ int one = 1 ;
1183+ /* TCP_NODELAY: stratum is tiny latency-sensitive JSON, don't Nagle. */
1184+ (void )setsockopt (fd , IPPROTO_TCP , TCP_NODELAY , & one , sizeof (one ));
1185+
1186+ /* Kernel keepalive. Default kernel setting is ~2h before probes even
1187+ * start, useless for our purposes — override with tight values. */
1188+ (void )setsockopt (fd , SOL_SOCKET , SO_KEEPALIVE , & one , sizeof (one ));
1189+ #ifdef TCP_KEEPIDLE
1190+ int idle = 120 ; /* start probing after 2 min of inactivity */
1191+ int intvl = 30 ; /* probe every 30s */
1192+ int cnt = 3 ; /* drop after 3 unacked probes ≈ 3.5 min total */
1193+ (void )setsockopt (fd , IPPROTO_TCP , TCP_KEEPIDLE , & idle , sizeof (idle ));
1194+ (void )setsockopt (fd , IPPROTO_TCP , TCP_KEEPINTVL , & intvl , sizeof (intvl ));
1195+ (void )setsockopt (fd , IPPROTO_TCP , TCP_KEEPCNT , & cnt , sizeof (cnt ));
1196+ #endif
1197+
1198+ if (idle_timeout_sec > 0 ) {
1199+ /* Poll interval: min(idle_timeout, 30s). Longer wastes the tail
1200+ * of the timeout; shorter costs one recv wake per fd per interval
1201+ * (500 conns × wake/30s = ~16/s, negligible). */
1202+ int poll_s = idle_timeout_sec < 30 ? idle_timeout_sec : 30 ;
1203+ struct timeval tv = { .tv_sec = poll_s , .tv_usec = 0 };
1204+ if (setsockopt (fd , SOL_SOCKET , SO_RCVTIMEO , & tv , sizeof (tv )) < 0 ) {
1205+ return -1 ;
1206+ }
1207+ }
1208+ return 0 ;
1209+ }
1210+
1211+ int stratum_socket_setup_for_test (int fd , int idle_timeout_sec ) {
1212+ return conn_socket_setup (fd , idle_timeout_sec );
1213+ }
1214+
11631215static void conn_register (stratum_server_t * s , stratum_conn_t * c ) {
11641216 pthread_mutex_lock (& s -> conns_lock );
11651217 c -> next = s -> conns_head ;
@@ -1183,9 +1235,34 @@ static void *conn_thread(void *arg) {
11831235 char buf [MAX_LINE_BYTES + 1 ];
11841236 size_t blen = 0 ;
11851237
1238+ /* Seed activity tracking at connect time — a client that never sends
1239+ * a single byte is still governed by cfg.idle_timeout_sec. */
1240+ c -> last_activity_ms = mono_ms ();
1241+ const uint64_t idle_timeout_ms =
1242+ s -> cfg .idle_timeout_sec > 0
1243+ ? (uint64_t )s -> cfg .idle_timeout_sec * 1000u
1244+ : 0 ;
1245+
11861246 while (!atomic_load (& s -> stop )) {
11871247 ssize_t n = recv (c -> fd , buf + blen , sizeof (buf ) - 1 - blen , 0 );
1188- if (n <= 0 ) break ;
1248+ if (n < 0 ) {
1249+ if (errno == EINTR ) continue ;
1250+ if (errno == EAGAIN || errno == EWOULDBLOCK ) {
1251+ /* SO_RCVTIMEO wake. Drop iff we've been silent past the
1252+ * configured budget. Otherwise loop and try again. */
1253+ if (idle_timeout_ms > 0 &&
1254+ mono_ms () - c -> last_activity_ms > idle_timeout_ms ) {
1255+ LOG_INFO ("stratum: idle timeout after %us — closing fd=%d worker='%s'" ,
1256+ s -> cfg .idle_timeout_sec , c -> fd ,
1257+ c -> worker_name [0 ] ? c -> worker_name : "(unauthorized)" );
1258+ goto done ;
1259+ }
1260+ continue ;
1261+ }
1262+ break ; /* real socket error */
1263+ }
1264+ if (n == 0 ) break ; /* peer closed */
1265+ c -> last_activity_ms = mono_ms ();
11891266 blen += (size_t )n ;
11901267 buf [blen ] = '\0' ;
11911268 for (;;) {
@@ -1234,8 +1311,12 @@ static void *listener_thread(void *arg) {
12341311 close (fd );
12351312 continue ;
12361313 }
1237- int one = 1 ;
1238- setsockopt (fd , IPPROTO_TCP , TCP_NODELAY , & one , sizeof (one ));
1314+ if (conn_socket_setup (fd , s -> cfg .idle_timeout_sec ) < 0 ) {
1315+ LOG_WARN ("stratum: socket setup failed for accepted fd: %s" ,
1316+ strerror (errno ));
1317+ close (fd );
1318+ continue ;
1319+ }
12391320 stratum_conn_t * c = stratum_conn_new_for_test (s );
12401321 if (!c ) { close (fd ); continue ; }
12411322 c -> fd = fd ;
@@ -1261,6 +1342,9 @@ int stratum_server_start(const stratum_cfg_t *cfg, stratum_server_t **out) {
12611342 s -> cfg = * cfg ;
12621343 if (s -> cfg .max_conns <= 0 ) s -> cfg .max_conns = 500 ;
12631344 if (s -> cfg .initial_diff <= 0 ) s -> cfg .initial_diff = 1.0 ;
1345+ /* Negative → explicit disable. 0 → apply default (10 min). Positive kept. */
1346+ if (s -> cfg .idle_timeout_sec == 0 ) s -> cfg .idle_timeout_sec = 600 ;
1347+ else if (s -> cfg .idle_timeout_sec < 0 ) s -> cfg .idle_timeout_sec = 0 ;
12641348 pthread_rwlock_init (& s -> job_lock , NULL );
12651349 pthread_mutex_init (& s -> recent_lock , NULL );
12661350 pthread_mutex_init (& s -> conns_lock , NULL );
0 commit comments