face: process face_system.udp config section in UdpFactory

refs #3904

Change-Id: I8edf69c152f7c164cbab2b482d6b138cbf89d3e2
diff --git a/.jenkins.d/20-tests.sh b/.jenkins.d/20-tests.sh
index 6f22700..b52304d 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 ./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 Face/TestEthernetTransport $(ut_log_args daemon-ethernet)
+sudo ./build/unit-tests-daemon -t Face/*Factory/ProcessConfig $(ut_log_args daemon-face-config)
 sudo ./build/unit-tests-daemon -t Mgmt/TestGeneralConfigSection/UserAndGroupConfig,NoUserConfig $(ut_log_args daemon-user-config)
diff --git a/daemon/face/face-system.cpp b/daemon/face/face-system.cpp
index e5b5175..5359e2d 100644
--- a/daemon/face/face-system.cpp
+++ b/daemon/face/face-system.cpp
@@ -24,7 +24,7 @@
  */
 
 #include "face-system.hpp"
-#include "core/logger.hpp"
+// #include "core/logger.hpp"
 #include "fw/face-table.hpp"
 
 // ProtocolFactory includes, sorted alphabetically
@@ -43,7 +43,7 @@
 namespace nfd {
 namespace face {
 
-NFD_LOG_INIT("FaceSystem");
+// NFD_LOG_INIT("FaceSystem");
 
 FaceSystem::FaceSystem(FaceTable& faceTable)
   : m_faceTable(faceTable)
@@ -56,6 +56,8 @@
 
   m_factories["tcp"] = make_shared<TcpFactory>();
 
+  m_factories["udp"] = make_shared<UdpFactory>();
+
 #ifdef HAVE_UNIX_SOCKETS
   m_factories["unix"] = make_shared<UnixStreamFactory>();
 #endif // HAVE_UNIX_SOCKETS
@@ -126,7 +128,7 @@
   std::set<std::string> seenSections;
   for (const auto& pair : configSection) {
     const std::string& sectionName = pair.first;
-    const ConfigSection& subSection = pair.second;
+    // const ConfigSection& subSection = pair.second;
 
     if (!seenSections.insert(sectionName).second) {
       BOOST_THROW_EXCEPTION(ConfigFile::Error("Duplicate section face_system." + sectionName));
@@ -138,168 +140,7 @@
 
     ///\todo #3521 nicfaces
 
-    ///\todo #3904 process these in protocol factory
-    if (sectionName == "udp") {
-      processSectionUdp(subSection, isDryRun, context.m_netifs);
-    }
-    else {
-      BOOST_THROW_EXCEPTION(ConfigFile::Error("Unrecognized option face_system." + sectionName));
-    }
-  }
-}
-
-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_factoryByScheme.count("udp") > 0) {
-      isReload = true;
-      factory = static_pointer_cast<UdpFactory>(m_factoryByScheme["udp"]);
-    }
-    else {
-      factory = make_shared<UdpFactory>();
-      m_factoryByScheme.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_factoryByScheme.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_factoryByScheme.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();
-    }
+    BOOST_THROW_EXCEPTION(ConfigFile::Error("Unrecognized option face_system." + sectionName));
   }
 }
 
diff --git a/daemon/face/face-system.hpp b/daemon/face/face-system.hpp
index 4d374c7..f6a7c71 100644
--- a/daemon/face/face-system.hpp
+++ b/daemon/face/face-system.hpp
@@ -98,10 +98,6 @@
   processConfig(const ConfigSection& configSection, bool isDryRun,
                 const std::string& filename);
 
-  void
-  processSectionUdp(const ConfigSection& configSection, bool isDryRun,
-                    const std::vector<NetworkInterfaceInfo>& nicList);
-
 PUBLIC_WITH_TESTS_ELSE_PRIVATE:
   /** \brief config section name => protocol factory
    *
diff --git a/daemon/face/udp-channel.hpp b/daemon/face/udp-channel.hpp
index a2506d4..e86e8e9 100644
--- a/daemon/face/udp-channel.hpp
+++ b/daemon/face/udp-channel.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2015,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -27,13 +27,10 @@
 #define NFD_DAEMON_FACE_UDP_CHANNEL_HPP
 
 #include "channel.hpp"
+#include "udp-protocol.hpp"
 
 namespace nfd {
 
-namespace udp {
-typedef boost::asio::ip::udp::endpoint Endpoint;
-} // namespace udp
-
 /**
  * \brief Class implementing UDP-based channel to create faces
  */
