Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions access.c
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
28 changes: 27 additions & 1 deletion clientname.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)

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


Expand Down
7 changes: 7 additions & 0 deletions rsync.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
8 changes: 7 additions & 1 deletion rsyncd.conf.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -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
Expand Down
88 changes: 88 additions & 0 deletions testsuite/daemon-access-unix_test.py
Original file line number Diff line number Diff line change
@@ -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')
Loading