From 5c19e6368f6d65dfbccd847fb34a9d99d7610db3 Mon Sep 17 00:00:00 2001 From: Vladimir Dubrovin <3proxy@3proxy.ru> Date: Sun, 18 Oct 2026 12:38:11 +0300 Subject: [PATCH] Unix sockets: remove on closing only the file this socket bound When a reload cannot take a listening Unix socket over (a bind-time -ol option changed), the new service binds the path anew, and the old listener, closing after it, removed the file, the new socket's by then, leaving nothing to connect to. The socket's file (device and inode) is now noted on binding and the path removed on closing only if it is still that file. structures.h includes for offsetof() in SASIZE(), which needs it wherever the header is used. Co-Authored-By: Claude Opus 5.5 --- src/proxymain.c | 32 ++++++++++++++++++++++++++++++-- src/structures.h | 3 +++ tests/cases/unixsock.py | 12 ++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/proxymain.c b/src/proxymain.c index 41cfc90..3c0090c 100644 --- a/src/proxymain.c +++ b/src/proxymain.c @@ -898,6 +898,7 @@ struct listener { char *netns, *device; struct sockoptset opts; int maxseg; + uint64_t undev, unino; /* its file, for a Unix socket on a path */ int optversion; /* the configuration whose options are set */ /* the services accepting on it, linked by lisnext */ struct srvparam *owners; @@ -972,6 +973,28 @@ static void listenerrelease(struct srvparam *srv); static int srvopen(struct srvstart *s); +#ifdef WITH_UN +/* A Unix socket bound to a path: its file is noted, as a service of a + newer configuration may bind the same path anew before this socket is + closed, when the file is the other's, which closing must not remove. */ +static void unbound(PROXYSOCKADDRTYPE *sa, uint64_t *dev, uint64_t *ino){ + struct stat st; + + if(*SAFAMILY(sa) == AF_UNIX && *SAADDR(sa) && !stat((char *)SAADDR(sa), &st)){ + *dev = (uint64_t)st.st_dev; + *ino = (uint64_t)st.st_ino; + } +} + +static void ununlink(PROXYSOCKADDRTYPE *sa, uint64_t dev, uint64_t ino){ + struct stat st; + + if(*SAFAMILY(sa) == AF_UNIX && *SAADDR(sa) && !stat((char *)SAADDR(sa), &st) && + (uint64_t)st.st_dev == dev && (uint64_t)st.st_ino == ino) + unlink((char *)SAADDR(sa)); +} +#endif + /* The listening socket of the service: its listener's, opened by the first of the listener's services to get here. */ static int srvlisten(struct srvstart *s){ @@ -993,6 +1016,8 @@ static int srvlisten(struct srvstart *s){ _3proxy_mutex_lock(&listener_mutex); l->sock = srv->srvsock; l->optversion = srv->version; + l->undev = srv->undev; + l->unino = srv->unino; _3proxy_mutex_unlock(&listener_mutex); } else if(srv->srvsock != INVALID_SOCKET){ @@ -1087,6 +1112,9 @@ static int srvopen(struct srvstart *s){ sleeptime = (sleeptime<<1); if(!sleeptime) return -3; } +#ifdef WITH_UN + unbound(&srv->intsa, &srv->undev, &srv->unino); +#endif #ifndef NOTCPMAIN if(!s->isudp){ if(srv->so._listen (srv->so.state, sock, srv->backlog?srv->backlog : 1+(srv->maxchild>>3))==-1) { @@ -1838,7 +1866,7 @@ static void listenerstop(struct srvparam *srv){ if(sock == INVALID_SOCKET) return; so._closesocket(so.state, sock); #ifdef WITH_UN - if(*SAFAMILY(&l->sa) == AF_UNIX && *SAADDR(&l->sa))unlink((char *)SAADDR(&l->sa)); + ununlink(&l->sa, l->undev, l->unino); #endif } @@ -2047,7 +2075,7 @@ void srvfree(struct srvparam * srv){ if(srv->srvsock != INVALID_SOCKET) { so._closesocket(srv->so.state, srv->srvsock); #ifdef WITH_UN - if(*SAFAMILY(&srv->intsa) == AF_UNIX && *SAADDR(&srv->intsa))unlink((char *)SAADDR(&srv->intsa)); + ununlink(&srv->intsa, srv->undev, srv->unino); #endif } srv->srvsock = INVALID_SOCKET; diff --git a/src/structures.h b/src/structures.h index d8159c5..10a762e 100644 --- a/src/structures.h +++ b/src/structures.h @@ -113,6 +113,7 @@ int #ifdef WITH_UN #include +#include #endif #define ALLOW 0 @@ -631,6 +632,8 @@ struct srvparam { AUTHFUNC authfunc; PROXYFUNC pf; SOCKET srvsock, cbsock; + /* the file of a Unix socket bound to a path, to remove only it */ + uint64_t undev, unino; int childcount; int maxchild; int backlog; diff --git a/tests/cases/unixsock.py b/tests/cases/unixsock.py index b965e99..2690917 100644 --- a/tests/cases/unixsock.py +++ b/tests/cases/unixsock.py @@ -157,6 +157,18 @@ def run(t): t.fail("its file is still there", path, "missing") t.eq((200, 1000), unix_get(path, url), "and the proxy answers on it") + # --- a bind-time option changed: a new socket on the same path, and + # the old one, closing after, must not remove the new one's file ------- + t.write_config("unixsock", config().replace(f"proxy -iunix:{path}", + f"proxy -olSO_REUSEADDR=0 -iunix:{path}")) + t.http(f"http://127.0.0.1:{adm}/R") + # the old services stop within a second of the reload + time.sleep(2) + t.ok("a new socket on the same path keeps its file when the old one closes") \ + if os.path.exists(path) else \ + t.fail("a new socket on the same path keeps its file when the old one closes", path, "missing") + t.eq((200, 1000), unix_get(path, url), "and the proxy answers on it") + # --- no service listens there any more: the file goes ------------------- t.write_config("unixsock", config(unix=False)) t.http(f"http://127.0.0.1:{adm}/R")