httpd: reap idle TCP connections (#390)

* uip: time out an ESTABLISHED connection whose peer went silent

uIP has no keepalive and no idle timeout for an ESTABLISHED connection, so
a peer that vanishes without FIN or RST holds it open forever. A crashed
client does it, and so does a SYN caught while the switch is still
booting. With UIP_CONF_MAX_CONNECTIONS = 1 that wedges the single slot:
the web UI stops answering, port 80 no longer even SYN-ACKs, and only a
power cycle recovers it. Every other TCP state already has an exit path
(UIP_MAXRTX, UIP_TIME_WAIT_TIMEOUT); idle ESTABLISHED was the one hole.

Close it inside uIP rather than in httpd, so any future TCP service is
covered too. The retransmission timer is unused while nothing is in
flight, so the idle count lives in it: a 1 Hz strobe derived from the
uip_periodic() rate ages it, any arriving segment clears it, and past the
limit the connection is aborted through the same path as UIP_MAXRTX. Two
new options in uipopt.h, default off; this project enables 30 s.

httpd also joins uip_timedout() to its cleanup: it only handled closed
and aborted, so both this timeout and the existing UIP_MAXRTX one would
have left the transfer state stuck on a dead connection.

* uip: keep the tuning values out of the prose

The comment named the timeout and the tick rate, which go stale the
moment either constant is edited. It now says why an idle connection
is aged out and leaves the numbers to the defines under it.
This commit is contained in:
d00f authored and GitHub committed 2026-09-04 07:11:49 +02:00
1 parent e26693826e
commit a7692022f1
4 files changed
+88 -1

No files matched your search

+1 -1
View File
@@ -842,7 +842,7 @@ void httpd_appcall(void)
} else if (uip_closed()) {
dbg_string("Connection closed\n");
s->tstate = TSTATE_CLOSED;
} else if (uip_aborted()) {
} else if (uip_aborted() || uip_timedout()) {
dbg_string("Connection aborted\n");
uip_close();
s->tstate = TSTATE_CLOSED;
+15
View File
@@ -96,6 +96,21 @@ typedef unsigned short uip_stats_t;
*/
#define UIP_CONF_MAX_CONNECTIONS 1
/**
* This httpd closes after every response, so an ESTABLISHED connection
* that stays idle is a peer that died or never sent its request. With a
* single connection slot it would hold the web UI until the next power
* cycle, so it is aged out instead.
*
* uip_periodic() runs from idle() once per system tick; interrupt
* wake-ups only add sweeps, so the timeout can fire early but never
* late.
*
* \hideinitializer
*/
#define UIP_CONF_IDLE_PERIODS 200
#define UIP_CONF_IDLE_TIMEOUT 30
/**
* Maximum number of listening TCP ports. TODO: increase this!
*
+43
View File
@@ -171,6 +171,11 @@ __xdata struct uip_udp_conn * __xdata uip_udp_conn;
__xdata struct uip_udp_conn uip_udp_conns[UIP_UDP_CONNS];
#endif /* UIP_UDP */
#if UIP_IDLE_TIMEOUT
__xdata static u8_t uip_idle_prescale;
__xdata static u8_t uip_idle_age;
#endif /* UIP_IDLE_TIMEOUT */
__xdata static u16_t ipid; /* Ths ipid variable is an increasing
number that is used for the IP ID
field. */
@@ -702,6 +707,15 @@ uip_process(u8_t flag) __banked
/* Check if we were invoked because of the perodic timer fireing. */
} else if(flag == UIP_TIMER) {
// print_string("T1\n");
#if UIP_IDLE_TIMEOUT
if(uip_connr == uip_conns) {
uip_idle_age = 0;
if(++uip_idle_prescale >= UIP_IDLE_PERIODS) {
uip_idle_prescale = 0;
uip_idle_age = 1;
}
}
#endif /* UIP_IDLE_TIMEOUT */
#if UIP_REASSEMBLY
if(uip_reasstmr != 0) {
--uip_reasstmr;
@@ -733,6 +747,21 @@ uip_process(u8_t flag) __banked
/* If the connection has outstanding data, we increase the
connection's timer and see if it has reached the RTO value
in which case we retransmit. */
#if UIP_IDLE_TIMEOUT
if(!uip_outstanding(uip_connr)) {
/* The retransmission timer is unused while nothing is in flight,
so idle time is counted in it instead. */
if(uip_idle_age &&
(uip_connr->tcpstateflags & UIP_TS_MASK) == UIP_ESTABLISHED &&
++(uip_connr->timer) >= UIP_IDLE_TIMEOUT) {
uip_connr->tcpstateflags = UIP_CLOSED;
uip_flags = UIP_TIMEDOUT;
UIP_APPCALL();
BUF->flags = TCP_RST | TCP_ACK;
goto tcp_send_nodata;
}
}
#endif /* UIP_IDLE_TIMEOUT */
if(uip_outstanding(uip_connr)) {
if(uip_connr->timer-- == 0) {
if(uip_connr->nrtx == UIP_MAXRTX ||
@@ -1550,6 +1579,11 @@ uip_process(u8_t flag) __banked
#endif /* UIP_ACTIVE_OPEN */
case UIP_ESTABLISHED:
#if UIP_IDLE_TIMEOUT
if(!uip_outstanding(uip_connr)) {
uip_connr->timer = 0;
}
#endif /* UIP_IDLE_TIMEOUT */
/* In the ESTABLISHED state, we call upon the application to feed
data into the uip_buf. If the UIP_ACKDATA flag is set, the
application should put new data into the buffer, otherwise we are
@@ -1576,6 +1610,9 @@ uip_process(u8_t flag) __banked
uip_connr->len = 1;
uip_connr->tcpstateflags = UIP_LAST_ACK;
uip_connr->nrtx = 0;
#if UIP_IDLE_TIMEOUT
uip_connr->timer = uip_connr->rto;
#endif /* UIP_IDLE_TIMEOUT */
tcp_send_finack:
BUF->flags = TCP_FIN | TCP_ACK;
goto tcp_send_nodata;
@@ -1665,6 +1702,9 @@ uip_process(u8_t flag) __banked
uip_connr->len = 1;
uip_connr->tcpstateflags = UIP_FIN_WAIT_1;
uip_connr->nrtx = 0;
#if UIP_IDLE_TIMEOUT
uip_connr->timer = uip_connr->rto;
#endif /* UIP_IDLE_TIMEOUT */
BUF->flags = TCP_FIN | TCP_ACK;
goto tcp_send_nodata;
}
@@ -1693,6 +1733,9 @@ uip_process(u8_t flag) __banked
/* Remember how much data we send out now so that we know
when everything has been acknowledged. */
uip_connr->len = uip_slen;
#if UIP_IDLE_TIMEOUT
uip_connr->timer = uip_connr->rto;
#endif /* UIP_IDLE_TIMEOUT */
} else {
/* If the application already had unacknowledged data, we
+29
View File
@@ -324,6 +324,35 @@
*/
#define UIP_TIME_WAIT_TIMEOUT 120
/**
* The number of UIP_TIMER sweeps that make one idle-aging period,
* i.e. how many times uip_periodic() runs per second in this project.
* Counted in an 8-bit variable, so 255 is the maximum.
*
* \hideinitializer
*/
#ifndef UIP_CONF_IDLE_PERIODS
#define UIP_IDLE_PERIODS 2
#else /* UIP_CONF_IDLE_PERIODS */
#define UIP_IDLE_PERIODS UIP_CONF_IDLE_PERIODS
#endif /* UIP_CONF_IDLE_PERIODS */
/**
* The number of idle-aging periods after which an ESTABLISHED
* connection with a silent peer is aborted, or 0 to keep such
* connections forever (the classic uIP behaviour). TCP itself has no
* idle timeout, so a peer that vanished without FIN or RST would
* otherwise occupy its connection slot until a reboot. Counted in the
* connection's 8-bit retransmission timer, so 255 is the maximum.
*
* \hideinitializer
*/
#ifndef UIP_CONF_IDLE_TIMEOUT
#define UIP_IDLE_TIMEOUT 0
#else /* UIP_CONF_IDLE_TIMEOUT */
#define UIP_IDLE_TIMEOUT UIP_CONF_IDLE_TIMEOUT
#endif /* UIP_CONF_IDLE_TIMEOUT */
/** @} */
/*------------------------------------------------------------------------------*/