face: EthernetChannel

Change-Id: I8d9be571a2b0abe0cfad09756ea6c691e0679450
Refs: #4011
diff --git a/.jenkins.d/20-tests.sh b/.jenkins.d/20-tests.sh
index 1e39e10..b5d512a 100755
--- a/.jenkins.d/20-tests.sh
+++ b/.jenkins.d/20-tests.sh
@@ -49,6 +49,6 @@
 
 # Then use sudo to run those tests that need superuser powers
 sudo -E ./build/unit-tests-core -t TestPrivilegeHelper $(ut_log_args core-privilege)
-sudo -E ./build/unit-tests-daemon -t Face/*EthernetTransport $(ut_log_args daemon-ethernet)
+sudo -E ./build/unit-tests-daemon -t Face/*Ethernet* $(ut_log_args daemon-ethernet)
 sudo -E ./build/unit-tests-daemon -t Face/*Factory/ProcessConfig $(ut_log_args daemon-face-config)
 sudo -E ./build/unit-tests-daemon -t Mgmt/TestGeneralConfigSection/UserAndGroupConfig,NoUserConfig $(ut_log_args daemon-user-config)
diff --git a/core/network-interface.hpp b/core/network-interface.hpp
index 0034052..32caaed 100644
--- a/core/network-interface.hpp
+++ b/core/network-interface.hpp
@@ -1,12 +1,12 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014,  Regents of the University of California,
- *                      Arizona Board of Regents,
- *                      Colorado State University,
- *                      University Pierre & Marie Curie, Sorbonne University,
- *                      Washington University in St. Louis,
- *                      Beijing Institute of Technology,
- *                      The University of Memphis
+ * Copyright (c) 2014-2017,  Regents of the University of California,
+ *                           Arizona Board of Regents,
+ *                           Colorado State University,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
  *
  * This file is part of NFD (Named Data Networking Forwarding Daemon).
  * See AUTHORS.md for complete list of NFD authors and contributors.
@@ -34,7 +34,9 @@
 
 namespace nfd {
 
-namespace ethernet = ndn::util::ethernet;
+namespace ethernet {
+using namespace ndn::util::ethernet;
+} // namespace ethernet
 
 /** \brief contains information about a network interface
  */
