net: decode setsockopt() multicast arguments

* configure.ac (AC_CHECK_FUNCS): Add inet_pton.
* net.c (print_mreq, print_mreq6): New functions.
(print_setsockopt): Use them to decode IP_ADD_MEMBERSHIP,
IP_DROP_MEMBERSHIP, IPV6_ADD_MEMBERSHIP, IPV6_DROP_MEMBERSHIP,
IPV6_JOIN_ANYCAST, and IPV6_LEAVE_ANYCAST.
* tests/ip_mreq.c: New file.
* tests/ip_mreq.expected: Likewise.
* tests/ip_mreq.test: New test.
* tests/Makefile.am (check_PROGRAMS): Add ip_mreq.
(TESTS): Add ip_mreq.test.
(EXTRA_DIST): ip_mreq.expected.
* tests/.gitignore: Add ip_mreq.

Based on patch by Ben Noordhuis <info@bnoordhuis.nl>.
diff --git a/configure.ac b/configure.ac
index 060a120..bb8bf46 100644
--- a/configure.ac
+++ b/configure.ac
@@ -233,6 +233,7 @@
 	fputs_unlocked
 	if_indextoname
 	inet_ntop
+	inet_pton
 	pipe2
 	prctl
 	preadv
diff --git a/net.c b/net.c
index fc325df..ff7b456 100644
--- a/net.c
+++ b/net.c
@@ -1307,6 +1307,64 @@
 	return 0;
 }
 
+#ifdef IP_ADD_MEMBERSHIP
+static void
+print_mreq(struct tcb *tcp, long addr, unsigned int len)
+{
+	struct ip_mreq mreq;
+
+	if (len < sizeof(mreq)) {
+		printstr(tcp, addr, len);
+		return;
+	}
+	if (umove(tcp, addr, &mreq) < 0) {
+		tprintf("%#lx", addr);
+		return;
+	}
+	tprints("{imr_multiaddr=inet_addr(");
+	print_quoted_string(inet_ntoa(mreq.imr_multiaddr),
+			    16, QUOTE_0_TERMINATED);
+	tprints("), imr_interface=inet_addr(");
+	print_quoted_string(inet_ntoa(mreq.imr_interface),
+			    16, QUOTE_0_TERMINATED);
+	tprints(")}");
+}
+#endif /* IP_ADD_MEMBERSHIP */
+
+#ifdef IPV6_ADD_MEMBERSHIP
+static void
+print_mreq6(struct tcb *tcp, long addr, unsigned int len)
+{
+	struct ipv6_mreq mreq;
+
+	if (len < sizeof(mreq))
+		goto fail;
+
+	if (umove(tcp, addr, &mreq) < 0) {
+		tprintf("%#lx", addr);
+		return;
+	}
+
+#ifdef HAVE_INET_NTOP
+	const struct in6_addr *in6 = &mreq.ipv6mr_multiaddr;
+	char address[INET6_ADDRSTRLEN];
+
+	if (!inet_ntop(AF_INET6, in6, address, sizeof(address)))
+		goto fail;
+
+	tprints("{ipv6mr_multiaddr=inet_pton(");
+	print_quoted_string(address, sizeof(address), QUOTE_0_TERMINATED);
+	tprints("), ipv6mr_interface=");
+	print_ifindex(mreq.ipv6mr_interface);
+	tprints("}");
+	return;
+#endif /* HAVE_INET_NTOP */
+
+fail:
+	printstr(tcp, addr, len);
+}
+#endif /* IPV6_ADD_MEMBERSHIP */
+
 #ifdef MCAST_JOIN_GROUP
 static void
 print_group_req(struct tcb *tcp, long addr, int len)
@@ -1420,6 +1478,12 @@
 
 	case SOL_IP:
 		switch (name) {
+#ifdef IP_ADD_MEMBERSHIP
+		case IP_ADD_MEMBERSHIP:
+		case IP_DROP_MEMBERSHIP:
+			print_mreq(tcp, addr, len);
+			goto done;
+#endif /* IP_ADD_MEMBERSHIP */
 #ifdef MCAST_JOIN_GROUP
 		case MCAST_JOIN_GROUP:
 		case MCAST_LEAVE_GROUP:
@@ -1429,6 +1493,23 @@
 		}
 		break;
 