diff --git a/daemon/face/udp-factory.cpp b/daemon/face/udp-factory.cpp
index 88ac221..c9b4ac4 100644
--- a/daemon/face/udp-factory.cpp
+++ b/daemon/face/udp-factory.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -27,21 +27,199 @@
 #include "generic-link-service.hpp"
 #include "multicast-udp-transport.hpp"
 #include "core/global-io.hpp"
-#include "core/network-interface.hpp"
+#include <boost/range/adaptors.hpp>
+#include <boost/range/algorithm/copy.hpp>
 
 #ifdef __linux__
 #include <cerrno>       // for errno
 #include <cstring>      // for std::strerror()
 #include <sys/socket.h> // for setsockopt()
-#endif
+#endif // __linux__
 
 namespace nfd {
+namespace face {
 
 namespace ip = boost::asio::ip;
 
 NFD_LOG_INIT("UdpFactory");
 
 void
+UdpFactory::processConfig(OptionalConfigSection configSection,
+                          FaceSystem::ConfigContext& context)
+{
+  // udp
+  // {
+  //   port 6363
+  //   enable_v4 yes
+  //   enable_v6 yes
+  //   idle_timeout 600
+  //   keep_alive_interval 25 ; acceptable but ignored
+  //   mcast yes
+  //   mcast_group 224.0.23.170
+  //   mcast_port 56363
+  // }
+
+  uint16_t port = 6363;
+  bool enableV4 = false;
+  bool enableV6 = false;
+  uint32_t idleTimeout = 600;
+  MulticastConfig mcastConfig;
+
+  if (configSection) {
+    // These default to 'yes' but only if face_system.udp section is present
+    enableV4 = enableV6 = mcastConfig.isEnabled = true;
+
+    for (const auto& pair : *configSection) {
+      const std::string& key = pair.first;
+      const ConfigSection& value = pair.second;
+
+      if (key == "port") {
+        port = ConfigFile::parseNumber<uint16_t>(pair, "face_system.udp");
+      }
+      else if (key == "enable_v4") {
+        enableV4 = ConfigFile::parseYesNo(pair, "face_system.udp");
+      }
+      else if (key == "enable_v6") {
+        enableV6 = ConfigFile::parseYesNo(pair, "face_system.udp");
+      }
+      else if (key == "idle_timeout") {
+        idleTimeout = ConfigFile::parseNumber<uint32_t>(pair, "face_system.udp");
+      }
+      else if (key == "keep_alive_interval") {
+        // ignored
+      }
+      else if (key == "mcast") {
+        mcastConfig.isEnabled = ConfigFile::parseYesNo(pair, "face_system.udp");
+      }
+      else if (key == "mcast_group") {
+        const std::string& valueStr = value.get_value<std::string>();
+        boost::system::error_code ec;
+        mcastConfig.group.address(boost::asio::ip::address_v4::from_string(valueStr, ec));
+        if (ec) {
+          BOOST_THROW_EXCEPTION(ConfigFile::Error("face_system.udp.mcast_group: '" +
+                                valueStr + "' cannot be parsed as an IPv4 address"));
+        }
+        else if (!mcastConfig.group.address().is_multicast()) {
+          BOOST_THROW_EXCEPTION(ConfigFile::Error("face_system.udp.mcast_group: '" +
+                                valueStr + "' is not a multicast address"));
+        }
+      }
+      else if (key == "mcast_port") {
+        mcastConfig.group.port(ConfigFile::parseNumber<uint16_t>(pair, "face_system.udp"));
+      }
+      else {
+        BOOST_THROW_EXCEPTION(ConfigFile::Error("Unrecognized option face_system.udp." + key));
+      }
+    }
+
+    if (!enableV4 && !enableV6 && !mcastConfig.isEnabled) {
+      BOOST_THROW_EXCEPTION(ConfigFile::Error(
+        "IPv4 and IPv6 UDP channels and UDP multicast have been disabled. "
+        "Remove face_system.udp section to disable UDP channels or enable at least one of them."));
+    }
+  }
+
+  if (!context.isDryRun) {
+    if (enableV4) {
+      udp::Endpoint endpoint(ip::udp::v4(), port);
+      shared_ptr<UdpChannel> v4Channel = this->createChannel(endpoint, time::seconds(idleTimeout));
+      if (!v4Channel->isListening()) {
+        v4Channel->listen(context.addFace, nullptr);
+      }
+      providedSchemes.insert("udp");
+      providedSchemes.insert("udp4");
+    }
+    else if (providedSchemes.count("udp4") > 0) {
+      NFD_LOG_WARN("Cannot close udp4 channel after its creation");
+    }
+
+    if (enableV6) {
+      udp::Endpoint endpoint(ip::udp::v6(), port);
+      shared_ptr<UdpChannel> v6Channel = this->createChannel(endpoint, time::seconds(idleTimeout));
+      if (!v6Channel->isListening()) {
+        v6Channel->listen(context.addFace, nullptr);
+      }
+      providedSchemes.insert("udp");
+      providedSchemes.insert("udp6");
+    }
+    else if (providedSchemes.count("udp6") > 0) {
+      NFD_LOG_WARN("Cannot close udp6 channel after its creation");
+    }
+
+    if (m_mcastConfig.isEnabled != mcastConfig.isEnabled) {
+      if (mcastConfig.isEnabled) {
+        NFD_LOG_INFO("enabling multicast on " << mcastConfig.group);
+      }
+      else {
+        NFD_LOG_INFO("disabling multicast");
+      }
+    }
+    else if (m_mcastConfig.group != mcastConfig.group) {
+      NFD_LOG_INFO("changing multicast group from " << m_mcastConfig.group <<
+                   " to " << mcastConfig.group);
+    }
+    else {
+      // There's no configuration change, but we still need to re-apply configuration because
+      // netifs may have changed.
+    }
+
+    m_mcastConfig = mcastConfig;
+    this->applyMulticastConfig(context);
+  }
+}
+
+void
+UdpFactory::createFace(const FaceUri& uri,
+                       ndn::nfd::FacePersistency persistency,
+                       bool wantLocalFieldsEnabled,
+                       const FaceCreatedCallback& onCreated,
+                       const FaceCreationFailedCallback& onFailure)
+{
+  BOOST_ASSERT(uri.isCanonical());
+
+  if (persistency == ndn::nfd::FACE_PERSISTENCY_ON_DEMAND) {
+    NFD_LOG_TRACE("createFace does not support FACE_PERSISTENCY_ON_DEMAND");
+    onFailure(406, "Outgoing unicast UDP faces do not support on-demand persistency");
+    return;
+  }
+
+  udp::Endpoint endpoint(ip::address::from_string(uri.getHost()),
+                         boost::lexical_cast<uint16_t>(uri.getPort()));
+
+  if (endpoint.address().is_multicast()) {
+    NFD_LOG_TRACE("createFace does not support multicast faces");
+    onFailure(406, "Cannot create multicast UDP faces");
+    return;
+  }
+
+  if (m_prohibitedEndpoints.find(endpoint) != m_prohibitedEndpoints.end()) {
+    NFD_LOG_TRACE("Requested endpoint is prohibited "
+                  "(reserved by this NFD or disallowed by face management protocol)");
+    onFailure(406, "Requested endpoint is prohibited");
+    return;
+  }
+
+  if (wantLocalFieldsEnabled) {
+    // UDP faces are never local
+    NFD_LOG_TRACE("createFace cannot create non-local face with local fields enabled");
+    onFailure(406, "Local fields can only be enabled on faces with local scope");
+    return;
+  }
+
+  // very simple logic for now
+  for (const auto& i : m_channels) {
+    if ((i.first.address().is_v4() && endpoint.address().is_v4()) ||
+        (i.first.address().is_v6() && endpoint.address().is_v6())) {
+      i.second->connect(endpoint, persistency, onCreated, onFailure);
+      return;
+    }
+  }
+
+  NFD_LOG_TRACE("No channels available to connect to " + boost::lexical_cast<std::string>(endpoint));
+  onFailure(504, "No channels available to connect");
+}
+
+void
 UdpFactory::prohibitEndpoint(const udp::Endpoint& endpoint)
 {
   if (endpoint.address().is_v4() &&
@@ -126,6 +304,22 @@
   return createChannel(endpoint, timeout);
 }
 
+std::vector<shared_ptr<const Channel>>
+UdpFactory::getChannels() const
+{
+  return getChannelsFromMap(m_channels);
+}
+
+shared_ptr<UdpChannel>
+UdpFactory::findChannel(const udp::Endpoint& localEndpoint) const
+{
+  auto i = m_channels.find(localEndpoint);
+  if (i != m_channels.end())
+    return i->second;
+  else
+    return nullptr;
+}
+
 shared_ptr<Face>
 UdpFactory::createMulticastFace(const udp::Endpoint& localEndpoint,
                                 const udp::Endpoint& multicastEndpoint,
@@ -203,16 +397,16 @@
                                   ": " + std::strerror(errno)));
     }
   }
