pg_hba.conf: samehost and samenet - Mailing list pgsql-hackers

From Stef Walter
Subject pg_hba.conf: samehost and samenet
Date
Msg-id 20090813225034.068DC3039745@mx.npubs.com
Whole thread Raw
Responses Re: pg_hba.conf: samehost and samenet  (Magnus Hagander <magnus@hagander.net>)
List pgsql-hackers
I love using postgresql, and have for a long time. I'm involved with
almost a hundred postgresql installs. But this is the first time I've
gotten into the code.

Renumbering networks happens often, and will happen more frequently as
IPv4 space runs low. The IP based restrictions in pg_hba.conf is one of
the places where renumbering can break running installs. In addition
when postgresql is run in BSD jails, 127.0.0.1 is not available for use
in pg_hba.conf.

It would be great if, in the cidr-address field of pg_hba.conf, we could
specify "samehost" and "samenet". These special values use the local
hosts network interface addresses. "samehost" allows an IP assigned to
the local machine. "samenet" allows any host on the subnets connected to
the local machine.

This is similar to the "sameuser" value that's allowed in the database
field.

A change like this would enable admins like myself to distribute
postgresql with something like this in the default pg_hba.conf file:

host      all     all    samenet     md5
hostssl   all     all   0.0.0.0/0    md5

I've attached an initial patch which implements "samehost" and
"samenet". The patch looks more invasive than it really is, due to
necessary indentation change (ie: a if block), and moving some code into
a separate function.

Thanks for your time. How can I help get a feature like this into
postgresql?

Cheers,

Stef
diff --git a/configure b/configure
index 61b3c72..7bcfcec 100755
*** a/configure
--- b/configure
*************** done
*** 9642,9648 ****



! for ac_header in crypt.h dld.h fp_class.h getopt.h ieeefp.h langinfo.h poll.h pwd.h sys/ipc.h sys/poll.h sys/pstat.h
sys/resource.hsys/select.h sys/sem.h sys/socket.h sys/shm.h sys/tas.h sys/time.h sys/un.h termios.h ucred.h utime.h
wchar.hwctype.h kernel/OS.h kernel/image.h SupportDefs.h 
  do
  as_ac_Header=`$as_echo "ac_cv_header_$ac_header" | $as_tr_sh`
  if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
--- 9642,9649 ----



!
! for ac_header in crypt.h dld.h fp_class.h getopt.h ieeefp.h langinfo.h poll.h pwd.h sys/ipc.h sys/poll.h sys/pstat.h
sys/resource.hsys/select.h sys/sem.h sys/socket.h sys/shm.h sys/tas.h sys/time.h sys/un.h termios.h ucred.h utime.h
wchar.hwctype.h kernel/OS.h kernel/image.h SupportDefs.h ifaddrs.h 
  do
  as_ac_Header=`$as_echo "ac_cv_header_$ac_header" | $as_tr_sh`
  if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
*************** fi
*** 17278,17284 ****



