tools: create face during nfdc route add

refs #4332

Change-Id: I1a30bf87abd57bab08a2c027870e779392de0104
diff --git a/docs/manpages/nfdc-route.rst b/docs/manpages/nfdc-route.rst
index 3b28439..8c7cffa 100644
--- a/docs/manpages/nfdc-route.rst
+++ b/docs/manpages/nfdc-route.rst
@@ -26,6 +26,8 @@
 If a route with the same prefix, nexthop, and origin already exists,
 it is updated with the specified cost, route inheritance flags, and expiration period.
 This command returns when the request has been accepted, but does not wait for RIB update completion.
+If no face matching the specified URI is found, nfdc will attempt to implicitly create a face with
+this URI before adding the route.
 
 The **nfdc route remove** command removes a route with matching prefix, nexthop, and origin.
 
diff --git a/tests/tools/nfdc/face-module.t.cpp b/tests/tools/nfdc/face-module.t.cpp
index 84031dd..e2c9a86 100644
--- a/tests/tools/nfdc/face-module.t.cpp
+++ b/tests/tools/nfdc/face-module.t.cpp
@@ -863,7 +863,8 @@
   this->execute("face create invalid://");
   BOOST_CHECK_EQUAL(exitCode, 4);
   BOOST_CHECK(out.is_empty());
-  BOOST_CHECK(err.is_equal("Error when canonizing 'invalid://': scheme not supported\n"));
+  BOOST_CHECK(err.is_equal("Error during canonization of remote FaceUri 'invalid://': "
+                           "scheme not supported\n"));
 }
 
 BOOST_AUTO_TEST_CASE(ErrorCanonizeLocal)
@@ -871,7 +872,8 @@
   this->execute("face create udp4://24.37.20.47:6363 local invalid://");
   BOOST_CHECK_EQUAL(exitCode, 4);
   BOOST_CHECK(out.is_empty());
-  BOOST_CHECK(err.is_equal("Error when canonizing 'invalid://': scheme not supported\n"));
+  BOOST_CHECK(err.is_equal("Error during canonization of local FaceUri 'invalid://': "
+                           "scheme not supported\n"));
 }
 
 BOOST_AUTO_TEST_CASE(ErrorCreate)
@@ -1002,7 +1004,7 @@
   this->execute("face destroy udp6://32.38.164.64:10445");
   BOOST_CHECK_EQUAL(exitCode, 4);
   BOOST_CHECK(out.is_empty());
-  BOOST_CHECK(err.is_equal("Error during remote FaceUri canonization: "
+  BOOST_CHECK(err.is_equal("Error during canonization of 'udp6://32.38.164.64:10445': "
                            "IPv4/v6 mismatch\n"));
 }
 
diff --git a/tests/tools/nfdc/mock-nfd-mgmt-fixture.hpp b/tests/tools/nfdc/mock-nfd-mgmt-fixture.hpp
index 5242c9b..239faa9 100644
--- a/tests/tools/nfdc/mock-nfd-mgmt-fixture.hpp
+++ b/tests/tools/nfdc/mock-nfd-mgmt-fixture.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2019,  Regents of the University of California,
+ * Copyright (c) 2014-2020,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -40,7 +40,7 @@
 class MockNfdMgmtFixture : public nfd::tools::tests::MockNfdMgmtFixture
 {
 protected:
-  /** \brief respond to specific FaceQuery requests
+  /** \brief respond to FaceQuery requests
    *  \retval true the Interest matches one of the defined patterns and is responded
    *  \retval false the Interest is not responded
    */
@@ -54,8 +54,8 @@
     if (!Name("/localhost/nfd/faces/query").isPrefixOf(interest.getName())) {
       return false;
     }
-    BOOST_CHECK_EQUAL(interest.getName().size(), 5);
-    FaceQueryFilter filter(interest.getName().at(4).blockFromValue());
+    BOOST_REQUIRE_EQUAL(interest.getName().size(), 5);
+    FaceQueryFilter filter(interest.getName()[-1].blockFromValue());
 
     if (filter == FaceQueryFilter().setFaceId(10156)) {
       FaceStatus faceStatus;
@@ -96,6 +96,8 @@
       return true;
     }
 
+    // Return empty dataset
+    this->sendEmptyDataset(interest.getName());
     return false;
   }
 };
