fw: Add default HopLimit to Interest when missing

Refs: #5171
Change-Id: I5973811b5ca7c7cf5ff344872afa51b253b9cae8
diff --git a/daemon/fw/forwarder.cpp b/daemon/fw/forwarder.cpp
index fea18f6..3683734 100644
--- a/daemon/fw/forwarder.cpp
+++ b/daemon/fw/forwarder.cpp
@@ -40,6 +40,8 @@
 
 NFD_LOG_INIT(Forwarder);
 
+const std::string CFGSEC_FORWARDER = "forwarder";
+
 static Name
 getDefaultStrategyName()
 {
@@ -186,6 +188,11 @@
   NFD_LOG_DEBUG("onContentStoreMiss interest=" << interest.getName());
   ++m_counters.nCsMisses;
 
+  // attach HopLimit if configured and not present in Interest
+  if (m_config.defaultHopLimit > 0 && !interest.getHopLimit()) {
+    const_cast<Interest&>(interest).setHopLimit(m_config.defaultHopLimit);
+  }
+
   // insert in-record
   pitEntry->insertOrUpdateInRecord(ingress.face, interest);
 
@@ -597,4 +604,30 @@
   }
 }
 
+void
+Forwarder::setConfigFile(ConfigFile& configFile)
+{
+  configFile.addSectionHandler(CFGSEC_FORWARDER, bind(&Forwarder::processConfig, this, _1, _2, _3));
+}
+
+void
+Forwarder::processConfig(const ConfigSection& configSection, bool isDryRun, const std::string&)
+{
+  Config config;
+
+  for (const auto& pair : configSection) {
+    const std::string& key = pair.first;
+    if (key == "default_hop_limit") {
+      config.defaultHopLimit = ConfigFile::parseNumber<uint8_t>(pair, CFGSEC_FORWARDER);
+    }
+    else {
+      NDN_THROW(ConfigFile::Error("Unrecognized option " + CFGSEC_FORWARDER + "." + key));
+    }
+  }
+
+  if (!isDryRun) {
+    m_config = config;
+  }
+}
+
 } // namespace nfd
diff --git a/daemon/fw/forwarder.hpp b/daemon/fw/forwarder.hpp
index 6a245de..47c6e03 100644
--- a/daemon/fw/forwarder.hpp
+++ b/daemon/fw/forwarder.hpp
@@ -29,6 +29,7 @@
 #include "face-table.hpp"
 #include "forwarder-counters.hpp"
 #include "unsolicited-data-policy.hpp"
+#include "common/config-file.hpp"
 #include "face/face-endpoint.hpp"
 #include "table/fib.hpp"
 #include "table/pit.hpp"
@@ -125,6 +126,11 @@
     return m_networkRegionTable;
   }
 
+  /** \brief register handler for forwarder section of NFD configuration file
+   */
+  void
+  setConfigFile(ConfigFile& configFile);
+
 NFD_PUBLIC_WITH_TESTS_ELSE_PRIVATE: // pipelines
   /** \brief incoming Interest pipeline
    *  \param interest the incoming Interest, must be well-formed and created with make_shared
@@ -213,6 +219,22 @@
   void
   insertDeadNonceList(pit::Entry& pitEntry, const Face* upstream);
 
+  void
+  processConfig(const ConfigSection& configSection, bool isDryRun,
+                const std::string& filename);
+
+NFD_PUBLIC_WITH_TESTS_ELSE_PRIVATE:
+  /**
+   * \brief Configuration options from "forwarder" section
+   */
+  struct Config
+  {
+    /// Initial value of HopLimit that should be added to Interests that don't have one.
+    /// A value of zero disables the feature.
+    uint8_t defaultHopLimit = 0;
+  };
+  Config m_config;
+
 private:
   ForwarderCounters m_counters;
 
diff --git a/daemon/nfd.cpp b/daemon/nfd.cpp
index e07b5d8..a178fe4 100644
--- a/daemon/nfd.cpp
+++ b/daemon/nfd.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-2021,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -150,6 +150,8 @@
   ConfigFile config(&ignoreRibAndLogSections);
   general::setConfigFile(config);
 
+  m_forwarder->setConfigFile(config);
+
   TablesConfigSection tablesConfig(*m_forwarder);
   tablesConfig.setConfigFile(config);
 
@@ -183,6 +185,8 @@
   ConfigFile config(&ignoreRibAndLogSections);
   general::setConfigFile(config);
 
+  m_forwarder->setConfigFile(config);
+
   TablesConfigSection tablesConfig(*m_forwarder);
   tablesConfig.setConfigFile(config);
 
diff --git a/nfd.conf.sample.in b/nfd.conf.sample.in
index 00f3bf6..09aa5c0 100644
--- a/nfd.conf.sample.in
+++ b/nfd.conf.sample.in
@@ -1,4 +1,4 @@
-; The general section contains settings of nfd process.
+; The general section contains global settings for the nfd process.
 general
 {
   ; Specify a user and/or group for NFD to drop privileges to
@@ -42,14 +42,23 @@
   ; Forwarder INFO
 }
 
