From 004da4be13b41c27045c1226712b7f77d86f04f7 Mon Sep 17 00:00:00 2001 From: Renaud Allard Date: Thu, 19 Mar 2026 17:45:31 +0100 Subject: [PATCH 1/2] Fix FIN_WAIT_2 accumulation by draining sockets before close After shutdown(SHUT_WR) sends a FIN to the remote, tinyproxy was calling close() without waiting for the remote FIN. The socket was orphaned in FIN_WAIT_2 state. On Linux this is masked by tcp_fin_timeout reaping orphaned sockets after 60s. On OpenBSD, without SO_KEEPALIVE, these sockets persist indefinitely and accumulate until the proxy stalls. Add close_socket() that calls shutdown(SHUT_WR), then drains the socket with a 2-second receive timeout to allow the remote FIN to arrive before calling close(). Use it in conn_destroy_contents() for both client and server file descriptors, covering all exit paths from relay_connection() including idle timeout and poll error returns. Also add the missing shutdown(server_fd, SHUT_WR) in relay_connection() after flushing remaining data to the upstream server, so the server receives a proper FIN rather than relying on the implicit close(). --- src/conns.c | 41 +++++++++++++++++++++++++++++++++++------ src/reqs.c | 1 + 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/conns.c b/src/conns.c index 19aaa49c..7bcde61d 100644 --- a/src/conns.c +++ b/src/conns.c @@ -77,19 +77,48 @@ int conn_init_contents (struct conn_s *connptr, const char *ipaddr, return 0; } +/* + * Gracefully close a socket by completing the TCP close handshake. + * Send FIN via shutdown(SHUT_WR), then drain remaining data with a + * short timeout to receive the remote FIN. This prevents connections + * from getting stuck in FIN_WAIT_2 on systems that do not aggressively + * reap orphaned sockets (e.g. OpenBSD without SO_KEEPALIVE). + */ +static void close_socket (int fd) +{ + char drain[4096]; + ssize_t n; + struct timeval tv; + + shutdown (fd, SHUT_WR); + + tv.tv_sec = 2; + tv.tv_usec = 0; + setsockopt (fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof (tv)); + + for (;;) { + n = read (fd, drain, sizeof (drain)); + if (n > 0) + continue; + if (n == 0) + break; + if (errno == EINTR) + continue; + break; + } + + close (fd); +} + void conn_destroy_contents (struct conn_s *connptr) { assert (connptr != NULL); if (connptr->client_fd != -1) - if (close (connptr->client_fd) < 0) - log_message (LOG_INFO, "Client (%d) close message: %s", - connptr->client_fd, strerror (errno)); + close_socket (connptr->client_fd); connptr->client_fd = -1; if (connptr->server_fd != -1) - if (close (connptr->server_fd) < 0) - log_message (LOG_INFO, "Server (%d) close message: %s", - connptr->server_fd, strerror (errno)); + close_socket (connptr->server_fd); connptr->server_fd = -1; if (connptr->cbuffer) diff --git a/src/reqs.c b/src/reqs.c index 94ce7673..e3e7e311 100644 --- a/src/reqs.c +++ b/src/reqs.c @@ -1281,6 +1281,7 @@ static void relay_connection (struct conn_s *connptr) if (write_buffer (connptr->server_fd, connptr->cbuffer) < 0) break; } + shutdown (connptr->server_fd, SHUT_WR); return; } From 1be64efa6aef16ad02957e0b63629b043f1e424e Mon Sep 17 00:00:00 2001 From: Renaud Allard Date: Sun, 29 Mar 2026 19:20:10 +0200 Subject: [PATCH 2/2] Address review: move close_socket to sock.c, bump drain timeout to 10s --- src/conns.c | 34 +--------------------------------- src/sock.c | 37 +++++++++++++++++++++++++++++++++++++ src/sock.h | 1 + 3 files changed, 39 insertions(+), 33 deletions(-) diff --git a/src/conns.c b/src/conns.c index 7bcde61d..b70fbdeb 100644 --- a/src/conns.c +++ b/src/conns.c @@ -28,6 +28,7 @@ #include "conns.h" #include "heap.h" #include "log.h" +#include "sock.h" #include "stats.h" void conn_struct_init(struct conn_s *connptr) { @@ -77,39 +78,6 @@ int conn_init_contents (struct conn_s *connptr, const char *ipaddr, return 0; } -/* - * Gracefully close a socket by completing the TCP close handshake. - * Send FIN via shutdown(SHUT_WR), then drain remaining data with a - * short timeout to receive the remote FIN. This prevents connections - * from getting stuck in FIN_WAIT_2 on systems that do not aggressively - * reap orphaned sockets (e.g. OpenBSD without SO_KEEPALIVE). - */ -static void close_socket (int fd) -{ - char drain[4096]; - ssize_t n; - struct timeval tv; - - shutdown (fd, SHUT_WR); - - tv.tv_sec = 2; - tv.tv_usec = 0; - setsockopt (fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof (tv)); - - for (;;) { - n = read (fd, drain, sizeof (drain)); - if (n > 0) - continue; - if (n == 0) - break; - if (errno == EINTR) - continue; - break; - } - - close (fd); -} - void conn_destroy_contents (struct conn_s *connptr) { assert (connptr != NULL); diff --git a/src/sock.c b/src/sock.c index b3b0920d..50385506 100644 --- a/src/sock.c +++ b/src/sock.c @@ -212,6 +212,43 @@ int opensock (const char *host, int port, const char *bind_to) return sockfd; } +/* + * Gracefully close a socket by completing the TCP close handshake. + * Send FIN via shutdown(SHUT_WR), then drain remaining data until + * the remote FIN arrives (read returns 0). This keeps the fd open + * until both sides have exchanged FINs, preventing the socket from + * being orphaned in FIN_WAIT_2. + * + * Without this, close() orphans the socket while still in FIN_WAIT_2. + * Linux reaps orphaned FIN_WAIT_2 via net.ipv4.tcp_fin_timeout, but + * OpenBSD has no equivalent, so they persist indefinitely. + */ +void close_socket (int fd) +{ + char drain[4096]; + ssize_t n; + struct timeval tv; + + shutdown (fd, SHUT_WR); + + tv.tv_sec = 10; + tv.tv_usec = 0; + setsockopt (fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof (tv)); + + for (;;) { + n = read (fd, drain, sizeof (drain)); + if (n > 0) + continue; + if (n == 0) + break; + if (errno == EINTR) + continue; + break; + } + + close (fd); +} + /* * Set the socket to non blocking -rjkaes */ diff --git a/src/sock.h b/src/sock.h index 70c44739..700e958e 100644 --- a/src/sock.h +++ b/src/sock.h @@ -53,6 +53,7 @@ union sockaddr_union { extern int opensock (const char *host, int port, const char *bind_to); extern int listen_sock (const char *addr, uint16_t port, sblist* listen_fds); +extern void close_socket (int fd); extern int socket_nonblocking (int sock); extern int socket_blocking (int sock);