EthernetFace: avoid putting the NIC in promiscuous mode if possible.

Try to use the PACKET_ADD_MEMBERSHIP socket option and the
SIOCADDMULTI ioctl, and only if they fail or are not available
we fall back to promiscuous mode.

Change-Id: Ib534e161ff86ca855e084240b94a166bc4d33ce1
Refs: #1278
diff --git a/daemon/face/ethernet-face.cpp b/daemon/face/ethernet-face.cpp
index ea9e522..4ffb9db 100644
--- a/daemon/face/ethernet-face.cpp
+++ b/daemon/face/ethernet-face.cpp
@@ -28,14 +28,26 @@
 
 #include <pcap/pcap.h>
 
-#include <cstring>        // for std::strncpy()
+#include <cerrno>         // for errno
+#include <cstdio>         // for std::snprintf()
+#include <cstring>        // for std::strerror() and std::strncpy()
 #include <arpa/inet.h>    // for htons() and ntohs()
 #include <net/ethernet.h> // for struct ether_header
 #include <net/if.h>       // for struct ifreq
-#include <stdio.h>        // for snprintf()
 #include <sys/ioctl.h>    // for ioctl()
 #include <unistd.h>       // for dup()
 
+#if defined(__linux__)
+#include <netpacket/packet.h> // for struct packet_mreq
+#include <sys/socket.h>       // for setsockopt()
+#endif
+
+#ifdef SIOCADDMULTI
+#if defined(__APPLE__) || defined(__FreeBSD__)
+#include <net/if_dl.h>    // for struct sockaddr_dl
+#endif
+#endif
+
 #if !defined(PCAP_NETMASK_UNKNOWN)
 /*
  * Value to pass to pcap_compile() as the netmask if you don't know what
@@ -53,6 +65,9 @@
                            const ethernet::Address& address)
   : Face(FaceUri(address), FaceUri::fromDev(interface.name))
   , m_socket(socket)
+#if defined(__linux__)
+  , m_interfaceIndex(interface.index)
+#endif
   , m_interfaceName(interface.name)
   , m_srcAddress(interface.etherAddress)
   , m_destAddress(address)
@@ -75,13 +90,15 @@
                 << "] Interface MTU is: " << m_interfaceMtu);
 
   char filter[100];
-  ::snprintf(filter, sizeof(filter),
-             "(ether proto 0x%x) && (ether dst %s) && (not ether src %s)",
-             ethernet::ETHERTYPE_NDN,
-             m_destAddress.toString().c_str(),
-             m_srcAddress.toString().c_str());
+  std::snprintf(filter, sizeof(filter),
+                "(ether proto 0x%x) && (ether dst %s) && (not ether src %s)",
+                ethernet::ETHERTYPE_NDN,
+                m_destAddress.toString().c_str(),
+                m_srcAddress.toString().c_str());
   setPacketFilter(filter);
 
+  joinMulticastGroup();
+
   m_socket->async_read_some(boost::asio::null_buffers(),
                             bind(&EthernetFace::handleRead, this,
                                  boost::asio::placeholders::error,
@@ -139,10 +156,6 @@
   pcap_set_immediate_mode(m_pcap, 1);
 #endif
 
-  /// \todo Do not rely on promisc mode, see task #1278
-  if (!m_destAddress.isBroadcast())
-    pcap_set_promisc(m_pcap, 1);
-
   if (pcap_activate(m_pcap) < 0)
     throw Error("pcap_activate() failed");
 
@@ -168,6 +181,72 @@
 }
 
 void
+EthernetFace::joinMulticastGroup()
+{
+#if defined(__linux__)
+  packet_mreq mr{};
+  mr.mr_ifindex = m_interfaceIndex;
+  mr.mr_type = PACKET_MR_MULTICAST;
+  mr.mr_alen = m_destAddress.size();
+  std::copy(m_destAddress.begin(), m_destAddress.end(), mr.mr_address);
+
+  if (::setsockopt(m_socket->native_handle(), SOL_PACKET,
+                   PACKET_ADD_MEMBERSHIP, &mr, sizeof(mr)) == 0)
+    return; // success
+
+  NFD_LOG_WARN("[id:" << getId() << ",endpoint:" << m_interfaceName
+               << "] setsockopt(PACKET_ADD_MEMBERSHIP) failed: " << std::strerror(errno));
+#endif
+
+#if defined(SIOCADDMULTI)
+  ifreq ifr{};
+  std::strncpy(ifr.ifr_name, m_interfaceName.c_str(), sizeof(ifr.ifr_name) - 1);
+
+#if defined(__APPLE__) || defined(__FreeBSD__)
+  /*
+   * Differences between Linux and the BSDs (including OS X):
+   *   o BSD does not have ifr_hwaddr; use ifr_addr instead.
+   *   o While OS X seems to accept both AF_LINK and AF_UNSPEC as the address
+   *     family, FreeBSD explicitly requires AF_LINK, so we have to use AF_LINK
+   *     and sockaddr_dl instead of the generic sockaddr structure.
+   *   o BSD's sockaddr (and sockaddr_dl in particular) contains an additional
+   *     field, sa_len (sdl_len), which must be set to the total length of the
+   *     structure, including the length field itself.
+   *   o We do not specify the interface name, thus sdl_nlen is left at 0 and
+   *     LLADDR is effectively the same as sdl_data.
+   */
+  sockaddr_dl* sdl = reinterpret_cast<sockaddr_dl*>(&ifr.ifr_addr);
+  sdl->sdl_len = sizeof(ifr.ifr_addr);
+  sdl->sdl_family = AF_LINK;
+  sdl->sdl_alen = m_destAddress.size();
+  std::copy(m_destAddress.begin(), m_destAddress.end(), LLADDR(sdl));
+
+  static_assert(sizeof(ifr.ifr_addr) >= offsetof(sockaddr_dl, sdl_data) + ethernet::ADDR_LEN,
+                "ifr_addr in struct ifreq is too small on this platform");
+#else
+  ifr.ifr_hwaddr.sa_family = AF_UNSPEC;
+  std::copy(m_destAddress.begin(), m_destAddress.end(), ifr.ifr_hwaddr.sa_data);
+
+  static_assert(sizeof(ifr.ifr_hwaddr) >= offsetof(sockaddr, sa_data) + ethernet::ADDR_LEN,
+                "ifr_hwaddr in struct ifreq is too small on this platform");
+#endif
+
+  if (::ioctl(m_socket->native_handle(), SIOCADDMULTI, &ifr) >= 0)
+    return; // success
+
+  NFD_LOG_WARN("[id:" << getId() << ",endpoint:" << m_interfaceName
+               << "] ioctl(SIOCADDMULTI) failed: " << std::strerror(errno));
+#endif
+
+  if (!m_destAddress.isBroadcast())
+    {
+      NFD_LOG_DEBUG("[id:" << getId() << ",endpoint:" << m_interfaceName
+                    << "] Falling back to promiscuous mode");
+      pcap_set_promisc(m_pcap, 1);
+    }
+}
+
+void
 EthernetFace::sendPacket(const ndn::Block& block)
 {
   if (!m_pcap)
@@ -315,13 +394,13 @@
   size_t mtu = ethernet::MAX_DATA_LEN;
 
 #ifdef SIOCGIFMTU
-  ifreq ifr = boost::initialized_value;
+  ifreq ifr{};
   std::strncpy(ifr.ifr_name, m_interfaceName.c_str(), sizeof(ifr.ifr_name) - 1);
 
   if (::ioctl(m_socket->native_handle(), SIOCGIFMTU, &ifr) < 0)
     {
       NFD_LOG_WARN("[id:" << getId() << ",endpoint:" << m_interfaceName
-                   << "] Failed to get interface MTU, assuming " << mtu);
+                   << "] Failed to get interface MTU: " << std::strerror(errno));
     }
   else
     {
diff --git a/daemon/face/ethernet-face.hpp b/daemon/face/ethernet-face.hpp
index 4383246..0c673c3 100644
--- a/daemon/face/ethernet-face.hpp
+++ b/daemon/face/ethernet-face.hpp
@@ -87,6 +87,9 @@
   setPacketFilter(const char* filterString);
 
   void
+  joinMulticastGroup();
+
+  void
   sendPacket(const ndn::Block& block);
 
   void
@@ -101,6 +104,9 @@
 
 private:
   shared_ptr<boost::asio::posix::stream_descriptor> m_socket;
+#if defined(__linux__)
+  int m_interfaceIndex;
+#endif
   std::string m_interfaceName;
   ethernet::Address m_srcAddress;
   ethernet::Address m_destAddress;