From 915170321da7dba4ba69a8a3fd72884c34ffc024 Mon Sep 17 00:00:00 2001 From: Renaud Allard Date: Fri, 19 Jun 2026 16:44:37 +0200 Subject: [PATCH] sock: drain sockets before close to avoid FIN_WAIT_2 buildup conn_destroy_contents() called close() right after a FIN was sent to the peer, orphaning the socket while it was still in FIN_WAIT_2. Linux reaps such sockets via net.ipv4.tcp_fin_timeout, but OpenBSD has no equivalent, so they pile up until the proxy stalls. Add close_socket(), which sends our FIN with shutdown(SHUT_WR), drains the peer until read() returns 0, then close()s. By then the close handshake is complete and the socket moves to TIME_WAIT, which the kernel reaps on its own. Use it for both descriptors in conn_destroy_contents() and add the matching shutdown(server_fd, SHUT_WR) in relay_connection(). --- src/conns.c | 9 +++------ src/reqs.c | 1 + src/sock.c | 34 ++++++++++++++++++++++++++++++++++ src/sock.h | 1 + 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/conns.c b/src/conns.c index 19aaa49c..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) { @@ -82,14 +83,10 @@ 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 0998324d..12f073b1 100644 --- a/src/reqs.c +++ b/src/reqs.c @@ -1259,6 +1259,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; } diff --git a/src/sock.c b/src/sock.c index ec8ad4c8..5e3f080d 100644 --- a/src/sock.c +++ b/src/sock.c @@ -212,6 +212,40 @@ int opensock (const char *host, int port, const char *bind_to) return sockfd; } +/* + * Gracefully close a socket by completing the TCP close handshake. + * + * shutdown(SHUT_WR) sends our FIN while the fd stays open, then we + * drain whatever the peer still sends until read() returns 0, which + * signals that the peer's FIN has arrived. Only then do we close(). + * By that point the four-way handshake is complete and the socket + * moves to TIME_WAIT, which the kernel reaps on its own. + * + * Calling close() on a socket still in FIN_WAIT_2 orphans it. Linux + * reaps orphaned FIN_WAIT_2 sockets via net.ipv4.tcp_fin_timeout, but + * OpenBSD has no equivalent, so they accumulate until the proxy + * stalls. See RFC 793 sec. 3.5 and Stevens, UNIX Network + * Programming Vol. 1 sec. 6.6. + */ +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, (void *) &tv, sizeof (tv)); + + do { + n = read (fd, drain, sizeof (drain)); + } while (n > 0 || (n < 0 && errno == EINTR)); + + close (fd); +} + /** * Try to listen on one socket based on the addrinfo * as returned from getaddrinfo. diff --git a/src/sock.h b/src/sock.h index 9763490d..86d7e7f0 100644 --- a/src/sock.h +++ b/src/sock.h @@ -52,6 +52,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 void set_socket_timeout(int fd);