Subject: bug#55335: [PATCH Shepherd 3/3] Interpret AF_INET6 endpoints as IPv6-only.

From: Ludovic Courtès <[email-masked]>
Date: Wed, 18 May 2022 16:06:45 +0200

* configure.ac: Check the values of IPPROTO_IPV6 and IPV6_V6ONLY.
* modules/shepherd/system.scm.in (ipv6-only): New procedure.
* modules/shepherd/service.scm (endpoint->listening-socket): Call it if
ADDRESS is AF_INET6.
(define-as-needed): New macro.
(IN6ADDR_LOOPBACK, IN6ADDR_ANY): New variables.
* tests/inetd.sh: Add 'test-inetd6' and 'test-inetd-v6-only' services.
Test them.
---
 NEWS                           | 11 +++++++
 configure.ac                   | 12 +++++++
 doc/shepherd.texi              | 14 ++++++++
 modules/shepherd/service.scm   | 19 +++++++++++
 modules/shepherd/system.scm.in | 11 +++++++
 tests/inetd.sh                 | 58 ++++++++++++++++++++++++++++++++++
 6 files changed, 125 insertions(+)

diff --git a/NEWS b/NEWS
index 4ce7a48..3798b31 100644
--- a/NEWS
+++ b/NEWS
@@ -25,6 +25,17 @@ For compatibility with 0.9.0, if the second argument to
 list of endpoints.  This behavior will be preserved for at least the whole
 0.9.x series.
 
+** ‘AF_INET6’ endpoints are now interpreted as IPv6-only
+
+In 0.9.0, using an ‘AF_INET6’ endpoint for ‘make-systemd-constructor’ would
+usually have the effect of making the service available on both IPv6 and IPv4.
+This is due to the default behavior of Linux, which is to bind IPv6 addresses
+as IPv4 as well (the default behavior can be changed by running
+‘sysctl net.ipv6.bindv6only 1’).
+
+‘AF_INET6’ endpoints are now interpreted as IPv6-only.  Thus, if a service is
+to be made available both as IPv6 and IPv4, two endpoints must be used.
+
 ** ‘shepherd’ reports whether a service is transient
 ** ‘herd status’ shows whether a service is transient
 ** Fix possible file descriptor leak in ‘make-inetd-constructor’
diff --git a/configure.ac b/configure.ac
index bf91560..b745813 100644
--- a/configure.ac
+++ b/configure.ac
@@ -141,6 +141,18 @@ AC_SUBST([SIG_BLOCK])
 AC_SUBST([SIG_UNBLOCK])
 AC_SUBST([SIG_SETMASK])
 
