Unix sockets: no addresses on streams, abstract names, tcppm port 0
Some checks are pending
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Waiting to run

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.

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: taken over each time, its file
kept, and removed once no service listens on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Vladimir Dubrovin 2026-10-18 12:05:27 +03:00
parent 6428c4423f
commit 9612bbdd22
7 changed files with 195 additions and 17 deletions

View File

@ -77,7 +77,7 @@ static void serveparam(struct clientparam *param){
continue;
}
#endif
if(socksendto(param, param->remsock, (struct sockaddr*)&param->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;
@ -802,7 +802,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<external_ip>] <port_to_bind>"
@ -1175,7 +1180,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;

View File

@ -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;
}

View File

@ -125,7 +125,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;
@ -294,8 +293,9 @@ log("send to server from buf");
param->clioffset = param->cliinbuf = 0;
if(fromclient) TOCLIENTBUF = 1;
}
sasize = SASIZE(&param->sinsr);
res = param->srv->so._sendto(param->sostate, param->remsock, (char *)param->clibuf + param->clioffset, (int)MIN(inclientbuf, fromclient), 0, (struct sockaddr*)&param->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){
@ -341,8 +341,7 @@ log("send to client from buf");
if(fromserver) TOSERVERBUF = 1;
continue;
}
sasize = SASIZE(&param->sincr);
res = param->srv->so._sendto(param->sostate, param->clisock, (char *)param->srvbuf + param->srvoffset, (int)MIN(inserverbuf,fromserver), 0, (struct sockaddr*)&param->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){
@ -590,8 +589,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)CLIROOM - param->cliinbuf, fromclient-inclientbuf), 0, (struct sockaddr *)&param->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)CLIROOM - param->cliinbuf, fromclient-inclientbuf), 0, NULL, NULL);
if(res <= 0) {
FROMCLIENT = 0;
if(res == 0) {
@ -646,8 +646,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)SRVROOM - param->srvinbuf, fromserver-inserverbuf), 0, (struct sockaddr *)&param->sinsr, &sasize);
res = param->srv->so._recvfrom(param->sostate, param->remsock, (char *)param->srvbuf + param->srvinbuf, (int)MIN((uint64_t)SRVROOM - param->srvinbuf, fromserver-inserverbuf), 0, NULL, NULL);
if(res <= 0) {
FROMSERVER = 0;
if(res == 0) {

View File

@ -226,7 +226,7 @@ fprintf(stderr, "%hu bound to communicate with server\n", *SAPORT(&param->sinsl)
fflush(stderr);
#endif
}
sasize = SASIZE(&param->sinsl);
sasize = sizeof(param->sinsl);
param->srv->so._getsockname(param->sostate, param->remsock, (struct sockaddr *)&param->sinsl, &sasize);
}
if(command == 3) {
@ -243,7 +243,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",

View File

@ -160,7 +160,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)

View File

@ -131,7 +131,7 @@ int udpbind(struct clientparam *param)
return 12;
}
}
sasize = SASIZE(&param->sinsl);
sasize = sizeof(param->sinsl);
param->srv->so._getsockname(param->sostate, param->remsock,
(struct sockaddr *)&param->sinsl, &sasize);
return 0;

171
tests/cases/unixsock.py Normal file
View File

@ -0,0 +1,171 @@
"""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: taken over, its file kept while a service
listens on it and removed once none does."""
import os
import socket
import sys
import threading
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: the socket is taken over, its file stays ------------------
done = {"ok": 0, "failed": 0, "errors": []}
stop = threading.Event()
def client():
while not stop.is_set():
got = unix_get(path, url)
if got == (200, 1000):
done["ok"] += 1
else:
done["failed"] += 1
if len(done["errors"]) < 3:
done["errors"].append(got)
time.sleep(0.02)
clients = [threading.Thread(target=client) for _ in range(3)]
for c in clients:
c.start()
mark = len(server.output())
# the log names a Unix socket by its file's name
taken_line = " px.sock:0 0.0.0.0:0 0 0 0 Accepting connections, taken over"
for i in range(3):
time.sleep(0.5)
t.write_config("unixsock", config())
t.http(f"http://127.0.0.1:{adm}/R")
# one reload done before the next is asked for
deadline = time.time() + 10
while time.time() < deadline and server.output()[mark:].count(taken_line) < i + 1:
time.sleep(0.05)
# the old services stop within a second of each reload
time.sleep(1.5)
stop.set()
for c in clients:
c.join(20)
t.ok(f"requests over the Unix socket go on through the reloads ({done['ok']})") \
if done["ok"] > 20 else \
t.fail("requests over the Unix socket go on through the reloads", "more than 20", done["ok"])
t.eq(0, done["failed"], "none is refused or reset by a reload "
f"(first errors: {done['errors']})")
taken = server.output()[mark:].count(taken_line)
t.eq(3, taken, "the Unix socket is taken over on each reload, not opened anew")
t.ok("its file is still there") if os.path.exists(path) else \
t.fail("its 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)