! for ac_func in cbrt dlopen fcvt fdatasync getpeereid getpeerucred getrlimit memmove poll pstat readlink setproctitle
setsidsigprocmask symlink sysconf towlower utime utimes waitpid wcstombs 
  do
  as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
  { $as_echo "$as_me:$LINENO: checking for $ac_func" >&5
--- 17279,17286 ----



!
! for ac_func in cbrt dlopen fcvt fdatasync getpeereid getpeerucred getrlimit memmove poll pstat readlink setproctitle
setsidsigprocmask symlink sysconf towlower utime utimes waitpid wcstombs getifaddrs 
  do
  as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
  { $as_echo "$as_me:$LINENO: checking for $ac_func" >&5
diff --git a/configure.in b/configure.in
index 505644a..bc37b1b 100644
*** a/configure.in
--- b/configure.in
*************** AC_SUBST(OSSP_UUID_LIBS)
*** 962,968 ****
  ##

  dnl sys/socket.h is required by AC_FUNC_ACCEPT_ARGTYPES
! AC_CHECK_HEADERS([crypt.h dld.h fp_class.h getopt.h ieeefp.h langinfo.h poll.h pwd.h sys/ipc.h sys/poll.h sys/pstat.h
sys/resource.hsys/select.h sys/sem.h sys/socket.h sys/shm.h sys/tas.h sys/time.h sys/un.h termios.h ucred.h utime.h
wchar.hwctype.h kernel/OS.h kernel/image.h SupportDefs.h]) 

  # At least on IRIX, cpp test for netinet/tcp.h will fail unless
  # netinet/in.h is included first.
--- 962,968 ----
  ##

  dnl sys/socket.h is required by AC_FUNC_ACCEPT_ARGTYPES
! AC_CHECK_HEADERS([crypt.h dld.h fp_class.h getopt.h ieeefp.h langinfo.h poll.h pwd.h sys/ipc.h sys/poll.h sys/pstat.h
sys/resource.hsys/select.h sys/sem.h sys/socket.h sys/shm.h sys/tas.h sys/time.h sys/un.h termios.h ucred.h utime.h
wchar.hwctype.h kernel/OS.h kernel/image.h SupportDefs.h ifaddrs.h]) 

  # At least on IRIX, cpp test for netinet/tcp.h will fail unless
  # netinet/in.h is included first.
*************** PGAC_VAR_INT_TIMEZONE
*** 1141,1147 ****
  AC_FUNC_ACCEPT_ARGTYPES
  PGAC_FUNC_GETTIMEOFDAY_1ARG

! AC_CHECK_FUNCS([cbrt dlopen fcvt fdatasync getpeereid getpeerucred getrlimit memmove poll pstat readlink setproctitle
setsidsigprocmask symlink sysconf towlower utime utimes waitpid wcstombs]) 

  # posix_fadvise() is a no-op on Solaris, so don't incur function overhead
  # by calling it, 2009-04-02
--- 1141,1147 ----
  AC_FUNC_ACCEPT_ARGTYPES
  PGAC_FUNC_GETTIMEOFDAY_1ARG

! AC_CHECK_FUNCS([cbrt dlopen fcvt fdatasync getpeereid getpeerucred getrlimit memmove poll pstat readlink setproctitle
setsidsigprocmask symlink sysconf towlower utime utimes waitpid wcstombs getifaddrs]) 

  # posix_fadvise() is a no-op on Solaris, so don't incur function overhead
  # by calling it, 2009-04-02
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index b3df7ee..ac99cce 100644
*** a/src/backend/libpq/hba.c
--- b/src/backend/libpq/hba.c
***************
*** 25,30 ****
--- 25,34 ----
  #include <arpa/inet.h>
  #include <unistd.h>

+ #ifdef HAVE_GETIFADDRS
+ #include <ifaddrs.h>
+ #endif
+
  #include "libpq/ip.h"
  #include "libpq/libpq.h"
  #include "regex/regex.h"
*************** check_db(const char *dbname, const char
*** 564,569 ****
--- 568,685 ----
      return false;
  }

+ /*
+  * Check to see if a connecting IP matches the address and netmask.
+  */
+ static bool
+ check_ip (SockAddr *raddr, struct sockaddr *addr, struct sockaddr *mask)
+ {
+     if (raddr->addr.ss_family == addr->sa_family)
+     {
+         /* Same address family */
+         if (!pg_range_sockaddr(&raddr->addr, (struct sockaddr_storage*)addr,
+                                (struct sockaddr_storage*)mask))
+             return false;
+     }
+ #ifdef HAVE_IPV6
+     else if (addr->sa_family == AF_INET &&
+              raddr->addr.ss_family == AF_INET6)
+     {
+         /*
+          * Wrong address family.  We allow only one case: if the file
+          * has IPv4 and the port is IPv6, promote the file address to
+          * IPv6 and try to match that way.
+          */
+         struct sockaddr_storage addrcopy,
+                     maskcopy;
+
+         memcpy(&addrcopy, &addr, sizeof(addrcopy));
+         memcpy(&maskcopy, &mask, sizeof(maskcopy));
+         pg_promote_v4_to_v6_addr(&addrcopy);
+         pg_promote_v4_to_v6_mask(&maskcopy);
+
+         if (!pg_range_sockaddr(&raddr->addr, &addrcopy, &maskcopy))
+             return false;
+     }
+ #endif   /* HAVE_IPV6 */
+     else
+     {
+         /* Wrong address family, no IPV6 */
+         return false;
+     }
+
+     return true;
+ }
+
+ #ifdef HAVE_GETIFADDRS
+
+ static bool
+ check_same_host_or_net (SockAddr *raddr, bool host)
+ {
+     bool result = false;
+     struct ifaddrs *ifa, *l;
+     struct sockaddr_storage mask;
+
+     if (getifaddrs (&ifa) < 0)
+     {
+         ereport(LOG,
+                 (errcode(ERRCODE_WARNING),
+                  errmsg("Could not get network interface addresses, when checking authentication")));
+         return false;
+     }
+
+     for (l = ifa; !result && l; l = l->ifa_next)
+     {
+         if (l->ifa_addr && l->ifa_netmask)
+         {
+             /* Make a fully 1's netmask of appropriate length */
+             if (host)
+             {
+                 pg_sockaddr_cidr_mask (&mask, NULL, l->ifa_addr->sa_family);
+                 result = check_ip (raddr, l->ifa_addr, (struct sockaddr*)&mask);
+             }
+
+             /* Use the netmask of the interface itself */
+             else
+             {
+                 result = check_ip (raddr, l->ifa_addr, l->ifa_netmask);
+             }
+         }
+     }
+
+     freeifaddrs (ifa);
+     return result;
+
+     return result;
+ }
+
+ #endif /* HAVE_GETIFADDRS */
+
+ static bool
+ check_same_host (SockAddr *raddr)
+ {
+ #ifdef HAVE_GETIFADDRS
+     return check_same_host_or_net (raddr, true);
+ #else
+     ereport(LOG,
+             (errcode(ERRCODE_WARNING),
+              errmsg("No support for lookup of network interface addresses, when checking authentication")));
+     return false;
+ #endif
+ }
+
+ static bool
+ check_same_net (SockAddr *raddr)
+ {
+ #ifdef HAVE_GETIFADDRS
+     return check_same_host_or_net (raddr, false);
+ #else
+     ereport(LOG,
+             (errcode(ERRCODE_WARNING),
+              errmsg("No support for lookup of network interface addresses, when checking authentication")));
+     return false;
+ #endif
+ }

  /*
   * Macros used to check and report on invalid configuration options.
*************** parse_hba_line(List *line, int line_num,
*** 710,808 ****
                                  line_num, HbaFileName)));
              return false;
          }
-         token = pstrdup(lfirst(line_item));

!         /* Check if it has a CIDR suffix and if so isolate it */
!         cidr_slash = strchr(token, '/');
!         if (cidr_slash)
!             *cidr_slash = '\0';
!
!         /* Get the IP address either way */
!         hints.ai_flags = AI_NUMERICHOST;
!         hints.ai_family = PF_UNSPEC;
!         hints.ai_socktype = 0;
!         hints.ai_protocol = 0;
!         hints.ai_addrlen = 0;
!         hints.ai_canonname = NULL;
!         hints.ai_addr = NULL;
!         hints.ai_next = NULL;

!         ret = pg_getaddrinfo_all(token, NULL, &hints, &gai_result);
!         if (ret || !gai_result)
          {
!             ereport(LOG,
!                     (errcode(ERRCODE_CONFIG_FILE_ERROR),
!                      errmsg("invalid IP address \"%s\": %s",
!                             token, gai_strerror(ret)),
!                      errcontext("line %d of configuration file \"%s\"",
!                                 line_num, HbaFileName)));
!             if (cidr_slash)
!                 *cidr_slash = '/';
!             if (gai_result)
!                 pg_freeaddrinfo_all(hints.ai_family, gai_result);
!             return false;
          }

!         if (cidr_slash)
!             *cidr_slash = '/';
!
!         memcpy(&parsedline->addr, gai_result->ai_addr, gai_result->ai_addrlen);
!         pg_freeaddrinfo_all(hints.ai_family, gai_result);
!
!         /* Get the netmask */
!         if (cidr_slash)
          {
!             if (pg_sockaddr_cidr_mask(&parsedline->mask, cidr_slash + 1,
!                                       parsedline->addr.ss_family) < 0)
!             {
!                 ereport(LOG,
!                         (errcode(ERRCODE_CONFIG_FILE_ERROR),
!                          errmsg("invalid CIDR mask in address \"%s\"",
!                                 token),
!                          errcontext("line %d of configuration file \"%s\"",
!                                     line_num, HbaFileName)));
!                 return false;
!             }
          }
          else
          {
!             /* Read the mask field. */
!             line_item = lnext(line_item);
!             if (!line_item)
!             {
!                 ereport(LOG,
!                         (errcode(ERRCODE_CONFIG_FILE_ERROR),
!                          errmsg("end-of-line before netmask specification"),
!                          errcontext("line %d of configuration file \"%s\"",
!                                     line_num, HbaFileName)));
!                 return false;
!             }
!             token = lfirst(line_item);

              ret = pg_getaddrinfo_all(token, NULL, &hints, &gai_result);
              if (ret || !gai_result)
              {
                  ereport(LOG,
                          (errcode(ERRCODE_CONFIG_FILE_ERROR),
!                          errmsg("invalid IP mask \"%s\": %s",
                                  token, gai_strerror(ret)),
                           errcontext("line %d of configuration file \"%s\"",
                                      line_num, HbaFileName)));
                  if (gai_result)
                      pg_freeaddrinfo_all(hints.ai_family, gai_result);
                  return false;
              }

!             memcpy(&parsedline->mask, gai_result->ai_addr, gai_result->ai_addrlen);
              pg_freeaddrinfo_all(hints.ai_family, gai_result);

!             if (parsedline->addr.ss_family != parsedline->mask.ss_family)
              {
!                 ereport(LOG,
!                         (errcode(ERRCODE_CONFIG_FILE_ERROR),
!                          errmsg("IP address and mask do not match in file \"%s\" line %d",
!                                 HbaFileName, line_num)));
!                 return false;
              }
          }
      }                            /* != ctLocal */
--- 826,945 ----
                                  line_num, HbaFileName)));
              return false;
          }

