]> git.ipfire.org Git - thirdparty/rsync.git/commitdiff
Add `haproxy header` parameter to rsync daemon
authorWayne Davison <wayne@opencoder.net>
Thu, 11 Jun 2020 21:03:11 +0000 (14:03 -0700)
committerWayne Davison <wayne@opencoder.net>
Thu, 11 Jun 2020 21:22:25 +0000 (14:22 -0700)
NEWS.md
clientname.c
clientserver.c
itypes.h
loadparm.c
rsyncd.conf.5.md

diff --git a/NEWS.md b/NEWS.md
index 88d4f388503c551440cce63ef45d4d25576dd271..4a29ec4580abd6b843507395d8daec37c47baab4 100644 (file)
--- a/NEWS.md
+++ b/NEWS.md
@@ -95,10 +95,13 @@ Protocol: 31 (unchanged)
    value is the user-specified port number (set via `--port` or an rsync://
    URL) or 0 if the user didn't override the port.
 
+ - Added the `haproxy header` daemon parameter that allows your rsyncd to know
+   the real remote IP when it is being proxied.
+
  - Added negated matching to the daemon's `refuse options` setting by using
    match strings that start with a `!` (such as `!compress*`).
 
- - Added an `early exec` daemon parameter that runs a script before the
+ - Added the `early exec` daemon parameter that runs a script before the
    transfer parameters are known, allowing some early setup based on module
    name.
 
index 46025cf3d4fcc71146670d89e0790c02f6472d59..6a9f98b96a29c97e6179a60795a5ad16d205c796 100644 (file)
@@ -3,7 +3,7 @@
  *
  * Copyright (C) 1992-2001 Andrew Tridgell <tridge@samba.org>
  * Copyright (C) 2001, 2002 Martin Pool <mbp@samba.org>
- * Copyright (C) 2002-2019 Wayne Davison
+ * Copyright (C) 2002-2020 Wayne Davison
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  */
 
 #include "rsync.h"
+#include "itypes.h"
+
+extern int am_daemon;
 
 static const char default_name[] = "UNKNOWN";
-extern int am_server;
+static const char proxyv2sig[] = "\r\n\r\n\0\r\nQUIT\n";
 
+static char ipaddr_buf[100];
 