+dnl Check for constants not exported by Guile as of 3.0.8.
+AC_MSG_CHECKING([<netinet/in.h> constants])
+AC_COMPUTE_INT([IPPROTO_IPV6], [IPPROTO_IPV6], [
+  #include <sys/socket.h>
+  #include <netinet/in.h>])
+AC_COMPUTE_INT([IPV6_V6ONLY], [IPV6_V6ONLY], [
+  #include <sys/socket.h>
+  #include <netinet/in.h>])
+AC_MSG_RESULT([done])
+AC_SUBST([IPPROTO_IPV6])
+AC_SUBST([IPV6_V6ONLY])
+
 AC_MSG_CHECKING([whether to build crash handler])
 case "$host_os" in
   linux-gnu*)  build_crash_handler=yes;;
diff --git a/doc/shepherd.texi b/doc/shepherd.texi
index 9efc48e..841b854 100644
--- a/doc/shepherd.texi
+++ b/doc/shepherd.texi
@@ -1093,6 +1093,20 @@ Return a new endpoint called @var{name} of @var{address}, an address as
 return by @code{make-socket-address}, with the given @var{style} and
 @var{backlog}.
 
+When @var{address} is of type @code{AF_INET6}, the endpoint is
+@emph{IPv6-only}.  Thus, if you want a service available both on IPv4
+and IPv6, you need two endpoints.  For example, below is a list of
+endpoints to listen on port 4444 on all the network interfaces, both in
+IPv4 and IPv6 (``0.0.0.0'' for IPv4 and ``::0'' for IPv6):
+
+@lisp
+(list (endpoint (make-socket-address AF_INET INADDR_ANY 4444))
+      (endpoint (make-socket-address AF_INET6 IN6ADDR_ANY 4444)))
+@end lisp
+
+This is the list you would pass to @code{make-inetd-constructor} or
+@code{make-systemd-constructor}---see below.
+
 When @var{address} is of type @code{AF_UNIX}, @var{socket-owner} and
 @var{socket-group} are strings or integers that specify its ownership and that
 of its parent directory; @var{socket-directory-permissions} specifies the
diff --git a/modules/shepherd/service.scm b/modules/shepherd/service.scm
index e93466a..6df550c 100644
--- a/modules/shepherd/service.scm
+++ b/modules/shepherd/service.scm
@@ -1251,6 +1251,10 @@ as argument, where SIGNAL defaults to `SIGTERM'."
 return by @code{make-socket-address}, with the given @var{style} and
 @var{backlog}.
 
+When @var{address} is of type @code{AF_INET6}, the endpoint is
+@emph{IPv6-only}.  Thus, if you want a service available both on IPv4 and
+IPv6, you need two endpoints.
+
 When @var{address} is of type @code{AF_UNIX}, @var{socket-owner} and
 @var{socket-group} are strings or integers that specify its ownership and that
 of its parent directory; @var{socket-directory-permissions} specifies the
@@ -1273,6 +1277,11 @@ permissions for its parent directory."
                          group
                          (group:gid (getgrnam group)))))
        (setsockopt sock SOL_SOCKET SO_REUSEADDR 1)
+       (when (= AF_INET6 (sockaddr:fam address))
+         ;; Interpret AF_INET6 endpoints as IPv6-only.  This is contrary to
+         ;; the Linux defaults where listening on an IPv6 address also listens
+         ;; on its IPv4 counterpart.
+         (ipv6-only sock))
        (when (= AF_UNIX (sockaddr:fam address))
          (mkdir-p (dirname (sockaddr:path address)) permissions)
          (chown (dirname (sockaddr:path address)) owner group)
@@ -1309,6 +1318,16 @@ thrown an previously-opened sockets are closed."
                        (apply throw args)))))
          (loop tail (cons sock result)))))))
 
+(define-syntax-rule (define-as-needed name value)
+  (unless (defined? 'name)
+    (module-define! (current-module) 'name value)
+    (module-export! (current-module) '(name))))
+
+;; These values are not defined as of Guile 3.0.8.  Provide them as a
+;; convenience.
+(define-as-needed IN6ADDR_LOOPBACK 1)
+(define-as-needed IN6ADDR_ANY 0)
+
 
 ;;;
 ;;; Inetd-style services.
diff --git a/modules/shepherd/system.scm.in b/modules/shepherd/system.scm.in
index 2562764..0978c18 100644
--- a/modules/shepherd/system.scm.in
+++ b/modules/shepherd/system.scm.in
@@ -32,6 +32,7 @@
             prctl
             PR_SET_CHILD_SUBREAPER
             getpgid
+            ipv6-only
             SFD_CLOEXEC
             signalfd
             consume-signalfd-siginfo
@@ -141,6 +142,16 @@ ctrlaltdel(8) and see kernel/reboot.c in Linux."
                    (list err))
             result)))))
 
+(define (ipv6-only port)
+  "Make PORT, a file port backed by a socket, IPv6-only (using the IPV6_V6ONLY
+socket option) and return PORT.
+
+This is useful when willing to make a listening socket that operates on IPv6
+only (by default, Linux binds AF_INET6 addresses on IPv4 as well)."
+  ;; As of Guile 3.0.8, IPPROTO_IPV6 and IPV6_V6ONLY are not exported.
+  (setsockopt port @IPPROTO_IPV6@ @IPV6_V6ONLY@ 1)
+  port)
+
 (define (allocate-sigset)
   (bytevector->pointer (make-bytevector @SIZEOF_SIGSET_T@)))
 
diff --git a/tests/inetd.sh b/tests/inetd.sh
index 83037bf..c05d6fe 100644
--- a/tests/inetd.sh
+++ b/tests/inetd.sh
@@ -48,6 +48,28 @@ cat > "$conf" <<EOF
                                                INADDR_LOOPBACK
                                                $PORT))))
    #:stop  (make-inetd-destructor))
+ (make <service>
+   #:provides '(test-inetd6)
+   #:start (make-inetd-constructor %command
+                                   (list
+                                    (endpoint (make-socket-address
+                                               AF_INET
+                                               INADDR_LOOPBACK
+                                               $PORT))
+                                    (endpoint (make-socket-address
+                                               AF_INET6
+                                               IN6ADDR_LOOPBACK
+                                               $PORT))))
+   #:stop  (make-inetd-destructor))
+ (make <service>
+   #:provides '(test-inetd-v6-only)
+   #:start (make-inetd-constructor %command
+                                   (list
+                                    (endpoint (make-socket-address
+                                               AF_INET6
+                                               IN6ADDR_LOOPBACK
+                                               $PORT))))
+   #:stop  (make-inetd-destructor))
  (make <service>
    #:provides '(test-inetd-unix)
    #:start (make-inetd-constructor %command
@@ -81,6 +103,7 @@ test $($herd status | grep '\+' | wc -l) -eq 2
 converse_with_echo_server ()
 {
     guile -c "(use-modules (ice-9 match) (ice-9 rdelim))
+      (define IN6ADDR_LOOPBACK 1)
       (define address $1)
       (define sock (socket (sockaddr:fam address) SOCK_STREAM 0))
       (connect sock address)
@@ -98,10 +121,45 @@ do
 	"(make-socket-address AF_INET INADDR_LOOPBACK $PORT)"
 done
 
+# Unavailable on IPv6.
+! converse_with_echo_server \
+    "(make-socket-address AF_INET6 IN6ADDR_LOOPBACK $PORT)"
+
 $herd stop test-inetd
 ! converse_with_echo_server \
   "(make-socket-address AF_INET INADDR_LOOPBACK $PORT)"
 
+if guile -c '(socket AF_INET6 SOCK_STREAM 0)'; then
+    # Test IPv6 support.
+    $herd start test-inetd6
+
+    converse_with_echo_server \
+	"(make-socket-address AF_INET6 IN6ADDR_LOOPBACK $PORT)"
+    converse_with_echo_server \
+	"(make-socket-address AF_INET INADDR_LOOPBACK $PORT)"
+
+    $herd stop test-inetd6
+
+    ! converse_with_echo_server \
+	"(make-socket-address AF_INET6 IN6ADDR_LOOPBACK $PORT)"
+    ! converse_with_echo_server \
+	"(make-socket-address AF_INET INADDR_LOOPBACK $PORT)"
+
+    $herd start test-inetd-v6-only
+
+    converse_with_echo_server \
+	"(make-socket-address AF_INET6 IN6ADDR_LOOPBACK $PORT)"
+    ! converse_with_echo_server \
+	"(make-socket-address AF_INET INADDR_LOOPBACK $PORT)"
+
+    $herd stop test-inetd-v6-only
+
+    ! converse_with_echo_server \
+	"(make-socket-address AF_INET6 IN6ADDR_LOOPBACK $PORT)"
+    ! converse_with_echo_server \
+	"(make-socket-address AF_INET INADDR_LOOPBACK $PORT)"
+fi
+
 # Now test inetd on a Unix-domain socket.
 
 $herd start test-inetd-unix
-- 
2.36.0