face: deduplicate multicast UDP socket setup

In preparation for adding IPv6 support.

Change-Id: I5a3be2007f7fecc44915a5d6794093143ce9c0f9
Refs: #4222
diff --git a/daemon/face/multicast-udp-transport.cpp b/daemon/face/multicast-udp-transport.cpp
index 7b3fca3..f8ee1a9 100644
--- a/daemon/face/multicast-udp-transport.cpp
+++ b/daemon/face/multicast-udp-transport.cpp
@@ -1,5 +1,5 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
-/**
+/*
  * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
@@ -26,6 +26,12 @@
 #include "multicast-udp-transport.hpp"
 #include "udp-protocol.hpp"
 
+#ifdef __linux__
+#include <cerrno>       // for errno
+#include <cstring>      // for std::strerror()
+#include <sys/socket.h> // for setsockopt()
+#endif // __linux__
+
 namespace nfd {
 namespace face {
 
@@ -79,6 +85,67 @@
   DatagramTransport::doClose();
 }
 
+void
+MulticastUdpTransport::openRxSocket(protocol::socket& sock,
+                                    const protocol::endpoint& multicastGroup,
+                                    const boost::asio::ip::address& localAddress,
+                                    const shared_ptr<const ndn::net::NetworkInterface>& netif)
+{
+  BOOST_ASSERT(!sock.is_open());
+
+  sock.open(multicastGroup.protocol());
+  sock.set_option(protocol::socket::reuse_address(true));
+  sock.bind(protocol::endpoint(multicastGroup.protocol(), multicastGroup.port()));
+
+  if (multicastGroup.address().is_v4()) {
+    BOOST_ASSERT(localAddress.is_v4());
+    sock.set_option(boost::asio::ip::multicast::join_group(multicastGroup.address().to_v4(),
+                                                           localAddress.to_v4()));
+  }
+  else {
+    // IPv6 multicast is not supported
+    BOOST_ASSERT(false);
+  }
+
+#ifdef __linux__
+  if (netif) {
+    // On Linux, if there is more than one MulticastUdpTransport for the same multicast
+    // group but they are on different network interfaces, each socket needs to be bound
+    // to the corresponding interface using SO_BINDTODEVICE, otherwise the transport will
+    // receive all packets sent to the other interfaces as well.
+    // This is needed only on Linux. On macOS, the boost::asio::ip::multicast::join_group
+    // option is sufficient to obtain the desired behavior.
+    if (::setsockopt(sock.native_handle(), SOL_SOCKET, SO_BINDTODEVICE,
+                     netif->getName().data(), netif->getName().size() + 1) < 0) {
+      BOOST_THROW_EXCEPTION(Error("Cannot bind multicast rx socket to " + netif->getName() +
+                                  ": " + std::strerror(errno)));
+    }
+  }
+#endif // __linux__
+}
+
+void
+MulticastUdpTransport::openTxSocket(protocol::socket& sock,
+                                    const protocol::endpoint& localEndpoint,
+                                    bool enableLoopback)
+{
+  BOOST_ASSERT(!sock.is_open());
+
+  sock.open(localEndpoint.protocol());
+  sock.set_option(protocol::socket::reuse_address(true));
+  sock.set_option(boost::asio::ip::multicast::enable_loopback(enableLoopback));
+  sock.bind(localEndpoint);
+
+  if (localEndpoint.address().is_v4()) {
+    if (!localEndpoint.address().is_unspecified())
+      sock.set_option(boost::asio::ip::multicast::outbound_interface(localEndpoint.address().to_v4()));
+  }
+  else {
+    // IPv6 multicast is not supported
+    BOOST_ASSERT(false);
+  }
+}
+
 template<>
 Transport::EndpointId
 DatagramTransport<boost::asio::ip::udp, Multicast>::makeEndpointId(const protocol::endpoint& ep)
diff --git a/daemon/face/multicast-udp-transport.hpp b/daemon/face/multicast-udp-transport.hpp
index 9b903d0..b0fb4d4 100644
--- a/daemon/face/multicast-udp-transport.hpp
+++ b/daemon/face/multicast-udp-transport.hpp
@@ -1,5 +1,5 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
-/**
+/*
  * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
@@ -28,6 +28,8 @@
 
 #include "datagram-transport.hpp"
 
+#include <ndn-cxx/net/network-interface.hpp>
+
 namespace nfd {
 namespace face {
 
@@ -45,6 +47,16 @@
 class MulticastUdpTransport final : public DatagramTransport<boost::asio::ip::udp, Multicast>
 {
 public:
+  class Error : public std::runtime_error
+  {
+  public:
+    explicit
+    Error(const std::string& what)
+      : std::runtime_error(what)
+    {
+    }
+  };
+
   /**
    * \brief Creates a UDP-based transport for multicast communication
    * \param localEndpoint local endpoint
@@ -59,6 +71,17 @@
                         protocol::socket&& sendSocket,
                         ndn::nfd::LinkType linkType);
 
+  static void
+  openRxSocket(protocol::socket& sock,
+               const protocol::endpoint& multicastGroup,
+               const boost::asio::ip::address& localAddress,
+               const shared_ptr<const ndn::net::NetworkInterface>& netif = nullptr);
+
+  static void
+  openTxSocket(protocol::socket& sock,
+               const protocol::endpoint& localEndpoint,
+               bool enableLoopback = false);
+
 private:
   void
   doSend(Transport::Packet&& packet) final;
diff --git a/daemon/face/udp-factory.cpp b/daemon/face/udp-factory.cpp
index 7b1f4f0..c9cab22 100644
--- a/daemon/face/udp-factory.cpp
+++ b/daemon/face/udp-factory.cpp
@@ -32,12 +32,6 @@
 #include <boost/range/adaptor/map.hpp>
 #include <boost/range/algorithm/copy.hpp>
 
-#ifdef __linux__
-#include <cerrno>       // for errno
-#include <cstring>      // for std::strerror()
-#include <sys/socket.h> // for setsockopt()
-#endif // __linux__
-
 namespace nfd {
 namespace face {
 
@@ -294,7 +288,7 @@
 shared_ptr<Face>
 UdpFactory::createMulticastFace(const udp::Endpoint& localEndpoint,
                                 const udp::Endpoint& multicastEndpoint,
-                                const net::NetworkInterface& netif)
+                                const shared_ptr<const ndn::net::NetworkInterface>& netif)
 {
   BOOST_ASSERT(multicastEndpoint.address().is_multicast());
   BOOST_ASSERT(localEndpoint.port() == multicastEndpoint.port());
@@ -321,42 +315,14 @@
                                 "address"));
   }
 
-  ip::udp::socket receiveSocket(getGlobalIoService());
-  receiveSocket.open(multicastEndpoint.protocol());
-  receiveSocket.set_option(ip::udp::socket::reuse_address(true));
-  receiveSocket.bind(multicastEndpoint);
-
-  ip::udp::socket sendSocket(getGlobalIoService());
-  sendSocket.open(multicastEndpoint.protocol());
-  sendSocket.set_option(ip::udp::socket::reuse_address(true));
-  sendSocket.set_option(ip::multicast::enable_loopback(false));
-  sendSocket.bind(udp::Endpoint(ip::address_v4::any(), multicastEndpoint.port()));
-  if (localEndpoint.address() != ip::address_v4::any())
-    sendSocket.set_option(ip::multicast::outbound_interface(localEndpoint.address().to_v4()));
-
-  sendSocket.set_option(ip::multicast::join_group(multicastEndpoint.address().to_v4(),
-                                                  localEndpoint.address().to_v4()));
-  receiveSocket.set_option(ip::multicast::join_group(multicastEndpoint.address().to_v4(),
-                                                     localEndpoint.address().to_v4()));
-
-#ifdef __linux__
-  // On Linux, if there is more than one multicast UDP face for the same multicast
-  // group but they are bound to different network interfaces, the socket needs
-  // to be bound to the specific interface using SO_BINDTODEVICE, otherwise the
-  // face will receive all packets sent to the other interfaces as well.
-  // This happens only on Linux. On macOS, the ip::multicast::join_group option
-  // is enough to get the desired behaviour.
-  if (::setsockopt(receiveSocket.native_handle(), SOL_SOCKET, SO_BINDTODEVICE,
-                   netif.getName().data(), netif.getName().size() + 1) < 0) {
-    BOOST_THROW_EXCEPTION(Error("Cannot bind multicast face to " + netif.getName() +
-                                ": " + std::strerror(errno)));
-  }
-#endif // __linux__
+  ip::udp::socket rxSock(getGlobalIoService());
+  MulticastUdpTransport::openRxSocket(rxSock, multicastEndpoint, localEndpoint.address(), netif);
+  ip::udp::socket txSock(getGlobalIoService());
+  MulticastUdpTransport::openTxSocket(txSock, localEndpoint);
 
   auto linkService = make_unique<GenericLinkService>();
   auto transport = make_unique<MulticastUdpTransport>(localEndpoint, multicastEndpoint,
-                                                      std::move(receiveSocket),
-                                                      std::move(sendSocket),
+                                                      std::move(rxSock), std::move(txSock),
                                                       m_mcastConfig.linkType);
   auto face = make_shared<Face>(std::move(linkService), std::move(transport));
 
@@ -411,15 +377,14 @@
   }
 
   NFD_LOG_DEBUG("Creating multicast face on " << netif->getName());
-  udp::Endpoint localEndpoint(*address, m_mcastConfig.group.port());
-  auto face = this->createMulticastFace(localEndpoint, m_mcastConfig.group, *netif);
-  // ifname is only used on Linux. It is not required if there is only one multicast-capable netif,
-  // but it is always supplied because a new netif can be added at anytime.
 
+  udp::Endpoint localEndpoint(*address, m_mcastConfig.group.port());
+  auto face = this->createMulticastFace(localEndpoint, m_mcastConfig.group, netif);
   if (face->getId() == INVALID_FACEID) {
     // new face: register with forwarding
     this->addFace(face);
   }
+
   return face;
 }
 
diff --git a/daemon/face/udp-factory.hpp b/daemon/face/udp-factory.hpp
index 9e20be4..3f644c4 100644
--- a/daemon/face/udp-factory.hpp
+++ b/daemon/face/udp-factory.hpp
@@ -116,7 +116,7 @@
   shared_ptr<Face>
   createMulticastFace(const udp::Endpoint& localEndpoint,
                       const udp::Endpoint& multicastEndpoint,
-                      const ndn::net::NetworkInterface& netif);
+                      const shared_ptr<const ndn::net::NetworkInterface>& netif);
 
 private:
   /** \brief Create UDP multicast face on \p netif if needed by \p m_mcastConfig
diff --git a/tests/daemon/face/multicast-udp-transport-fixture.hpp b/tests/daemon/face/multicast-udp-transport-fixture.hpp
index 7c3ca21..2b86833 100644
--- a/tests/daemon/face/multicast-udp-transport-fixture.hpp
+++ b/tests/daemon/face/multicast-udp-transport-fixture.hpp
@@ -55,12 +55,16 @@
   void
   initialize(ip::address address)
   {
-    openMulticastSockets(remoteSockRx, remoteSockTx, multicastEp.port());
+    localEp = udp::endpoint(address, 7001);
+
+    MulticastUdpTransport::openRxSocket(remoteSockRx, multicastEp, ip::address_v4::any());
+    MulticastUdpTransport::openTxSocket(remoteSockTx, udp::endpoint(udp::v4(), 0), true);
 
     udp::socket sockRx(g_io);
     udp::socket sockTx(g_io);
-    localEp = udp::endpoint(address, 7001);
-    openMulticastSockets(sockRx, sockTx, localEp.port());
+    MulticastUdpTransport::openRxSocket(sockRx, udp::endpoint(multicastEp.address(), localEp.port()),
+                                        ip::address_v4::any());
+    MulticastUdpTransport::openTxSocket(sockTx, udp::endpoint(udp::v4(), 0), true);
 
     face = make_unique<Face>(
              make_unique<DummyReceiveLinkService>(),
@@ -73,20 +77,6 @@
   }
 
   void
-  openMulticastSockets(udp::socket& rx, udp::socket& tx, uint16_t port)
-  {
-    rx.open(udp::v4());
-    rx.set_option(udp::socket::reuse_address(true));
-    rx.bind(udp::endpoint(multicastEp.address(), port));
-    rx.set_option(ip::multicast::join_group(multicastEp.address()));
-
-    tx.open(udp::v4());
-    tx.set_option(udp::socket::reuse_address(true));
-    tx.set_option(ip::multicast::enable_loopback(true));
-    tx.bind(udp::endpoint(ip::address_v4::any(), port));
-  }
-
-  void
   remoteRead(std::vector<uint8_t>& buf, bool needToCheck = true)
   {
     remoteSockRx.async_receive(boost::asio::buffer(buf),
diff --git a/tests/daemon/face/multicast-udp-transport.t.cpp b/tests/daemon/face/multicast-udp-transport.t.cpp
index 6e4a8cf..51acb08 100644
--- a/tests/daemon/face/multicast-udp-transport.t.cpp
+++ b/tests/daemon/face/multicast-udp-transport.t.cpp
@@ -79,10 +79,7 @@
 
   // remoteSockRx2 unnecessary for this test case - only remoteSockTx2 is needed
   udp::socket remoteSockTx2(this->g_io);
-  remoteSockTx2.open(udp::v4());
-  remoteSockTx2.set_option(udp::socket::reuse_address(true));
-  remoteSockTx2.set_option(ip::multicast::enable_loopback(true));
-  remoteSockTx2.bind(udp::endpoint(ip::address_v4::any(), 7071));
+  MulticastUdpTransport::openTxSocket(remoteSockTx2, udp::endpoint(udp::v4(), 7071), true);
 
   Block pkt1 = ndn::encoding::makeStringBlock(300, "hello");
   ndn::Buffer buf1(pkt1.begin(), pkt1.end());
diff --git a/tests/daemon/face/udp-factory.t.cpp b/tests/daemon/face/udp-factory.t.cpp
index 84d4529..5e7e615 100644
--- a/tests/daemon/face/udp-factory.t.cpp
+++ b/tests/daemon/face/udp-factory.t.cpp
@@ -68,7 +68,7 @@
     BOOST_ASSERT(!netifs.empty());
     udp::Endpoint localEndpoint(ndn::ip::addressFromString(localIp), mcastPort);
     udp::Endpoint mcastEndpoint(ndn::ip::addressFromString(mcastIp), mcastPort);
-    return factory.createMulticastFace(localEndpoint, mcastEndpoint, *netifs.front());
+    return factory.createMulticastFace(localEndpoint, mcastEndpoint, netifs.front());
   }
 
   std::vector<const Face*>