-#endif
+#endif // __linux__
 
-  auto linkService = make_unique<face::GenericLinkService>();
-  auto transport = make_unique<face::MulticastUdpTransport>(localEndpoint, multicastEndpoint,
-                                                            std::move(receiveSocket),
-                                                            std::move(sendSocket));
+  auto linkService = make_unique<GenericLinkService>();
+  auto transport = make_unique<MulticastUdpTransport>(localEndpoint, multicastEndpoint,
+                                                      std::move(receiveSocket),
+                                                      std::move(sendSocket));
   face = make_shared<Face>(std::move(linkService), std::move(transport));
 
-  m_multicastFaces[localEndpoint] = face;
-  connectFaceClosedSignal(*face, [this, localEndpoint] { m_multicastFaces.erase(localEndpoint); });
+  m_mcastFaces[localEndpoint] = face;
+  connectFaceClosedSignal(*face, [this, localEndpoint] { m_mcastFaces.erase(localEndpoint); });
 
   return face;
 }
@@ -230,87 +424,63 @@
   return createMulticastFace(localEndpoint, multicastEndpoint, networkInterfaceName);
 }
 
-void
-UdpFactory::createFace(const FaceUri& uri,
-                       ndn::nfd::FacePersistency persistency,
-                       bool wantLocalFieldsEnabled,
-                       const FaceCreatedCallback& onCreated,
-                       const FaceCreationFailedCallback& onFailure)
-{
-  BOOST_ASSERT(uri.isCanonical());
-
-  if (persistency == ndn::nfd::FACE_PERSISTENCY_ON_DEMAND) {
-    NFD_LOG_TRACE("createFace does not support FACE_PERSISTENCY_ON_DEMAND");
-    onFailure(406, "Outgoing unicast UDP faces do not support on-demand persistency");
-    return;
-  }
-
-  udp::Endpoint endpoint(ip::address::from_string(uri.getHost()),
-                         boost::lexical_cast<uint16_t>(uri.getPort()));
-
-  if (endpoint.address().is_multicast()) {
-    NFD_LOG_TRACE("createFace does not support multicast faces");
-    onFailure(406, "Cannot create multicast UDP faces");
-    return;
-  }
-
-  if (m_prohibitedEndpoints.find(endpoint) != m_prohibitedEndpoints.end()) {
-    NFD_LOG_TRACE("Requested endpoint is prohibited "
-                  "(reserved by this NFD or disallowed by face management protocol)");
-    onFailure(406, "Requested endpoint is prohibited");
-    return;
-  }
-
-  if (wantLocalFieldsEnabled) {
-    // UDP faces are never local
-    NFD_LOG_TRACE("createFace cannot create non-local face with local fields enabled");
-    onFailure(406, "Local fields can only be enabled on faces with local scope");
-    return;
-  }
-
-  // very simple logic for now
-  for (const auto& i : m_channels) {
-    if ((i.first.address().is_v4() && endpoint.address().is_v4()) ||
-        (i.first.address().is_v6() && endpoint.address().is_v6())) {
-      i.second->connect(endpoint, persistency, onCreated, onFailure);
-      return;
-    }
-  }
-
-  NFD_LOG_TRACE("No channels available to connect to " + boost::lexical_cast<std::string>(endpoint));
-  onFailure(504, "No channels available to connect");
-}
-
-std::vector<shared_ptr<const Channel>>
-UdpFactory::getChannels() const
-{
-  std::vector<shared_ptr<const Channel>> channels;
-  channels.reserve(m_channels.size());
-
-  for (const auto& i : m_channels)
-    channels.push_back(i.second);
-
-  return channels;
-}
-
-shared_ptr<UdpChannel>
-UdpFactory::findChannel(const udp::Endpoint& localEndpoint) const
-{
-  auto i = m_channels.find(localEndpoint);
-  if (i != m_channels.end())
-    return i->second;
-  else
-    return nullptr;
-}
-
 shared_ptr<Face>
 UdpFactory::findMulticastFace(const udp::Endpoint& localEndpoint) const
 {
-  auto i = m_multicastFaces.find(localEndpoint);
-  if (i != m_multicastFaces.end())
+  auto i = m_mcastFaces.find(localEndpoint);
+  if (i != m_mcastFaces.end())
     return i->second;
   else
     return nullptr;
 }
 
