face: FaceSystem class

FaceSystem class is the entry point of NFD's face system. It owns
ProtocolFactory objects that are created from face_system section of
NFD configuration file. This responsibility is split from FaceManager.

This commit also adds test coverage for face_system.websocket config
section processing routine.

refs #3904

Change-Id: If9a23c23b81f03d2b76c29febb3f29e6007d595b
diff --git a/.jenkins.d/20-tests.sh b/.jenkins.d/20-tests.sh
index 4903359..6f22700 100755
--- a/.jenkins.d/20-tests.sh
+++ b/.jenkins.d/20-tests.sh
@@ -50,5 +50,5 @@
 # Then use sudo to run those tests that need superuser powers
 sudo ./build/unit-tests-core -t TestPrivilegeHelper $(ut_log_args core-privilege)
 sudo ./build/unit-tests-daemon -t Face/TestEthernetFactory,TestEthernetTransport $(ut_log_args daemon-ethernet)
+sudo ./build/unit-tests-daemon -t Face/TestFaceSystem/Config* $(ut_log_args daemon-face-config)
 sudo ./build/unit-tests-daemon -t Mgmt/TestGeneralConfigSection/UserAndGroupConfig,NoUserConfig $(ut_log_args daemon-user-config)
-sudo ./build/unit-tests-daemon -t Mgmt/TestFaceManager/ProcessConfig/ProcessSectionUdp,ProcessSectionUdpMulticastReinit,ProcessSectionEther,ProcessSectionEtherMulticastReinit $(ut_log_args daemon-face-manager-config)
diff --git a/daemon/face/face-system.cpp b/daemon/face/face-system.cpp
new file mode 100644
index 0000000..0d4a308
--- /dev/null
+++ b/daemon/face/face-system.cpp
@@ -0,0 +1,541 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2016,  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-system.hpp"
+#include "core/logger.hpp"
+#include "core/network-interface.hpp"
+#include "core/network-interface-predicate.hpp"
+#include "fw/face-table.hpp"
+
+// ProtocolFactory includes, sorted alphabetically
+#ifdef HAVE_LIBPCAP
+#include "ethernet-factory.hpp"
+#include "ethernet-transport.hpp"
+#endif // HAVE_LIBPCAP
+#include "tcp-factory.hpp"
+#include "udp-factory.hpp"
+#ifdef HAVE_UNIX_SOCKETS
+#include "unix-stream-factory.hpp"
+#endif // HAVE_UNIX_SOCKETS
+#ifdef HAVE_WEBSOCKET
+#include "websocket-factory.hpp"
+#endif // HAVE_WEBSOCKET
+
+namespace nfd {
+namespace face {
+
+NFD_LOG_INIT("FaceSystem");
+
+FaceSystem::FaceSystem(FaceTable& faceTable)
+  : m_faceTable(faceTable)
+{
+}
+
+std::set<const ProtocolFactory*>
+FaceSystem::listProtocolFactories() const
+{
+  std::set<const ProtocolFactory*> factories;
+  for (const auto& p : m_factories) {
+    factories.insert(p.second.get());
+  }
+  return factories;
+}
+
+ProtocolFactory*
+FaceSystem::getProtocolFactory(const std::string& scheme)
+{
+  auto found = m_factories.find(scheme);
+  return found == m_factories.end() ? nullptr : found->second.get();
+}
+
+void
+FaceSystem::setConfigFile(ConfigFile& configFile)
+{
+  configFile.addSectionHandler("face_system", bind(&FaceSystem::processConfig, this, _1, _2, _3));
+}
+
+void
+FaceSystem::processConfig(const ConfigSection& configSection, bool isDryRun, const std::string& filename)
+{
+  std::set<std::string> seenSections;
+  auto nicList = listNetworkInterfaces();
+
+  for (const auto& item : configSection) {
+    if (!seenSections.insert(item.first).second) {
+      BOOST_THROW_EXCEPTION(ConfigFile::Error("Duplicate \"" + item.first + "\" section"));
+    }
+
+    if (item.first == "unix") {
+      processSectionUnix(item.second, isDryRun);
+    }
+    else if (item.first == "tcp") {
+      processSectionTcp(item.second, isDryRun);
+    }
+    else if (item.first == "udp") {
+      processSectionUdp(item.second, isDryRun, nicList);
+    }
+    else if (item.first == "ether") {
+      processSectionEther(item.second, isDryRun, nicList);
+    }
+    else if (item.first == "websocket") {
+      processSectionWebSocket(item.second, isDryRun);
+    }
+    else {
+      BOOST_THROW_EXCEPTION(ConfigFile::Error("Unrecognized option \"" + item.first + "\""));
+    }
+  }
+}
+
+void
+FaceSystem::processSectionUnix(const ConfigSection& configSection, bool isDryRun)
+{
+  // ; the unix section contains settings of Unix stream faces and channels
+  // unix
+  // {
+  //   path /var/run/nfd.sock ; Unix stream listener path
+  // }
+
+#if defined(HAVE_UNIX_SOCKETS)
+  std::string path = "/var/run/nfd.sock";
+
+  for (const auto& i : configSection) {
+    if (i.first == "path") {
+      path = i.second.get_value<std::string>();
+    }
+    else {
+      BOOST_THROW_EXCEPTION(ConfigFile::Error("Unrecognized option \"" +
+                                              i.first + "\" in \"unix\" section"));
+    }
+  }
+
+  if (!isDryRun) {
+    if (m_factories.count("unix") > 0) {
+      return;
+    }
+
+    auto factory = make_shared<UnixStreamFactory>();
+    m_factories.emplace("unix", factory);
+
+    auto channel = factory->createChannel(path);
+    channel->listen(bind(&FaceTable::add, &m_faceTable, _1), nullptr);
+  }
+#else
+  BOOST_THROW_EXCEPTION(ConfigFile::Error("NFD was compiled without Unix sockets support, "
+                                          "cannot process \"unix\" section"));
+#endif // HAVE_UNIX_SOCKETS
+}
+
+void
+FaceSystem::processSectionTcp(const ConfigSection& configSection, bool isDryRun)
+{
+  // ; the tcp section contains settings of TCP faces and channels
+  // tcp
+  // {
+  //   listen yes ; set to 'no' to disable TCP listener, default 'yes'
+  //   port 6363 ; TCP listener port number
+  // }
+
+  uint16_t port = 6363;
+  bool needToListen = true;
+  bool enableV4 = true;
+  bool enableV6 = true;
+
+  for (const auto& i : configSection) {
+    if (i.first == "port") {
+      port = ConfigFile::parseNumber<uint16_t>(i, "tcp");
+      NFD_LOG_TRACE("TCP port set to " << port);
+    }
+    else if (i.first == "listen") {
+      needToListen = ConfigFile::parseYesNo(i, "tcp");
+    }
+    else if (i.first == "enable_v4") {
+      enableV4 = ConfigFile::parseYesNo(i, "tcp");
+    }
+    else if (i.first == "enable_v6") {
+      enableV6 = ConfigFile::parseYesNo(i, "tcp");
+    }
+    else {
+      BOOST_THROW_EXCEPTION(ConfigFile::Error("Unrecognized option \"" +
+                                              i.first + "\" in \"tcp\" section"));
+    }
+  }
+
+  if (!enableV4 && !enableV6) {
+    BOOST_THROW_EXCEPTION(ConfigFile::Error("IPv4 and IPv6 TCP channels have been disabled."
+                                            " Remove \"tcp\" section to disable TCP channels or"
+                                            " re-enable at least one channel type."));
+  }
+
+  if (!isDryRun) {
+    if (m_factories.count("tcp") > 0) {
+      return;
+    }
+
+    auto factory = make_shared<TcpFactory>();
+    m_factories.emplace("tcp", factory);
+
+    if (enableV4) {
+      tcp::Endpoint endpoint(boost::asio::ip::tcp::v4(), port);
+      shared_ptr<TcpChannel> v4Channel = factory->createChannel(endpoint);
+      if (needToListen) {
+        v4Channel->listen(bind(&FaceTable::add, &m_faceTable, _1), nullptr);
+      }
+
+      m_factories.emplace("tcp4", factory);
+    }
+
+    if (enableV6) {
+      tcp::Endpoint endpoint(boost::asio::ip::tcp::v6(), port);
+      shared_ptr<TcpChannel> v6Channel = factory->createChannel(endpoint);
+      if (needToListen) {
+        v6Channel->listen(bind(&FaceTable::add, &m_faceTable, _1), nullptr);
+      }
+
+      m_factories.emplace("tcp6", factory);
+    }
+  }
+}
+
+void
+FaceSystem::processSectionUdp(const ConfigSection& configSection, bool isDryRun,
+                              const std::vector<NetworkInterfaceInfo>& nicList)
+{
+  // ; the udp section contains settings of UDP faces and channels
+  // udp
+  // {
+  //   port 6363 ; UDP unicast port number
+  //   idle_timeout 600 ; idle time (seconds) before closing a UDP unicast face
+  //   keep_alive_interval 25 ; interval (seconds) between keep-alive refreshes
+
+  //   ; NFD creates one UDP multicast face per NIC
+  //   mcast yes ; set to 'no' to disable UDP multicast, default 'yes'
+  //   mcast_port 56363 ; UDP multicast port number
+  //   mcast_group 224.0.23.170 ; UDP multicast group (IPv4 only)
+  // }
+
+  uint16_t port = 6363;
+  bool enableV4 = true;
+  bool enableV6 = true;
+  size_t timeout = 600;
+  size_t keepAliveInterval = 25;
+  bool useMcast = true;
+  auto mcastGroup = boost::asio::ip::address_v4::from_string("224.0.23.170");
+  uint16_t mcastPort = 56363;
+
+  for (const auto& i : configSection) {
+    if (i.first == "port") {
+      port = ConfigFile::parseNumber<uint16_t>(i, "udp");
+      NFD_LOG_TRACE("UDP unicast port set to " << port);
+    }
+    else if (i.first == "enable_v4") {
+      enableV4 = ConfigFile::parseYesNo(i, "udp");
+    }
+    else if (i.first == "enable_v6") {
+      enableV6 = ConfigFile::parseYesNo(i, "udp");
+    }
+    else if (i.first == "idle_timeout") {
+      try {
+        timeout = i.second.get_value<size_t>();
+      }
+      catch (const boost::property_tree::ptree_bad_data&) {
+        BOOST_THROW_EXCEPTION(ConfigFile::Error("Invalid value for option \"" +
+                                                i.first + "\" in \"udp\" section"));
+      }
+    }
+    else if (i.first == "keep_alive_interval") {
+      try {
+        keepAliveInterval = i.second.get_value<size_t>();
+        /// \todo Make use of keepAliveInterval
+        (void)(keepAliveInterval);
+      }
+      catch (const boost::property_tree::ptree_bad_data&) {
+        BOOST_THROW_EXCEPTION(ConfigFile::Error("Invalid value for option \"" +
+                                                i.first + "\" in \"udp\" section"));
+      }
+    }
+    else if (i.first == "mcast") {
+      useMcast = ConfigFile::parseYesNo(i, "udp");
+    }
+    else if (i.first == "mcast_port") {
+      mcastPort = ConfigFile::parseNumber<uint16_t>(i, "udp");
+      NFD_LOG_TRACE("UDP multicast port set to " << mcastPort);
+    }
+    else if (i.first == "mcast_group") {
+      boost::system::error_code ec;
+      mcastGroup = boost::asio::ip::address_v4::from_string(i.second.get_value<std::string>(), ec);
+      if (ec) {
+        BOOST_THROW_EXCEPTION(ConfigFile::Error("Invalid value for option \"" +
+                                                i.first + "\" in \"udp\" section"));
+      }
+      NFD_LOG_TRACE("UDP multicast group set to " << mcastGroup);
+    }
+    else {
+      BOOST_THROW_EXCEPTION(ConfigFile::Error("Unrecognized option \"" +
+                                              i.first + "\" in \"udp\" section"));
+    }
+  }
+
+  if (!enableV4 && !enableV6) {
+    BOOST_THROW_EXCEPTION(ConfigFile::Error("IPv4 and IPv6 UDP channels have been disabled."
+                                            " Remove \"udp\" section to disable UDP channels or"
+                                            " re-enable at least one channel type."));
+  }
+  else if (useMcast && !enableV4) {
+    BOOST_THROW_EXCEPTION(ConfigFile::Error("IPv4 multicast requested, but IPv4 channels"
+                                            " have been disabled (conflicting configuration options set)"));
+  }
+
+  if (!isDryRun) {
+    shared_ptr<UdpFactory> factory;
+    bool isReload = false;
+    if (m_factories.count("udp") > 0) {
+      isReload = true;
+      factory = static_pointer_cast<UdpFactory>(m_factories["udp"]);
+    }
+    else {
+      factory = make_shared<UdpFactory>();
+      m_factories.emplace("udp", factory);
+    }
+
+    if (!isReload && enableV4) {
+      udp::Endpoint endpoint(boost::asio::ip::udp::v4(), port);
+      shared_ptr<UdpChannel> v4Channel = factory->createChannel(endpoint, time::seconds(timeout));
+      v4Channel->listen(bind(&FaceTable::add, &m_faceTable, _1), nullptr);
+
+      m_factories.emplace("udp4", factory);
+    }
+
+    if (!isReload && enableV6) {
+      udp::Endpoint endpoint(boost::asio::ip::udp::v6(), port);
+      shared_ptr<UdpChannel> v6Channel = factory->createChannel(endpoint, time::seconds(timeout));
+      v6Channel->listen(bind(&FaceTable::add, &m_faceTable, _1), nullptr);
+
+      m_factories.emplace("udp6", factory);
+    }
+
+    std::set<shared_ptr<Face>> multicastFacesToRemove;
+    for (const auto& i : factory->getMulticastFaces()) {
+      multicastFacesToRemove.insert(i.second);
+    }
+
+    if (useMcast && enableV4) {
+      std::vector<NetworkInterfaceInfo> ipv4MulticastInterfaces;
+      for (const auto& nic : nicList) {
+        if (nic.isUp() && nic.isMulticastCapable() && !nic.ipv4Addresses.empty()) {
+          ipv4MulticastInterfaces.push_back(nic);
+        }
+      }
+
+      bool isNicNameNecessary = false;
+#if defined(__linux__)
+      if (ipv4MulticastInterfaces.size() > 1) {
+        // On Linux if we have more than one MulticastUdpFace
+        // we need to specify the name of the interface
+        isNicNameNecessary = true;
+      }
+#endif
+
+      udp::Endpoint mcastEndpoint(mcastGroup, mcastPort);
+      for (const auto& nic : ipv4MulticastInterfaces) {
+        udp::Endpoint localEndpoint(nic.ipv4Addresses[0], mcastPort);
+        auto newFace = factory->createMulticastFace(localEndpoint, mcastEndpoint,
+                                                    isNicNameNecessary ? nic.name : "");
+        m_faceTable.add(newFace);
+        multicastFacesToRemove.erase(newFace);
+      }
+    }
+
+    for (const auto& face : multicastFacesToRemove) {
+      face->close();
+    }
+  }
+}
+
+void
+FaceSystem::processSectionEther(const ConfigSection& configSection, bool isDryRun,
+                                const std::vector<NetworkInterfaceInfo>& nicList)
+{
+  // ; the ether section contains settings of Ethernet faces and channels
+  // ether
+  // {
+  //   ; NFD creates one Ethernet multicast face per NIC
+  //   mcast yes ; set to 'no' to disable Ethernet multicast, default 'yes'
+  //   mcast_group 01:00:5E:00:17:AA ; Ethernet multicast group
+  // }
+
+#if defined(HAVE_LIBPCAP)
+  NetworkInterfacePredicate nicPredicate;
+  bool useMcast = true;
+  ethernet::Address mcastGroup(ethernet::getDefaultMulticastAddress());
+
+  for (const auto& i : configSection) {
+    if (i.first == "mcast") {
+      useMcast = ConfigFile::parseYesNo(i, "ether");
+    }
+    else if (i.first == "mcast_group") {
+      mcastGroup = ethernet::Address::fromString(i.second.get_value<std::string>());
+      if (mcastGroup.isNull()) {
+        BOOST_THROW_EXCEPTION(ConfigFile::Error("Invalid value for option \"" +
+                                                i.first + "\" in \"ether\" section"));
+      }
+      NFD_LOG_TRACE("Ethernet multicast group set to " << mcastGroup);
+    }
+    else if (i.first == "whitelist") {
+      nicPredicate.parseWhitelist(i.second);
+    }
+    else if (i.first == "blacklist") {
+      nicPredicate.parseBlacklist(i.second);
+    }
+    else {
+      BOOST_THROW_EXCEPTION(ConfigFile::Error("Unrecognized option \"" +
+                                              i.first + "\" in \"ether\" section"));
+    }
+  }
+
+  if (!isDryRun) {
+    shared_ptr<EthernetFactory> factory;
+    if (m_factories.count("ether") > 0) {
+      factory = static_pointer_cast<EthernetFactory>(m_factories["ether"]);
+    }
+    else {
+      factory = make_shared<EthernetFactory>();
+      m_factories.emplace("ether", factory);
+    }
+
+    std::set<shared_ptr<Face>> multicastFacesToRemove;
+    for (const auto& i : factory->getMulticastFaces()) {
+      multicastFacesToRemove.insert(i.second);
+    }
+
+    if (useMcast) {
+      for (const auto& nic : nicList) {
+        if (nic.isUp() && nic.isMulticastCapable() && nicPredicate(nic)) {
+          try {
+            auto newFace = factory->createMulticastFace(nic, mcastGroup);
+            m_faceTable.add(newFace);
+            multicastFacesToRemove.erase(newFace);
+          }
+          catch (const EthernetFactory::Error& factoryError) {
+            NFD_LOG_ERROR(factoryError.what() << ", continuing");
+          }
+          catch (const EthernetTransport::Error& faceError) {
+            NFD_LOG_ERROR(faceError.what() << ", continuing");
+          }
+        }
+      }
+    }
+
+    for (const auto& face : multicastFacesToRemove) {
+      face->close();
+    }
+  }
+#else
+  BOOST_THROW_EXCEPTION(ConfigFile::Error("NFD was compiled without libpcap, cannot process \"ether\" section"));
+#endif // HAVE_LIBPCAP
+}
+
+void
+FaceSystem::processSectionWebSocket(const ConfigSection& configSection, bool isDryRun)
+{
+  // ; the websocket section contains settings of WebSocket faces and channels
+  // websocket
+  // {
+  //   listen yes ; set to 'no' to disable WebSocket listener, default 'yes'
+  //   port 9696 ; WebSocket listener port number
+  //   enable_v4 yes ; set to 'no' to disable listening on IPv4 socket, default 'yes'
+  //   enable_v6 yes ; set to 'no' to disable listening on IPv6 socket, default 'yes'
+  // }
+
+#if defined(HAVE_WEBSOCKET)
+  uint16_t port = 9696;
+  bool needToListen = true;
+  bool enableV4 = true;
+  bool enableV6 = true;
+
+  for (const auto& i : configSection) {
+    if (i.first == "port") {
+      port = ConfigFile::parseNumber<uint16_t>(i, "websocket");
+      NFD_LOG_TRACE("WebSocket port set to " << port);
+    }
+    else if (i.first == "listen") {
+      needToListen = ConfigFile::parseYesNo(i, "websocket");
+    }
+    else if (i.first == "enable_v4") {
+      enableV4 = ConfigFile::parseYesNo(i, "websocket");
+    }
+    else if (i.first == "enable_v6") {
+      enableV6 = ConfigFile::parseYesNo(i, "websocket");
+    }
+    else {
+      BOOST_THROW_EXCEPTION(ConfigFile::Error("Unrecognized option \"" +
+                                              i.first + "\" in \"websocket\" section"));
+    }
+  }
+
+  if (!enableV4 && !enableV6) {
+    BOOST_THROW_EXCEPTION(ConfigFile::Error("IPv4 and IPv6 WebSocket channels have been disabled."
+                                            " Remove \"websocket\" section to disable WebSocket channels or"
+                                            " re-enable at least one channel type."));
+  }
+
+  if (!enableV4 && enableV6) {
+    BOOST_THROW_EXCEPTION(ConfigFile::Error("NFD does not allow pure IPv6 WebSocket channel."));
+  }
+
+  if (!isDryRun) {
+    if (m_factories.count("websocket") > 0) {
+      return;
+    }
+
+    auto factory = make_shared<WebSocketFactory>();
+    m_factories.emplace("websocket", factory);
+
+    shared_ptr<WebSocketChannel> channel;
+
+    if (enableV6 && enableV4) {
+      websocket::Endpoint endpoint(boost::asio::ip::address_v6::any(), port);
+      channel = factory->createChannel(endpoint);
+
+      m_factories.emplace("websocket46", factory);
+    }
+    else if (enableV4) {
+      websocket::Endpoint endpoint(boost::asio::ip::address_v4::any(), port);
+      channel = factory->createChannel(endpoint);
+
+      m_factories.emplace("websocket4", factory);
+    }
+
+    if (channel && needToListen) {
+      channel->listen(bind(&FaceTable::add, &m_faceTable, _1));
+    }
+  }
+#else
+  BOOST_THROW_EXCEPTION(ConfigFile::Error("NFD was compiled without WebSocket, "
+                                          "cannot process \"websocket\" section"));
+#endif // HAVE_WEBSOCKET
+}
+
+} // namespace face
+} // namespace nfd
diff --git a/daemon/face/face-system.hpp b/daemon/face/face-system.hpp
new file mode 100644
index 0000000..22b2a14
--- /dev/null
+++ b/daemon/face/face-system.hpp
@@ -0,0 +1,103 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2016,  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_FACE_SYSTEM_HPP
+#define NFD_DAEMON_FACE_FACE_SYSTEM_HPP
+
+#include "core/config-file.hpp"
+#include "protocol-factory.hpp"
+
+namespace nfd {
+
+class FaceTable;
+class NetworkInterfaceInfo;
+
+namespace face {
+
+/** \brief entry point of the face system
+ *
+ *  FaceSystem class is the entry point of NFD's face system.
+ *  It owns ProtocolFactory objects that are created from face_system section of NFD configuration file.
+ */
+class FaceSystem : noncopyable
+{
+public:
+  explicit
+  FaceSystem(FaceTable& faceTable);
+
+  /** \return ProtocolFactory objects owned by the FaceSystem
+   */
+  std::set<const ProtocolFactory*>
+  listProtocolFactories() const;
+
+  /** \return ProtocolFactory for specified protocol scheme, or nullptr if not found
+   */
+  ProtocolFactory*
+  getProtocolFactory(const std::string& scheme);
+
+  /** \brief register handler for face_system section of NFD configuration file
+   */
+  void
+  setConfigFile(ConfigFile& configFile);
+
+private:
+  void
+  processConfig(const ConfigSection& configSection, bool isDryRun,
+                const std::string& filename);
+
+  void
+  processSectionUnix(const ConfigSection& configSection, bool isDryRun);
+
+  void
+  processSectionTcp(const ConfigSection& configSection, bool isDryRun);
+
+  void
+  processSectionUdp(const ConfigSection& configSection, bool isDryRun,
+                    const std::vector<NetworkInterfaceInfo>& nicList);
+
+  void
+  processSectionEther(const ConfigSection& configSection, bool isDryRun,
+                      const std::vector<NetworkInterfaceInfo>& nicList);
+
+  void
+  processSectionWebSocket(const ConfigSection& configSection, bool isDryRun);
+
+PUBLIC_WITH_TESTS_ELSE_PRIVATE:
+  /** \brief scheme => protocol factory
+   *
+   *  The same protocol factory may be available under multiple schemes.
+   */
+  std::map<std::string, shared_ptr<ProtocolFactory>> m_factories;
+
+  FaceTable& m_faceTable;
+};
+
+} // namespace face
+
+using face::FaceSystem;
+
+} // namespace nfd
+
+#endif // NFD_DAEMON_FACE_FACE_SYSTEM_HPP
diff --git a/daemon/mgmt/face-manager.cpp b/daemon/mgmt/face-manager.cpp
index 8a9bb1c..e8a79eb 100644
--- a/daemon/mgmt/face-manager.cpp
+++ b/daemon/mgmt/face-manager.cpp
@@ -24,35 +24,20 @@
  */
 
 #include "face-manager.hpp"