diff --git a/tests/tools/nfdc/rib-module.t.cpp b/tests/tools/nfdc/rib-module.t.cpp
index 624fca6..7f268dd 100644
--- a/tests/tools/nfdc/rib-module.t.cpp
+++ b/tests/tools/nfdc/rib-module.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2019,  Regents of the University of California,
+ * Copyright (c) 2014-2020,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -263,7 +263,7 @@
   BOOST_CHECK(err.is_empty());
 }
 
-BOOST_AUTO_TEST_CASE(FaceNotExist)
+BOOST_AUTO_TEST_CASE(FaceNotExistFaceId)
 {
   this->processInterest = [this] (const Interest& interest) {
     BOOST_CHECK(this->respondFaceQuery(interest));
@@ -275,6 +275,76 @@
   BOOST_CHECK(err.is_equal("Face not found\n"));
 }
 
+BOOST_AUTO_TEST_CASE(FaceNotExistFaceUri)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    if (Name("/localhost/nfd/faces/query").isPrefixOf(interest.getName())) {
+      this->respondFaceQuery(interest);
+    }
+    else if (Name("/localhost/nfd/faces/create").isPrefixOf(interest.getName())) {
+      ControlParameters req = MOCK_NFD_MGMT_REQUIRE_COMMAND_IS("/localhost/nfd/faces/create");
+      ndn::nfd::FaceCreateCommand cmd;
+      cmd.validateRequest(req);
+      cmd.applyDefaultsToRequest(req);
+      BOOST_CHECK_EQUAL(req.getUri(), "udp4://202.83.168.28:6363");
+
+      ControlParameters resp = req;
+      resp.setFaceId(255);
+      resp.setLocalUri("udp4://32.121.182.82:50000");
+      resp.setFacePersistency(FacePersistency::FACE_PERSISTENCY_PERSISTENT);
+      resp.setBaseCongestionMarkingInterval(100_ms);
+      resp.setDefaultCongestionThreshold(65536);
+      resp.setMtu(8800);
+      resp.setFlags(0);
+      this->succeedCommand(interest, resp);
+    }
+    else if (Name("/localhost/nfd/rib/register").isPrefixOf(interest.getName())) {
+      ControlParameters req = MOCK_NFD_MGMT_REQUIRE_COMMAND_IS("/localhost/nfd/rib/register");
+      ndn::nfd::RibRegisterCommand cmd;
+      cmd.validateRequest(req);
+      cmd.applyDefaultsToRequest(req);
+      BOOST_CHECK_EQUAL(req.getName(), "/634jfAfdf");
+      BOOST_CHECK_EQUAL(req.getFaceId(), 255);
+      BOOST_CHECK_EQUAL(req.getOrigin(), 17591);
+      BOOST_CHECK_EQUAL(req.getCost(), 702);
+      BOOST_CHECK_EQUAL(req.getFlags(), ndn::nfd::ROUTE_FLAG_CHILD_INHERIT |
+                                        ndn::nfd::ROUTE_FLAG_CAPTURE);
+      BOOST_REQUIRE_EQUAL(req.hasExpirationPeriod(), true);
+      BOOST_REQUIRE_EQUAL(req.getExpirationPeriod(), 727411987_ms);
+
+      ControlParameters resp = req;
+      resp.setExpirationPeriod(727411154_ms); // server side may change expiration
+      this->succeedCommand(interest, resp);
+    }
+  };
+
+  this->execute("route add /634jfAfdf udp4://202.83.168.28:6363 "
+                "origin 17591 cost 702 capture expires 727411987");
+  BOOST_CHECK(out.is_equal("face-created id=255 local=udp4://32.121.182.82:50000 "
+                           "remote=udp4://202.83.168.28:6363 persistency=persistent "
+                           "reliability=off congestion-marking=off "
+                           "congestion-marking-interval=100ms default-congestion-threshold=65536B "
+                           "mtu=8800\n"
+                           "route-add-accepted prefix=/634jfAfdf nexthop=255 origin=17591 "
+                           "cost=702 flags=child-inherit|capture expires=727411154ms\n"));
+  BOOST_CHECK(err.is_empty());
+  BOOST_CHECK_EQUAL(exitCode, 0);
+}
+
+BOOST_AUTO_TEST_CASE(FaceNotExistNotCanonizable)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    BOOST_CHECK(this->respondFaceQuery(interest));
+  };
+
+  this->execute("route add /634jfAfdf udp6://202.83.168.28:6363 "
+                "origin 17591 cost 702 capture expires 727411987");
+  BOOST_CHECK(out.is_empty());
+  BOOST_CHECK(err.is_equal("Error during canonization of 'udp6://202.83.168.28:6363': "
+                           "IPv4/v6 mismatch\n"));
+  BOOST_CHECK_EQUAL(exitCode, 4);
+}
+
 BOOST_AUTO_TEST_CASE(Ambiguous)
 {
   this->processInterest = [this] (const Interest& interest) {
@@ -295,7 +365,7 @@
   this->execute("route add /bxJfGsVtDt udp6://32.38.164.64:10445");
   BOOST_CHECK_EQUAL(exitCode, 4);
   BOOST_CHECK(out.is_empty());
-  BOOST_CHECK(err.is_equal("Error during remote FaceUri canonization: "
+  BOOST_CHECK(err.is_equal("Error during canonization of 'udp6://32.38.164.64:10445': "
                            "IPv4/v6 mismatch\n"));
 }
 
diff --git a/tools/nfdc/canonizer.cpp b/tools/nfdc/canonizer.cpp
new file mode 100644
index 0000000..3e526cd
--- /dev/null
+++ b/tools/nfdc/canonizer.cpp
@@ -0,0 +1,61 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2014-2020,  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 "canonizer.hpp"
+
+namespace nfd {
+namespace tools {
+namespace nfdc {
+
+std::pair<optional<FaceUri>, std::string>
+canonize(ExecuteContext& ctx, const FaceUri& uri)
+{
+  optional<FaceUri> result;
+  std::string error;
+  uri.canonize(
+    [&result] (const FaceUri& canonicalUri) { result = canonicalUri; },
+    [&error] (const std::string& errorReason) { error = errorReason; },
+    ctx.face.getIoService(), ctx.getTimeout());
+  ctx.face.processEvents();
+
+  return {result, error};
+}
+
+std::pair<FindFace::Code, std::string>
+canonizeErrorHelper(const FaceUri& uri,
+                    const std::string& error,
+                    const std::string& field)
+{
+  std::string msg = "Error during canonization of ";
+  if (!field.empty()) {
+    msg += field + " ";
+  }
+  msg += "'" + uri.toString() + "': " + error;
+  return {FindFace::Code::CANONIZE_ERROR, msg};
+}
+
+} // namespace nfdc
+} // namespace tools
+} // namespace nfd
diff --git a/tools/nfdc/canonizer.hpp b/tools/nfdc/canonizer.hpp
new file mode 100644
index 0000000..b58372d
--- /dev/null
+++ b/tools/nfdc/canonizer.hpp
@@ -0,0 +1,60 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2014-2020,  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_TOOLS_NFDC_CANONIZER_HPP
+#define NFD_TOOLS_NFDC_CANONIZER_HPP
+
+#include "core/common.hpp"
+#include "execute-command.hpp"
+#include "find-face.hpp"
+
+#include <ndn-cxx/net/face-uri.hpp>
+
+namespace nfd {
+namespace tools {
+namespace nfdc {
+
+/** \brief canonize FaceUri
+ *  \return pair of canonical FaceUri (nullopt if failure) and error string
+ */
+std::pair<optional<FaceUri>, std::string>
+canonize(ExecuteContext& ctx, const FaceUri& uri);
+
+/** \brief helper to generate exit code and error message for face canonization failures
+ *  \param uri FaceUri
+ *  \param error error string returned by canonization process
+ *  \param field optional field identifier to include with message
+ *  \return pair of exit code and error message
+ */
+std::pair<FindFace::Code, std::string>
+canonizeErrorHelper(const FaceUri& uri,
+                    const std::string& error,
+                    const std::string& field = "");
+
+} // namespace nfdc
+} // namespace tools
+} // namespace nfd
+
+#endif // NFD_TOOLS_NFDC_CANONIZER_HPP
diff --git a/tools/nfdc/face-module.cpp b/tools/nfdc/face-module.cpp
index 37f5bd9..e46279d2 100644
--- a/tools/nfdc/face-module.cpp
+++ b/tools/nfdc/face-module.cpp
@@ -24,6 +24,7 @@
  */
 
 #include "face-module.hpp"
+#include "canonizer.hpp"
 #include "find-face.hpp"
 
 namespace nfd {
@@ -179,34 +180,19 @@
     mtu = static_cast<uint64_t>(v);
   }
 
-  FaceUri canonicalRemote;
+  optional<FaceUri> canonicalRemote;
   optional<FaceUri> canonicalLocal;
 
-  auto handleCanonizeError = [&] (const FaceUri& faceUri, const std::string& error) {
-    ctx.exitCode = static_cast<int>(FindFace::Code::CANONIZE_ERROR);
-    ctx.err << "Error when canonizing '" << faceUri << "': " << error << '\n';
-  };
-
-  auto printPositiveResult = [&] (const std::string& actionSummary, const ControlParameters& resp) {
-    text::ItemAttributes ia;
-    ctx.out << actionSummary << ' '
-            << ia("id") << resp.getFaceId()
-            << ia("local") << resp.getLocalUri()
-            << ia("remote") << resp.getUri()
-            << ia("persistency") << resp.getFacePersistency();
-    printFaceParams(ctx.out, ia, resp);
-  };
-
-  auto updateFace = [&printPositiveResult] (ControlParameters respParams, ControlParameters resp) {
+  auto updateFace = [&] (ControlParameters respParams, ControlParameters resp) {
     // faces/update response does not have FaceUris, copy from faces/create response
     resp.setLocalUri(respParams.getLocalUri())
         .setUri(respParams.getUri());
-    printPositiveResult("face-updated", resp);
+    printSuccess(ctx.out, "face-updated", resp);
   };
 
   auto handle409 = [&] (const ControlResponse& resp) {
     ControlParameters respParams(resp.getBody());
-    if (respParams.getUri() != canonicalRemote.toString()) {
+    if (respParams.getUri() != canonicalRemote->toString()) {
       // we are conflicting with a different face, which is a general error
       return false;
     }
@@ -256,7 +242,7 @@
     }
     else {
       // don't do anything
-      printPositiveResult("face-exists", respParams);
+      printSuccess(ctx.out, "face-exists", respParams);
     }
 
     return true;
@@ -264,7 +250,7 @@
 
   auto doCreateFace = [&] {
     ControlParameters params;
-    params.setUri(canonicalRemote.toString());
+    params.setUri(canonicalRemote->toString());
     if (canonicalLocal) {
       params.setLocalUri(canonicalLocal->toString());
     }
@@ -287,7 +273,9 @@
 
     ctx.controller.start<ndn::nfd::FaceCreateCommand>(
       params,
-      bind(printPositiveResult, "face-created", _1),
+      [&] (const ControlParameters& resp) {
+        printSuccess(ctx.out, "face-created", resp);
+      },
       [&] (const ControlResponse& resp) {
         if (resp.getCode() == 409 && handle409(resp)) {
           return;
@@ -297,24 +285,33 @@
       ctx.makeCommandOptions());
   };
 
-  remoteUri.canonize(
-    [&] (const FaceUri& canonicalUri) {
-      canonicalRemote = canonicalUri;
-      if (localUri) {
-        localUri->canonize(
-          [&] (const FaceUri& canonicalUri) {
-            canonicalLocal = canonicalUri;
-            doCreateFace();
-          },
-          bind(handleCanonizeError, *localUri, _1),
-          ctx.face.getIoService(), ctx.getTimeout());
-      }
-      else {
+  std::string error;
+  std::tie(canonicalRemote, error) = canonize(ctx, remoteUri);
+  if (canonicalRemote) {
+    // RemoteUri canonization successful
+    if (localUri) {
+      std::tie(canonicalLocal, error) = canonize(ctx, *localUri);
+      if (canonicalLocal) {
+        // LocalUri canonization successful
         doCreateFace();
       }
-    },
-    bind(handleCanonizeError, remoteUri, _1),
-    ctx.face.getIoService(), ctx.getTimeout());
+      else {
+        // LocalUri canonization failure
+        auto canonizationError = canonizeErrorHelper(*localUri, error, "local FaceUri");
+        ctx.exitCode = static_cast<int>(canonizationError.first);
+        ctx.err << canonizationError.second << '\n';
+      }
+    }
+    else {
+      doCreateFace();
+    }
+  }
+  else {
+    // RemoteUri canonization failure
+    auto canonizationError = canonizeErrorHelper(remoteUri, error, "remote FaceUri");
+    ctx.exitCode = static_cast<int>(canonizationError.first);
+    ctx.err << canonizationError.second << '\n';
+  }
 
   ctx.face.processEvents();
 }
@@ -349,6 +346,7 @@
   ctx.controller.start<ndn::nfd::FaceDestroyCommand>(
     ControlParameters().setFaceId(face.getFaceId()),
     [&] (const ControlParameters& resp) {
+      // We can't use printSuccess because some face attributes come from FaceStatus not ControlResponse
       ctx.out << "face-destroyed ";
       text::ItemAttributes ia;
       ctx.out << ia("id") << face.getFaceId()
@@ -528,6 +526,20 @@
 }
 
 void
+FaceModule::printSuccess(std::ostream& os,
+                         const std::string& actionSummary,
+                         const ControlParameters& resp)
+{
+  text::ItemAttributes ia;
+  os << actionSummary << ' '
+     << ia("id") << resp.getFaceId()
+     << ia("local") << resp.getLocalUri()
+     << ia("remote") << resp.getUri()
+     << ia("persistency") << resp.getFacePersistency();
+  printFaceParams(os, ia, resp);
+}
+
+void
 FaceModule::printFaceParams(std::ostream& os, text::ItemAttributes& ia, const ControlParameters& resp)
 {
   os << ia("reliability") << text::OnOff{resp.getFlagBit(ndn::nfd::BIT_LP_RELIABILITY_ENABLED)}
diff --git a/tools/nfdc/face-module.hpp b/tools/nfdc/face-module.hpp
index 8083e74..cffd98b 100644
--- a/tools/nfdc/face-module.hpp
+++ b/tools/nfdc/face-module.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2018,  Regents of the University of California,
+ * Copyright (c) 2014-2020,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -94,6 +94,14 @@
   static void
   formatItemText(std::ostream& os, const FaceStatus& item, bool wantMultiLine);
 
+  /** \brief print face action success message to specified ostream
+   *  \param os output stream
+   *  \param actionSummary description of action taken
+   *  \param resp response control parameters to print
+   */
+  static void
+  printSuccess(std::ostream& os, const std::string& actionSummary, const ControlParameters& resp);
+
   /** \brief print face response parameters to specified ostream
    *  \param os output stream
    *  \param ia ItemAttributes used to format output
diff --git a/tools/nfdc/find-face.cpp b/tools/nfdc/find-face.cpp
index bb6183d..de0fd8e 100644
--- a/tools/nfdc/find-face.cpp
+++ b/tools/nfdc/find-face.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2018,  Regents of the University of California,
+ * Copyright (c) 2014-2020,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -24,6 +24,7 @@
  */
 
 #include "find-face.hpp"
+#include "canonizer.hpp"
 #include "format-helpers.hpp"
 
 #include <ndn-cxx/util/logger.hpp>
@@ -75,18 +76,16 @@
   m_filter = filter;
 
   if (m_filter.hasRemoteUri()) {
-    auto remoteUri = this->canonize("remote", FaceUri(m_filter.getRemoteUri()));
+    auto remoteUri = canonize("remote FaceUri", FaceUri(m_filter.getRemoteUri()));
     if (!remoteUri) {
-      m_res = Code::CANONIZE_ERROR;
       return m_res;
     }
     m_filter.setRemoteUri(remoteUri->toString());
   }
 
   if (m_filter.hasLocalUri()) {
-    auto localUri = this->canonize("local", FaceUri(m_filter.getLocalUri()));
+    auto localUri = canonize("local FaceUri", FaceUri(m_filter.getLocalUri()));
     if (!localUri) {
-      m_res = Code::CANONIZE_ERROR;
       return m_res;
     }
     m_filter.setLocalUri(localUri->toString());
@@ -107,23 +106,27 @@
 }
 
 optional<FaceUri>
-FindFace::canonize(const std::string& fieldName, const FaceUri& input)
+FindFace::canonize(const std::string& fieldName, const FaceUri& uri)
 {
-  if (!FaceUri::canCanonize(input.getScheme())) {
-    NDN_LOG_DEBUG("Using " << fieldName << '=' << input << " without canonization");
-    return input;
+  // We use a wrapper because we want to accept FaceUris that cannot be canonized
+  if (!FaceUri::canCanonize(uri.getScheme())) {
+    NDN_LOG_DEBUG("Using " << fieldName << "=" << uri << " without canonization");
+    return uri;
   }
 
   optional<FaceUri> result;
-  input.canonize(
-    [&result] (const FaceUri& canonicalUri) { result = canonicalUri; },
-    [this, fieldName] (const std::string& errorReason) {
-      m_errorReason = "Error during " + fieldName + " FaceUri canonization: " + errorReason;
-    },
-    m_ctx.face.getIoService(), m_ctx.getTimeout());
-  m_ctx.face.processEvents();
+  std::string error;
+  std::tie(result, error) = nfdc::canonize(m_ctx, uri);
 
-  return result;
+  if (result) {
+    // Canonization succeeded
+    return result;
+  }
+  else {
+    // Canonization failed
+    std::tie(m_res, m_errorReason) = canonizeErrorHelper(uri, error);
+    return nullopt;
+  }
 }
 
 void
diff --git a/tools/nfdc/find-face.hpp b/tools/nfdc/find-face.hpp
index ea1245b..3a6bba5 100644
--- a/tools/nfdc/find-face.hpp
+++ b/tools/nfdc/find-face.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2018,  Regents of the University of California,
+ * Copyright (c) 2014-2020,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -121,12 +121,8 @@
   printDisambiguation(std::ostream& os, DisambiguationStyle style) const;
 
 private:
-  /** \brief canonize FaceUri
-   *  \return canonical FaceUri if canonization succeeds, input if canonization is unsupported
-   *  \retval nullopt canonization fails; m_errorReason describes the failure
-   */
   optional<FaceUri>
-  canonize(const std::string& fieldName, const FaceUri& input);
+  canonize(const std::string& fieldName, const FaceUri& uri);
 
   /** \brief retrieve FaceStatus from filter
    *  \post m_res == Code::OK and m_results is populated if retrieval succeeds
diff --git a/tools/nfdc/rib-module.cpp b/tools/nfdc/rib-module.cpp
index b41d33a..544da6e 100644
--- a/tools/nfdc/rib-module.cpp
+++ b/tools/nfdc/rib-module.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2018,  Regents of the University of California,
+ * Copyright (c) 2014-2020,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -24,6 +24,8 @@
  */
 
 #include "rib-module.hpp"
+#include "canonizer.hpp"
+#include "face-module.hpp"
 #include "find-face.hpp"
 #include "format-helpers.hpp"
 
@@ -151,18 +153,96 @@
   bool wantCapture = ctx.args.get<bool>("capture", false);
   auto expiresMillis = ctx.args.getOptional<uint64_t>("expires");
 
+  auto registerRoute = [&] (uint64_t faceId) {
+    ControlParameters registerParams;
+    registerParams
+      .setName(prefix)
+      .setFaceId(faceId)
+      .setOrigin(origin)
+      .setCost(cost)
+      .setFlags((wantChildInherit ? ndn::nfd::ROUTE_FLAG_CHILD_INHERIT : ndn::nfd::ROUTE_FLAGS_NONE) |
+                (wantCapture ? ndn::nfd::ROUTE_FLAG_CAPTURE : ndn::nfd::ROUTE_FLAGS_NONE));
+    if (expiresMillis) {
+      registerParams.setExpirationPeriod(time::milliseconds(*expiresMillis));
+    }
+
+    ctx.controller.start<ndn::nfd::RibRegisterCommand>(
+      registerParams,
+      [&] (const ControlParameters& resp) {
+        ctx.exitCode = static_cast<int>(FindFace::Code::OK);
+        ctx.out << "route-add-accepted ";
+        text::ItemAttributes ia;
+        ctx.out << ia("prefix") << resp.getName()
+                << ia("nexthop") << resp.getFaceId()
+                << ia("origin") << resp.getOrigin()
+                << ia("cost") << resp.getCost()
+                << ia("flags") << static_cast<ndn::nfd::RouteFlags>(resp.getFlags());
+        if (resp.hasExpirationPeriod()) {
+          ctx.out << ia("expires") << text::formatDuration<time::milliseconds>(resp.getExpirationPeriod()) << "\n";
+        }
+        else {
+          ctx.out<< ia("expires") << "never\n";
+        }
+      },
+      ctx.makeCommandFailureHandler("adding route"),
+      ctx.makeCommandOptions());
+  };
+
+  auto handleFaceNotFound = [&] {
+    const FaceUri* faceUri = ndn::any_cast<FaceUri>(&nexthop);
+    if (faceUri == nullptr) {
+      ctx.err << "Face not found\n";
+      return;
+    }
+
+    if (faceUri->getScheme() == "ether") {
+      // Unicast Ethernet faces require a LocalUri, which hasn't been provided
+      // Multicast Ethernet faces cannot be created via management (already exist on each interface)
+      ctx.err << "Unable to implicitly create Ethernet faces\n";
+      ctx.err << "Please create the face with 'nfdc face create' before adding the route\n";
+      return;
+    }
+
+    optional<FaceUri> canonized;
+    std::string error;
+    std::tie(canonized, error) = canonize(ctx, *faceUri);
+    if (!canonized) {
+      // Canonization failed
+      auto canonizationError = canonizeErrorHelper(*faceUri, error);
+      ctx.exitCode = static_cast<int>(canonizationError.first);
+      ctx.err << canonizationError.second << '\n';
+      return;
+    }
+
+    ControlParameters faceCreateParams;
+    faceCreateParams.setUri(canonized->toString());
+
+    ctx.controller.start<ndn::nfd::FaceCreateCommand>(
+      faceCreateParams,
+      [&] (const ControlParameters& resp) {
+        FaceModule::printSuccess(ctx.out, "face-created", resp);
+        registerRoute(resp.getFaceId());
+      },
+      ctx.makeCommandFailureHandler("implicitly creating face"),
+      ctx.makeCommandOptions());
+  };
+
   FindFace findFace(ctx);
   FindFace::Code res = findFace.execute(nexthop);
 
   ctx.exitCode = static_cast<int>(res);
   switch (res) {
     case FindFace::Code::OK:
+      registerRoute(findFace.getFaceId());
       break;
     case FindFace::Code::ERROR:
     case FindFace::Code::CANONIZE_ERROR:
-    case FindFace::Code::NOT_FOUND:
       ctx.err << findFace.getErrorReason() << '\n';
       return;
+    case FindFace::Code::NOT_FOUND:
+      // Attempt to create face if it doesn't exist
+      handleFaceNotFound();
+      break;
     case FindFace::Code::AMBIGUOUS:
       ctx.err << "Multiple faces match specified remote FaceUri. Re-run the command with a FaceId:";
       findFace.printDisambiguation(ctx.err, FindFace::DisambiguationStyle::LOCAL_URI);
@@ -173,38 +253,6 @@
       return;
   }
 
-  ControlParameters registerParams;
-  registerParams
-    .setName(prefix)
-    .setFaceId(findFace.getFaceId())
-    .setOrigin(origin)
-    .setCost(cost)
-    .setFlags((wantChildInherit ? ndn::nfd::ROUTE_FLAG_CHILD_INHERIT : ndn::nfd::ROUTE_FLAGS_NONE) |
-              (wantCapture ? ndn::nfd::ROUTE_FLAG_CAPTURE : ndn::nfd::ROUTE_FLAGS_NONE));
-  if (expiresMillis) {
-    registerParams.setExpirationPeriod(time::milliseconds(*expiresMillis));
-  }
-
-  ctx.controller.start<ndn::nfd::RibRegisterCommand>(
-    registerParams,
-    [&] (const ControlParameters& resp) {
-      ctx.out << "route-add-accepted ";
-      text::ItemAttributes ia;
-      ctx.out << ia("prefix") << resp.getName()
-              << ia("nexthop") << resp.getFaceId()
-              << ia("origin") << resp.getOrigin()
-              << ia("cost") << resp.getCost()
-              << ia("flags") << static_cast<ndn::nfd::RouteFlags>(resp.getFlags());
-      if (resp.hasExpirationPeriod()) {
-        ctx.out << ia("expires") << text::formatDuration<time::milliseconds>(resp.getExpirationPeriod()) << "\n";
-      }
-      else {
-        ctx.out<< ia("expires") << "never\n";
-      }
-    },
-    ctx.makeCommandFailureHandler("adding route"),
-    ctx.makeCommandOptions());
-
   ctx.face.processEvents();
 }