+void
+UdpFactory::applyMulticastConfig(const FaceSystem::ConfigContext& context)
+{
+  // collect old faces
+  std::set<shared_ptr<Face>> oldFaces;
+  boost::copy(m_mcastFaces | boost::adaptors::map_values,
+              std::inserter(oldFaces, oldFaces.end()));
+
+  if (m_mcastConfig.isEnabled) {
+    // determine interfaces on which faces should be created or retained
+    auto capableNetifRange = context.listNetifs() |
+                             boost::adaptors::filtered([this] (const NetworkInterfaceInfo& netif) {
+                               return netif.isUp() && netif.isMulticastCapable() &&
+                                      !netif.ipv4Addresses.empty();
+                             });
+
+    bool needIfname = false;
+#ifdef __linux__
+    std::vector<NetworkInterfaceInfo> capableNetifs;
+    boost::copy(capableNetifRange, std::back_inserter(capableNetifs));
+    // on Linux, ifname is needed to create more than one UDP multicast face on the same group
+    needIfname = capableNetifs.size() > 1;
+#else
+    auto& capableNetifs = capableNetifRange;
+#endif // __linux__
+
+    // create faces
+    for (const auto& netif : capableNetifs) {
+      udp::Endpoint localEndpoint(netif.ipv4Addresses.front(), m_mcastConfig.group.port());
+      shared_ptr<Face> face = this->createMulticastFace(localEndpoint, m_mcastConfig.group,
+                                                        needIfname ? netif.name : "");
+      if (face->getId() == INVALID_FACEID) {
+        // new face: register with forwarding
+        context.addFace(face);
+      }
+      else {
+        // existing face: don't destroy
+        oldFaces.erase(face);
+      }
+    }
+  }
+
+  // destroy old faces that are not needed in new configuration
+  for (const auto& face : oldFaces) {
+    face->close();
+  }
+}
+
+} // namespace face
 } // namespace nfd