-/**
- * Return the IP addr of the client as a string
- **/
+#define PROXY_V2_SIG_SIZE ((int)sizeof proxyv2sig - 1)
+#define PROXY_V2_HEADER_SIZE (PROXY_V2_SIG_SIZE + 1 + 1 + 2)
+
+#define CMD_LOCAL 0
+#define CMD_PROXY 1
+
+#define PROXY_FAM_TCPv4 0x11
+#define PROXY_FAM_TCPv6 0x21
+
+#define GET_SOCKADDR_FAMILY(ss) ((struct sockaddr*)ss)->sa_family
+
+static void client_sockaddr(int fd, struct sockaddr_storage *ss, socklen_t *ss_len);
+static int check_name(const char *ipaddr, const struct sockaddr_storage *ss, char *name_buf, size_t name_buf_size);
+static int valid_ipaddr(const char *s);
+
+/* Return the IP addr of the client as a string. */
 char *client_addr(int fd)
 {
-       static char addr_buf[100];
-       static int initialised;
        struct sockaddr_storage ss;
        socklen_t length = sizeof ss;
 
-       if (initialised)
-               return addr_buf;
+       if (*ipaddr_buf)
+               return ipaddr_buf;
 
-       initialised = 1;
-
-       if (am_server) {        /* daemon over --rsh mode */
+       if (am_daemon < 0) {    /* daemon over --rsh mode */
                char *env_str;
-               strlcpy(addr_buf, "0.0.0.0", sizeof addr_buf);
+               strlcpy(ipaddr_buf, "0.0.0.0", sizeof ipaddr_buf);
                if ((env_str = getenv("REMOTE_HOST")) != NULL
                 || (env_str = getenv("SSH_CONNECTION")) != NULL
                 || (env_str = getenv("SSH_CLIENT")) != NULL
                 || (env_str = getenv("SSH2_CLIENT")) != NULL) {
                        char *p;
-                       strlcpy(addr_buf, env_str, sizeof addr_buf);
+                       strlcpy(ipaddr_buf, env_str, sizeof ipaddr_buf);
                        /* Truncate the value to just the IP address. */
-                       if ((p = strchr(addr_buf, ' ')) != NULL)
+                       if ((p = strchr(ipaddr_buf, ' ')) != NULL)
                                *p = '\0';
                }
-       } else {
-               client_sockaddr(fd, &ss, &length);
-               getnameinfo((struct sockaddr *)&ss, length,
-                           addr_buf, sizeof addr_buf, NULL, 0, NI_NUMERICHOST);
+               if (valid_ipaddr(ipaddr_buf))
+                       return ipaddr_buf;
        }
 
-       return addr_buf;
-}
+       client_sockaddr(fd, &ss, &length);
+       getnameinfo((struct sockaddr *)&ss, length, ipaddr_buf, sizeof ipaddr_buf, NULL, 0, NI_NUMERICHOST);
 
-
-static int get_sockaddr_family(const struct sockaddr_storage *ss)
-{
-       return ((struct sockaddr *) ss)->sa_family;
+       return ipaddr_buf;
 }
 
 
@@ -89,71 +97,216 @@ static int get_sockaddr_family(const struct sockaddr_storage *ss)
  * After translation from sockaddr to name we do a forward lookup to
  * make sure nobody is spoofing PTR records.
  **/
-char *client_name(int fd)
+char *client_name(const char *ipaddr)
 {
        static char name_buf[100];
-       static char port_buf[100];
-       static int initialised;
+       char port_buf[100];
        struct sockaddr_storage ss;
        socklen_t ss_len;
+       struct addrinfo hint, *answer;
+       int err;
 
-       if (initialised)
+       if (*name_buf)
                return name_buf;
 
        strlcpy(name_buf, default_name, sizeof name_buf);
-       initialised = 1;
-
-       memset(&ss, 0, sizeof ss);
-
-       if (am_server) {        /* daemon over --rsh mode */
-               char *addr = client_addr(fd);
-               struct addrinfo hint, *answer;
-               int err;
 
-               if (strcmp(addr, "0.0.0.0") == 0)
-                       return name_buf;
+       if (strcmp(ipaddr, "0.0.0.0") == 0)
+               return name_buf;
 
-               memset(&hint, 0, sizeof hint);
+       memset(&ss, 0, sizeof ss);
+       memset(&hint, 0, sizeof hint);
 
 #ifdef AI_NUMERICHOST
-               hint.ai_flags = AI_NUMERICHOST;
+       hint.ai_flags = AI_NUMERICHOST;
 #endif
-               hint.ai_socktype = SOCK_STREAM;
+       hint.ai_socktype = SOCK_STREAM;
 
-               if ((err = getaddrinfo(addr, NULL, &hint, &answer)) != 0) {
-                       rprintf(FLOG, "malformed address %s: %s\n",
-                               addr, gai_strerror(err));
-                       return name_buf;
-               }
+       if ((err = getaddrinfo(ipaddr, NULL, &hint, &answer)) != 0) {
+               rprintf(FLOG, "malformed address %s: %s\n", ipaddr, gai_strerror(err));
+               return name_buf;
+       }
 
-               switch (answer->ai_family) {
-               case AF_INET:
-                       ss_len = sizeof (struct sockaddr_in);
-                       memcpy(&ss, answer->ai_addr, ss_len);
-                       break;
+       switch (answer->ai_family) {
+       case AF_INET:
+               ss_len = sizeof (struct sockaddr_in);
+               memcpy(&ss, answer->ai_addr, ss_len);
+               break;
 #ifdef INET6
-               case AF_INET6:
-                       ss_len = sizeof (struct sockaddr_in6);
-                       memcpy(&ss, answer->ai_addr, ss_len);
-                       break;
+       case AF_INET6:
+               ss_len = sizeof (struct sockaddr_in6);
+               memcpy(&ss, answer->ai_addr, ss_len);
+               break;
 #endif
-               default:
-                       exit_cleanup(RERR_SOCKETIO);
-               }
-               freeaddrinfo(answer);
-       } else {
-               ss_len = sizeof ss;
-               client_sockaddr(fd, &ss, &ss_len);
+       default:
+               assert(0);
        }
+       freeaddrinfo(answer);
 
-       if (lookup_name(fd, &ss, ss_len, name_buf, sizeof name_buf,
-                       port_buf, sizeof port_buf) == 0)
-               check_name(fd, &ss, name_buf, sizeof name_buf);
+       /* reverse lookup */
+       err = getnameinfo((struct sockaddr*)&ss, ss_len, name_buf, sizeof name_buf,
+                         port_buf, sizeof port_buf, NI_NAMEREQD | NI_NUMERICSERV);
+       if (err) {
+               strlcpy(name_buf, default_name, sizeof name_buf);
+               rprintf(FLOG, "name lookup failed for %s: %s\n", ipaddr, gai_strerror(err));
+       } else
+               check_name(ipaddr, &ss, name_buf, sizeof name_buf);
 
        return name_buf;
 }
 
 
+/* Try to read an haproxy header (V1 or V2). Returns 1 on success or 0 on failure. */
+int read_haproxy_header(int fd)
+{
+       union {
+               struct {
+                       char line[108];
+               } v1;
+               struct {
+                       char sig[PROXY_V2_SIG_SIZE];
+                       char ver_cmd;
+                       char fam;
+                       char len[2];
+                       union {
+                               struct {
+                                       char src_addr[4];
+                                       char dst_addr[4];
+                                       char src_port[2];
+                                       char dst_port[2];
+                               } ip4;
+                               struct {
+                                       char src_addr[16];
+                                       char dst_addr[16];
+                                       char src_port[2];
+                                       char dst_port[2];
+                               } ip6;
+                               struct {
+                                       char src_addr[108];
+                                       char dst_addr[108];
+                               } unx;
+                       } addr;
+               } v2;
+       } hdr;
+
+       read_buf(fd, (char*)&hdr, PROXY_V2_SIG_SIZE);
+
+       if (memcmp(hdr.v2.sig, proxyv2sig, PROXY_V2_SIG_SIZE) == 0) { /* Proxy V2 */
+               int ver, cmd, size;
+
+               read_buf(fd, (char*)&hdr + PROXY_V2_SIG_SIZE, PROXY_V2_HEADER_SIZE - PROXY_V2_SIG_SIZE);
+
+               ver = (hdr.v2.ver_cmd & 0xf0) >> 4;
+               cmd = (hdr.v2.ver_cmd & 0x0f);
+               size = (hdr.v2.len[0] << 8) + hdr.v2.len[1];
+
+               if (ver != 2 || size + PROXY_V2_HEADER_SIZE > (int)sizeof hdr)
+                       return 0;
+
+               /* Grab all the remaining data in the binary request. */
+               read_buf(fd, (char*)&hdr + PROXY_V2_HEADER_SIZE, size);
+
+               switch (cmd) {
+               case CMD_PROXY:
+                       switch (hdr.v2.fam) {
+                       case PROXY_FAM_TCPv4:
+                               if (size != sizeof hdr.v2.addr.ip4)
+                                       return 0;
+                               inet_ntop(AF_INET, hdr.v2.addr.ip4.src_addr, ipaddr_buf, sizeof ipaddr_buf);
+                               return valid_ipaddr(ipaddr_buf);
+                       case PROXY_FAM_TCPv6:
+                               if (size != sizeof hdr.v2.addr.ip6)
+                                       return 0;
+                               inet_ntop(AF_INET6, hdr.v2.addr.ip6.src_addr, ipaddr_buf, sizeof ipaddr_buf);
+                               return valid_ipaddr(ipaddr_buf);
+                       default:
+                               break;
+                       }
+                       /* For an unsupported protocol we'll ignore the proxy data (leaving ipaddr_buf unset)
+                        * and accept the connection, which will get handled as a normal socket addr. */
+                       return 1;
+               case CMD_LOCAL:
+                       return 1;
+               default:
+                       break;
+               }
+
+               return 0;
+       }
+
+       if (memcmp(hdr.v1.line, "PROXY", 5) == 0) { /* Proxy V1 */
+               char *endc, *sp, *p = hdr.v1.line + PROXY_V2_SIG_SIZE;
+               int port_chk;
+
+               *p = '\0';
+               if (!strchr(hdr.v1.line, '\n')) {
+                       while (1) {
+                               read_buf(fd, p, 1);
+                               if (*p++ == '\n')
+                                       break;
+                               if (p - hdr.v1.line >= (int)sizeof hdr.v1.line - 1)
+                                       return 0;
+                       }
+                       *p = '\0';
+               }
+
+               endc = strchr(hdr.v1.line, '\r');
+               if (!endc || endc[1] != '\n' || endc[2])
+                       return 0;
+               *endc = '\0';
+
+               p = hdr.v1.line + 5;
+
+               if (!isSpace(p++))
+                       return 0;
+               if (strncmp(p, "TCP4", 4) == 0)
+                       p += 4;
+               else if (strncmp(p, "TCP6", 4) == 0)
+                       p += 4;
+               else if (strncmp(p, "UNKNOWN", 7) == 0)
+                       return 1;
+               else
+                       return 0;
+
+               if (!isSpace(p++))
+                       return 0;
+
+               if ((sp = strchr(p, ' ')) == NULL)
+                       return 0;
+               *sp = '\0';
+               if (!valid_ipaddr(p))
+                       return 0;
+               strlcpy(ipaddr_buf, p, sizeof ipaddr_buf); /* It will always fit when valid. */
+
+               p = sp + 1;
+               if ((sp = strchr(p, ' ')) == NULL)
+                       return 0;
+               *sp = '\0';
+               if (!valid_ipaddr(p))
+                       return 0;
+               /* Ignore destination address. */
+
+               p = sp + 1;
+               if ((sp = strchr(p, ' ')) == NULL)
+                       return 0;
+               *sp = '\0';
+               port_chk = strtol(p, &endc, 10);
+               if (*endc || port_chk == 0)
+                       return 0;
+               /* Ignore source port. */
+
+               p = sp + 1;
+               port_chk = strtol(p, &endc, 10);
+               if (*endc || port_chk == 0)
+                       return 0;
+               /* Ignore destination port. */
+
+               return 1;
+       }
+
+       return 0;
+}
+
 
 /**
  * Get the sockaddr for the client.
@@ -161,9 +314,7 @@ char *client_name(int fd)
  * If it comes in as an ipv4 address mapped into IPv6 format then we
  * convert it back to a regular IPv4.
  **/
-void client_sockaddr(int fd,
-                    struct sockaddr_storage *ss,
-                    socklen_t *ss_len)
+static void client_sockaddr(int fd, struct sockaddr_storage *ss, socklen_t *ss_len)
 {
        memset(ss, 0, sizeof *ss);
 
@@ -174,7 +325,7 @@ void client_sockaddr(int fd,
        }
 
 #ifdef INET6
-       if (get_sockaddr_family(ss) == AF_INET6 &&
+       if (GET_SOCKADDR_FAMILY(ss) == AF_INET6 &&
            IN6_IS_ADDR_V4MAPPED(&((struct sockaddr_in6 *)ss)->sin6_addr)) {
                /* OK, so ss is in the IPv6 family, but it is really
                 * an IPv4 address: something like
@@ -205,44 +356,14 @@ void client_sockaddr(int fd,
 }
 
 
-/**
- * Look up a name from @p ss into @p name_buf.
- *
- * @param fd file descriptor for client socket.
- **/
-int lookup_name(int fd, const struct sockaddr_storage *ss,
-               socklen_t ss_len,
-               char *name_buf, size_t name_buf_size,
-               char *port_buf, size_t port_buf_size)
-{
-       int name_err;
-
-       /* reverse lookup */
-       name_err = getnameinfo((struct sockaddr *) ss, ss_len,
-                              name_buf, name_buf_size,
-                              port_buf, port_buf_size,
-                              NI_NAMEREQD | NI_NUMERICSERV);
-       if (name_err != 0) {
-               strlcpy(name_buf, default_name, name_buf_size);
-               rprintf(FLOG, "name lookup failed for %s: %s\n",
-                       client_addr(fd), gai_strerror(name_err));
-               return name_err;
-       }
-
-       return 0;
-}
-
-
-
 /**
  * Compare an addrinfo from the resolver to a sockinfo.
  *
  * Like strcmp, returns 0 for identical.
  **/
-int compare_addrinfo_sockaddr(const struct addrinfo *ai,
-                             const struct sockaddr_storage *ss)
+static int compare_addrinfo_sockaddr(const struct addrinfo *ai, const struct sockaddr_storage *ss)
 {
-       int ss_family = get_sockaddr_family(ss);
+       int ss_family = GET_SOCKADDR_FAMILY(ss);
        const char fn[] = "compare_addrinfo_sockaddr";
 
        if (ai->ai_family != ss_family) {
@@ -302,13 +423,11 @@ int compare_addrinfo_sockaddr(const struct addrinfo *ai,
  * because it doesn't seem that it could be spoofed in any way, and
  * getaddrinfo on random service names seems to cause problems on AIX.
  **/
-int check_name(int fd,
-              const struct sockaddr_storage *ss,
-              char *name_buf, size_t name_buf_size)
+static int check_name(const char *ipaddr, const struct sockaddr_storage *ss, char *name_buf, size_t name_buf_size)
 {
        struct addrinfo hints, *res, *res0;
        int error;
-       int ss_family = get_sockaddr_family(ss);
+       int ss_family = GET_SOCKADDR_FAMILY(ss);
 
        memset(&hints, 0, sizeof hints);
        hints.ai_family = ss_family;
@@ -339,10 +458,74 @@ int check_name(int fd,
                /* We hit the end of the list without finding an
                 * address that was the same as ss. */
                rprintf(FLOG, "%s is not a known address for \"%s\": "
-                       "spoofed address?\n", client_addr(fd), name_buf);
+                       "spoofed address?\n", ipaddr, name_buf);
                strlcpy(name_buf, default_name, name_buf_size);
        }
 
        freeaddrinfo(res0);
        return 0;
 }
+
+/* Returns 1 for a valid IPv4 or IPv6 addr, or 0 for a bad one. */
+static int valid_ipaddr(const char *s)
+{
+       int i;
+
+       if (strchr(s, ':') != NULL) { /* Only IPv6 has a colon. */
+               int count, saw_double_colon = 0;
+               int ipv4_at_end = 0;
+
+               if (*s == ':') { /* A colon at the start must be a :: */
+                       if (*++s != ':')
+                               return 0;
+                       saw_double_colon = 1;
+                       s++;
+               }
+
+               for (count = 0; count < 8; count++) {
+                       if (!*s)
+                               return saw_double_colon && count < 7;
+
+                       if (strchr(s, ':') == NULL && strchr(s, '.') != NULL) {
+                               if ((!saw_double_colon && count != 6) || (saw_double_colon && count > 6))
+                                       return 0;
+                               ipv4_at_end = 1;
+                               break;
+                       }
+
+                       if (!isHexDigit(s++)) /* Need 1-4 hex digits */
+                               return 0;
+                       if (isHexDigit(s) && isHexDigit(++s) && isHexDigit(++s) && isHexDigit(++s))
+                               return 0;
+
+                       if (*s == ':') {
+                               if (!*++s)
+                                       return 0;
+                               if (*s == ':') {
+                                       if (saw_double_colon)
+                                               return 0;
+                                       saw_double_colon = 1;
+                                       s++;
+                               }
+                       }
+               }
+
+               if (!ipv4_at_end)
+                       return !*s;
+       }
+
+       /* IPv4 */
+       for (i = 0; i < 4; i++) {
+               long n;
+               char *end;
+
+               if (i && *s++ != '.')
+                       return 0;
+               n = strtol(s, &end, 10);
+               if (n > 255 || n < 0 || end <= s || end > s+3)
+                       return 0;
+               s = end;
+       }
+
+       return !*s;
+}
index 2208e1bec7e7f1bf64fa512cb4d5994fa14ac4b1..b0908e3508cdc130231aa54c64858ebfb5ebf852 100644 (file)
@@ -615,7 +615,7 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
        /* If reverse lookup is disabled globally but enabled for this module,
         * we need to do it now before the access check. */
        if (host == undetermined_hostname && lp_reverse_lookup(i))
-               host = client_name(f_in);
+               host = client_name(client_addr(f_in));
        set_env_str("RSYNC_HOST_NAME", host);
        set_env_str("RSYNC_HOST_ADDR", addr);
 
@@ -1132,6 +1132,9 @@ int start_daemon(int f_in, int f_out)
        if (!load_config(0))
                exit_cleanup(RERR_SYNTAX);
 
+       if (lp_haproxy_header() && !read_haproxy_header(f_in))
+               return -1;
+
        p = lp_daemon_chroot();
        if (*p) {
                log_init(0); /* Make use we've initialized syslog before chrooting. */
@@ -1169,7 +1172,7 @@ int start_daemon(int f_in, int f_out)
        }
 
        addr = client_addr(f_in);
-       host = lp_reverse_lookup(-1) ? client_name(f_in) : undetermined_hostname;
+       host = lp_reverse_lookup(-1) ? client_name(addr) : undetermined_hostname;
        rprintf(FLOG, "connect from %s (%s)\n", host, addr);
 
        if (am_daemon > 0) {
index 08759a2219d6f0245803ebb6a45584854c31bcd5..db997bba19c247057261c85de068f431aa4a5d5c 100644 (file)
--- a/itypes.h
+++ b/itypes.h
@@ -1,6 +1,6 @@
 /* Inline functions for rsync.
  *
- * Copyright (C) 2007-2019 Wayne Davison
+ * Copyright (C) 2007-2020 Wayne Davison
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,6 +22,12 @@ isDigit(const char *ptr)
        return isdigit(*(unsigned char *)ptr);
 }
 
+static inline int
+isHexDigit(const char *ptr)
+{
+       return isxdigit(*(unsigned char *)ptr);
+}
+
 static inline int
 isPrint(const char *ptr)
 {
index e21bf51e288c971f6e20a3e0b165715d9cabe681..f84af5320d2620c3e52dcf89c6615895e8bd7b4e 100644 (file)
@@ -111,6 +111,8 @@ typedef struct {
 
        int listen_backlog;
        int rsync_port;
+
+       BOOL haproxy_header;
 } global_vars;
 
 /* This structure describes a single section.  Their order must match the
@@ -230,6 +232,8 @@ static const all_vars Defaults = {
 
  /* listen_backlog; */         5,
  /* rsync_port; */             0,
+
+ /* haproxy_header; */         False,
  },
 
  /* ==== local_vars ==== */
@@ -403,6 +407,7 @@ static struct parm_struct parm_table[] =
  {"pid file",          P_STRING, P_GLOBAL,&Vars.g.pid_file,            NULL,0},
  {"port",              P_INTEGER,P_GLOBAL,&Vars.g.rsync_port,          NULL,0},
  {"socket options",    P_STRING, P_GLOBAL,&Vars.g.socket_options,      NULL,0},
+ {"haproxy header",    P_BOOL,   P_LOCAL, &Vars.g.haproxy_header,      NULL,0},
 
  {"auth users",        P_STRING, P_LOCAL, &Vars.l.auth_users,          NULL,0},
  {"charset",           P_STRING, P_LOCAL, &Vars.l.charset,             NULL,0},
@@ -544,6 +549,8 @@ FN_GLOBAL_STRING(lp_socket_options, socket_options)
 FN_GLOBAL_INTEGER(lp_listen_backlog, listen_backlog)
 FN_GLOBAL_INTEGER(lp_rsync_port, rsync_port)
 
+FN_GLOBAL_BOOL(lp_haproxy_header, haproxy_header)
+
 FN_LOCAL_STRING(lp_auth_users, auth_users)
 FN_LOCAL_STRING(lp_charset, charset)
 FN_LOCAL_STRING(lp_comment, comment)
index e4b673ef2a12fc604c370383e4e19118523cd203..f1e335c1eb85a3ef5e655b08b05f77de2dc3fa7c 100644 (file)
@@ -234,6 +234,24 @@ the values of parameters.  See the GLOBAL PARAMETERS section for more details.
     allow the daemon to function.  By default the daemon runs without any
     chrooting.
 
+0.  `haproxy header`
+
+    This parameter indicates that all incoming connections must start with a V1
+    or V2 haproxy header. If the header is not found, the connection is closed.
+
+    Setting this allows a proxy server to forward the source IP information to
+    rsync, allowing you to make use of IP restrictions that don't all match the
+    source IP of the proxy server.
+
+    _CAUTION_: when using this option you _must_ make sure that only the proxy
+    is allowed to connect to the rsync port via some kind of firewall rules
+    (such as iptables).  If any non-proxied connections are allowed through,
+    the client will be able to spoof any remote IP address that they desire.
+
+    This setting is global.  If you need some modules to require this and not
+    others, then you will need to setup multiple rsync daemon processes on
+    different ports.
+
 0.  `numeric ids`
 
     Enabling this parameter disables the mapping of users and groups by name