diff --git a/src/proxymain.c b/src/proxymain.c index a8771e2..41cfc90 100644 --- a/src/proxymain.c +++ b/src/proxymain.c @@ -77,7 +77,7 @@ static void serveparam(struct clientparam *param){ 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; @@ -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] " @@ -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; diff --git a/src/sockgetchar.c b/src/sockgetchar.c index ec25730..4cce3b1 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 509fb44..e8715e1 100644 --- a/src/sockmap.c +++ b/src/sockmap.c @@ -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(¶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){ @@ -341,8 +341,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){ @@ -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 *)¶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)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 *)¶m->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) { diff --git a/src/socks.c b/src/socks.c index 8937bb4..0bd7368 100644 --- a/src/socks.c +++ b/src/socks.c @@ -226,7 +226,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) { @@ -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", diff --git a/src/structures.h b/src/structures.h index 626f6ee..d8159c5 100644 --- a/src/structures.h +++ b/src/structures.h @@ -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) diff --git a/src/udpsockmap.c b/src/udpsockmap.c index e5769e8..0d7834d 100644 --- a/src/udpsockmap.c +++ b/src/udpsockmap.c @@ -131,7 +131,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..b965e99 --- /dev/null +++ b/tests/cases/unixsock.py @@ -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)