From 746d68916f94dbcb775bb300ad100bea4b1b9a9a Mon Sep 17 00:00:00 2001 From: zhanghongyu Date: Wed, 6 Aug 2025 21:40:47 +0800 Subject: [PATCH] net/tcp: add support for the CLOSE_WAIT state CLOSE-WAIT - represents waiting for a connection termination request from the local user. TCP A TCP B 1. ESTABLISHED ESTABLISHED 2. (Close) FIN-WAIT-1 --> --> CLOSE-WAIT 3. FIN-WAIT-2 <-- <-- CLOSE-WAIT 4. (Close) TIME-WAIT <-- <-- LAST-ACK 5. TIME-WAIT --> --> CLOSED 6. (2 MSL) CLOSED in the current state, we can continue to send data until the user calls shutdown or close, then directly enter the TCP_LAST_ACK state Signed-off-by: zhanghongyu --- Documentation/components/net/index.rst | 1 + .../components/net/tcp_state_machine.rst | 327 ++++++++++++++++++ include/nuttx/net/tcp.h | 7 +- net/tcp/tcp_appsend.c | 5 +- net/tcp/tcp_close.c | 6 +- net/tcp/tcp_devpoll.c | 3 +- net/tcp/tcp_input.c | 66 ++-- net/tcp/tcp_send_buffered.c | 10 +- net/tcp/tcp_shutdown.c | 3 +- net/tcp/tcp_timer.c | 4 +- 10 files changed, 395 insertions(+), 37 deletions(-) create mode 100644 Documentation/components/net/tcp_state_machine.rst diff --git a/Documentation/components/net/index.rst b/Documentation/components/net/index.rst index 148880d8982..f1cd6010832 100644 --- a/Documentation/components/net/index.rst +++ b/Documentation/components/net/index.rst @@ -19,6 +19,7 @@ Network Support wqueuedeadlocks.rst tcp_network_perf.rst delay_act_and_tcp_perf.rst + tcp_state_machine.rst ``net`` Directory Structure :: diff --git a/Documentation/components/net/tcp_state_machine.rst b/Documentation/components/net/tcp_state_machine.rst new file mode 100644 index 00000000000..da4b6156136 --- /dev/null +++ b/Documentation/components/net/tcp_state_machine.rst @@ -0,0 +1,327 @@ +============================= +NuttX TCP State Machine Notes +============================= + +This document describes how the current NuttX TCP stack implements TCP +state transitions. It is based on the in-tree implementation (primarily +in ``net/tcp``) and focuses on *what the code does today* rather than a +generic RFC 793 description. + +Scope +===== + +* TCP connection state is tracked per ``struct tcp_conn_s``. +* State transitions happen mainly in: + + * ``net/tcp/tcp_input.c`` (incoming segments and most transitions) + * ``net/tcp/tcp_timer.c`` (timeouts and retransmissions) + * ``net/tcp/tcp_conn.c`` (connect/listen-side allocation and initial state) + * ``net/tcp/tcp_close.c`` (active close initiation) + +State Representation +==================== + +NuttX stores TCP state in ``tcp_conn_s::tcpstateflags``. + +* Bits 0-3 are the state (``TCP_STATE_MASK``). +* Bit 4 is a flag (``TCP_STOPPED``) used by the socket layer to stop data flow. + +The state values are defined in ``include/nuttx/net/tcp.h``: + +* ``TCP_CLOSED`` +* ``TCP_ALLOCATED`` (NuttX-internal: allocated but not yet connected) +* ``TCP_SYN_RCVD`` +* ``TCP_SYN_SENT`` +* ``TCP_ESTABLISHED`` +* ``TCP_FIN_WAIT_1`` +* ``TCP_FIN_WAIT_2`` +* ``TCP_CLOSE_WAIT`` +* ``TCP_CLOSING`` +* ``TCP_TIME_WAIT`` +* ``TCP_LAST_ACK`` +* ``TCP_STOPPED`` + +Supported vs Unsupported (RFC State View) +========================================= + +NuttX largely follows the classic TCP state machine, the table below maps the traditional RFC 793 state names to what exists in +NuttX today. + +.. list-table:: RFC TCP states and their NuttX support + :header-rows: 1 + :widths: auto + + * - RFC state name + - NuttX representation + - Supported + - Notes + * - CLOSED + - ``TCP_CLOSED`` + - Yes + - Connection is unused/available. + * - LISTEN + - No ``tcpstateflags`` state + - Partially + - Listening is implemented via the listener table in ``net/tcp/tcp_listen.c``(``tcp_listenports[]``) rather than a per-connection LISTEN state. + * - SYN-SENT + - ``TCP_SYN_SENT`` + - Yes + - Set by ``tcp_connect()`` in ``net/tcp/tcp_conn.c``. + * - SYN-RECEIVED + - ``TCP_SYN_RCVD`` + - Yes + - Set when accepting an incoming SYN (new connection allocated for a listener). + * - ESTABLISHED + - ``TCP_ESTABLISHED`` + - Yes + - Data transfer state. + * - FIN-WAIT-1 + - ``TCP_FIN_WAIT_1`` + - Yes + - Entered on active close (local FIN sent). However, it is currently unable to continue receiving data in this state + * - FIN-WAIT-2 + - ``TCP_FIN_WAIT_2`` + - Yes + - Entered after ACK for local FIN (when peer hasn't closed yet). However, it is currently unable to continue receiving data in this state + * - CLOSE-WAIT + - Not implemented + - Yes + - The TCP input path explicitly notes CLOSE_WAIT is not implemented; NuttX forces the application to close when FIN is received and moves directly toward ``TCP_LAST_ACK``. + * - CLOSING + - ``TCP_CLOSING`` + - Yes + - Used for simultaneous close handling. + * - LAST-ACK + - ``TCP_LAST_ACK`` + - Yes + - Used after receiving FIN and sending FIN in response. + * - TIME-WAIT + - ``TCP_TIME_WAIT`` + - Yes + - Used after the close handshake; timer-driven cleanup. + +Note on ``TCP_ALLOCATED`` +------------------------- + +``TCP_ALLOCATED`` is NuttX-specific and has no direct RFC state name. +It is the pre-connect/pre-accept state for a newly created socket connection. + +High-level Transition Summary +============================= + +This section summarizes the most common state paths. + +Active open (connect) +--------------------- + +Typical client-side flow: + +:: + + TCP_ALLOCATED + -> TCP_SYN_SENT (tcp_connect() prepares SYN) + -> TCP_ESTABLISHED (tcp_input receives SYN|ACK and replies ACK) + +Passive open (listen/accept) +---------------------------- + +Listening sockets are registered in the listener table (not a LISTEN state). +When a SYN arrives: + +:: + + listener in tcp_listenports[] + -> new conn: TCP_SYN_RCVD (tcp_allocaccept() in tcp_conn.c) + -> TCP_ESTABLISHED (tcp_input receives final ACK) + -> accept() wakes up (tcp_accept_connection()) + +Graceful close (active close) +----------------------------- + +When the application initiates a close (or ``shutdown(SHUT_WR)``), the stack +sends FIN and transitions: + +:: + + TCP_ESTABLISHED + -> TCP_FIN_WAIT_1 + -> TCP_FIN_WAIT_2 (ACK of our FIN) + -> TCP_TIME_WAIT (FIN from peer) + -> TCP_CLOSED (timer expiry) + +Simultaneous close +------------------ + +If FIN is received while we are in ``TCP_FIN_WAIT_1`` and our FIN has not been +fully ACKed, NuttX can enter ``TCP_CLOSING``: + +:: + + TCP_FIN_WAIT_1 + -> TCP_CLOSING + -> TCP_TIME_WAIT (ACK of our FIN) + +Passive close (peer closes first) +--------------------------------- + +When FIN is received in ESTABLISHED, the application is notified +via callbacks. the stack sends ACK and goes to ``TCP_CLOSE_WAIT``: + +:: + + TCP_ESTABLISHED + -> TCP_CLOSE_WAIT (FIN received) + -> TCP_CLOSED (ACK of our FIN) + +Detailed State Handling +======================= + +TCP_SYN_SENT +------------ + +* Entered by ``tcp_connect()`` (``net/tcp/tcp_conn.c``). +* On receiving ``SYN|ACK`` with a valid ACK: + + * Parses options (e.g., MSS). + * Sets ``TCP_ESTABLISHED``. + * Updates ``rcvseq`` and window tracking. + * Notifies the socket layer using ``TCP_CONNECTED``. + +* On unexpected control segments or failure: + + * The connection is aborted (``TCP_ABORT`` callback) and a RST may be sent. + +TCP_SYN_RCVD +------------ + +* Entered for a newly accepted connection when a SYN matches a listener. + Allocation and initialization occur in ``tcp_allocaccept()`` + (``net/tcp/tcp_conn.c``). +* A SYN-ACK is sent. The retransmission is handled by ``tcp_timer.c``. +* On receiving the final ACK (``TCP_ACKDATA``): + + * Transition to ``TCP_ESTABLISHED``. + * ``tcp_accept_connection()`` is called to hand the connection to the + listening socket/accept logic. + +TCP_ESTABLISHED +--------------- + +* Normal data transfer occurs here. +* Incoming data and ACK processing is handled in ``net/tcp/tcp_input.c``. +* If a FIN is received: + + * The application is notified (``TCP_CLOSE`` flag is included in callback). + * NuttX transitions to ``TCP_CLOSE_WAIT`` and sends ``ACK``. + +TCP_CLOSE_WAIT +-------------- + +* Only entered when a FIN is received in ESTABLISHED. +* The application is notified (``TCP_CLOSE`` flag in callback). +* NuttX can send data until the application initiates close. +* On application close request: + * NuttX sends FIN and transitions to ``TCP_LAST_ACK``. + +TCP_FIN_WAIT_1 +-------------- + +* Entered when the application requests a graceful close. + This is initiated in ``net/tcp/tcp_appsend.c`` when the callback result + contains ``TCP_CLOSE``. + +* On receiving FIN: + + * If the FIN also ACKs our FIN and ``tx_unacked == 0``: transition to + ``TCP_TIME_WAIT``. + * Otherwise: transition to ``TCP_CLOSING``. + * In both cases, ACK the peer FIN. + +* On receiving an ACK that completes ACK of our FIN (and no FIN from peer): + + * Transition to ``TCP_FIN_WAIT_2``. + +* Data received in FIN_WAIT_1: + + * Current behavior is to send a RST and force ``TCP_CLOSED``. + * The implementation notes this as a TODO to improve shutdown behavior. + +TCP_FIN_WAIT_2 +-------------- + +* Waiting for the peer FIN after our FIN was ACKed. +* On receiving FIN: + + * Transition to ``TCP_TIME_WAIT``. + * ACK the FIN and notify close. + +* Data received in FIN_WAIT_2: + + * Current behavior is to send a RST and force ``TCP_CLOSED``. + +TCP_CLOSING +----------- + +* Simultaneous close case. +* When the ACK for our FIN is received (``TCP_ACKDATA``): + + * Transition to ``TCP_TIME_WAIT``. + +TCP_LAST_ACK +------------ + +* Entered after FIN is received in ESTABLISHED and the application chooses + to close, causing the stack to send FIN. +* On receiving ACK for our FIN (``TCP_ACKDATA``): + + * Transition to ``TCP_CLOSED``. + * Notify close via callback. + +TCP_TIME_WAIT +------------- + +* NuttX responds to segments by sending an ACK. +* Cleanup is timer-driven (see ``tcp_timer.c``): + + * ``TCP_TIME_WAIT`` are handled as "wait for timeout" states. + * When the per-connection timer expires, the state becomes ``TCP_CLOSED``. + +Timers, Retransmissions, and Failure Handling +============================================= + +The TCP timer handler in ``net/tcp/tcp_timer.c`` drives: + +* Retransmission for connections with ``tx_unacked > 0``. +* State-specific retransmit behavior: + + * ``TCP_SYN_RCVD``: retransmit SYN-ACK. + * ``TCP_SYN_SENT``: retransmit SYN. + * ``TCP_ESTABLISHED``: request retransmit via callback (``TCP_REXMIT``). + * ``TCP_FIN_WAIT_1``, ``TCP_CLOSING``, ``TCP_LAST_ACK``: retransmit FIN|ACK. + +* Timeout cleanup: + + * ``TCP_SYN_RCVD``: if SYN-ACK retransmits exceed limit, the half-open + connection is closed and freed. + * ``TCP_SYN_SENT`` and established cases: if retransmits exceed limit, the + connection is closed, the socket is notified (``TCP_TIMEDOUT``), and a + RST may be sent. + +Deviations and Notable Simplifications +====================================== + +* LISTEN is not an explicit TCP state; it is represented by listener table entries. +* FIN_WAIT_* data handling is currently strict: received payload data in + FIN_WAIT_1/2 results in sending RST and closing the connection. +* RST processing is intentionally simple (accept RST and close). + +Where to Look in the Code +========================= + +* State definitions: ``include/nuttx/net/tcp.h`` +* Incoming-segment state logic: ``net/tcp/tcp_input.c`` +* Retransmission/timeout logic: ``net/tcp/tcp_timer.c`` +* Connect path / SYN_SENT setup: ``net/tcp/tcp_conn.c`` +* Accept path / SYN_RCVD allocation: ``net/tcp/tcp_conn.c`` +* Active close initiation: ``net/tcp/tcp_close.c`` and ``net/tcp/tcp_shutdown.c`` +* Listener table (LISTEN semantics): ``net/tcp/tcp_listen.c`` diff --git a/include/nuttx/net/tcp.h b/include/nuttx/net/tcp.h index 5d921d6deb2..5804b19f5a0 100644 --- a/include/nuttx/net/tcp.h +++ b/include/nuttx/net/tcp.h @@ -88,9 +88,10 @@ # define TCP_ESTABLISHED 0x04 # define TCP_FIN_WAIT_1 0x05 # define TCP_FIN_WAIT_2 0x06 -# define TCP_CLOSING 0x07 -# define TCP_TIME_WAIT 0x08 -# define TCP_LAST_ACK 0x09 +# define TCP_CLOSE_WAIT 0x07 +# define TCP_CLOSING 0x08 +# define TCP_TIME_WAIT 0x09 +# define TCP_LAST_ACK 0x0a # define TCP_STOPPED 0x10 /* Bit 4: stopped */ /* Bit 5-7: Unused, but not available */ diff --git a/net/tcp/tcp_appsend.c b/net/tcp/tcp_appsend.c index 97b1cf16d7a..bf1f2e6d6a8 100644 --- a/net/tcp/tcp_appsend.c +++ b/net/tcp/tcp_appsend.c @@ -194,13 +194,14 @@ void tcp_appsend(FAR struct net_driver_s *dev, FAR struct tcp_conn_s *conn, else if ((result & TCP_CLOSE) != 0) { - conn->tcpstateflags = TCP_FIN_WAIT_1; + conn->tcpstateflags = conn->tcpstateflags == TCP_CLOSE_WAIT ? + TCP_LAST_ACK : TCP_FIN_WAIT_1; conn->tx_unacked = 1; conn->nrtx = 0; #ifdef CONFIG_NET_TCP_WRITE_BUFFERS conn->sndseq_max = tcp_getsequence(conn->sndseq) + 1; #endif - ninfo("TCP state: TCP_FIN_WAIT_1\n"); + ninfo("TCP state: %d\n", conn->tcpstateflags); dev->d_sndlen = 0; tcp_send(dev, conn, TCP_FIN | TCP_ACK, hdrlen); diff --git a/net/tcp/tcp_close.c b/net/tcp/tcp_close.c index d392d684b5d..d91e2b1227c 100644 --- a/net/tcp/tcp_close.c +++ b/net/tcp/tcp_close.c @@ -153,7 +153,8 @@ static uint16_t tcp_close_eventhandler(FAR struct net_driver_s *dev, * TCP_CLOSE is handled above. */ - DEBUGASSERT(conn->tcpstateflags == TCP_ESTABLISHED); + DEBUGASSERT(conn->tcpstateflags == TCP_ESTABLISHED || + conn->tcpstateflags == TCP_CLOSE_WAIT); /* Drop data received in this state and make sure that TCP_CLOSE * is set in the response @@ -236,7 +237,8 @@ static inline int tcp_close_disconnect(FAR struct socket *psock) */ if ((conn->tcpstateflags == TCP_ESTABLISHED || - conn->tcpstateflags == TCP_LAST_ACK) && + conn->tcpstateflags == TCP_LAST_ACK || + conn->tcpstateflags == TCP_CLOSE_WAIT) && (conn->clscb = tcp_callback_alloc(conn)) != NULL) { /* Free rx buffers of the connection immediately */ diff --git a/net/tcp/tcp_devpoll.c b/net/tcp/tcp_devpoll.c index 741b13ff72c..b3e37dd4b5c 100644 --- a/net/tcp/tcp_devpoll.c +++ b/net/tcp/tcp_devpoll.c @@ -109,7 +109,8 @@ void tcp_poll(FAR struct net_driver_s *dev, FAR struct tcp_conn_s *conn) /* Verify that the connection is established. */ - if ((conn->tcpstateflags & TCP_STATE_MASK) == TCP_ESTABLISHED) + if ((conn->tcpstateflags & TCP_STATE_MASK) == TCP_ESTABLISHED || + (conn->tcpstateflags & TCP_STATE_MASK) == TCP_CLOSE_WAIT) { /* Set up for the callback. We can't know in advance if the * application is going to send a IPv4 or an IPv6 packet, so this diff --git a/net/tcp/tcp_input.c b/net/tcp/tcp_input.c index 1bb3378d258..07780467135 100644 --- a/net/tcp/tcp_input.c +++ b/net/tcp/tcp_input.c @@ -982,8 +982,7 @@ found: g_netstats.tcp.drop, seq, TCP_SEQ_ADD(seq, dev->d_len), dev->d_len); - dev->d_len = 0; - return; + goto drop; } } #endif @@ -1046,7 +1045,8 @@ found: * bytes */ - if ((conn->tcpstateflags & TCP_STATE_MASK) == TCP_ESTABLISHED) + if ((conn->tcpstateflags & TCP_STATE_MASK) == TCP_ESTABLISHED || + (conn->tcpstateflags & TCP_STATE_MASK) == TCP_CLOSE_WAIT) { nwarn("WARNING: ackseq > unackseq\n"); nwarn("sndseq=%" PRIu32 " tx_unacked=%" PRIu32 @@ -1109,7 +1109,8 @@ found: /* Check if no packet need to retransmission, clear timer. */ - if (conn->tx_unacked == 0 && conn->tcpstateflags == TCP_ESTABLISHED) + if (conn->tx_unacked == 0 && (conn->tcpstateflags == TCP_ESTABLISHED || + conn->tcpstateflags == TCP_CLOSE_WAIT)) { timeout = 0; } @@ -1397,8 +1398,6 @@ found: * has been closed. */ - flags |= TCP_CLOSE; - if (dev->d_len > 0) { flags |= TCP_NEWDATA; @@ -1406,23 +1405,10 @@ found: result = tcp_callback(dev, conn, flags); - if ((result & TCP_CLOSE) != 0) - { - conn->tcpstateflags = TCP_LAST_ACK; - conn->tx_unacked = 1; - conn->nrtx = 0; - net_incr32(conn->rcvseq, 1); /* ack FIN */ -#ifdef CONFIG_NET_TCP_WRITE_BUFFERS - conn->sndseq_max = tcp_getsequence(conn->sndseq) + 1; -#endif - ninfo("TCP state: TCP_LAST_ACK\n"); - tcp_send(dev, conn, TCP_FIN | TCP_ACK, tcpiplen); - } - else - { - ninfo("TCP: Dropped a FIN\n"); - tcp_appsend(dev, conn, result); - } + conn->tcpstateflags = TCP_CLOSE_WAIT; + net_incr32(conn->rcvseq, 1); /* ack FIN */ + ninfo("TCP state: TCP_CLOSE_WAIT\n"); + tcp_appsend(dev, conn, result | TCP_SNDACK); return; } @@ -1681,6 +1667,40 @@ found: ninfo("TCP state: TCP_TIME_WAIT\n"); } + goto drop; + + case TCP_CLOSE_WAIT: +#ifdef CONFIG_NET_TCP_KEEPALIVE + /* If the established socket receives an ACK or any kind of data + * from the remote peer (whether we accept it or not), then reset + * the keep alive timer. + */ + + if (conn->keepalive && (tcp->flags & TCP_ACK) != 0) + { + /* Reset the "alive" timer. */ + + tcp_update_keeptimer(conn, conn->keepidle); + conn->keepretries = 0; + } +#endif + + if ((flags & TCP_ACKDATA) != 0) + { + dev->d_sndlen = 0; + + /* Provide the packet to the application */ + + result = tcp_callback(dev, conn, flags); + + /* Send the response, ACKing the data or not, as appropriate */ + + tcp_appsend(dev, conn, result); + return; + } + + goto drop; + default: break; } diff --git a/net/tcp/tcp_send_buffered.c b/net/tcp/tcp_send_buffered.c index 1e47081aefb..bbf1fa6957f 100644 --- a/net/tcp/tcp_send_buffered.c +++ b/net/tcp/tcp_send_buffered.c @@ -197,8 +197,8 @@ static void retransmit_segment(FAR struct tcp_conn_s *conn, * retransmitted, and un-ACKed, if expired is not zero, the * connection will be closed. * - * field expired can only be updated at TCP_ESTABLISHED - * state + * field expired can only be updated at TCP_ESTABLISHED and + * TCP_CLOSE_WAIT state. */ conn->expired++; @@ -936,7 +936,8 @@ static uint16_t psock_send_eventhandler(FAR struct net_driver_s *dev, * retransmitted, and un-ACKed, if expired is not zero, the * connection will be closed. * - * field expired can only be updated at TCP_ESTABLISHED state + * field expired can only be updated at TCP_ESTABLISHED and + * TCP_CLOSE_WAIT state. */ conn->expired++; @@ -1012,7 +1013,8 @@ static uint16_t psock_send_eventhandler(FAR struct net_driver_s *dev, * will have to wait for the next polling cycle. */ - if ((conn->tcpstateflags & TCP_ESTABLISHED) && + if ((conn->tcpstateflags & TCP_ESTABLISHED || + conn->tcpstateflags & TCP_CLOSE_WAIT) && ((flags & TCP_NEWDATA) == 0) && (flags & (TCP_POLL | TCP_REXMIT | TCP_ACKDATA)) && !(sq_empty(&conn->write_q)) && diff --git a/net/tcp/tcp_shutdown.c b/net/tcp/tcp_shutdown.c index 2f18a9814f2..840a2160eef 100644 --- a/net/tcp/tcp_shutdown.c +++ b/net/tcp/tcp_shutdown.c @@ -112,7 +112,8 @@ static inline int tcp_send_fin(FAR struct socket *psock) if ((conn->tcpstateflags == TCP_ESTABLISHED || conn->tcpstateflags == TCP_SYN_SENT || - conn->tcpstateflags == TCP_SYN_RCVD)) + conn->tcpstateflags == TCP_SYN_RCVD || + conn->tcpstateflags == TCP_CLOSE_WAIT)) { if ((conn->shdcb = tcp_callback_alloc(conn)) == NULL) { diff --git a/net/tcp/tcp_timer.c b/net/tcp/tcp_timer.c index d178e2f51bc..76715c0471c 100644 --- a/net/tcp/tcp_timer.c +++ b/net/tcp/tcp_timer.c @@ -623,6 +623,7 @@ void tcp_timer(FAR struct net_driver_s *dev, FAR struct tcp_conn_s *conn) goto done; case TCP_ESTABLISHED: + case TCP_CLOSE_WAIT: /* In the ESTABLISHED state, we call upon the application * to do the actual retransmit after which we jump into @@ -673,7 +674,8 @@ void tcp_timer(FAR struct net_driver_s *dev, FAR struct tcp_conn_s *conn) * connection has been established. */ - else if ((conn->tcpstateflags & TCP_STATE_MASK) == TCP_ESTABLISHED) + else if ((conn->tcpstateflags & TCP_STATE_MASK) == TCP_ESTABLISHED || + (conn->tcpstateflags & TCP_STATE_MASK) == TCP_CLOSE_WAIT) { #ifdef CONFIG_NET_TCP_KEEPALIVE /* Is this an established connected with KeepAlive enabled? */