diff --git a/daemon/face/udp-factory.hpp b/daemon/face/udp-factory.hpp
index c788a3f..7ec42b3 100644
--- a/daemon/face/udp-factory.hpp
+++ b/daemon/face/udp-factory.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -30,9 +30,13 @@
 #include "udp-channel.hpp"
 
 namespace nfd {
+namespace face {
 
-/// @todo IPv6 multicast support not implemented
-
+/** \brief protocol factory for UDP over IPv4 and IPv6
+ *
+ *  UDP unicast is available over both IPv4 and IPv6.
+ *  UDP multicast is available over IPv4 only.
+ */
 class UdpFactory : public ProtocolFactory
 {
 public:
@@ -49,7 +53,18 @@
     }
   };
 
-  typedef std::map<udp::Endpoint, shared_ptr<Face>> MulticastFaceMap;
+  /** \brief process face_system.udp config section
+   */
+  void
+  processConfig(OptionalConfigSection configSection,
+                FaceSystem::ConfigContext& context) override;
+
+  void
+  createFace(const FaceUri& uri,
+             ndn::nfd::FacePersistency persistency,
+             bool wantLocalFieldsEnabled,
+             const FaceCreatedCallback& onCreated,
+             const FaceCreationFailedCallback& onFailure) override;
 
   /**
    * \brief Create UDP-based channel using udp::Endpoint
@@ -97,6 +112,9 @@
   createChannel(const std::string& localIp, const std::string& localPort,
                 const time::seconds& timeout = time::seconds(600));
 
+  std::vector<shared_ptr<const Channel>>
+  getChannels() const override;
+
   /**
    * \brief Create MulticastUdpFace using udp::Endpoint
    *
@@ -139,23 +157,6 @@
                       const std::string& multicastPort,
                       const std::string& networkInterfaceName = "");
 
-  /**
-   * \brief Get map of configured multicast faces
-   */
-  const MulticastFaceMap&
-  getMulticastFaces() const;
-
-public: // from ProtocolFactory
-  virtual void
-  createFace(const FaceUri& uri,
-             ndn::nfd::FacePersistency persistency,
-             bool wantLocalFieldsEnabled,
-             const FaceCreatedCallback& onCreated,
-             const FaceCreationFailedCallback& onFailure) override;
-
-  virtual std::vector<shared_ptr<const Channel>>
-  getChannels() const override;
-
 PUBLIC_WITH_TESTS_ELSE_PRIVATE:
   void
   prohibitEndpoint(const udp::Endpoint& endpoint);
@@ -185,20 +186,26 @@
   shared_ptr<Face>
   findMulticastFace(const udp::Endpoint& localEndpoint) const;
 
+  void
+  applyMulticastConfig(const FaceSystem::ConfigContext& context);
+
 private:
   std::map<udp::Endpoint, shared_ptr<UdpChannel>> m_channels;
-  MulticastFaceMap m_multicastFaces;
+
+  struct MulticastConfig
+  {
+    bool isEnabled = false;
+    udp::Endpoint group = udp::getDefaultMulticastGroup();
+  };
+
+  MulticastConfig m_mcastConfig;
+  std::map<udp::Endpoint, shared_ptr<Face>> m_mcastFaces;
 
 PUBLIC_WITH_TESTS_ELSE_PRIVATE:
   std::set<udp::Endpoint> m_prohibitedEndpoints;
 };
 