!         /* Is it equal to 'samehost' or 'samenet'? */
!         token = lfirst(line_item);

!         /* Any IP on this host is allowed to connect */
!         if (strcmp (token, "samehost") == 0)
          {
!             parsedline->net_method = nmSameHost;
          }

!         /* Any IP on the host's subnets is allowed to connect */
!         else if (strcmp (token, "samenet") == 0)
          {
!             parsedline->net_method = nmSameNet;
          }
+
+         /* IP and netmask are specified */
          else
          {
!             parsedline->net_method = nmCompare;
!             token = pstrdup(token);
!
!             /* Check if it has a CIDR suffix and if so isolate it */
!             cidr_slash = strchr(token, '/');
!             if (cidr_slash)
!                 *cidr_slash = '\0';
!
!             /* Get the IP address either way */
!             hints.ai_flags = AI_NUMERICHOST;
!             hints.ai_family = PF_UNSPEC;
!             hints.ai_socktype = 0;
!             hints.ai_protocol = 0;
!             hints.ai_addrlen = 0;
!             hints.ai_canonname = NULL;
!             hints.ai_addr = NULL;
!             hints.ai_next = NULL;

              ret = pg_getaddrinfo_all(token, NULL, &hints, &gai_result);
              if (ret || !gai_result)
              {
                  ereport(LOG,
                          (errcode(ERRCODE_CONFIG_FILE_ERROR),
!                          errmsg("invalid IP address \"%s\": %s",
                                  token, gai_strerror(ret)),
                           errcontext("line %d of configuration file \"%s\"",
                                      line_num, HbaFileName)));
+                 if (cidr_slash)
+                     *cidr_slash = '/';
                  if (gai_result)
                      pg_freeaddrinfo_all(hints.ai_family, gai_result);
                  return false;
              }

!             if (cidr_slash)
!                 *cidr_slash = '/';
!
!             memcpy(&parsedline->addr, gai_result->ai_addr, gai_result->ai_addrlen);
              pg_freeaddrinfo_all(hints.ai_family, gai_result);

!             /* Get the netmask */
!             if (cidr_slash)
              {
!                 if (pg_sockaddr_cidr_mask(&parsedline->mask, cidr_slash + 1,
!                                           parsedline->addr.ss_family) < 0)
!                 {
!                     ereport(LOG,
!                             (errcode(ERRCODE_CONFIG_FILE_ERROR),
!                              errmsg("invalid CIDR mask in address \"%s\"",
!                                     token),
!                              errcontext("line %d of configuration file \"%s\"",
!                                         line_num, HbaFileName)));
!                     return false;
!                 }
!             }
!             else
!             {
!                 /* Read the mask field. */
!                 line_item = lnext(line_item);
!                 if (!line_item)
!                 {
!                     ereport(LOG,
!                             (errcode(ERRCODE_CONFIG_FILE_ERROR),
!                              errmsg("end-of-line before netmask specification"),
!                              errcontext("line %d of configuration file \"%s\"",
!                                         line_num, HbaFileName)));
!                     return false;
!                 }
!                 token = lfirst(line_item);
!
!                 ret = pg_getaddrinfo_all(token, NULL, &hints, &gai_result);
!                 if (ret || !gai_result)
!                 {
!                     ereport(LOG,
!                             (errcode(ERRCODE_CONFIG_FILE_ERROR),
!                              errmsg("invalid IP mask \"%s\": %s",
!                                     token, gai_strerror(ret)),
!                              errcontext("line %d of configuration file \"%s\"",
!                                         line_num, HbaFileName)));
!                     if (gai_result)
!                         pg_freeaddrinfo_all(hints.ai_family, gai_result);
!                     return false;
!                 }
!
!                 memcpy(&parsedline->mask, gai_result->ai_addr, gai_result->ai_addrlen);
!                 pg_freeaddrinfo_all(hints.ai_family, gai_result);
!
!                 if (parsedline->addr.ss_family != parsedline->mask.ss_family)
!                 {
!                     ereport(LOG,
!                             (errcode(ERRCODE_CONFIG_FILE_ERROR),
!                              errmsg("IP address and mask do not match in file \"%s\" line %d",
!                                     HbaFileName, line_num)));
!                     return false;
!                 }
              }
          }
      }                            /* != ctLocal */