diff --git a/daemon/face/ethernet-channel.cpp b/daemon/face/ethernet-channel.cpp
new file mode 100644
index 0000000..d0676cc
--- /dev/null
+++ b/daemon/face/ethernet-channel.cpp
@@ -0,0 +1,234 @@
+/* -*- 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,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * NFD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ethernet-channel.hpp"
+#include "ethernet-protocol.hpp"
+#include "generic-link-service.hpp"
+#include "unicast-ethernet-transport.hpp"
+#include "core/global-io.hpp"
+
+#include <boost/range/adaptor/map.hpp>
+#include <pcap/pcap.h>
+
+namespace nfd {
+namespace face {
+
+NFD_LOG_INIT("EthernetChannel");
+
+EthernetChannel::EthernetChannel(const NetworkInterfaceInfo& localEndpoint,
+                                 time::nanoseconds idleTimeout)
+  : m_localEndpoint(localEndpoint)
+  , m_isListening(false)
+  , m_socket(getGlobalIoService())
+  , m_pcap(m_localEndpoint.name)
+  , m_idleFaceTimeout(idleTimeout)
+#ifdef _DEBUG
+  , m_nDropped(0)
+#endif
+{
+  setUri(FaceUri::fromDev(m_localEndpoint.name));
+  NFD_LOG_CHAN_INFO("Creating channel");
+}
+
+void
+EthernetChannel::connect(const ethernet::Address& remoteEndpoint,
+                         ndn::nfd::FacePersistency persistency,
+                         const FaceCreatedCallback& onFaceCreated,
+                         const FaceCreationFailedCallback& onConnectFailed)
+{
+  shared_ptr<Face> face;
+  try {
+    face = createFace(remoteEndpoint, persistency).second;
+  }
+  catch (const boost::system::system_error& e) {
+    NFD_LOG_CHAN_DEBUG("Face creation for " << remoteEndpoint << " failed: " << e.what());
+    if (onConnectFailed)
+      onConnectFailed(504, std::string("Face creation failed: ") + e.what());
+    return;
+  }
+
+  // Need to invoke the callback regardless of whether or not we had already
+  // created the face so that control responses and such can be sent
+  onFaceCreated(face);
+}
+
+void
+EthernetChannel::listen(const FaceCreatedCallback& onFaceCreated,
+                        const FaceCreationFailedCallback& onFaceCreationFailed)
+{
+  if (isListening()) {
+    NFD_LOG_CHAN_WARN("Already listening");
+    return;
+  }
+  m_isListening = true;
+
+  try {
+    m_pcap.activate(DLT_EN10MB);
+    m_socket.assign(m_pcap.getFd());
+  }
+  catch (const PcapHelper::Error& e) {
+    BOOST_THROW_EXCEPTION(Error(e.what()));
+  }
+  updateFilter();
+
+  asyncRead(onFaceCreated, onFaceCreationFailed);
+  NFD_LOG_CHAN_DEBUG("Started listening");
+}
+
+void
+EthernetChannel::asyncRead(const FaceCreatedCallback& onFaceCreated,
+                           const FaceCreationFailedCallback& onReceiveFailed)
+{
+  m_socket.async_read_some(boost::asio::null_buffers(),
+                           bind(&EthernetChannel::handleRead, this,
+                                boost::asio::placeholders::error,
+                                onFaceCreated, onReceiveFailed));
+}
+
+void
+EthernetChannel::handleRead(const boost::system::error_code& error,
+                            const FaceCreatedCallback& onFaceCreated,
+                            const FaceCreationFailedCallback& onReceiveFailed)
+{
+  if (error) {
+    if (error != boost::asio::error::operation_aborted) {
+      NFD_LOG_CHAN_DEBUG("Receive failed: " << error.message());
+      if (onReceiveFailed)
+        onReceiveFailed(500, "Receive failed: " + error.message());
+    }
+    return;
+  }
+
+  const uint8_t* pkt;
+  size_t len;
+  std::string err;
+  std::tie(pkt, len, err) = m_pcap.readNextPacket();
+
+  if (pkt == nullptr) {
+    NFD_LOG_CHAN_WARN("Read error: " << err);
+  }
+  else {
+    const ether_header* eh;
+    std::tie(eh, err) = ethernet::checkFrameHeader(pkt, len, m_localEndpoint.etherAddress,
+                                                   m_localEndpoint.etherAddress);
+    if (eh == nullptr) {
+      NFD_LOG_CHAN_DEBUG(err);
+    }
+    else {
+      ethernet::Address sender(eh->ether_shost);
+      pkt += ethernet::HDR_LEN;
+      len -= ethernet::HDR_LEN;
+      processIncomingPacket(pkt, len, sender, onFaceCreated, onReceiveFailed);
+    }
+  }
+
+#ifdef _DEBUG
+  size_t nDropped = m_pcap.getNDropped();
+  if (nDropped - m_nDropped > 0)
+    NFD_LOG_CHAN_DEBUG("Detected " << nDropped - m_nDropped << " dropped frame(s)");
+  m_nDropped = nDropped;
+#endif
+
+  asyncRead(onFaceCreated, onReceiveFailed);
+}
+
+void
+EthernetChannel::processIncomingPacket(const uint8_t* packet, size_t length,
+                                       const ethernet::Address& sender,
+                                       const FaceCreatedCallback& onFaceCreated,
+                                       const FaceCreationFailedCallback& onReceiveFailed)
+{
+  NFD_LOG_CHAN_TRACE("New peer " << sender);
+
+  bool isCreated = false;
+  shared_ptr<Face> face;
+  try {
+    std::tie(isCreated, face) = createFace(sender, ndn::nfd::FACE_PERSISTENCY_ON_DEMAND);
+  }
+  catch (const EthernetTransport::Error& e) {
+    NFD_LOG_CHAN_DEBUG("Face creation for " << sender << " failed: " << e.what());
+    if (onReceiveFailed)
+      onReceiveFailed(504, std::string("Face creation failed: ") + e.what());
+    return;
+  }
+
+  if (isCreated)
+    onFaceCreated(face);
+  else
+    NFD_LOG_CHAN_DEBUG("Received frame for existing face");
+
+  // dispatch the packet to the face for processing
+  auto* transport = static_cast<UnicastEthernetTransport*>(face->getTransport());
+  transport->receivePayload(packet, length, sender);
+}
+
+std::pair<bool, shared_ptr<Face>>
+EthernetChannel::createFace(const ethernet::Address& remoteEndpoint,
+                            ndn::nfd::FacePersistency persistency)
+{
+  auto it = m_channelFaces.find(remoteEndpoint);
+  if (it != m_channelFaces.end()) {
+    // we already have a face for this endpoint, so reuse it
+    NFD_LOG_CHAN_TRACE("Reusing existing face for " << remoteEndpoint);
+    return {false, it->second};
+  }
+
+  // else, create a new face
+  auto linkService = make_unique<GenericLinkService>();
+  auto transport = make_unique<UnicastEthernetTransport>(m_localEndpoint, remoteEndpoint,
+                                                         persistency, m_idleFaceTimeout);
+  auto face = make_shared<Face>(std::move(linkService), std::move(transport));
+
+  m_channelFaces[remoteEndpoint] = face;
+  connectFaceClosedSignal(*face, [this, remoteEndpoint] {
+    m_channelFaces.erase(remoteEndpoint);
+    updateFilter();
+  });
+  updateFilter();
+
+  return {true, face};
+}
+
+void
+EthernetChannel::updateFilter()
+{
+  if (!isListening())
+    return;
+
+  std::string filter = "(ether proto " + to_string(ethernet::ETHERTYPE_NDN) +
+                       ") && (ether dst " + m_localEndpoint.etherAddress.toString() + ")";
+  for (const auto& addr : m_channelFaces | boost::adaptors::map_keys) {
+    filter += " && (not ether src " + addr.toString() + ")";
+  }
+  // "not vlan" must appear last in the filter expression, or the
+  // rest of the filter won't work as intended, see pcap-filter(7)
+  filter += " && (not vlan)";
+
+  NFD_LOG_CHAN_TRACE("Updating filter: " << filter);
+  m_pcap.setPacketFilter(filter.data());
+}
+
+} // namespace face
+} // namespace nfd
diff --git a/daemon/face/ethernet-channel.hpp b/daemon/face/ethernet-channel.hpp
new file mode 100644
index 0000000..cb4fbe9
--- /dev/null
+++ b/daemon/face/ethernet-channel.hpp
@@ -0,0 +1,146 @@
+/* -*- 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,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * NFD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef NFD_DAEMON_FACE_ETHERNET_CHANNEL_HPP
+#define NFD_DAEMON_FACE_ETHERNET_CHANNEL_HPP
+
+#include "channel.hpp"
+#include "pcap-helper.hpp"
+#include "core/network-interface.hpp"
+
+namespace nfd {
+namespace face {
+
+/**
+ * \brief Class implementing Ethernet-based channel to create faces
+ */
+class EthernetChannel : public Channel
+{
+public:
+  /**
+   * \brief EthernetChannel-related error
+   */
+  class Error : public std::runtime_error
+  {
+  public:
+    explicit
+    Error(const std::string& what)
+      : std::runtime_error(what)
+    {
+    }
+  };
+
+  /**
+   * \brief Create an Ethernet channel on the given \p localEndpoint (network interface)
+   *
+   * To enable creation of faces upon incoming connections,
+   * one needs to explicitly call EthernetChannel::listen method.
+   */
+  EthernetChannel(const NetworkInterfaceInfo& localEndpoint,
+                  time::nanoseconds idleTimeout);
+
+  bool
+  isListening() const override
+  {
+    return m_isListening;
+  }
+
+  size_t
+  size() const override
+  {
+    return m_channelFaces.size();
+  }
+
+  /**
+   * \brief Create a unicast Ethernet face toward \p remoteEndpoint
+   *
+   * \param remoteEndpoint The remote Ethernet endpoint
+   * \param persistency Persistency of the newly created face
+   * \param onFaceCreated Callback to notify successful creation of the face
+   * \param onConnectFailed Callback to notify errors
+   */
+  void
+  connect(const ethernet::Address& remoteEndpoint,
+          ndn::nfd::FacePersistency persistency,
+          const FaceCreatedCallback& onFaceCreated,
+          const FaceCreationFailedCallback& onConnectFailed);
+
+  /**
+   * \brief Start listening
+   *
+   * Enable listening on the local endpoint, waiting for incoming frames,
+   * and creating a face when a frame is received from a new remote host.
+   *
+   * Faces created in this way will have on-demand persistency.
+   *
+   * \param onFaceCreated Callback to notify successful creation of a face
+   * \param onFaceCreationFailed Callback to notify errors
+   * \throw Error
+   */
+  void
+  listen(const FaceCreatedCallback& onFaceCreated,
+         const FaceCreationFailedCallback& onFaceCreationFailed);
+
+private:
+  void
+  asyncRead(const FaceCreatedCallback& onFaceCreated,
+            const FaceCreationFailedCallback& onReceiveFailed);
+
+  void
+  handleRead(const boost::system::error_code& error,
+             const FaceCreatedCallback& onFaceCreated,
+             const FaceCreationFailedCallback& onReceiveFailed);
+
+  void
+  processIncomingPacket(const uint8_t* packet, size_t length,
+                        const ethernet::Address& sender,
+                        const FaceCreatedCallback& onFaceCreated,
+                        const FaceCreationFailedCallback& onReceiveFailed);
+
+  std::pair<bool, shared_ptr<Face>>
+  createFace(const ethernet::Address& remoteEndpoint,
+             ndn::nfd::FacePersistency persistency);
+
+  void
+  updateFilter();
+
+private:
+  const NetworkInterfaceInfo m_localEndpoint;
+  bool m_isListening;
+  boost::asio::posix::stream_descriptor m_socket;
+  PcapHelper m_pcap;
+  std::map<ethernet::Address, shared_ptr<Face>> m_channelFaces;
+  const time::nanoseconds m_idleFaceTimeout; ///< Timeout for automatic closure of idle on-demand faces
+
+#ifdef _DEBUG
+  /// number of frames dropped by the kernel, as reported by libpcap
+  size_t m_nDropped;
+#endif
+};
+
+} // namespace face
+} // namespace nfd
+
+#endif // NFD_DAEMON_FACE_ETHERNET_CHANNEL_HPP
diff --git a/daemon/face/ethernet-protocol.cpp b/daemon/face/ethernet-protocol.cpp
new file mode 100644
index 0000000..2ec0bb7
--- /dev/null
+++ b/daemon/face/ethernet-protocol.cpp
@@ -0,0 +1,61 @@
+/* -*- 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,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * NFD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ethernet-protocol.hpp"
+
+#include <arpa/inet.h>  // for ntohs()
+
+namespace nfd {
+namespace ethernet {
+
+std::pair<const ether_header*, std::string>
+checkFrameHeader(const uint8_t* packet, size_t length,
+                 const Address& localAddr, const Address& destAddr)
+{
+  if (length < HDR_LEN + MIN_DATA_LEN)
+    return {nullptr, "Received frame too short: " + to_string(length) + " bytes"};
+
+  const ether_header* eh = reinterpret_cast<const ether_header*>(packet);
+
+  // in some cases VLAN-tagged frames may survive the BPF filter,
+  // make sure we do not process those frames (see #3348)
+  if (ntohs(eh->ether_type) != ETHERTYPE_NDN)
+    return {nullptr, "Received frame with wrong ethertype: " + to_string(ntohs(eh->ether_type))};
+
+#ifdef _DEBUG
+  Address shost(eh->ether_shost);
+  if (shost == localAddr)
+    return {nullptr, "Received frame sent by this host"};
+
+  Address dhost(eh->ether_dhost);
+  if (dhost != destAddr)
+    return {nullptr, "Received frame addressed to another host or multicast group: " + dhost.toString()};
+#endif
+
+  return {eh, ""};
+}
+
+} // namespace ethernet
+} // namespace nfd
diff --git a/daemon/face/ethernet-protocol.hpp b/daemon/face/ethernet-protocol.hpp
new file mode 100644
index 0000000..4eca7cf
--- /dev/null
+++ b/daemon/face/ethernet-protocol.hpp
@@ -0,0 +1,43 @@
+/* -*- 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,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * NFD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef NFD_DAEMON_FACE_ETHERNET_PROTOCOL_HPP
+#define NFD_DAEMON_FACE_ETHERNET_PROTOCOL_HPP
+
+#include "core/network-interface.hpp"
+
+#include <net/ethernet.h>
+
+namespace nfd {
+namespace ethernet {
+
+std::pair<const ether_header*, std::string>
+checkFrameHeader(const uint8_t* packet, size_t length,
+                 const Address& localAddr, const Address& destAddr);
+
+} // namespace ethernet
+} // namespace nfd
+
+#endif // NFD_DAEMON_FACE_ETHERNET_PROTOCOL_HPP
diff --git a/daemon/face/ethernet-transport.cpp b/daemon/face/ethernet-transport.cpp
index 0773e85..9f6d465 100644
--- a/daemon/face/ethernet-transport.cpp
+++ b/daemon/face/ethernet-transport.cpp
@@ -24,16 +24,16 @@
  */
 
 #include "ethernet-transport.hpp"