+; The forwarder section contains settings that affect the core forwarding behavior of nfd.
+forwarder
+{
+  ; Specify the HopLimit that is added to incoming Interests without a HopLimit element.
+  ; A value of 0 disables adding the HopLimit.
+  ; Must be between 0 and 255. The default is 0.
+  default_hop_limit 0
+}
+
 ; The tables section configures the CS, PIT, FIB, Strategy Choice, and Measurements
 tables
 {
-  ; ContentStore size limit in number of packets
-  ; default is 65536, about 500MB with 8KB packet size
+  ; Content Store capacity limit in number of packets.
+  ; The default is 65536, equivalent to about 500MB with 8KB packet size.
   cs_max_packets 65536
 
-  ; Set the CS replacement policy.
+  ; Content Store replacement policy.
   ; Available policies are: priority_fifo, lru
   cs_policy lru
 
diff --git a/tests/daemon/fw/forwarder.t.cpp b/tests/daemon/fw/forwarder.t.cpp
index 12ced16..5fb2776 100644
--- a/tests/daemon/fw/forwarder.t.cpp
+++ b/tests/daemon/fw/forwarder.t.cpp
@@ -265,6 +265,38 @@
   BOOST_CHECK_EQUAL(faceRemote->sentInterests.size(), 2);
 }
 
+BOOST_AUTO_TEST_CASE(AddDefaultHopLimit)
+{
+  auto face = addFace();
+  auto faceEndpoint = FaceEndpoint(*face, 0);
+  Pit& pit = forwarder.getPit();
+  auto i1 = makeInterest("/A");
+  auto pitA = pit.insert(*i1).first;
+
+  // By default, no HopLimit should be added
+  auto i2 = makeInterest("/A");
+  BOOST_TEST(!i2->getHopLimit().has_value());
+  forwarder.onContentStoreMiss(*i2, faceEndpoint, pitA);
+  BOOST_TEST(!i2->getHopLimit().has_value());
+
+  // Change config value to 10
+  forwarder.m_config.defaultHopLimit = 10;
+
+  // HopLimit should be set to 10 now
+  auto i3 = makeInterest("/A");
+  BOOST_TEST(!i3->getHopLimit().has_value());
+  forwarder.onContentStoreMiss(*i3, faceEndpoint, pitA);
+  BOOST_REQUIRE(i3->getHopLimit().has_value());
+  BOOST_TEST(*i3->getHopLimit() == 10);
+
+  // An existing HopLimit should be preserved
+  auto i4 = makeInterest("/A");
+  i4->setHopLimit(50);
+  forwarder.onContentStoreMiss(*i4, faceEndpoint, pitA);
+  BOOST_REQUIRE(i4->getHopLimit().has_value());
+  BOOST_TEST(*i4->getHopLimit() == 50);
+}
+
 BOOST_AUTO_TEST_CASE(ScopeLocalhostIncoming)
 {
   auto face1 = addFace("dummy://", "dummy://", ndn::nfd::FACE_SCOPE_LOCAL);
@@ -768,6 +800,84 @@
   BOOST_TEST(strategy.afterNewNextHopCalls[1] == "/A");
 }
 
+BOOST_AUTO_TEST_SUITE(ProcessConfig)
+
+BOOST_AUTO_TEST_CASE(DefaultHopLimit)
+{
+  ConfigFile cf;
+  forwarder.setConfigFile(cf);
+
+  std::string config = R"CONFIG(
+    forwarder
+    {
+      default_hop_limit 10
+    }
+  )CONFIG";
+
+  // The default value is 0
+  BOOST_TEST(forwarder.m_config.defaultHopLimit == 0);
+
+  // Dry run parsing should not change the default config
+  cf.parse(config, true, "dummy-config");
+  BOOST_TEST(forwarder.m_config.defaultHopLimit == 0);
+
+  // Check if the actual parsing works
+  cf.parse(config, false, "dummy-config");
+  BOOST_TEST(forwarder.m_config.defaultHopLimit == 10);
+
+  // After removing default_hop_limit from the config file,
+  // the default value of zero should be restored
+  config = R"CONFIG(
+    forwarder
+    {
+    }
+  )CONFIG";
+
+  cf.parse(config, false, "dummy-config");
+  BOOST_TEST(forwarder.m_config.defaultHopLimit == 0);
+}
+
+BOOST_AUTO_TEST_CASE(BadDefaultHopLimit)
+{
+  ConfigFile cf;
+  forwarder.setConfigFile(cf);
+
+  // not a number
+  std::string config = R"CONFIG(
+    forwarder
+    {
+      default_hop_limit hello
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(cf.parse(config, true, "dummy-config"), ConfigFile::Error);
+  BOOST_CHECK_THROW(cf.parse(config, false, "dummy-config"), ConfigFile::Error);
+
+  // negative number
+  config = R"CONFIG(
+    forwarder
+    {
+      default_hop_limit -1
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(cf.parse(config, true, "dummy-config"), ConfigFile::Error);
+  BOOST_CHECK_THROW(cf.parse(config, false, "dummy-config"), ConfigFile::Error);
+
+  // out of range
+  config = R"CONFIG(
+    forwarder
+    {
+      default_hop_limit 256
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(cf.parse(config, true, "dummy-config"), ConfigFile::Error);
+  BOOST_CHECK_THROW(cf.parse(config, false, "dummy-config"), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // ProcessConfig
+
 BOOST_AUTO_TEST_SUITE_END() // TestForwarder
 BOOST_AUTO_TEST_SUITE_END() // Fw