BUG/MINOR: reload: detect the OS's v6only status before choosing an old socket

The v4v6 and v6only options are passed as data during the socket transfer
between processes so that the new process can decide whether it wants to
reuse a socket or not. But this actually misses one point: if no such option
is set and the OS defaults are changed between the reloads, then the socket
will still be inherited and will never be rebound using the new options.

This can be seen by starting the following config:

  global
    stats socket /tmp/haproxy.sock level admin expose-fd listeners

  frontend testme
    bind :::1234
    timeout client          2000ms

Having a look at the OS settins, v6only is disabled:

  $ cat /proc/sys/net/ipv6/bindv6only
  0

A first check shows it's indeed bound to v4 and v6:

  $ ss -an -6|grep 1234
  tcp   LISTEN 0      2035                                   *:1234             *:*

Reloading the process doesn't change anything (which is expected). Now let's set
bindv6only:

  $ echo 1 | sudo tee /proc/sys/net/ipv6/bindv6only
  1
  $ cat /proc/sys/net/ipv6/bindv6only
  1

Reloading gives the same state:

  $ ss -an -6|grep 1234
  tcp   LISTEN 0      2035                                   *:1234             *:*

However a restart properly shows a correct bind:

  $ ss -an -6|grep 1234
  tcp   LISTEN 0      2035                                [::]:1234          [::]:*

This one doesn't change once bindv6only is reset, for the same reason.

This patch attacks this problem differently. Instead of passing the two
options at once for each listening fd, it ignores the options and reads
the socket's current state for the IPV6_V6ONLY flag and sets it only.
Then before looking for a compatible FD, it checks the OS's defaults
before deciding which of the v4v6 and v6only needs to be kept on the
listener. And the selection is only made on this.

First, it addresses this issue. Second, it also ensures that if such
options are changed between reloads to identical states, the socket
can still be inherited. For example adding v4v6 when bindv6only is not
set will allow the socket to still be usable. Third, it avoids an
undesired dependency on the LI_O_* bit values between processes across
a reload (for these ones at least).

It might make sense to backport this to some recent stable versions, but
quite frankly the likelyhood that anyone will ever notice it is extremely
faint.
diff --git a/src/haproxy.c b/src/haproxy.c
index e891eb0..028ebcd 100644
--- a/src/haproxy.c
+++ b/src/haproxy.c
@@ -1298,6 +1298,26 @@
 		    sizeof(xfer_sock->options));
 		curoff += sizeof(xfer_sock->options);
 
+		/* keep only the v6only flag depending on what's currently
+		 * active on the socket, and always drop the v4v6 one.
+		 */
+		{
+			int val = 0;
+#if defined(IPV6_V6ONLY)
+			socklen_t len = sizeof(val);
+
+			if (xfer_sock->addr.ss_family == AF_INET6 &&
+			    getsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &val, &len) != 0)
+				val = 0;
+#endif
+
+			if (val)
+				xfer_sock->options |= LI_O_V6ONLY;
+			else
+				xfer_sock->options &= ~LI_O_V6ONLY;
+			xfer_sock->options &= ~LI_O_V4V6;
+		}
+
 		xfer_sock->fd = fd;
 		if (xfer_sock_list)
 			xfer_sock_list->prev = xfer_sock;
diff --git a/src/proto_tcp.c b/src/proto_tcp.c
index eb0e668..3d56cf6 100644
--- a/src/proto_tcp.c
+++ b/src/proto_tcp.c
@@ -115,6 +115,11 @@
 static THREAD_LOCAL int default_tcp6_maxseg = -1;
 #endif
 
+/* determine if the operating system uses IPV6_V6ONLY by default.
+ * -1=unknown, 0=no, 1=yes.
+ */
+static int v6only_default = -1;
+
 /* Binds ipv4/ipv6 address <local> to socket <fd>, unless <flags> is set, in which
  * case we try to bind <remote>. <flags> is a 2-bit field consisting of :
  *  - 0 : ignore remote address (may even be a NULL pointer)
@@ -674,24 +679,62 @@
 		return (-1);
 	}
 
+}
+
+/* sets the v6only_default flag according to the OS' default settings; for
+ * simplicity it's set to zero if not supported.
+ */
+static inline void tcp_test_v6only_default()
+{
+	if (v6only_default == -1) {
+#if defined(IPV6_V6ONLY)
+		int fd, val;
+		socklen_t len = sizeof(val);
+
+		v6only_default = 0;
+
+		fd = socket(AF_INET6, SOCK_STREAM, 0);
+		if (fd < 0)
+			return;
+
+		if (getsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &val, &len) == 0 && val > 0)
+			v6only_default = 1;
+
+		close(fd);
+#else
+		v6only_default = 0;
+#endif
+	}
 }
 
-#define LI_MANDATORY_FLAGS	(LI_O_FOREIGN | LI_O_V6ONLY | LI_O_V4V6)
+#define LI_MANDATORY_FLAGS	(LI_O_FOREIGN | LI_O_V6ONLY)
 /* When binding the listeners, check if a socket has been sent to us by the
  * previous process that we could reuse, instead of creating a new one.
  */
 static int tcp_find_compatible_fd(struct listener *l)
 {
 	struct xfer_sock_list *xfer_sock = xfer_sock_list;
+	int options = l->options & (LI_MANDATORY_FLAGS | LI_O_V4V6);
 	int ret = -1;
 
+	tcp_test_v6only_default();
+
+	/* Prepare to match the v6only option against what we really want. Note
+	 * that sadly the two options are not exclusive to each other and that
+	 * v6only is stronger than v4v6.
+	 */
+	if ((options & LI_O_V6ONLY) || (v6only_default && !(options & LI_O_V4V6)))
+		options |= LI_O_V6ONLY;
+	else if ((options & LI_O_V4V6) || !v6only_default)
+		options &= ~LI_O_V6ONLY;
+	options &= ~LI_O_V4V6;
+
 	while (xfer_sock) {
 		if (!compare_sockaddr(&xfer_sock->addr, &l->addr)) {
 			if ((l->interface == NULL && xfer_sock->iface == NULL) ||
 			    (l->interface != NULL && xfer_sock->iface != NULL &&
 			     !strcmp(l->interface, xfer_sock->iface))) {
-				if ((l->options & LI_MANDATORY_FLAGS) ==
-				    (xfer_sock->options & LI_MANDATORY_FLAGS)) {
+				if (options == (xfer_sock->options & LI_MANDATORY_FLAGS)) {
 					if ((xfer_sock->namespace == NULL &&
 					    l->netns == NULL)
 #ifdef USE_NS