+#include "ethernet-protocol.hpp"
 #include "core/global-io.hpp"
 
 #include <pcap/pcap.h>
 
-#include <cerrno>         // for errno
-#include <cstring>        // for memcpy(), strerror(), 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 <sys/ioctl.h>    // for ioctl()
+#include <cerrno>       // for errno
+#include <cstring>      // for memcpy(), strerror(), strncpy()
+#include <arpa/inet.h>  // for htons()
+#include <net/if.h>     // for struct ifreq
+#include <sys/ioctl.h>  // for ioctl()
 
 namespace nfd {
 namespace face {
@@ -62,10 +62,7 @@
   // do this after assigning m_socket because getInterfaceMtu uses it
   this->setMtu(getInterfaceMtu());
 
-  m_socket.async_read_some(boost::asio::null_buffers(),
-                           bind(&EthernetTransport::handleRead, this,
-                                boost::asio::placeholders::error,
-                                boost::asio::placeholders::bytes_transferred));
+  asyncRead();
 }
 
 void
@@ -114,19 +111,27 @@
   buffer.prependByteArray(m_srcAddress.data(), m_srcAddress.size());
   buffer.prependByteArray(m_destAddress.data(), m_destAddress.size());
 
-  // send the packet
+  // send the frame
   int sent = pcap_inject(m_pcap, buffer.buf(), buffer.size());
   if (sent < 0)
     NFD_LOG_FACE_ERROR("pcap_inject failed: " << m_pcap.getLastError());
   else if (static_cast<size_t>(sent) < buffer.size())
-    NFD_LOG_FACE_ERROR("Failed to send the full frame: bufsize=" << buffer.size() << " sent=" << sent);
+    NFD_LOG_FACE_ERROR("Failed to send the full frame: size=" << buffer.size() << " sent=" << sent);
   else
     // print block size because we don't want to count the padding in buffer
     NFD_LOG_FACE_TRACE("Successfully sent: " << block.size() << " bytes");
 }
 
 void