*************** check_hba(hbaPort *port)
*** 1144,1179 ****
                  continue;
  #endif

!             /* Check IP address */
!             if (port->raddr.addr.ss_family == hba->addr.ss_family)
              {
!                 if (!pg_range_sockaddr(&port->raddr.addr, &hba->addr, &hba->mask))
                      continue;
!             }
! #ifdef HAVE_IPV6
!             else if (hba->addr.ss_family == AF_INET &&
!                      port->raddr.addr.ss_family == AF_INET6)
!             {
!                 /*
!                  * Wrong address family.  We allow only one case: if the file
!                  * has IPv4 and the port is IPv6, promote the file address to
!                  * IPv6 and try to match that way.
!                  */
!                 struct sockaddr_storage addrcopy,
!                             maskcopy;
!
!                 memcpy(&addrcopy, &hba->addr, sizeof(addrcopy));
!                 memcpy(&maskcopy, &hba->mask, sizeof(maskcopy));
!                 pg_promote_v4_to_v6_addr(&addrcopy);
!                 pg_promote_v4_to_v6_mask(&maskcopy);
!
!                 if (!pg_range_sockaddr(&port->raddr.addr, &addrcopy, &maskcopy))
                      continue;
!             }
! #endif   /* HAVE_IPV6 */
!             else
!                 /* Wrong address family, no IPV6 */
                  continue;
          }                        /* != ctLocal */

          /* Check database and role */
