From c4e6c59bd7f3ebd1684f88920a2297a1f6cdccc0 Mon Sep 17 00:00:00 2001 From: Miao Wang Date: Tue, 10 Nov 2026 21:44:17 +0800 Subject: [PATCH] Support Unix daemon client access rules Add special address identifiers for Unix and unknown client socket families, recognize them in hosts access rules, and document the identifiers. Add a daemon regression test for Unix socket access filtering. --- access.c | 5 ++ clientname.c | 28 ++++++++- rsync.h | 7 +++ rsyncd.conf.5.md | 8 ++- testsuite/daemon-access-unix_test.py | 88 ++++++++++++++++++++++++++++ 5 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 testsuite/daemon-access-unix_test.py diff --git a/access.c b/access.c index 8031feb72..708bd453f 100644 --- a/access.c +++ b/access.c @@ -198,6 +198,11 @@ static int match_address(const char *addr, char *tok) if (!addr || !*addr) return 0; + for (const char * const *special = client_special_addrs; *special != NULL; special ++) { + if (addr == *special && strcmp(tok, *special) == 0) + return 1; + } + p = strchr(tok,'/'); if (p) *p = '\0'; diff --git a/clientname.c b/clientname.c index dbac38b99..a92aace90 100644 --- a/clientname.c +++ b/clientname.c @@ -36,6 +36,12 @@ static const char proxyv2sig[] = "\r\n\r\n\0\r\nQUIT\n"; static char ipaddr_buf[100]; +const char * const client_special_addrs[] = { + [CLIENT_UNKNOWN_ADDR] = "unknown:", + [CLIENT_UNIX_ADDR] = "unix:", + NULL, +}; + #define PROXY_V2_SIG_SIZE ((int)sizeof proxyv2sig - 1) #define PROXY_V2_HEADER_SIZE (PROXY_V2_SIG_SIZE + 1 + 1 + 2) @@ -52,13 +58,16 @@ static int check_name(const char *ipaddr, const struct sockaddr_storage *ss, cha static int valid_ipaddr(const char *s, int allow_scope); /* Return the IP addr of the client as a string. */ -char *client_addr(int fd) +const char *client_addr(int fd) { + static const char *ret_special_val = NULL; struct sockaddr_storage ss; socklen_t length = sizeof ss; if (*ipaddr_buf) return ipaddr_buf; + if (ret_special_val) + return ret_special_val; if (am_daemon < 0) { /* daemon over --rsh mode */ char *env_str; @@ -78,9 +87,26 @@ char *client_addr(int fd) } client_sockaddr(fd, &ss, &length); + if (length < sizeof(sa_family_t)) { + ret_special_val = client_special_addrs[CLIENT_UNKNOWN_ADDR]; + goto return_special; + } + switch (((struct sockaddr *)&ss)->sa_family) { + case AF_UNIX: + ret_special_val = client_special_addrs[CLIENT_UNIX_ADDR]; + goto return_special; + case AF_INET: + case AF_INET6: + break; + default: + ret_special_val = client_special_addrs[CLIENT_UNKNOWN_ADDR]; + goto return_special; + } getnameinfo((struct sockaddr *)&ss, length, ipaddr_buf, sizeof ipaddr_buf, NULL, 0, NI_NUMERICHOST); return ipaddr_buf; +return_special: + return ret_special_val; } diff --git a/rsync.h b/rsync.h index adf9bf1f1..62a154b6d 100644 --- a/rsync.h +++ b/rsync.h @@ -1562,3 +1562,10 @@ const char *get_panic_action(void); #elif defined HAVE_MALLINFO #define MEM_ALLOC_INFO mallinfo #endif + +/* sentinel value for client from special address families */ +extern const char * const client_special_addrs[]; +enum { + CLIENT_UNKNOWN_ADDR, + CLIENT_UNIX_ADDR, +}; diff --git a/rsyncd.conf.5.md b/rsyncd.conf.5.md index d16d6f3b4..fa0bb5351 100644 --- a/rsyncd.conf.5.md +++ b/rsyncd.conf.5.md @@ -302,7 +302,9 @@ in the values of parameters. See that section for details. This global parameter lists the socket peer IP addresses that are allowed to supply a `proxy protocol` header. The syntax is the same token format used by `hosts allow`. When `proxy protocol = true`, this list must match - the direct peer before rsync trusts the forwarded client address. + the direct peer before rsync trusts the forwarded client address. Please + note the special values `unix:` and `unknown:` as described in `hosts allow` + might be useful here. 0. `name converter` @@ -852,6 +854,10 @@ in the values of parameters. See that section for details. is enabled, as it is by default). Any match will be allowed in. - an '@' followed by a netgroup name, which will match if the reverse DNS of the connecting IP is in the specified netgroup. + - special values. `unix:` matches when the incoming connection is from a Unix + domain socket; `unknown:` matches when the incoming connection's address + family is unknown (i.e. not AF_INET, AF_INET6, or AF_UNIX), which might + happen when the incoming connection is handed over from inetd-like services. If rsync was built with IDN support (look for "IDN" in `rsync --version`), a hostname pattern may contain non-ASCII characters: this file is read as diff --git a/testsuite/daemon-access-unix_test.py b/testsuite/daemon-access-unix_test.py new file mode 100644 index 000000000..58ad36743 --- /dev/null +++ b/testsuite/daemon-access-unix_test.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Daemon access control recognizes Unix-domain client addresses.""" + +import os +import socket +import subprocess + +from rsyncfns import ( + SCRATCHDIR, rsync_argv, test_fail, test_skipped, write_daemon_conf, +) + +if not hasattr(socket, 'AF_UNIX') or not hasattr(socket, 'socketpair'): + test_skipped('Unix-domain socket pairs are unavailable') + +module_path = SCRATCHDIR / 'access-unix-module' +module_path.mkdir(parents=True, exist_ok=True) +conf = write_daemon_conf([ + ('allow-unix', { + 'path': str(module_path), 'read only': 'yes', 'hosts allow': 'unix:', + }), + ('deny-unix', { + 'path': str(module_path), 'read only': 'yes', 'hosts deny': 'unix:', + }), + ('allow-ip', { + 'path': str(module_path), 'read only': 'yes', + 'hosts allow': '127.0.0.1', + }), +], globals={'hosts allow': ''}) + +def module_response(name): + """Return the response to a module request over an AF_UNIX socket pair.""" + try: + listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + except (OSError, ValueError) as e: + test_skipped(f'Unix-domain sockets are unavailable: {e}') + + randomAddress = f'{SCRATCHDIR}/rsyncd.sock' + os.unlink(randomAddress) if os.path.exists(randomAddress) else None + listener.bind(randomAddress) + listener.listen(1) + client.connect(randomAddress) + server, _ = listener.accept() + + proc = None + try: + proc = subprocess.Popen( + rsync_argv('--daemon', '--no-detach', f'--config={conf}'), + stdin=server.fileno(), stdout=server.fileno(), + stderr=subprocess.PIPE, close_fds=True, + ) + server.close() + client.settimeout(10) + stream = client.makefile('rwb', buffering=0) + greeting = stream.readline() + if not greeting.startswith(b'@RSYNCD: '): + test_fail(f'unexpected daemon greeting: {greeting!r}') + stream.write(b'@RSYNCD: 31.0\n') + stream.write(name.encode('ascii') + b'\n') + response = stream.readline() + stream.close() + return response + finally: + client.close() + server.close() + if proc is not None: + try: + proc.wait(timeout=2) + except subprocess.TimeoutExpired: + proc.terminate() + try: + proc.wait(timeout=2) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait(timeout=2) + + +for name in ('allow-unix',): + response = module_response(name) + if response != b'@RSYNCD: OK\n': + test_fail(f'{name} should allow a Unix-domain client, got {response!r}') + +for name in ('deny-unix', 'allow-ip'): + response = module_response(name) + if not response.startswith(b'@ERROR: access denied'): + test_fail(f'{name} should deny a Unix-domain client, got {response!r}') + +print('daemon-access-unix: hosts allow/deny matching verified')