-EthernetTransport::handleRead(const boost::system::error_code& error, size_t)
+EthernetTransport::asyncRead()
+{
+  m_socket.async_read_some(boost::asio::null_buffers(),
+                           bind(&EthernetTransport::handleRead, this,
+                                boost::asio::placeholders::error));
+}
+
+void
+EthernetTransport::handleRead(const boost::system::error_code& error)
 {
   if (error)
     return processErrorCode(error);
@@ -136,63 +141,54 @@
   std::string err;
   std::tie(pkt, len, err) = m_pcap.readNextPacket();
 
-  if (pkt == nullptr)
+  if (pkt == nullptr) {
     NFD_LOG_FACE_ERROR("Read error: " << err);
-  else
-    processIncomingPacket(pkt, len);
+  }
+  else {
+    const ether_header* eh;
+    std::tie(eh, err) = ethernet::checkFrameHeader(pkt, len, m_srcAddress,
+                                                   m_destAddress.isMulticast() ? m_destAddress : m_srcAddress);
+    if (eh == nullptr) {
+      NFD_LOG_FACE_DEBUG(err);
+    }
+    else {
+      ethernet::Address sender(eh->ether_shost);
+      pkt += ethernet::HDR_LEN;
+      len -= ethernet::HDR_LEN;
+      receivePayload(pkt, len, sender);
+    }
+  }
 
 #ifdef _DEBUG
   size_t nDropped = m_pcap.getNDropped();
   if (nDropped - m_nDropped > 0)
-    NFD_LOG_FACE_DEBUG("Detected " << nDropped - m_nDropped << " dropped packet(s)");
+    NFD_LOG_FACE_DEBUG("Detected " << nDropped - m_nDropped << " dropped frame(s)");
   m_nDropped = nDropped;
 #endif
 
-  m_socket.async_read_some(boost::asio::null_buffers(),
-                           bind(&EthernetTransport::handleRead, this,
-                                boost::asio::placeholders::error,
-                                boost::asio::placeholders::bytes_transferred));
+  asyncRead();
 }
 
 void