-#include "core/network-interface.hpp"
-#include "core/network-interface-predicate.hpp"
 #include "face/generic-link-service.hpp"
-#include "face/tcp-factory.hpp"
-#include "face/udp-factory.hpp"
+#include "face/protocol-factory.hpp"
 #include "fw/face-table.hpp"
 
 #include <ndn-cxx/lp/tags.hpp>
 #include <ndn-cxx/mgmt/nfd/channel-status.hpp>
 
-#ifdef HAVE_UNIX_SOCKETS
-#include "face/unix-stream-factory.hpp"
-#endif // HAVE_UNIX_SOCKETS
-
-#ifdef HAVE_LIBPCAP
-#include "face/ethernet-factory.hpp"
-#include "face/ethernet-transport.hpp"
-#endif // HAVE_LIBPCAP
-
-#ifdef HAVE_WEBSOCKET
-#include "face/websocket-factory.hpp"
-#endif // HAVE_WEBSOCKET
-
 namespace nfd {
 
 NFD_LOG_INIT("FaceManager");
 
 FaceManager::FaceManager(FaceTable& faceTable, Dispatcher& dispatcher, CommandAuthenticator& authenticator)
   : NfdManagerBase(dispatcher, authenticator, "faces")
+  , m_faceSystem(faceTable)
   , m_faceTable(faceTable)
 {
   registerCommandHandler<ndn::nfd::FaceCreateCommand>("create",
@@ -87,7 +72,7 @@
 void
 FaceManager::setConfigFile(ConfigFile& configFile)
 {
-  configFile.addSectionHandler("face_system", bind(&FaceManager::processConfig, this, _1, _2, _3));
+  m_faceSystem.setConfigFile(configFile);
 }
 
 void
@@ -108,22 +93,20 @@
     return;
   }
 
-  auto factory = m_factories.find(uri.getScheme());
-  if (factory == m_factories.end()) {
+  ProtocolFactory* factory = m_faceSystem.getProtocolFactory(uri.getScheme());
+  if (factory == nullptr) {
     NFD_LOG_TRACE("received create request for unsupported protocol");
     done(ControlResponse(406, "Unsupported protocol"));
     return;
   }
 
   try {
-    factory->second->createFace(uri,
-                                parameters.getFacePersistency(),
-                                parameters.hasFlagBit(ndn::nfd::BIT_LOCAL_FIELDS_ENABLED) ?
-                                  parameters.getFlagBit(ndn::nfd::BIT_LOCAL_FIELDS_ENABLED) : false,
-                                bind(&FaceManager::afterCreateFaceSuccess,
-                                     this, parameters, _1, done),
-                                bind(&FaceManager::afterCreateFaceFailure,
-                                     this, _1, _2, done));
+    factory->createFace(uri,
+      parameters.getFacePersistency(),
+      parameters.hasFlagBit(ndn::nfd::BIT_LOCAL_FIELDS_ENABLED) &&
+        parameters.getFlagBit(ndn::nfd::BIT_LOCAL_FIELDS_ENABLED),
+      bind(&FaceManager::afterCreateFaceSuccess, this, parameters, _1, done),
+      bind(&FaceManager::afterCreateFaceFailure, this, _1, _2, done));
   }
   catch (const std::runtime_error& error) {
     NFD_LOG_ERROR("Face creation failed: " << error.what());
@@ -386,22 +369,14 @@
 FaceManager::listChannels(const Name& topPrefix, const Interest& interest,
                           ndn::mgmt::StatusDatasetContext& context)
 {
-  std::set<const ProtocolFactory*> seenFactories;
-
-  for (const auto& kv : m_factories) {
-    const ProtocolFactory* factory = kv.second.get();
-    bool inserted;
-    std::tie(std::ignore, inserted) = seenFactories.insert(factory);
-
-    if (inserted) {
-      for (const auto& channel : factory->getChannels()) {
-        ndn::nfd::ChannelStatus entry;
-        entry.setLocalUri(channel->getUri().toString());
-        context.append(entry.wireEncode());
-      }
+  std::set<const ProtocolFactory*> factories = m_faceSystem.listProtocolFactories();
+  for (const ProtocolFactory* factory : factories) {
+    for (const auto& channel : factory->getChannels()) {
+      ndn::nfd::ChannelStatus entry;
+      entry.setLocalUri(channel->getUri().toString());
+      context.append(entry.wireEncode());
     }
   }
-
   context.end();
 }
 
@@ -548,492 +523,4 @@
     });
 }
 
-void
-FaceManager::processConfig(const ConfigSection& configSection,
-                           bool isDryRun,
-                           const std::string& filename)
-{
-  bool hasSeenUnix = false;
-  bool hasSeenTcp = false;
-  bool hasSeenUdp = false;
-  bool hasSeenEther = false;
-  bool hasSeenWebSocket = false;
-  auto nicList = listNetworkInterfaces();
-
-  for (const auto& item : configSection) {
-    if (item.first == "unix") {
-      if (hasSeenUnix) {
-        BOOST_THROW_EXCEPTION(Error("Duplicate \"unix\" section"));
-      }
-      hasSeenUnix = true;
-
-      processSectionUnix(item.second, isDryRun);
-    }
-    else if (item.first == "tcp") {
-      if (hasSeenTcp) {
-        BOOST_THROW_EXCEPTION(Error("Duplicate \"tcp\" section"));
-      }
-      hasSeenTcp = true;
-
-      processSectionTcp(item.second, isDryRun);
-    }
-    else if (item.first == "udp") {
-      if (hasSeenUdp) {
-        BOOST_THROW_EXCEPTION(Error("Duplicate \"udp\" section"));
-      }
-      hasSeenUdp = true;
-
-      processSectionUdp(item.second, isDryRun, nicList);
-    }
-    else if (item.first == "ether") {
-      if (hasSeenEther) {
-        BOOST_THROW_EXCEPTION(Error("Duplicate \"ether\" section"));
-      }
-      hasSeenEther = true;
-
-      processSectionEther(item.second, isDryRun, nicList);
-    }
-    else if (item.first == "websocket") {
-      if (hasSeenWebSocket) {
-        BOOST_THROW_EXCEPTION(Error("Duplicate \"websocket\" section"));
-      }
-      hasSeenWebSocket = true;
-
-      processSectionWebSocket(item.second, isDryRun);
-    }
-    else {
-      BOOST_THROW_EXCEPTION(Error("Unrecognized option \"" + item.first + "\""));
-    }
-  }
-}
-
-void
-FaceManager::processSectionUnix(const ConfigSection& configSection, bool isDryRun)
-{
-  // ; the unix section contains settings of Unix stream faces and channels
-  // unix
-  // {
-  //   path /var/run/nfd.sock ; Unix stream listener path
-  // }
-
-#if defined(HAVE_UNIX_SOCKETS)
-  std::string path = "/var/run/nfd.sock";
-
-  for (const auto& i : configSection) {
-    if (i.first == "path") {
-      path = i.second.get_value<std::string>();
-    }
-    else {
-      BOOST_THROW_EXCEPTION(ConfigFile::Error("Unrecognized option \"" +
-                                              i.first + "\" in \"unix\" section"));
-    }
-  }
-
-  if (!isDryRun) {
-    if (m_factories.count("unix") > 0) {
-      return;
-    }
-
-    auto factory = make_shared<UnixStreamFactory>();
-    m_factories.insert(std::make_pair("unix", factory));
-
-    auto channel = factory->createChannel(path);
-    channel->listen(bind(&FaceTable::add, &m_faceTable, _1), nullptr);
-  }
-#else
-  BOOST_THROW_EXCEPTION(ConfigFile::Error("NFD was compiled without Unix sockets support, "
-                                          "cannot process \"unix\" section"));
-#endif // HAVE_UNIX_SOCKETS
-}
-
-void
-FaceManager::processSectionTcp(const ConfigSection& configSection, bool isDryRun)
-{
-  // ; the tcp section contains settings of TCP faces and channels
-  // tcp
-  // {
-  //   listen yes ; set to 'no' to disable TCP listener, default 'yes'
-  //   port 6363 ; TCP listener port number
-  // }
-
-  uint16_t port = 6363;
-  bool needToListen = true;
-  bool enableV4 = true;
-  bool enableV6 = true;
-
-  for (const auto& i : configSection) {
-    if (i.first == "port") {
-      port = ConfigFile::parseNumber<uint16_t>(i, "tcp");
-      NFD_LOG_TRACE("TCP port set to " << port);
-    }
-    else if (i.first == "listen") {
-      needToListen = ConfigFile::parseYesNo(i, "tcp");
-    }
-    else if (i.first == "enable_v4") {
-      enableV4 = ConfigFile::parseYesNo(i, "tcp");
-    }
-    else if (i.first == "enable_v6") {
-      enableV6 = ConfigFile::parseYesNo(i, "tcp");
-    }
-    else {
-      BOOST_THROW_EXCEPTION(ConfigFile::Error("Unrecognized option \"" +
-                                              i.first + "\" in \"tcp\" section"));
-    }
-  }
-
-  if (!enableV4 && !enableV6) {
-    BOOST_THROW_EXCEPTION(ConfigFile::Error("IPv4 and IPv6 TCP channels have been disabled."
-                                            " Remove \"tcp\" section to disable TCP channels or"
-                                            " re-enable at least one channel type."));
-  }
-
-  if (!isDryRun) {
-    if (m_factories.count("tcp") > 0) {
-      return;
-    }
-
-    auto factory = make_shared<TcpFactory>();
-    m_factories.insert(std::make_pair("tcp", factory));
-
-    if (enableV4) {
-      tcp::Endpoint endpoint(boost::asio::ip::tcp::v4(), port);
-      shared_ptr<TcpChannel> v4Channel = factory->createChannel(endpoint);
-      if (needToListen) {
-        v4Channel->listen(bind(&FaceTable::add, &m_faceTable, _1), nullptr);
-      }
-
-      m_factories.insert(std::make_pair("tcp4", factory));
-    }
-
-    if (enableV6) {
-      tcp::Endpoint endpoint(boost::asio::ip::tcp::v6(), port);
-      shared_ptr<TcpChannel> v6Channel = factory->createChannel(endpoint);
-      if (needToListen) {
-        v6Channel->listen(bind(&FaceTable::add, &m_faceTable, _1), nullptr);
-      }
-
-      m_factories.insert(std::make_pair("tcp6", factory));
-    }
-  }
-}
-
-void
-FaceManager::processSectionUdp(const ConfigSection& configSection, bool isDryRun,
-                               const std::vector<NetworkInterfaceInfo>& nicList)
-{
-  // ; the udp section contains settings of UDP faces and channels
-  // udp
-  // {
-  //   port 6363 ; UDP unicast port number
-  //   idle_timeout 600 ; idle time (seconds) before closing a UDP unicast face
-  //   keep_alive_interval 25 ; interval (seconds) between keep-alive refreshes
-
-  //   ; NFD creates one UDP multicast face per NIC
-  //   mcast yes ; set to 'no' to disable UDP multicast, default 'yes'
-  //   mcast_port 56363 ; UDP multicast port number
-  //   mcast_group 224.0.23.170 ; UDP multicast group (IPv4 only)
-  // }
-
-  uint16_t port = 6363;
-  bool enableV4 = true;
-  bool enableV6 = true;
-  size_t timeout = 600;
-  size_t keepAliveInterval = 25;
-  bool useMcast = true;
-  auto mcastGroup = boost::asio::ip::address_v4::from_string("224.0.23.170");
-  uint16_t mcastPort = 56363;
-
-  for (const auto& i : configSection) {
-    if (i.first == "port") {
-      port = ConfigFile::parseNumber<uint16_t>(i, "udp");
-      NFD_LOG_TRACE("UDP unicast port set to " << port);
-    }
-    else if (i.first == "enable_v4") {
-      enableV4 = ConfigFile::parseYesNo(i, "udp");
-    }
-    else if (i.first == "enable_v6") {
-      enableV6 = ConfigFile::parseYesNo(i, "udp");
-    }
-    else if (i.first == "idle_timeout") {
-      try {
-        timeout = i.second.get_value<size_t>();
-      }
-      catch (const boost::property_tree::ptree_bad_data&) {
-        BOOST_THROW_EXCEPTION(ConfigFile::Error("Invalid value for option \"" +
-                                                i.first + "\" in \"udp\" section"));
-      }
-    }
-    else if (i.first == "keep_alive_interval") {
-      try {
-        keepAliveInterval = i.second.get_value<size_t>();
-        /// \todo Make use of keepAliveInterval
-        (void)(keepAliveInterval);
-      }
-      catch (const boost::property_tree::ptree_bad_data&) {
-        BOOST_THROW_EXCEPTION(ConfigFile::Error("Invalid value for option \"" +
-                                                i.first + "\" in \"udp\" section"));
-      }
-    }
-    else if (i.first == "mcast") {
-      useMcast = ConfigFile::parseYesNo(i, "udp");
-    }
-    else if (i.first == "mcast_port") {
-      mcastPort = ConfigFile::parseNumber<uint16_t>(i, "udp");
-      NFD_LOG_TRACE("UDP multicast port set to " << mcastPort);
-    }
-    else if (i.first == "mcast_group") {
-      boost::system::error_code ec;
-      mcastGroup = boost::asio::ip::address_v4::from_string(i.second.get_value<std::string>(), ec);
-      if (ec) {
-        BOOST_THROW_EXCEPTION(ConfigFile::Error("Invalid value for option \"" +
-                                                i.first + "\" in \"udp\" section"));
-      }
-      NFD_LOG_TRACE("UDP multicast group set to " << mcastGroup);
-    }
-    else {
-      BOOST_THROW_EXCEPTION(ConfigFile::Error("Unrecognized option \"" +
-                                              i.first + "\" in \"udp\" section"));
-    }
-  }
-
-  if (!enableV4 && !enableV6) {
-    BOOST_THROW_EXCEPTION(ConfigFile::Error("IPv4 and IPv6 UDP channels have been disabled."
-                                            " Remove \"udp\" section to disable UDP channels or"
-                                            " re-enable at least one channel type."));
-  }
-  else if (useMcast && !enableV4) {
-    BOOST_THROW_EXCEPTION(ConfigFile::Error("IPv4 multicast requested, but IPv4 channels"
-                                            " have been disabled (conflicting configuration options set)"));
-  }
-
-  if (!isDryRun) {
-    shared_ptr<UdpFactory> factory;
-    bool isReload = false;
-    if (m_factories.count("udp") > 0) {
-      isReload = true;
-      factory = static_pointer_cast<UdpFactory>(m_factories["udp"]);
-    }
-    else {
-      factory = make_shared<UdpFactory>();
-      m_factories.insert(std::make_pair("udp", factory));
-    }
-
-    if (!isReload && enableV4) {
-      udp::Endpoint endpoint(boost::asio::ip::udp::v4(), port);
-      shared_ptr<UdpChannel> v4Channel = factory->createChannel(endpoint, time::seconds(timeout));
-      v4Channel->listen(bind(&FaceTable::add, &m_faceTable, _1), nullptr);
-
-      m_factories.insert(std::make_pair("udp4", factory));
-    }
-
-    if (!isReload && enableV6) {
-      udp::Endpoint endpoint(boost::asio::ip::udp::v6(), port);
-      shared_ptr<UdpChannel> v6Channel = factory->createChannel(endpoint, time::seconds(timeout));
-      v6Channel->listen(bind(&FaceTable::add, &m_faceTable, _1), nullptr);
-
-      m_factories.insert(std::make_pair("udp6", factory));
-    }
-
-    std::set<shared_ptr<Face>> multicastFacesToRemove;
-    for (const auto& i : factory->getMulticastFaces()) {
-      multicastFacesToRemove.insert(i.second);
-    }
-
-    if (useMcast && enableV4) {
-      std::vector<NetworkInterfaceInfo> ipv4MulticastInterfaces;
-      for (const auto& nic : nicList) {
-        if (nic.isUp() && nic.isMulticastCapable() && !nic.ipv4Addresses.empty()) {
-          ipv4MulticastInterfaces.push_back(nic);
-        }
-      }
-
-      bool isNicNameNecessary = false;
-#if defined(__linux__)
-      if (ipv4MulticastInterfaces.size() > 1) {
-        // On Linux if we have more than one MulticastUdpFace
-        // we need to specify the name of the interface
-        isNicNameNecessary = true;
-      }
-#endif
-
-      udp::Endpoint mcastEndpoint(mcastGroup, mcastPort);
-      for (const auto& nic : ipv4MulticastInterfaces) {
-        udp::Endpoint localEndpoint(nic.ipv4Addresses[0], mcastPort);
-        auto newFace = factory->createMulticastFace(localEndpoint, mcastEndpoint,
-                                                    isNicNameNecessary ? nic.name : "");
-        m_faceTable.add(newFace);
-        multicastFacesToRemove.erase(newFace);
-      }
-    }
-
-    for (const auto& face : multicastFacesToRemove) {
-      face->close();
-    }
-  }
-}
-
-void
-FaceManager::processSectionEther(const ConfigSection& configSection, bool isDryRun,
-                                 const std::vector<NetworkInterfaceInfo>& nicList)
-{
-  // ; the ether section contains settings of Ethernet faces and channels
-  // ether
-  // {
-  //   ; NFD creates one Ethernet multicast face per NIC
-  //   mcast yes ; set to 'no' to disable Ethernet multicast, default 'yes'
-  //   mcast_group 01:00:5E:00:17:AA ; Ethernet multicast group
-  // }
-
-#if defined(HAVE_LIBPCAP)
-  NetworkInterfacePredicate nicPredicate;
-  bool useMcast = true;
-  ethernet::Address mcastGroup(ethernet::getDefaultMulticastAddress());
-
-  for (const auto& i : configSection) {
-    if (i.first == "mcast") {
-      useMcast = ConfigFile::parseYesNo(i, "ether");
-    }
-    else if (i.first == "mcast_group") {
-      mcastGroup = ethernet::Address::fromString(i.second.get_value<std::string>());
-      if (mcastGroup.isNull()) {
-        BOOST_THROW_EXCEPTION(ConfigFile::Error("Invalid value for option \"" +
-                                                i.first + "\" in \"ether\" section"));
-      }
-      NFD_LOG_TRACE("Ethernet multicast group set to " << mcastGroup);
-    }
-    else if (i.first == "whitelist") {
-      nicPredicate.parseWhitelist(i.second);
-    }
-    else if (i.first == "blacklist") {
-      nicPredicate.parseBlacklist(i.second);
-    }
-    else {
-      BOOST_THROW_EXCEPTION(ConfigFile::Error("Unrecognized option \"" +
-                                              i.first + "\" in \"ether\" section"));
-    }
-  }
-
-  if (!isDryRun) {
-    shared_ptr<EthernetFactory> factory;
-    if (m_factories.count("ether") > 0) {
-      factory = static_pointer_cast<EthernetFactory>(m_factories["ether"]);
-    }
-    else {
-      factory = make_shared<EthernetFactory>();
-      m_factories.insert(std::make_pair("ether", factory));
-    }
-
-    std::set<shared_ptr<Face>> multicastFacesToRemove;
-    for (const auto& i : factory->getMulticastFaces()) {
-      multicastFacesToRemove.insert(i.second);
-    }
-
-    if (useMcast) {
-      for (const auto& nic : nicList) {
-        if (nic.isUp() && nic.isMulticastCapable() && nicPredicate(nic)) {
-          try {
-            auto newFace = factory->createMulticastFace(nic, mcastGroup);
-            m_faceTable.add(newFace);
-            multicastFacesToRemove.erase(newFace);
-          }
-          catch (const EthernetFactory::Error& factoryError) {
-            NFD_LOG_ERROR(factoryError.what() << ", continuing");
-          }
-          catch (const face::EthernetTransport::Error& faceError) {
-            NFD_LOG_ERROR(faceError.what() << ", continuing");
-          }
-        }
-      }
-    }
-
-    for (const auto& face : multicastFacesToRemove) {
-      face->close();
-    }
-  }
-#else
-  BOOST_THROW_EXCEPTION(ConfigFile::Error("NFD was compiled without libpcap, cannot process \"ether\" section"));
-#endif // HAVE_LIBPCAP
-}
-
-void
-FaceManager::processSectionWebSocket(const ConfigSection& configSection, bool isDryRun)
-{
-  // ; the websocket section contains settings of WebSocket faces and channels
-  // websocket
-  // {
-  //   listen yes ; set to 'no' to disable WebSocket listener, default 'yes'
-  //   port 9696 ; WebSocket listener port number
-  //   enable_v4 yes ; set to 'no' to disable listening on IPv4 socket, default 'yes'
-  //   enable_v6 yes ; set to 'no' to disable listening on IPv6 socket, default 'yes'
-  // }
-
-#if defined(HAVE_WEBSOCKET)
-  uint16_t port = 9696;
-  bool needToListen = true;
-  bool enableV4 = true;
-  bool enableV6 = true;
-
-  for (const auto& i : configSection) {
-    if (i.first == "port") {
-      port = ConfigFile::parseNumber<uint16_t>(i, "websocket");
-      NFD_LOG_TRACE("WebSocket port set to " << port);
-    }
-    else if (i.first == "listen") {
-      needToListen = ConfigFile::parseYesNo(i, "websocket");
-    }
-    else if (i.first == "enable_v4") {
-      enableV4 = ConfigFile::parseYesNo(i, "websocket");
-    }
-    else if (i.first == "enable_v6") {
-      enableV6 = ConfigFile::parseYesNo(i, "websocket");
-    }
-    else {
-      BOOST_THROW_EXCEPTION(ConfigFile::Error("Unrecognized option \"" +
-                                              i.first + "\" in \"websocket\" section"));
-    }
-  }
-
-  if (!enableV4 && !enableV6) {
-    BOOST_THROW_EXCEPTION(ConfigFile::Error("IPv4 and IPv6 WebSocket channels have been disabled."
-                                            " Remove \"websocket\" section to disable WebSocket channels or"
-                                            " re-enable at least one channel type."));
-  }
-
-  if (!enableV4 && enableV6) {
-    BOOST_THROW_EXCEPTION(ConfigFile::Error("NFD does not allow pure IPv6 WebSocket channel."));
-  }
-
-  if (!isDryRun) {
-    if (m_factories.count("websocket") > 0) {
-      return;
-    }
-
-    auto factory = make_shared<WebSocketFactory>();
-    m_factories.insert(std::make_pair("websocket", factory));
-
-    shared_ptr<WebSocketChannel> channel;
-
-    if (enableV6 && enableV4) {
-      websocket::Endpoint endpoint(boost::asio::ip::address_v6::any(), port);
-      channel = factory->createChannel(endpoint);
-
-      m_factories.insert(std::make_pair("websocket46", factory));
-    }
-    else if (enableV4) {
-      websocket::Endpoint endpoint(boost::asio::ip::address_v4::any(), port);
-      channel = factory->createChannel(endpoint);
-
-      m_factories.insert(std::make_pair("websocket4", factory));
-    }
-
-    if (channel && needToListen) {
-      channel->listen(bind(&FaceTable::add, &m_faceTable, _1));
-    }
-  }
-#else
-  BOOST_THROW_EXCEPTION(ConfigFile::Error("NFD was compiled without WebSocket, "
-                                          "cannot process \"websocket\" section"));
-#endif // HAVE_WEBSOCKET
-}
-
 } // namespace nfd
diff --git a/daemon/mgmt/face-manager.hpp b/daemon/mgmt/face-manager.hpp
index c4a2c48..3c92e93 100644
--- a/daemon/mgmt/face-manager.hpp
+++ b/daemon/mgmt/face-manager.hpp
@@ -27,17 +27,13 @@
 #define NFD_DAEMON_MGMT_FACE_MANAGER_HPP
 
 #include "nfd-manager-base.hpp"
+#include "face/face-system.hpp"
 #include <ndn-cxx/mgmt/nfd/face-status.hpp>
 #include <ndn-cxx/mgmt/nfd/face-query-filter.hpp>
 #include <ndn-cxx/mgmt/nfd/face-event-notification.hpp>
-#include "face/face.hpp"
 
 namespace nfd {
 
-class FaceTable;
-class NetworkInterfaceInfo;
-class ProtocolFactory;
-
 /**
  * @brief implement the Face Management of NFD Management Protocol.
  * @sa http://redmine.named-data.net/projects/nfd/wiki/FaceMgmt
@@ -144,30 +140,8 @@
   void
   connectFaceStateChangeSignal(const Face& face);
 
-private: // configuration
-  void
-  processConfig(const ConfigSection& configSection, bool isDryRun,
-                const std::string& filename);
-
-  void
-  processSectionUnix(const ConfigSection& configSection, bool isDryRun);
-
-  void
-  processSectionTcp(const ConfigSection& configSection, bool isDryRun);
-
-  void
-  processSectionUdp(const ConfigSection& configSection, bool isDryRun,
-                    const std::vector<NetworkInterfaceInfo>& nicList);
-
-  void
-  processSectionEther(const ConfigSection& configSection, bool isDryRun,
-                      const std::vector<NetworkInterfaceInfo>& nicList);
-
-  void
-  processSectionWebSocket(const ConfigSection& configSection, bool isDryRun);
-
 PUBLIC_WITH_TESTS_ELSE_PRIVATE:
-  std::map<std::string /*protocol*/, shared_ptr<ProtocolFactory>> m_factories;
+  FaceSystem m_faceSystem; ///\todo #3904 accept FaceSystem& in constructor; don't own FaceSystem
   FaceTable& m_faceTable;
   std::map<FaceId, signal::ScopedConnection> m_faceStateChangeConn;
 
diff --git a/tests/daemon/face/face-system.t.cpp b/tests/daemon/face/face-system.t.cpp
new file mode 100644
index 0000000..bcae725
--- /dev/null
+++ b/tests/daemon/face/face-system.t.cpp
@@ -0,0 +1,590 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2016,  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/face-system.hpp"
+#include "fw/face-table.hpp"
+
+// ProtocolFactory includes, sorted alphabetically
+#ifdef HAVE_LIBPCAP
+#include "face/ethernet-factory.hpp"
+#endif // HAVE_LIBPCAP
+#include "face/tcp-factory.hpp"
+#include "face/udp-factory.hpp"
+#ifdef HAVE_UNIX_SOCKETS
+#include "face/unix-stream-factory.hpp"
+#endif // HAVE_UNIX_SOCKETS
+#ifdef HAVE_WEBSOCKET
+#include "face/websocket-factory.hpp"
+#endif // HAVE_WEBSOCKET
+
+#include "tests/test-common.hpp"
+
+namespace nfd {
+namespace face {
+namespace tests {
+
+using namespace nfd::tests;
+
+class FaceSystemFixture : public BaseFixture
+{
+public:
+  FaceSystemFixture()
+    : faceSystem(faceTable)
+  {
+    faceSystem.setConfigFile(configFile);
+  }
+
+  void
+  parseConfig(const std::string& text, bool isDryRun)
+  {
+    configFile.parse(text, isDryRun, "test-config");
+  }
+
+protected:
+  FaceTable faceTable;
+  FaceSystem faceSystem;
+  ConfigFile configFile;
+};
+
+BOOST_AUTO_TEST_SUITE(Face)
+BOOST_FIXTURE_TEST_SUITE(TestFaceSystem, FaceSystemFixture)
+
+#ifdef HAVE_UNIX_SOCKETS
+BOOST_AUTO_TEST_SUITE(ConfigUnix)
+
+BOOST_AUTO_TEST_CASE(Normal)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      unix
+      {
+        path /tmp/nfd.sock
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_NO_THROW(parseConfig(CONFIG, true));
+  BOOST_CHECK_NO_THROW(parseConfig(CONFIG, false));
+
+  auto factory = dynamic_cast<UnixStreamFactory*>(faceSystem.getProtocolFactory("unix"));
+  BOOST_REQUIRE(factory != nullptr);
+  BOOST_CHECK_EQUAL(factory->getChannels().size(), 1);
+}
+
+BOOST_AUTO_TEST_CASE(UnknownOption)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      unix
+      {
+        hello
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
+  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // ConfigUnix
+#endif // HAVE_UNIX_SOCKETS
+
+BOOST_AUTO_TEST_SUITE(ConfigTcp)
+
+BOOST_AUTO_TEST_CASE(Normal)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      tcp
+      {
+        listen yes
+        port 16363
+        enable_v4 yes
+        enable_v6 yes
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_NO_THROW(parseConfig(CONFIG, true));
+  BOOST_CHECK_NO_THROW(parseConfig(CONFIG, false));
+
+  auto factory = dynamic_cast<TcpFactory*>(faceSystem.getProtocolFactory("tcp"));
+  BOOST_REQUIRE(factory != nullptr);
+  BOOST_CHECK_EQUAL(factory->getChannels().size(), 2);
+}
+
+BOOST_AUTO_TEST_CASE(BadListen)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      tcp
+      {
+        listen hello
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
+  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_CASE(ChannelsDisabled)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      tcp
+      {
+        port 6363
+        enable_v4 no
+        enable_v6 no
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
+  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_CASE(UnknownOption)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      tcp
+      {
+        hello
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
+  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // ConfigTcp
+
+BOOST_AUTO_TEST_SUITE(ConfigUdp)
+
+BOOST_AUTO_TEST_CASE(Normal)
+{
+  SKIP_IF_NOT_SUPERUSER();
+
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      udp
+      {
+        port 6363
+        enable_v4 yes
+        enable_v6 yes
+        idle_timeout 30
+        keep_alive_interval 25
+        mcast yes
+        mcast_port 56363
+        mcast_group 224.0.23.170
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_NO_THROW(parseConfig(CONFIG, true));
+  BOOST_CHECK_NO_THROW(parseConfig(CONFIG, false));
+
+  auto factory = dynamic_cast<UdpFactory*>(faceSystem.getProtocolFactory("udp"));
+  BOOST_REQUIRE(factory != nullptr);
+  BOOST_CHECK_EQUAL(factory->getChannels().size(), 2);
+}
+
+BOOST_AUTO_TEST_CASE(BadIdleTimeout)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      udp
+      {
+        idle_timeout hello
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
+  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_CASE(BadMcast)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      udp
+      {
+        mcast hello
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
+  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_CASE(BadMcastGroup)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      udp
+      {
+        mcast no
+        mcast_port 50
+        mcast_group hello
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
+  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_CASE(BadMcastGroupV6)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      udp
+      {
+        mcast no
+        mcast_port 50
+        mcast_group ::1
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
+  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_CASE(ChannelsDisabled)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      udp
+      {
+        port 6363
+        enable_v4 no
+        enable_v6 no
+        idle_timeout 30
+        keep_alive_interval 25
+        mcast yes
+        mcast_port 56363
+        mcast_group 224.0.23.170
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
+  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_CASE(ConflictingMcast)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      udp
+      {
+        port 6363
+        enable_v4 no
+        enable_v6 yes
+        idle_timeout 30
+        keep_alive_interval 25
+        mcast yes
+        mcast_port 56363
+        mcast_group 224.0.23.170
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
+  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_CASE(UnknownOption)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      udp
+      {
+        hello
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
+  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_CASE(MulticastReinit)
+{
+  SKIP_IF_NOT_SUPERUSER();
+
+  const std::string CONFIG_WITH_MCAST = R"CONFIG(
+    face_system
+    {
+      udp
+      {
+        mcast yes
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_NO_THROW(parseConfig(CONFIG_WITH_MCAST, false));
+
+  auto factory = dynamic_cast<UdpFactory*>(faceSystem.getProtocolFactory("udp"));
+  BOOST_REQUIRE(factory != nullptr);
+
+  if (factory->getMulticastFaces().empty()) {
+    BOOST_WARN_MESSAGE(false, "skipping assertions that require at least one UDP multicast face");
+    return;
+  }
+
+  const std::string CONFIG_WITHOUT_MCAST = R"CONFIG(
+    face_system
+    {
+      udp
+      {
+        mcast no
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_NO_THROW(parseConfig(CONFIG_WITHOUT_MCAST, false));
+  BOOST_REQUIRE_NO_THROW(g_io.poll());
+  BOOST_CHECK_EQUAL(factory->getMulticastFaces().size(), 0);
+}
+BOOST_AUTO_TEST_SUITE_END() // ConfigUdp
+
+#ifdef HAVE_LIBPCAP
+BOOST_AUTO_TEST_SUITE(ConfigEther)
+
+BOOST_AUTO_TEST_CASE(Normal)
+{
+  SKIP_IF_NOT_SUPERUSER();
+
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      ether
+      {
+        mcast yes
+        mcast_group 01:00:5E:00:17:AA
+        whitelist
+        {
+          *
+        }
+        blacklist
+        {
+        }
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_NO_THROW(parseConfig(CONFIG, true));
+  BOOST_CHECK_NO_THROW(parseConfig(CONFIG, false));
+
+  auto factory = dynamic_cast<EthernetFactory*>(faceSystem.getProtocolFactory("ether"));
+  BOOST_REQUIRE(factory != nullptr);
+  BOOST_CHECK_EQUAL(factory->getChannels().size(), 0);
+}
+
+BOOST_AUTO_TEST_CASE(BadMcast)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      ether
+      {
+        mcast hello
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
+  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_CASE(BadMcastGroup)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      ether
+      {
+        mcast yes
+        mcast_group
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
+  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_CASE(UnknownOption)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      ether
+      {
+        hello
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
+  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_CASE(MulticastReinit)
+{
+  SKIP_IF_NOT_SUPERUSER();
+
+  const std::string CONFIG_WITH_MCAST = R"CONFIG(
+    face_system
+    {
+      ether
+      {
+        mcast yes
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_NO_THROW(parseConfig(CONFIG_WITH_MCAST, false));
+
+  auto factory = dynamic_cast<EthernetFactory*>(faceSystem.getProtocolFactory("ether"));
+  BOOST_REQUIRE(factory != nullptr);
+
+  if (factory->getMulticastFaces().empty()) {
+    BOOST_WARN_MESSAGE(false, "skipping assertions that require at least one Ethernet multicast face");
+    return;
+  }
+
+  const std::string CONFIG_WITHOUT_MCAST = R"CONFIG(
+    face_system
+    {
+      ether
+      {
+        mcast no
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_NO_THROW(parseConfig(CONFIG_WITHOUT_MCAST, false));
+  BOOST_REQUIRE_NO_THROW(g_io.poll());
+  BOOST_CHECK_EQUAL(factory->getMulticastFaces().size(), 0);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // ConfigEther
+#endif // HAVE_LIBPCAP
+
+#ifdef HAVE_WEBSOCKET
+BOOST_AUTO_TEST_SUITE(ConfigWebSocket)
+
+BOOST_AUTO_TEST_CASE(Normal)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      websocket
+      {
+        listen yes
+        port 9696
+        enable_v4 yes
+        enable_v6 yes
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_NO_THROW(parseConfig(CONFIG, true));
+  BOOST_CHECK_NO_THROW(parseConfig(CONFIG, false));
+
+  auto factory = dynamic_cast<WebSocketFactory*>(faceSystem.getProtocolFactory("websocket"));
+  BOOST_REQUIRE(factory != nullptr);
+  BOOST_CHECK_EQUAL(factory->getChannels().size(), 1);
+}
+
+BOOST_AUTO_TEST_CASE(ChannelsDisabled)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      websocket
+      {
+        listen yes
+        port 9696
+        enable_v4 no
+        enable_v6 no
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
+  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_CASE(Ipv4ChannelDisabled)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      websocket
+      {
+        listen yes
+        port 9696
+        enable_v4 no
+        enable_v6 yes
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
+  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // ConfigWebSocket
+#endif // HAVE_WEBSOCKET
+
+BOOST_AUTO_TEST_SUITE_END() // TestFaceSystem
+BOOST_AUTO_TEST_SUITE_END() // Mgmt
+
+} // namespace tests
+} // namespace face
+} // namespace nfd
diff --git a/tests/daemon/mgmt/face-manager-process-config.t.cpp b/tests/daemon/mgmt/face-manager-process-config.t.cpp
deleted file mode 100644
index 82495d5..0000000
--- a/tests/daemon/mgmt/face-manager-process-config.t.cpp
+++ /dev/null
@@ -1,443 +0,0 @@
-/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
-/**
- * Copyright (c) 2014-2016,  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 "mgmt/face-manager.hpp"
-#include "face/udp-factory.hpp"
-
-#ifdef HAVE_LIBPCAP
-#include "face/ethernet-factory.hpp"
-#endif // HAVE_LIBPCAP
-
-#include "nfd-manager-common-fixture.hpp"
-
-namespace nfd {
-namespace tests {
-
-BOOST_AUTO_TEST_SUITE(Mgmt)
-BOOST_AUTO_TEST_SUITE(TestFaceManager)
-
-class FaceManagerProcessConfigFixture : public NfdManagerCommonFixture
-{
-public:
-  FaceManagerProcessConfigFixture()
-    : m_manager(m_forwarder.getFaceTable(), m_dispatcher, *m_authenticator)
-  {
-    m_manager.setConfigFile(m_config);
-  }
-
-  void
-  parseConfig(const std::string& type, bool isDryRun)
-  {
-    m_config.parse(type, isDryRun, "test-config");
-  }
-
-protected:
-  FaceManager m_manager;
-  ConfigFile m_config;
-};
-
-BOOST_FIXTURE_TEST_SUITE(ProcessConfig, FaceManagerProcessConfigFixture)
-
-#ifdef HAVE_UNIX_SOCKETS
-
-BOOST_AUTO_TEST_CASE(ProcessSectionUnix)
-{
-  const std::string CONFIG =
-    "face_system\n"
-    "{\n"
-    "  unix\n"
-    "  {\n"
-    "    path /tmp/nfd.sock\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_NO_THROW(parseConfig(CONFIG, true));
-  BOOST_CHECK_NO_THROW(parseConfig(CONFIG, false));
-}
-
-BOOST_AUTO_TEST_CASE(ProcessSectionUnixUnknownOption)
-{
-  const std::string CONFIG =
-    "face_system\n"
-    "{\n"
-    "  unix\n"
-    "  {\n"
-    "    hello\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
-  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
-}
-
-#endif // HAVE_UNIX_SOCKETS
-
-BOOST_AUTO_TEST_CASE(ProcessSectionTcp)
-{
-  const std::string CONFIG =
-    "face_system\n"
-    "{\n"
-    "  tcp\n"
-    "  {\n"
-    "    listen yes\n"
-    "    port 16363\n"
-    "    enable_v4 yes\n"
-    "    enable_v6 yes\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_NO_THROW(parseConfig(CONFIG, true));
-  BOOST_CHECK_NO_THROW(parseConfig(CONFIG, false));
-}
-
-BOOST_AUTO_TEST_CASE(ProcessSectionTcpBadListen)
-{
-  const std::string CONFIG =
-    "face_system\n"
-    "{\n"
-    "  tcp\n"
-    "  {\n"
-    "    listen hello\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
-  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
-}
-
-BOOST_AUTO_TEST_CASE(ProcessSectionTcpChannelsDisabled)
-{
-  const std::string CONFIG =
-    "face_system\n"
-    "{\n"
-    "  tcp\n"
-    "  {\n"
-    "    port 6363\n"
-    "    enable_v4 no\n"
-    "    enable_v6 no\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
-  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
-}
-
-BOOST_AUTO_TEST_CASE(ProcessSectionTcpUnknownOption)
-{
-  const std::string CONFIG =
-    "face_system\n"
-    "{\n"
-    "  tcp\n"
-    "  {\n"
-    "    hello\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
-  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
-}
-
-BOOST_AUTO_TEST_CASE(ProcessSectionUdp)
-{
-  SKIP_IF_NOT_SUPERUSER();
-
-  const std::string CONFIG =
-    "face_system\n"
-    "{\n"
-    "  udp\n"
-    "  {\n"
-    "    port 6363\n"
-    "    enable_v4 yes\n"
-    "    enable_v6 yes\n"
-    "    idle_timeout 30\n"
-    "    keep_alive_interval 25\n"
-    "    mcast yes\n"
-    "    mcast_port 56363\n"
-    "    mcast_group 224.0.23.170\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_NO_THROW(parseConfig(CONFIG, true));
-  BOOST_CHECK_NO_THROW(parseConfig(CONFIG, false));
-}
-
-BOOST_AUTO_TEST_CASE(ProcessSectionUdpBadIdleTimeout)
-{
-  const std::string CONFIG =
-    "face_system\n"
-    "{\n"
-    "  udp\n"
-    "  {\n"
-    "    idle_timeout hello\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
-  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
-}
-
-BOOST_AUTO_TEST_CASE(ProcessSectionUdpBadMcast)
-{
-  const std::string CONFIG =
-    "face_system\n"
-    "{\n"
-    "  udp\n"
-    "  {\n"
-    "    mcast hello\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
-  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
-}
-
-BOOST_AUTO_TEST_CASE(ProcessSectionUdpBadMcastGroup)
-{
-  const std::string CONFIG =
-    "face_system\n"
-    "{\n"
-    "  udp\n"
-    "  {\n"
-    "    mcast no\n"
-    "    mcast_port 50\n"
-    "    mcast_group hello\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
-  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
-}
-
-BOOST_AUTO_TEST_CASE(ProcessSectionUdpBadMcastGroupV6)
-{
-  const std::string CONFIG =
-    "face_system\n"
-    "{\n"
-    "  udp\n"
-    "  {\n"
-    "    mcast no\n"
-    "    mcast_port 50\n"
-    "    mcast_group ::1\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
-  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
-}
-
-BOOST_AUTO_TEST_CASE(ProcessSectionUdpChannelsDisabled)
-{
-  const std::string CONFIG =
-    "face_system\n"
-    "{\n"
-    "  udp\n"
-    "  {\n"
-    "    port 6363\n"
-    "    enable_v4 no\n"
-    "    enable_v6 no\n"
-    "    idle_timeout 30\n"
-    "    keep_alive_interval 25\n"
-    "    mcast yes\n"
-    "    mcast_port 56363\n"
-    "    mcast_group 224.0.23.170\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
-  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
-}
-
-BOOST_AUTO_TEST_CASE(ProcessSectionUdpConflictingMcast)
-{
-  const std::string CONFIG =
-    "face_system\n"
-    "{\n"
-    "  udp\n"
-    "  {\n"
-    "    port 6363\n"
-    "    enable_v4 no\n"
-    "    enable_v6 yes\n"
-    "    idle_timeout 30\n"
-    "    keep_alive_interval 25\n"
-    "    mcast yes\n"
-    "    mcast_port 56363\n"
-    "    mcast_group 224.0.23.170\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
-  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
-}
-
-BOOST_AUTO_TEST_CASE(ProcessSectionUdpUnknownOption)
-{
-  const std::string CONFIG =
-    "face_system\n"
-    "{\n"
-    "  udp\n"
-    "  {\n"
-    "    hello\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
-  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
-}
-
-BOOST_AUTO_TEST_CASE(ProcessSectionUdpMulticastReinit)
-{
-  SKIP_IF_NOT_SUPERUSER();
-
-  const std::string CONFIG_WITH_MCAST =
-    "face_system\n"
-    "{\n"
-    "  udp\n"
-    "  {\n"
-    "    mcast yes\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_NO_THROW(parseConfig(CONFIG_WITH_MCAST, false));
-
-  BOOST_REQUIRE(m_manager.m_factories.find("udp") != m_manager.m_factories.end());
-  auto factory = dynamic_pointer_cast<UdpFactory>(m_manager.m_factories.find("udp")->second);
-  BOOST_REQUIRE(factory != nullptr);
-
-  if (factory->getMulticastFaces().empty()) {
-    BOOST_WARN_MESSAGE(false, "skipping assertions that require at least one UDP multicast face");
-    return;
-  }
-
-  const std::string CONFIG_WITHOUT_MCAST =
-    "face_system\n"
-    "{\n"
-    "  udp\n"
-    "  {\n"
-    "    mcast no\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_NO_THROW(parseConfig(CONFIG_WITHOUT_MCAST, false));
-  BOOST_REQUIRE_NO_THROW(g_io.poll());
-  BOOST_CHECK_EQUAL(factory->getMulticastFaces().size(), 0);
-}
-
-#ifdef HAVE_LIBPCAP
-
-BOOST_AUTO_TEST_CASE(ProcessSectionEther)
-{
-  SKIP_IF_NOT_SUPERUSER();
-
-  const std::string CONFIG =
-    "face_system\n"
-    "{\n"
-    "  ether\n"
-    "  {\n"
-    "    mcast yes\n"
-    "    mcast_group 01:00:5E:00:17:AA\n"
-    "    whitelist\n"
-    "    {\n"
-    "      *\n"
-    "    }\n"
-    "    blacklist\n"
-    "    {\n"
-    "    }\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_NO_THROW(parseConfig(CONFIG, true));
-  BOOST_CHECK_NO_THROW(parseConfig(CONFIG, false));
-}
-
-BOOST_AUTO_TEST_CASE(ProcessSectionEtherBadMcast)
-{
-  const std::string CONFIG =
-    "face_system\n"
-    "{\n"
-    "  ether\n"
-    "  {\n"
-    "    mcast hello\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
-  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
-}
-
-BOOST_AUTO_TEST_CASE(ProcessSectionEtherBadMcastGroup)
-{
-  const std::string CONFIG =
-    "face_system\n"
-    "{\n"
-    "  ether\n"
-    "  {\n"
-    "    mcast yes\n"
-    "    mcast_group\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
-  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
-}
-
-BOOST_AUTO_TEST_CASE(ProcessSectionEtherUnknownOption)
-{
-  const std::string CONFIG =
-    "face_system\n"
-    "{\n"
-    "  ether\n"
-    "  {\n"
-    "    hello\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
-  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
-}
-
-BOOST_AUTO_TEST_CASE(ProcessSectionEtherMulticastReinit)
-{
-  SKIP_IF_NOT_SUPERUSER();
-
-  const std::string CONFIG_WITH_MCAST =
-    "face_system\n"
-    "{\n"
-    "  ether\n"
-    "  {\n"
-    "    mcast yes\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_NO_THROW(parseConfig(CONFIG_WITH_MCAST, false));
-
-  BOOST_REQUIRE(m_manager.m_factories.find("ether") != m_manager.m_factories.end());
-  auto factory = dynamic_pointer_cast<EthernetFactory>(m_manager.m_factories.find("ether")->second);
-  BOOST_REQUIRE(factory != nullptr);
-
-  if (factory->getMulticastFaces().empty()) {
-    BOOST_WARN_MESSAGE(false, "skipping assertions that require at least one Ethernet multicast face");
-    return;
-  }
-
-  const std::string CONFIG_WITHOUT_MCAST =
-    "face_system\n"
-    "{\n"
-    "  ether\n"
-    "  {\n"
-    "    mcast no\n"
-    "  }\n"
-    "}\n";
-  BOOST_CHECK_NO_THROW(parseConfig(CONFIG_WITHOUT_MCAST, false));
-  BOOST_REQUIRE_NO_THROW(g_io.poll());
-  BOOST_CHECK_EQUAL(factory->getMulticastFaces().size(), 0);
-}
-
-#endif // HAVE_LIBPCAP
-
-BOOST_AUTO_TEST_SUITE_END() // ProcessConfig
-BOOST_AUTO_TEST_SUITE_END() // TestFaceManager
-BOOST_AUTO_TEST_SUITE_END() // Mgmt
-
-} // namespace tests
-} // namespace nfd
diff --git a/tests/daemon/mgmt/face-manager.t.cpp b/tests/daemon/mgmt/face-manager.t.cpp
index 8a4907c..1dbeaf7 100644
--- a/tests/daemon/mgmt/face-manager.t.cpp
+++ b/tests/daemon/mgmt/face-manager.t.cpp
@@ -281,7 +281,7 @@
 BOOST_AUTO_TEST_CASE(ChannelDataset)
 {
   auto factory = make_shared<TestProtocolFactory>();
-  m_manager.m_factories["test"] = factory;
+  m_manager.m_faceSystem.m_factories["test"] = factory;
 
   std::map<std::string, shared_ptr<TestChannel>> addedChannels;
   size_t nEntries = 404;