-inline const UdpFactory::MulticastFaceMap&
-UdpFactory::getMulticastFaces() const
-{
-  return m_multicastFaces;
-}
-
+} // namespace face
 } // namespace nfd
 
 #endif // NFD_DAEMON_FACE_UDP_FACTORY_HPP
diff --git a/daemon/face/udp-protocol.cpp b/daemon/face/udp-protocol.cpp
index fb51a3a..0d842bc 100644
--- a/daemon/face/udp-protocol.cpp
+++ b/daemon/face/udp-protocol.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2015,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -29,7 +29,7 @@
 namespace udp {
 
 ssize_t
-computeMtu(const boost::asio::ip::udp::endpoint& localEndpoint)
+computeMtu(const Endpoint& localEndpoint)
 {
   size_t mtu = 0;
   if (localEndpoint.address().is_v4()) { // IPv4
diff --git a/daemon/face/udp-protocol.hpp b/daemon/face/udp-protocol.hpp
index 5f7c25a..f716658 100644
--- a/daemon/face/udp-protocol.hpp
+++ b/daemon/face/udp-protocol.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2016,  Regents of the University of California,
+ * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -31,10 +31,21 @@
 namespace nfd {
 namespace udp {
 
+typedef boost::asio::ip::udp::endpoint Endpoint;
+
 /** \brief computes maximum payload size in a UDP packet
  */
 ssize_t
-computeMtu(const boost::asio::ip::udp::endpoint& localEndpoint);
+computeMtu(const Endpoint& localEndpoint);
+
+/** \return default multicast group: 224.0.23.170:56363
+ */
+inline Endpoint
+getDefaultMulticastGroup()
+{
+  // 224.0.23.170:56363
+  return {boost::asio::ip::address_v4(0xE00017AA), 56363};
+}
 
 } // namespace udp
 } // namespace nfd
diff --git a/tests/daemon/face/face-system-fixture.hpp b/tests/daemon/face/face-system-fixture.hpp
index 2198c7e..42ed02c 100644
--- a/tests/daemon/face/face-system-fixture.hpp
+++ b/tests/daemon/face/face-system-fixture.hpp
@@ -28,6 +28,7 @@
 
 #include "face/face.hpp"
 #include "face/face-system.hpp"
+#include "face/protocol-factory.hpp"
 #include "fw/face-table.hpp"
 
 #include "tests/test-common.hpp"
diff --git a/tests/daemon/face/face-system.t.cpp b/tests/daemon/face/face-system.t.cpp
index cd90078..de1b6b1 100644
--- a/tests/daemon/face/face-system.t.cpp
+++ b/tests/daemon/face/face-system.t.cpp
@@ -26,9 +26,6 @@
 #include "face/face-system.hpp"
 #include "face-system-fixture.hpp"
 
-// ProtocolFactory includes, sorted alphabetically
-#include "face/udp-factory.hpp"
-
 #include "tests/test-common.hpp"
 
 namespace nfd {
@@ -199,207 +196,6 @@
 
 BOOST_AUTO_TEST_SUITE_END() // ProcessConfig
 
-///\todo #3904 move Config* to *Factory test suite
-
-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 = this->getFactoryByScheme<UdpFactory>("udp");
-  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 = this->getFactoryByScheme<UdpFactory>("udp");
-
-  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
-
 BOOST_AUTO_TEST_SUITE_END() // TestFaceSystem
 BOOST_AUTO_TEST_SUITE_END() // Mgmt
 
diff --git a/tests/daemon/face/udp-factory.t.cpp b/tests/daemon/face/udp-factory.t.cpp
index d6ef39c..ac468cb 100644
--- a/tests/daemon/face/udp-factory.t.cpp
+++ b/tests/daemon/face/udp-factory.t.cpp
@@ -26,17 +26,343 @@
 #include "face/udp-factory.hpp"
 
 #include "factory-test-common.hpp"
-#include "core/network-interface.hpp"
+#include "face-system-fixture.hpp"
 #include "tests/limited-io.hpp"
 
 namespace nfd {
+namespace face {
 namespace tests {
 
+using namespace nfd::tests;
+
 BOOST_AUTO_TEST_SUITE(Face)
 BOOST_FIXTURE_TEST_SUITE(TestUdpFactory, BaseFixture)
 
 using nfd::Face;
 
+BOOST_FIXTURE_TEST_SUITE(ProcessConfig, FaceSystemFixture)
+
+BOOST_AUTO_TEST_CASE(Channels)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      udp
+      {
+        port 7001
+        enable_v4 yes
+        enable_v6 yes
+        idle_timeout 30
+        mcast no
+      }
+    }
+  )CONFIG";
+
+  parseConfig(CONFIG, true);
+  parseConfig(CONFIG, false);
+
+  auto& factory = this->getFactoryById<UdpFactory>("udp");
+  checkChannelListEqual(factory, {"udp4://0.0.0.0:7001", "udp6://[::]:7001"});
+}
+
+BOOST_AUTO_TEST_CASE(ChannelV4)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      udp
+      {
+        port 7001
+        enable_v4 yes
+        enable_v6 no
+        mcast no
+      }
+    }
+  )CONFIG";
+
+  parseConfig(CONFIG, true);
+  parseConfig(CONFIG, false);
+
+  auto& factory = this->getFactoryById<UdpFactory>("udp");
+  checkChannelListEqual(factory, {"udp4://0.0.0.0:7001"});
+}
+
+BOOST_AUTO_TEST_CASE(ChannelV6)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      udp
+      {
+        port 7001
+        enable_v4 no
+        enable_v6 yes
+        mcast no
+      }
+    }
+  )CONFIG";
+
+  parseConfig(CONFIG, true);
+  parseConfig(CONFIG, false);
+
+  auto& factory = this->getFactoryById<UdpFactory>("udp");
+  checkChannelListEqual(factory, {"udp6://[::]:7001"});
+}
+
+class UdpMcastConfigFixture : public FaceSystemFixture
+{
+protected:
+  UdpMcastConfigFixture()
+  {
+    for (const auto& netif : listNetworkInterfaces()) {
+      if (netif.isUp() && netif.isMulticastCapable() && !netif.ipv4Addresses.empty()) {
+        netifs.push_back(netif);
+      }
+    }
+  }
+
+  std::vector<const Face*>
+  listUdpMcastFaces() const
+  {
+    return this->listFacesByScheme("udp4", ndn::nfd::LINK_TYPE_MULTI_ACCESS);
+  }
+
+  size_t
+  countUdpMcastFaces() const
+  {
+    return this->listUdpMcastFaces().size();
+  }
+
+protected:
+  /** \brief MulticastUdpTransport-capable network interfaces
+   */
+  std::vector<NetworkInterfaceInfo> netifs;
+};
+
+#define SKIP_IF_UDP_MCAST_NETIF_COUNT_LT(n) \
+  do { \
+    if (this->netifs.size() < (n)) { \
+      BOOST_WARN_MESSAGE(false, "skipping assertions that require " #n \
+                                " or more MulticastUdpTransport-capable network interfaces"); \
+      return; \
+    } \
+  } while (false)
+
+BOOST_FIXTURE_TEST_CASE(EnableDisableMcast, UdpMcastConfigFixture)
+{
+#ifdef __linux__
+  // need superuser privilege for creating multicast faces on linux
+  SKIP_IF_NOT_SUPERUSER();
+#endif // __linux__
+
+  const std::string CONFIG_WITH_MCAST = R"CONFIG(
+    face_system
+    {
+      udp
+      {
+        mcast yes
+      }
+    }
+  )CONFIG";
+  const std::string CONFIG_WITHOUT_MCAST = R"CONFIG(
+    face_system
+    {
+      udp
+      {
+        mcast no
+      }
+    }
+  )CONFIG";
+
+  parseConfig(CONFIG_WITHOUT_MCAST, false);
+  BOOST_CHECK_EQUAL(this->countUdpMcastFaces(), 0);
+
+  SKIP_IF_UDP_MCAST_NETIF_COUNT_LT(1);
+
+  parseConfig(CONFIG_WITH_MCAST, false);
+  g_io.poll();
+  BOOST_CHECK_EQUAL(this->countUdpMcastFaces(), netifs.size());
+
+  parseConfig(CONFIG_WITHOUT_MCAST, false);
+  g_io.poll();
+  BOOST_CHECK_EQUAL(this->countUdpMcastFaces(), 0);
+}
+
+BOOST_FIXTURE_TEST_CASE(ChangeMcastEndpoint, UdpMcastConfigFixture)
+{
+#ifdef __linux__
+  // need superuser privilege for creating multicast faces on linux
+  SKIP_IF_NOT_SUPERUSER();
+#endif // __linux__
+  SKIP_IF_UDP_MCAST_NETIF_COUNT_LT(1);
+
+  const std::string CONFIG1 = R"CONFIG(
+    face_system
+    {
+      udp
+      {
+        mcast_group 239.66.30.1
+        mcast_port 7011
+      }
+    }
+  )CONFIG";
+  const std::string CONFIG2 = R"CONFIG(
+    face_system
+    {
+      udp
+      {
+        mcast_group 239.66.30.2
+        mcast_port 7012
+      }
+    }
+  )CONFIG";
+
+  parseConfig(CONFIG1, false);
+  auto udpMcastFaces = this->listUdpMcastFaces();
+  BOOST_REQUIRE_EQUAL(udpMcastFaces.size(), netifs.size());
+  BOOST_CHECK_EQUAL(udpMcastFaces.front()->getRemoteUri(),
+                    FaceUri("udp4://239.66.30.1:7011"));
+
+  parseConfig(CONFIG2, false);
+  g_io.poll();
+  udpMcastFaces = this->listUdpMcastFaces();
+  BOOST_REQUIRE_EQUAL(udpMcastFaces.size(), netifs.size());
+  BOOST_CHECK_EQUAL(udpMcastFaces.front()->getRemoteUri(),
+                    FaceUri("udp4://239.66.30.2:7012"));
+}
+
+BOOST_AUTO_TEST_CASE(Omitted)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+    }
+  )CONFIG";
+
+  parseConfig(CONFIG, true);
+  parseConfig(CONFIG, false);
+
+  auto& factory = this->getFactoryById<UdpFactory>("udp");
+  BOOST_CHECK_EQUAL(factory.getChannels().size(), 0);
+  BOOST_CHECK_EQUAL(this->listFacesByScheme("udp4", ndn::nfd::LINK_TYPE_MULTI_ACCESS).size(), 0);
+}
+
+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_group hello
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
+  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_CASE(BadMcastGroupV4Unicast)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      udp
+      {
+        mcast_group 10.0.0.1
+      }
+    }
+  )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_group ff00::1
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
+  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_CASE(AllDisabled)
+{
+  const std::string CONFIG = R"CONFIG(
+    face_system
+    {
+      udp
+      {
+        enable_v4 no
+        enable_v6 no
+        mcast 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
+    {
+      udp
+      {
+        hello
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(parseConfig(CONFIG, true), ConfigFile::Error);
+  BOOST_CHECK_THROW(parseConfig(CONFIG, false), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // ProcessConfig
+
 BOOST_AUTO_TEST_CASE(GetChannels)
 {
   UdpFactory factory;
@@ -277,4 +603,5 @@
 BOOST_AUTO_TEST_SUITE_END() // Face
 
 } // namespace tests
+} // namespace face
 } // namespace nfd