-EthernetTransport::processIncomingPacket(const uint8_t* packet, size_t length)
+EthernetTransport::receivePayload(const uint8_t* payload, size_t length,
+                                  const ethernet::Address& sender)
 {
-  if (length < ethernet::HDR_LEN + ethernet::MIN_DATA_LEN) {
-    NFD_LOG_FACE_WARN("Received frame is too short (" << length << " bytes)");
-    return;
-  }
-
-  const ether_header* eh = reinterpret_cast<const ether_header*>(packet);
-  const ethernet::Address sourceAddress(eh->ether_shost);
-
-  // in some cases VLAN-tagged frames may survive the BPF filter,
-  // make sure we do not process those frames (see #3348)
-  if (ntohs(eh->ether_type) != ethernet::ETHERTYPE_NDN)
-    return;
-
-  // check that our BPF filter is working correctly
-  BOOST_ASSERT_MSG(ethernet::Address(eh->ether_dhost) == m_destAddress,
-                   "Received frame addressed to another host or multicast group");
-  BOOST_ASSERT_MSG(sourceAddress != m_srcAddress,
-                   "Received frame sent by this host");
-
-  packet += ethernet::HDR_LEN;
-  length -= ethernet::HDR_LEN;
-
   bool isOk = false;
   Block element;
-  std::tie(isOk, element) = Block::fromBuffer(packet, length);
+  std::tie(isOk, element) = Block::fromBuffer(payload, length);
   if (!isOk) {
-    NFD_LOG_FACE_WARN("Received invalid packet from " << sourceAddress.toString());
+    NFD_LOG_FACE_WARN("Received invalid packet from " << sender);
     return;
   }
 
-  NFD_LOG_FACE_TRACE("Received: " << element.size() << " bytes from " << sourceAddress.toString());
+  NFD_LOG_FACE_TRACE("Received: " << element.size() << " bytes from " << sender);
 
   Transport::Packet tp(std::move(element));
   static_assert(sizeof(tp.remoteEndpoint) >= ethernet::ADDR_LEN,
                 "Transport::Packet::remoteEndpoint is too small");
-  std::memcpy(&tp.remoteEndpoint, sourceAddress.data(), sourceAddress.size());
+  if (m_destAddress.isMulticast()) {
+    std::memcpy(&tp.remoteEndpoint, sender.data(), sender.size());
+  }
   this->receive(std::move(tp));
 }
 