+	case SOL_IPV6:
+		switch (name) {
+#ifdef IPV6_ADD_MEMBERSHIP
+		case IPV6_ADD_MEMBERSHIP:
+		case IPV6_DROP_MEMBERSHIP:
+# ifdef IPV6_JOIN_ANYCAST
+		case IPV6_JOIN_ANYCAST:
+# endif
+# ifdef IPV6_LEAVE_ANYCAST
+		case IPV6_LEAVE_ANYCAST:
+# endif
+			print_mreq6(tcp, addr, len);
+			goto done;
+#endif /* IPV6_ADD_MEMBERSHIP */
+		}
+		break;
+
 	case SOL_PACKET:
 		switch (name) {
 #ifdef PACKET_RX_RING
diff --git a/tests/.gitignore b/tests/.gitignore
index 6903a5c..c3eada9 100644
--- a/tests/.gitignore
+++ b/tests/.gitignore
@@ -3,6 +3,7 @@
 getrandom
 inet-accept-connect-send-recv
 ioctl
+ip_mreq
 ipc_msg
 ipc_sem
 ipc_shm
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 7ace705..f1e8820 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -14,6 +14,7 @@
 	getrandom \
 	inet-accept-connect-send-recv \
 	ioctl \
+	ip_mreq \
 	ipc_msg \
 	ipc_sem \
 	ipc_shm \
@@ -61,6 +62,7 @@
 	getdents.test \
 	getrandom.test \
 	ioctl.test \
+	ip_mreq.test \
 	ipc_msg.test \
 	ipc_shm.test \
 	ipc_sem.test \
@@ -109,6 +111,7 @@
 	     getdents.out \
 	     getrandom.awk \
 	     ioctl.expected \
+	     ip_mreq.expected \
 	     ipc.sh \
 	     mmsg.expected \
 	     net.expected \
diff --git a/tests/ip_mreq.c b/tests/ip_mreq.c
new file mode 100644
index 0000000..db208b8
--- /dev/null
+++ b/tests/ip_mreq.c
@@ -0,0 +1,45 @@
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+#include <assert.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+int
+main(void)
+{
+#if defined IP_ADD_MEMBERSHIP && defined IPV6_ADD_MEMBERSHIP \
+ && defined IPV6_JOIN_ANYCAST && defined HAVE_INET_PTON
+	struct ip_mreq m4;
+	struct ipv6_mreq m6;
+
+	inet_pton(AF_INET, "224.0.0.3", &m4.imr_multiaddr);
+	inet_pton(AF_INET, "127.0.0.1", &m4.imr_interface);
+	inet_pton(AF_INET6, "ff01::c", &m6.ipv6mr_multiaddr);
+	m6.ipv6mr_interface = 1;
+
+	(void) close(0);
+	assert(socket(AF_INET, SOCK_DGRAM, 0) == 0);
+
+	assert(setsockopt(0, SOL_IP, IP_ADD_MEMBERSHIP, &m4, 1) == -1);
+	assert(setsockopt(0, SOL_IP, IP_DROP_MEMBERSHIP, &m4, 1) == -1);
+	assert(setsockopt(0, SOL_IP, IP_ADD_MEMBERSHIP, &m4, sizeof(m4)) == 0);
+	assert(setsockopt(0, SOL_IP, IP_DROP_MEMBERSHIP, &m4, sizeof(m4)) == 0);
+
+	assert(setsockopt(0, SOL_IPV6, IPV6_ADD_MEMBERSHIP, &m6, 1) == -1);
+	assert(setsockopt(0, SOL_IPV6, IPV6_DROP_MEMBERSHIP, &m6, 1) == -1);
+	assert(setsockopt(0, SOL_IPV6, IPV6_ADD_MEMBERSHIP, &m6, sizeof(m6)) == -1);
+	assert(setsockopt(0, SOL_IPV6, IPV6_DROP_MEMBERSHIP, &m6, sizeof(m6)) == -1);
+
+	assert(setsockopt(0, SOL_IPV6, IPV6_JOIN_ANYCAST, &m6, 1) == -1);
+	assert(setsockopt(0, SOL_IPV6, IPV6_LEAVE_ANYCAST, &m6, 1) == -1);
+	assert(setsockopt(0, SOL_IPV6, IPV6_JOIN_ANYCAST, &m6, sizeof(m6)) == -1);
+	assert(setsockopt(0, SOL_IPV6, IPV6_LEAVE_ANYCAST, &m6, sizeof(m6)) == -1);
+
+	return 0;
+#else
+	return 77;
+#endif
+}
diff --git a/tests/ip_mreq.expected b/tests/ip_mreq.expected
new file mode 100644
index 0000000..e694c73
--- /dev/null
+++ b/tests/ip_mreq.expected
@@ -0,0 +1,12 @@
+setsockopt\(0, SOL_IP, IP_ADD_MEMBERSHIP, "\\340", 1\) = -1 EINVAL .*
+setsockopt\(0, SOL_IP, IP_DROP_MEMBERSHIP, "\\340", 1\) = -1 EINVAL .*
+setsockopt\(0, SOL_IP, IP_ADD_MEMBERSHIP, \{imr_multiaddr=inet_addr\("224\.0\.0\.3"\), imr_interface=inet_addr\("127\.0\.0\.1"\)\}, 8\) = 0
+setsockopt\(0, SOL_IP, IP_DROP_MEMBERSHIP, \{imr_multiaddr=inet_addr\("224\.0\.0\.3"\), imr_interface=inet_addr\("127\.0\.0\.1"\)\}, 8\) = 0
+setsockopt\(0, SOL_IPV6, IPV6_ADD_MEMBERSHIP, "\\377", 1\) = -1 ENOPROTOOPT .*
+setsockopt\(0, SOL_IPV6, IPV6_DROP_MEMBERSHIP, "\\377", 1\) = -1 ENOPROTOOPT .*
+setsockopt\(0, SOL_IPV6, IPV6_ADD_MEMBERSHIP, \{ipv6mr_multiaddr=inet_pton\("ff01::c"\), ipv6mr_interface=if_nametoindex\("lo"\)\}, 20\) = -1 ENOPROTOOPT .*
+setsockopt\(0, SOL_IPV6, IPV6_DROP_MEMBERSHIP, \{ipv6mr_multiaddr=inet_pton\("ff01::c"\), ipv6mr_interface=if_nametoindex\("lo"\)\}, 20\) = -1 ENOPROTOOPT .*
+setsockopt\(0, SOL_IPV6, IPV6_JOIN_ANYCAST, "\\377", 1\) = -1 ENOPROTOOPT .*
+setsockopt\(0, SOL_IPV6, IPV6_LEAVE_ANYCAST, "\\377", 1\) = -1 ENOPROTOOPT .*
+setsockopt\(0, SOL_IPV6, IPV6_JOIN_ANYCAST, \{ipv6mr_multiaddr=inet_pton\("ff01::c"\), ipv6mr_interface=if_nametoindex\("lo"\)\}, 20\) = -1 ENOPROTOOPT .*
+setsockopt\(0, SOL_IPV6, IPV6_LEAVE_ANYCAST, \{ipv6mr_multiaddr=inet_pton\("ff01::c"\), ipv6mr_interface=if_nametoindex\("lo"\)\}, 20\) = -1 ENOPROTOOPT .*
diff --git a/tests/ip_mreq.test b/tests/ip_mreq.test
new file mode 100755
index 0000000..d423b1b
--- /dev/null
+++ b/tests/ip_mreq.test
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+# Check {IP,IPV6}_{ADD,DROP}_MEMBERSHIP setsockopt decoding.
+
+. "${srcdir=.}/init.sh"
+
+run_prog
+run_strace -e setsockopt $args
+match_grep
+
+exit 0