--- 1281,1304 ----
                  continue;
  #endif

!             switch (hba->net_method)
              {
!             case nmCompare:
!                 if (!check_ip (&port->raddr, (struct sockaddr*)&hba->addr,
!                                (struct sockaddr*)&hba->mask))
                      continue;
!                 break;
!             case nmSameHost:
!                 if (!check_same_host (&port->raddr))
                      continue;
!                 break;
!             case nmSameNet:
!                 if (!check_same_net (&port->raddr))
!                     continue;
!                 break;
!             default:
                  continue;
+             }
          }                        /* != ctLocal */

          /* Check database and role */
diff --git a/src/backend/libpq/ip.c b/src/backend/libpq/ip.c
index 0c35ddd..835dc28 100644
*** a/src/backend/libpq/ip.c
--- b/src/backend/libpq/ip.c
*************** range_sockaddr_AF_INET6(const struct soc
*** 333,338 ****
--- 333,340 ----
   *    pg_sockaddr_cidr_mask - make a network mask of the appropriate family
   *      and required number of significant bits
   *
+  * numbits can be null, in which case the mask is fully set.
+  *
   * The resulting mask is placed in *mask, which had better be big enough.
   *
   * Return value is 0 if okay, -1 if not.
*************** pg_sockaddr_cidr_mask(struct sockaddr_st
*** 343,352 ****
      long        bits;
      char       *endptr;

!     bits = strtol(numbits, &endptr, 10);
!
!     if (*numbits == '\0' || *endptr != '\0')
!         return -1;

      switch (family)
      {
--- 345,360 ----
      long        bits;
      char       *endptr;

!     if (numbits == NULL)
!     {
!         bits = (family == AF_INET) ? 32 : 128;
!     }
!     else
!     {
!         bits = strtol(numbits, &endptr, 10);
!         if (*numbits == '\0' || *endptr != '\0')
!             return -1;
!     }

      switch (family)
      {
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index f1c0457..c6775e6 100644
*** a/src/backend/libpq/pg_hba.conf.sample
--- b/src/backend/libpq/pg_hba.conf.sample
***************
*** 33,38 ****
--- 33,41 ----
  # (between 0 and 32 (IPv4) or 128 (IPv6) inclusive) that specifies
  # the number of significant bits in the mask.  Alternatively, you can write
  # an IP address and netmask in separate columns to specify the set of hosts.
+ # You can also specify "samehost" to limit connections to those from addresses
+ # of the local machine. Or you can specify "samenet" to limit connections
+ # to addresses on the subnets of the local network.
  #
  # METHOD can be "trust", "reject", "md5", "password", "gss", "sspi", "krb5",
  # "ident", "pam", "ldap" or "cert".  Note that "password" sends passwords
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8083fdc..61bc286 100644
*** a/src/include/libpq/hba.h
--- b/src/include/libpq/hba.h
*************** typedef enum UserAuth
*** 30,35 ****
--- 30,42 ----
      uaCert
  } UserAuth;

+ typedef enum NetMethod
+ {
+     nmCompare,
+     nmSameHost,
+     nmSameNet
+ } NetMethod;
+
  typedef enum ConnType
  {
      ctLocal,
*************** typedef struct
*** 44,49 ****
--- 51,57 ----
      ConnType    conntype;
      char       *database;
      char       *role;
+     NetMethod    net_method;
      struct sockaddr_storage addr;
      struct sockaddr_storage mask;
      UserAuth    auth_method;
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index ba807f4..c2f453b 100644
*** a/src/include/pg_config.h.in
--- b/src/include/pg_config.h.in
***************
*** 176,181 ****
--- 176,184 ----
  /* Define to 1 if you have the `gethostbyname_r' function. */
  #undef HAVE_GETHOSTBYNAME_R

+ /* Define to 1 if you have the `getifaddrs' function. */
+ #undef HAVE_GETIFADDRS
+
  /* Define to 1 if you have the `getopt' function. */
  #undef HAVE_GETOPT

***************
*** 215,220 ****
--- 218,226 ----
  /* Define to 1 if you have the <ieeefp.h> header file. */
  #undef HAVE_IEEEFP_H

+ /* Define to 1 if you have the <ifaddrs.h> header file. */
+ #undef HAVE_IFADDRS_H
+
  /* Define to 1 if you have the `inet_aton' function. */
  #undef HAVE_INET_ATON


pgsql-hackers by date:

Previous
From: Josh Berkus
Date:
Subject: Re: freezing tuples ( was: Why is vacuum_freeze_min_age 100m? )
Next
From: Jeff Davis
Date:
Subject: Re: freezing tuples ( was: Why is vacuum_freeze_min_age 100m? )