diff --git a/daemon/face/ethernet-transport.hpp b/daemon/face/ethernet-transport.hpp
index 57a7edc..575ea72 100644
--- a/daemon/face/ethernet-transport.hpp
+++ b/daemon/face/ethernet-transport.hpp
@@ -49,6 +49,16 @@
     }
   };
 
+  /**
+   * @brief Processes the payload of an incoming frame
+   * @param payload Pointer to the first byte of data after the Ethernet header
+   * @param length Payload length
+   * @param sender Sender address
+   */
+  void
+  receivePayload(const uint8_t* payload, size_t length,
+                 const ethernet::Address& sender);
+
 protected:
   EthernetTransport(const NetworkInterfaceInfo& localEndpoint,
                     const ethernet::Address& remoteEndpoint);
@@ -66,19 +76,11 @@
   void
   sendPacket(const ndn::Block& block);
 
-  /**
-   * @brief async_read_some() callback
-   */
   void
-  handleRead(const boost::system::error_code& error, size_t nBytesRead);
+  asyncRead();
 
-  /**
-   * @brief Processes an incoming packet as captured by libpcap
-   * @param packet Pointer to the received packet, including the link-layer header
-   * @param length Packet length
-   */
   void
-  processIncomingPacket(const uint8_t* packet, size_t length);
+  handleRead(const boost::system::error_code& error);
 
   /**
    * @brief Handles errors encountered by Boost.Asio on the receive path
@@ -101,7 +103,7 @@
 
 private:
 #ifdef _DEBUG
-  /// number of packets dropped by the kernel, as reported by libpcap
+  /// number of frames dropped by the kernel, as reported by libpcap
   size_t m_nDropped;
 #endif
 };
diff --git a/daemon/face/udp-channel.cpp b/daemon/face/udp-channel.cpp
index dfbfefc..3ab9e1f 100644
--- a/daemon/face/udp-channel.cpp
+++ b/daemon/face/udp-channel.cpp
@@ -36,10 +36,10 @@
 namespace ip = boost::asio::ip;
 
 UdpChannel::UdpChannel(const udp::Endpoint& localEndpoint,
-                       const time::seconds& timeout)
+                       time::nanoseconds idleTimeout)
   : m_localEndpoint(localEndpoint)
   , m_socket(getGlobalIoService())
-  , m_idleFaceTimeout(timeout)
+  , m_idleFaceTimeout(idleTimeout)
 {
   setUri(FaceUri(m_localEndpoint));
   NFD_LOG_CHAN_INFO("Creating channel");
@@ -129,6 +129,8 @@
 
   if (isCreated)
     onFaceCreated(face);
+  else
+    NFD_LOG_CHAN_DEBUG("Received datagram for existing face");
 
   // dispatch the datagram to the face for processing
   auto* transport = static_cast<UnicastUdpTransport*>(face->getTransport());
diff --git a/daemon/face/udp-channel.hpp b/daemon/face/udp-channel.hpp
index 403afac..b239157 100644
--- a/daemon/face/udp-channel.hpp
+++ b/daemon/face/udp-channel.hpp
@@ -41,17 +41,14 @@
 {
 public:
   /**
-   * \brief Create UDP channel for the local endpoint
+   * \brief Create a UDP channel on the given \p localEndpoint
    *
    * To enable creation of faces upon incoming connections,
    * one needs to explicitly call UdpChannel::listen method.
-   * The created socket is bound to the localEndpoint.
-   * reuse_address option is set
-   *
-   * \throw UdpChannel::Error if bind on the socket fails
+   * The created socket is bound to \p localEndpoint.
    */
   UdpChannel(const udp::Endpoint& localEndpoint,
-             const time::seconds& timeout);
+             time::nanoseconds idleTimeout);
 
   bool
   isListening() const override
