From ef4969da19390119015317d5d5fdc38ef6e54efa Mon Sep 17 00:00:00 2001 From: Vladimir Dubrovin <3proxy@3proxy.ru> Date: Sun, 18 Oct 2026 12:05:27 +0300 Subject: [PATCH] Unix sockets: no addresses on streams, abstract names, tcppm port 0 On Linux a proxy listening on a Unix socket relayed only the first bytes of an answer (result 95): sockmap() read with recvfrom() into the client's address, which for a Unix stream socket Linux fills with an empty AF_UNIX one, and then sent with sendto() to that address, which a connected Unix stream socket refuses. A TCP connection has no use for addresses: sockmap() now sends and receives with NULL ones, and connect-back sends its command with socksend() and reads the answer with sockrecvfrom() without an address, which a NULL sin now allows. An abstract socket name (-iunix:@NAME) was bound with the whole size of struct sockaddr_un, padded with zeros, so a client naming it by its bytes, as most do, reached another name. SASIZE() now gives an abstract address its exact size; getsockname() calls that took SASIZE() of the old contents as the buffer size take the buffer's size instead. tcppm refused port 0 for a unix: target, though the manual gives "tcppm 8080 unix:/var/run/app.sock 0" as the example: the port of a Unix socket target is ignored, so 0 is allowed. On a reload the new service bound the path anew and the old one, closing after it, removed the file, the new service's by then, leaving nothing to connect to. A service now notes the file it bound (device and inode) and removes the path on closing only if it is still that file. A test case covers a proxy on a Unix socket and on an abstract one on Linux, tcppm to a Unix socket, a parent reached through one, and a listening Unix socket across reloads: its file kept and the proxy answering, and the file removed once no service listens on it. Co-Authored-By: Claude Opus 5.5 --- src/proxymain.c | 45 +++++++++++-- src/sockgetchar.c | 3 +- src/sockmap.c | 17 +++-- src/socks.c | 4 +- src/structures.h | 7 +- src/udpsockmap.c | 2 +- tests/cases/unixsock.py | 139 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 199 insertions(+), 18 deletions(-) create mode 100644 tests/cases/unixsock.py diff --git a/src/proxymain.c b/src/proxymain.c index 70eb533..d7ba248 100644 --- a/src/proxymain.c +++ b/src/proxymain.c @@ -84,7 +84,7 @@ void * threadfunc (void *p) { continue; } #endif - if(socksendto(param, param->remsock, (struct sockaddr*)¶m->sinsr, (unsigned char *)"C", 1, CONNBACK_TO*1000) != 1){ + if(socksend(param, param->remsock, (unsigned char *)"C", 1, CONNBACK_TO) != 1){ dolog(param, (unsigned char *)"Connect back sending command failed"); param->srv->so._closesocket(param->sostate, param->remsock); param->remsock = INVALID_SOCKET; @@ -705,7 +705,12 @@ static int srvprepare(struct srvstart *s, int argc, char **argv){ else { #endif #ifndef NOPORTMAP - if (error || argc != i+3 || *argv[i]=='-'|| (*SAPORT(&srv->intsa) = htons((uint16_t)atoi(argv[i])))==0 || (srv->targetport = htons((uint16_t)atoi(argv[i+2])))==0) { + /* the port of a Unix socket target is ignored, 0 as the manual has it */ + if (error || argc != i+3 || *argv[i]=='-'|| (*SAPORT(&srv->intsa) = htons((uint16_t)atoi(argv[i])))==0 || ((srv->targetport = htons((uint16_t)atoi(argv[i+2])))==0 +#ifdef WITH_UN + && strncmp(argv[i+1], "unix:", 5) +#endif + )) { fprintf(stderr, "%s of %s\n" "Usage: %s options" " [-e] " @@ -761,6 +766,11 @@ static int srvprepare(struct srvstart *s, int argc, char **argv){ /* The listening socket. A UDP one also receives the datagrams of new clients, so it is their client socket too. 0, or the code to exit with. */ +#ifdef WITH_UN +static void unbound(struct srvparam *srv); +static void ununlink(struct srvparam *srv); +#endif + static int srvlisten(struct srvstart *s){ struct srvparam *srv = &s->srv; struct clientparam *defparam = &s->defparam; @@ -815,6 +825,9 @@ static int srvlisten(struct srvstart *s){ sleeptime = (sleeptime<<1); if(!sleeptime) return -3; } +#ifdef WITH_UN + unbound(srv); +#endif #ifndef NOTCPMAIN if(!s->isudp){ if(srv->so._listen (srv->so.state, sock, srv->backlog?srv->backlog : 1+(srv->maxchild>>3))==-1) { @@ -909,7 +922,7 @@ static int tcpnext(struct srvstart *s, SOCKET *client, int *nlog){ return NEXT_RETRY; } - if(sockrecvfrom(NULL, new_sock,(struct sockaddr*)&defparam->sincr,buf,1,60*1000) != 1 || *buf!='C') { + if(sockrecvfrom(NULL, new_sock, NULL, buf, 1, 60*1000) != 1 || *buf!='C') { so._closesocket(so.state, new_sock); usleep(SLEEPTIME); return NEXT_RETRY; @@ -1478,13 +1491,37 @@ void srvinit2(struct srvparam * srv, struct clientparam *param){ param->sinsr = srv->extsa; } +#ifdef WITH_UN +/* A service bound to a Unix socket path notes its file, as a service of a + newer configuration may bind the same path before this one closes: the + file is then the other's, which closing must not remove. */ +static void unbound(struct srvparam *srv){ + struct stat st; + + if(*SAFAMILY(&srv->intsa) == AF_UNIX && *SAADDR(&srv->intsa) && + !stat((char *)SAADDR(&srv->intsa), &st)){ + srv->undev = (uint64_t)st.st_dev; + srv->unino = (uint64_t)st.st_ino; + } +} + +static void ununlink(struct srvparam *srv){ + struct stat st; + + if(*SAFAMILY(&srv->intsa) == AF_UNIX && *SAADDR(&srv->intsa) && + !stat((char *)SAADDR(&srv->intsa), &st) && + (uint64_t)st.st_dev == srv->undev && (uint64_t)st.st_ino == srv->unino) + unlink((char *)SAADDR(&srv->intsa)); +} +#endif + void srvfree(struct srvparam * srv){ int i; 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); #endif } srv->srvsock = INVALID_SOCKET; diff --git a/src/sockgetchar.c b/src/sockgetchar.c index 076cb51..a48a628 100644 --- a/src/sockgetchar.c +++ b/src/sockgetchar.c @@ -64,8 +64,9 @@ int sockrecvfrom(struct clientparam *param, SOCKET sock, struct sockaddr * sin, res = param?param->srv->so._poll(param->sostate, &fds, 1, to):so._poll(so.state, &fds, 1, to); if (res<1) return 0; sasize = sizeof(PROXYSOCKADDRTYPE); + /* sin NULL for a stream socket: nothing to learn the address into */ do { - res = param?param->srv->so._recvfrom(param->sostate, sock, (char *)buf, bufsize, 0, (struct sockaddr *)sin, &sasize):so._recvfrom(so.state, sock, (char *)buf, bufsize, 0, (struct sockaddr *)sin, &sasize); + res = param?param->srv->so._recvfrom(param->sostate, sock, (char *)buf, bufsize, 0, (struct sockaddr *)sin, sin? &sasize : NULL):so._recvfrom(so.state, sock, (char *)buf, bufsize, 0, (struct sockaddr *)sin, sin? &sasize : NULL); } while (res < 0 && (errno == EAGAIN || errno == EINTR)); return res; } diff --git a/src/sockmap.c b/src/sockmap.c index 223ca42..e63c6f6 100644 --- a/src/sockmap.c +++ b/src/sockmap.c @@ -107,7 +107,6 @@ int sockmap(struct clientparam * param, int timeo, int usesplice){ int sleeptime = 0; FILTER_ACTION action; int res = 0; - SASIZETYPE sasize; int needaction = 0; int graceclinum=0, gracesrvnum=0, graceclitraf=0, gracesrvtraf=0; time_t gracetime = 0; @@ -275,8 +274,9 @@ log("send to server from buf"); param->clioffset = param->cliinbuf = 0; if(fromclient) TOCLIENTBUF = 1; } - sasize = SASIZE(¶m->sinsr); - res = param->srv->so._sendto(param->sostate, param->remsock, (char *)param->clibuf + param->clioffset, (int)MIN(inclientbuf, fromclient), 0, (struct sockaddr*)¶m->sinsr, sasize); + /* a connected stream: no address, which a Unix socket on Linux + would refuse */ + res = param->srv->so._sendto(param->sostate, param->remsock, (char *)param->clibuf + param->clioffset, (int)MIN(inclientbuf, fromclient), 0, NULL, 0); if(res <= 0) { TOSERVER = 0; if(errno && errno != EAGAIN && errno != EINTR){ @@ -322,8 +322,7 @@ log("send to client from buf"); if(fromserver) TOSERVERBUF = 1; continue; } - sasize = SASIZE(¶m->sincr); - res = param->srv->so._sendto(param->sostate, param->clisock, (char *)param->srvbuf + param->srvoffset, (int)MIN(inserverbuf,fromserver), 0, (struct sockaddr*)¶m->sincr, sasize); + res = param->srv->so._sendto(param->sostate, param->clisock, (char *)param->srvbuf + param->srvoffset, (int)MIN(inserverbuf,fromserver), 0, NULL, 0); if(res <= 0) { TOCLIENT = 0; if(errno && errno != EAGAIN && errno != EINTR){ @@ -543,8 +542,9 @@ log("done read from server to pipe\n"); #ifdef WITHLOG log("read from client to buf"); #endif - sasize = sizeof(param->sincr); - res = param->srv->so._recvfrom(param->sostate, param->clisock, (char *)param->clibuf + param->cliinbuf, (int)MIN((uint64_t)param->clibufsize - param->cliinbuf, fromclient-inclientbuf), 0, (struct sockaddr *)¶m->sincr, &sasize); + /* no address: it would overwrite the client's with what a + stream socket gives, an empty one for a Unix socket */ + res = param->srv->so._recvfrom(param->sostate, param->clisock, (char *)param->clibuf + param->cliinbuf, (int)MIN((uint64_t)param->clibufsize - param->cliinbuf, fromclient-inclientbuf), 0, NULL, NULL); if(res <= 0) { FROMCLIENT = 0; if(res == 0) { @@ -587,8 +587,7 @@ log("done read from client to buf"); #ifdef WITHLOG log("read from server to buf"); #endif - sasize = sizeof(param->sinsr); - res = param->srv->so._recvfrom(param->sostate, param->remsock, (char *)param->srvbuf + param->srvinbuf, (int)MIN((uint64_t)param->srvbufsize - param->srvinbuf, fromserver-inserverbuf), 0, (struct sockaddr *)¶m->sinsr, &sasize); + res = param->srv->so._recvfrom(param->sostate, param->remsock, (char *)param->srvbuf + param->srvinbuf, (int)MIN((uint64_t)param->srvbufsize - param->srvinbuf, fromserver-inserverbuf), 0, NULL, NULL); if(res <= 0) { FROMSERVER = 0; if(res == 0) { diff --git a/src/socks.c b/src/socks.c index fbbd80a..3eebb95 100644 --- a/src/socks.c +++ b/src/socks.c @@ -230,7 +230,7 @@ fprintf(stderr, "%hu bound to communicate with server\n", *SAPORT(¶m->sinsl) fflush(stderr); #endif } - sasize = SASIZE(¶m->sinsl); + sasize = sizeof(param->sinsl); param->srv->so._getsockname(param->sostate, param->remsock, (struct sockaddr *)¶m->sinsl, &sasize); } if(command == 3) { @@ -250,7 +250,7 @@ fflush(stderr); *SAPORT(&sin) = 0; /* the port the client is told to send its datagrams to */ if(bindwithrange(param, param->clisock, &sin, param->intport)) {RETURN (12);} - sasize = SASIZE(&sin); + sasize = sizeof(sin); param->srv->so._getsockname(param->sostate, param->clisock, (struct sockaddr *)&sin, &sasize); #if SOCKSTRACE > 0 fprintf(stderr, "%hu binded to communicate with client\n", diff --git a/src/structures.h b/src/structures.h index 2c261a8..f7eb490 100644 --- a/src/structures.h +++ b/src/structures.h @@ -113,6 +113,7 @@ int #ifdef WITH_UN #include +#include #endif #define ALLOW 0 @@ -160,7 +161,9 @@ int #define UN_SAADDR(sa) (((struct sockaddr_un *)sa)->sun_family == AF_UNIX)? (unsigned char *)((struct sockaddr_un *)sa)->sun_path : #define UN_SAADDRLEN(sa) (((struct sockaddr_un *)sa)->sun_family == AF_UNIX)? (int)sizeof(((struct sockaddr_un *)sa)->sun_path) : #define UN_SASOCK(sa) (((struct sockaddr_un *)sa)->sun_family == AF_UNIX)? PF_UNIX : -#define UN_SASIZE(sa) (((struct sockaddr_un *)sa)->sun_family == AF_UNIX)? sizeof(struct sockaddr_un) : +/* an abstract name (Linux) is its bytes after the leading 0, no more: + padded with zeros it would be another name */ +#define UN_SASIZE(sa) (((struct sockaddr_un *)sa)->sun_family == AF_UNIX)? (((struct sockaddr_un *)sa)->sun_path[0] || !((struct sockaddr_un *)sa)->sun_path[1]? sizeof(struct sockaddr_un) : offsetof(struct sockaddr_un, sun_path) + 1 + strlen(((struct sockaddr_un *)sa)->sun_path + 1)) : #define UN_SAISNULL(sa) (((struct sockaddr_un *)sa)->sun_family == AF_UNIX)? (((struct sockaddr_un *)sa)->sun_path[0] == 0 && ((struct sockaddr_un *)sa)->sun_path[1] == 0) : #else #define UN_SAPORT(sa) @@ -608,6 +611,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/src/udpsockmap.c b/src/udpsockmap.c index ff84a7d..a4999bd 100644 --- a/src/udpsockmap.c +++ b/src/udpsockmap.c @@ -140,7 +140,7 @@ int udpbind(struct clientparam *param) return 12; } } - sasize = SASIZE(¶m->sinsl); + sasize = sizeof(param->sinsl); param->srv->so._getsockname(param->sostate, param->remsock, (struct sockaddr *)¶m->sinsl, &sasize); return 0; diff --git a/tests/cases/unixsock.py b/tests/cases/unixsock.py new file mode 100644 index 0000000..0bc9d09 --- /dev/null +++ b/tests/cases/unixsock.py @@ -0,0 +1,139 @@ +"""Unix domain sockets: a service listening on one (-iunix:PATH, and on +Linux an abstract -iunix:@NAME), tcppm forwarding to one (DSTADDR +unix:PATH), a parent reached through one (parent ... unix:PATH), and a +listening one across reloads: each reload's service binding it anew, its +file kept while a service listens on it and removed once none does.""" + +import os +import socket +import sys +import time + + +def unix_get(path, url, timeout=10): + """A GET for url sent to the proxy at the Unix socket path (a leading @ + for an abstract one): (status, body length), or (None, error).""" + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.settimeout(timeout) + try: + s.connect("\0" + path[1:] if path.startswith("@") else path) + s.sendall(f"GET {url} HTTP/1.0\r\nHost: unix\r\n\r\n".encode()) + got = b"" + while True: + b = s.recv(65536) + if not b: + break + got += b + except OSError as e: + return None, str(e) + finally: + s.close() + head, _, body = got.partition(b"\r\n\r\n") + try: + return int(head.split()[1]), len(body) + except (IndexError, ValueError): + return None, head[:80] + + +def run(t): + if not hasattr(socket, "AF_UNIX") or sys.platform.startswith("win"): + t.skip("Unix domain sockets (not on this platform)") + return + + origin = t.free_port() + mapped = t.free_port() + chained = t.free_port() + adm = t.free_port() + path = os.path.join(t.tmpdir, "px.sock") + abstract = f"@3proxy-test-{os.getpid()}" if sys.platform.startswith("linux") else None + url = f"http://127.0.0.1:{origin}/data?size=1000" + + def config(unix=True): + services = f""" + flush + auth iponly + allow * + proxy -iunix:{path} + """ if unix else "" + if unix and abstract: + services += f""" + proxy -iunix:{abstract} + """ + if unix: + services += f""" + flush + auth iponly + allow * + tcppm {mapped} unix:{path} 0 + + flush + auth iponly + allow * + parent 1000 http unix:{path} 0 + proxy -p{chained} + """ + return f""" + log + auth iponly + allow * + http data * /data + httpsrv -p{origin} + {services} + flush + auth iponly + allow * + admin -p{adm} + """ + + server = t.start("unixsock", config(), ports=[origin, mapped, chained, adm]) + # a build without Unix sockets (NO_UN) never makes the file + deadline = time.time() + 5 + while time.time() < deadline and not os.path.exists(path): + time.sleep(0.05) + if not os.path.exists(path): + t.skip("Unix domain sockets (not supported by this build)") + return + + # --- a proxy listening on a Unix socket --------------------------------- + t.eq((200, 1000), unix_get(path, url), "a proxy listening on a Unix socket answers") + if abstract: + t.eq((200, 1000), unix_get(abstract, url), + "a proxy listening on an abstract Unix socket answers") + + # --- tcppm forwarding to a Unix socket: the proxy behind it answers ----- + r = t.http(url, proxy=f"127.0.0.1:{mapped}") + t.eq(200, r.status, "tcppm forwards a TCP port to a Unix socket") + t.eq(1000, r.length, "and the whole answer comes back through it") + + # --- a parent reached through a Unix socket ------------------------------ + r = t.http(url, proxy=f"127.0.0.1:{chained}") + t.eq(200, r.status, "a proxy chains to a parent on a Unix socket") + t.eq(1000, r.length, "and the whole answer comes back through the chain") + + # --- reloads: each new service binds the path anew; the old one, closing + # after, must not remove the new one's file ---------------------------- + for i in range(3): + before = server.output().count("Accepting connections") + t.write_config("unixsock", config()) + t.http(f"http://127.0.0.1:{adm}/R") + deadline = time.time() + 10 + while time.time() < deadline and server.output().count("Accepting connections") < before + 5: + time.sleep(0.05) + # the old services stop within a second of each reload + time.sleep(1.5) + t.ok("after three reloads the socket file is still there") if os.path.exists(path) else \ + t.fail("after three reloads the socket file is still there", 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") + deadline = time.time() + 5 + while time.time() < deadline and os.path.exists(path): + time.sleep(0.1) + t.ok("once no service listens on it the socket file is removed") \ + if not os.path.exists(path) else \ + t.fail("once no service listens on it the socket file is removed", "no file", path) + got = unix_get(path, url, timeout=2) + t.ok("and nothing answers there") if got[0] is None else \ + t.fail("and nothing answers there", "no connection", got)