@@ -66,9 +63,12 @@
   }
 
   /**
-   * \brief Create a face by establishing connection to remote endpoint
+   * \brief Create a unicast UDP face toward \p remoteEndpoint
    *
-   * \throw UdpChannel::Error if bind or connect on the socket fail
+   * \param remoteEndpoint The remote UDP endpoint
+   * \param persistency Persistency of the newly created face
+   * \param onFaceCreated Callback to notify successful creation of the face
+   * \param onConnectFailed Callback to notify errors
    */
   void
   connect(const udp::Endpoint& remoteEndpoint,
@@ -77,14 +77,15 @@
           const FaceCreationFailedCallback& onConnectFailed);
 
   /**
-   * \brief Enable listening on the local endpoint, accept connections,
-   *        and create faces when remote host makes a connection
+   * \brief Start listening
+   *
+   * Enable listening on the local endpoint, waiting for incoming datagrams,
+   * and creating a face when a datagram is received from a new remote host.
+   *
+   * Faces created in this way will have on-demand persistency.
    *
    * \param onFaceCreated Callback to notify successful creation of a face
    * \param onFaceCreationFailed Callback to notify errors
-   *
-   * Once a face is created, if it doesn't send/receive anything for
-   * a period of time equal to timeout, it will be destroyed
    */
   void
   listen(const FaceCreatedCallback& onFaceCreated,
@@ -96,8 +97,8 @@
                  const FaceCreationFailedCallback& onReceiveFailed);
 
   /**
-   * \brief The channel has received a new packet from a remote
-   *        endpoint that is not associated with any UdpFace yet
+   * \brief The channel has received a packet from a remote
+   *        endpoint not associated with any UDP face yet
    */
   void
   handleNewPeer(const boost::system::error_code& error,
@@ -113,9 +114,9 @@
   const udp::Endpoint m_localEndpoint;
   udp::Endpoint m_remoteEndpoint; ///< The latest peer that started communicating with us
   boost::asio::ip::udp::socket m_socket; ///< Socket used to "accept" new peers
-  std::map<udp::Endpoint, shared_ptr<Face>> m_channelFaces;
   std::array<uint8_t, ndn::MAX_NDN_PACKET_SIZE> m_receiveBuffer;
-  time::seconds m_idleFaceTimeout; ///< Timeout for automatic closure of idle on-demand faces
+  std::map<udp::Endpoint, shared_ptr<Face>> m_channelFaces;
+  const time::nanoseconds m_idleFaceTimeout; ///< Timeout for automatic closure of idle on-demand faces
 };
 
 } // namespace face
diff --git a/daemon/face/unix-stream-channel.hpp b/daemon/face/unix-stream-channel.hpp
index 310030b..e396d8e 100644
--- a/daemon/face/unix-stream-channel.hpp
+++ b/daemon/face/unix-stream-channel.hpp
@@ -82,13 +82,19 @@
   }
 
   /**
-   * \brief Enable listening on the local endpoint, accept connections,
-   *        and create faces when a connection is made
+   * \brief Start listening
+   *
+   * Enable listening on the Unix socket, waiting for incoming connections,
+   * and creating a face when a connection is made.
+   *
+   * Faces created in this way will have on-demand persistency.
+   *
    * \param onFaceCreated  Callback to notify successful creation of the face
    * \param onAcceptFailed Callback to notify when channel fails (accept call
    *                       returns an error)
    * \param backlog        The maximum length of the queue of pending incoming
    *                       connections
+   * \throw Error
    */
   void
   listen(const FaceCreatedCallback& onFaceCreated,
diff --git a/tests/daemon/face/ethernet-channel.t.cpp b/tests/daemon/face/ethernet-channel.t.cpp
new file mode 100644
index 0000000..238e193
--- /dev/null
+++ b/tests/daemon/face/ethernet-channel.t.cpp
@@ -0,0 +1,101 @@
+/* -*- 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,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * NFD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "face/ethernet-channel.hpp"
+
+#include "ethernet-fixture.hpp"
+
+namespace nfd {
+namespace face {
+namespace tests {
+
+class EthernetChannelFixture : public EthernetFixture
+{
+protected:
+  unique_ptr<EthernetChannel>
+  makeChannel()
+  {
+    BOOST_ASSERT(netifs.size() > 0);
+    return make_unique<EthernetChannel>(netifs.front(), time::seconds(2));
+  }
+};
+
+BOOST_AUTO_TEST_SUITE(Face)
+BOOST_FIXTURE_TEST_SUITE(TestEthernetChannel, EthernetChannelFixture)
+
+BOOST_AUTO_TEST_CASE(Uri)
+{
+  SKIP_IF_ETHERNET_NETIF_COUNT_LT(1);
+
+  auto channel = makeChannel();
+  BOOST_CHECK_EQUAL(channel->getUri(), FaceUri::fromDev(netifs.front().name));
+}
+
+BOOST_AUTO_TEST_CASE(Listen)
+{
+  SKIP_IF_ETHERNET_NETIF_COUNT_LT(1);
+
+  auto channel = makeChannel();
+  BOOST_CHECK_EQUAL(channel->isListening(), false);
+
+  channel->listen(nullptr, nullptr);
+  BOOST_CHECK_EQUAL(channel->isListening(), true);
+
+  // listen() is idempotent
+  BOOST_CHECK_NO_THROW(channel->listen(nullptr, nullptr));
+  BOOST_CHECK_EQUAL(channel->isListening(), true);
+}
+
+BOOST_AUTO_TEST_CASE(FaceClosure)
+{
+  SKIP_IF_ETHERNET_NETIF_COUNT_LT(1);
+
+  auto channel = makeChannel();
+  BOOST_CHECK_EQUAL(channel->size(), 0);
+
+  shared_ptr<nfd::Face> face;
+  channel->connect({0x00, 0x00, 0x5e, 0x00, 0x53, 0x5e},
+                   ndn::nfd::FACE_PERSISTENCY_PERSISTENT,
+                   [&face] (const shared_ptr<nfd::Face>& newFace) {
+                     BOOST_REQUIRE(newFace != nullptr);
+                     face = newFace;
+                   },
+                   [] (uint32_t status, const std::string& reason) {
+                     BOOST_FAIL("No error expected, but got: [" << status << ": " << reason << "]");
+                   });
+  BOOST_CHECK_EQUAL(channel->size(), 1);
+  BOOST_REQUIRE(face != nullptr);
+
+  face->close();
+  g_io.poll();
+  BOOST_CHECK_EQUAL(channel->size(), 0);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestEthernetChannel
+BOOST_AUTO_TEST_SUITE_END() // Face
+
+} // namespace tests
+} // namespace face
+} // namespace nfd
diff --git a/tests/daemon/face/ethernet-factory.t.cpp b/tests/daemon/face/ethernet-factory.t.cpp
index b8b07d1..5e7c6d2 100644
--- a/tests/daemon/face/ethernet-factory.t.cpp
+++ b/tests/daemon/face/ethernet-factory.t.cpp
@@ -153,7 +153,7 @@
     {
       ether
       {
-        mcast_group 01:00:00:00:00:01
+        mcast_group 01:00:5e:90:10:01
       }
     }
   )CONFIG";
@@ -162,7 +162,7 @@
     {
       ether
       {
-        mcast_group 01:00:00:00:00:02
+        mcast_group 01:00:5e:90:10:02
       }
     }
   )CONFIG";
@@ -171,14 +171,14 @@
   auto etherMcastFaces = this->listEtherMcastFaces();
   BOOST_REQUIRE_EQUAL(etherMcastFaces.size(), netifs.size());
   BOOST_CHECK_EQUAL(etherMcastFaces.front()->getRemoteUri(),
-                    FaceUri(ethernet::Address(0x01, 0x00, 0x00, 0x00, 0x00, 0x01)));
+                    FaceUri(ethernet::Address{0x01, 0x00, 0x5e, 0x90, 0x10, 0x01}));
 
   parseConfig(CONFIG2, false);
   g_io.poll();
   etherMcastFaces = this->listEtherMcastFaces();
   BOOST_REQUIRE_EQUAL(etherMcastFaces.size(), netifs.size());
   BOOST_CHECK_EQUAL(etherMcastFaces.front()->getRemoteUri(),
-                    FaceUri(ethernet::Address(0x01, 0x00, 0x00, 0x00, 0x00, 0x02)));
+                    FaceUri(ethernet::Address{0x01, 0x00, 0x5e, 0x90, 0x10, 0x02}));
 }
 
 BOOST_AUTO_TEST_CASE(Whitelist)
@@ -286,7 +286,7 @@
       ether
       {
         mcast yes
-        mcast_group 02:00:00:00:00:01
+        mcast_group 00:00:5e:00:53:5e
       }
     }
   )CONFIG";
diff --git a/tests/daemon/face/ethernet-fixture.hpp b/tests/daemon/face/ethernet-fixture.hpp
index 029bb6e..5e3b612 100644
--- a/tests/daemon/face/ethernet-fixture.hpp
+++ b/tests/daemon/face/ethernet-fixture.hpp
@@ -56,7 +56,7 @@
   }
 
   void
-  initializeUnicast(ethernet::Address remoteAddr = {0x0A, 0x01, 0x23, 0x45, 0x67, 0x89},
+  initializeUnicast(ethernet::Address remoteAddr = {0x00, 0x00, 0x5e, 0x00, 0x53, 0x5e},
                     ndn::nfd::FacePersistency persistency = ndn::nfd::FACE_PERSISTENCY_PERSISTENT)
   {
     BOOST_ASSERT(netifs.size() > 0);
@@ -67,7 +67,7 @@
   }
 
   void
-  initializeMulticast(ethernet::Address mcastGroup = ethernet::getDefaultMulticastAddress(),
+  initializeMulticast(ethernet::Address mcastGroup = {0x01, 0x00, 0x5e, 0x90, 0x10, 0x5e},
                       ndn::nfd::LinkType linkType = ndn::nfd::LINK_TYPE_MULTI_ACCESS)
   {
     BOOST_ASSERT(netifs.size() > 0);