fw: add strategy parameters to configure exponential retx suppression

Supported by ASF, BestRoute, and Multicast strategies

Refs: #4924
Change-Id: I215d9212d90b93fa622cc65278703dc5198d0c9d
diff --git a/daemon/fw/asf-probing-module.cpp b/daemon/fw/asf-probing-module.cpp
index af29a1b..425b39c 100644
--- a/daemon/fw/asf-probing-module.cpp
+++ b/daemon/fw/asf-probing-module.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2021,  Regents of the University of California,
+ * Copyright (c) 2014-2022,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -171,10 +171,10 @@
 }
 
 void
-ProbingModule::setProbingInterval(size_t probingInterval)
+ProbingModule::setProbingInterval(time::milliseconds probingInterval)
 {
-  if (time::milliseconds(probingInterval) >= MIN_PROBING_INTERVAL) {
-    m_probingInterval = time::milliseconds(probingInterval);
+  if (probingInterval >= MIN_PROBING_INTERVAL) {
+    m_probingInterval = probingInterval;
   }
   else {
     NDN_THROW(std::invalid_argument("Probing interval must be >= " +
diff --git a/daemon/fw/asf-probing-module.hpp b/daemon/fw/asf-probing-module.hpp
index 82e7bad..654b569 100644
--- a/daemon/fw/asf-probing-module.hpp
+++ b/daemon/fw/asf-probing-module.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2021,  Regents of the University of California,
+ * Copyright (c) 2014-2022,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -54,7 +54,7 @@
   afterForwardingProbe(const fib::Entry& fibEntry, const Name& interestName);
 
   void
-  setProbingInterval(size_t probingInterval);
+  setProbingInterval(time::milliseconds probingInterval);
 
   time::milliseconds
   getProbingInterval() const
diff --git a/daemon/fw/asf-strategy.cpp b/daemon/fw/asf-strategy.cpp
index df38099..79181a2 100644
--- a/daemon/fw/asf-strategy.cpp
+++ b/daemon/fw/asf-strategy.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2021,  Regents of the University of California,
+ * Copyright (c) 2014-2022,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -35,28 +35,27 @@
 NFD_LOG_INIT(AsfStrategy);
 NFD_REGISTER_STRATEGY(AsfStrategy);
 
-const time::milliseconds AsfStrategy::RETX_SUPPRESSION_INITIAL(10);
-const time::milliseconds AsfStrategy::RETX_SUPPRESSION_MAX(250);
-
 AsfStrategy::AsfStrategy(Forwarder& forwarder, const Name& name)
   : Strategy(forwarder)
   , m_measurements(getMeasurements())
   , m_probing(m_measurements)
-  , m_retxSuppression(RETX_SUPPRESSION_INITIAL,
-                      RetxSuppressionExponential::DEFAULT_MULTIPLIER,
-                      RETX_SUPPRESSION_MAX)
 {
   ParsedInstanceName parsed = parseInstanceName(name);
-  if (!parsed.parameters.empty()) {
-    processParams(parsed.parameters);
-  }
-
   if (parsed.version && *parsed.version != getStrategyName()[-1].toVersion()) {
     NDN_THROW(std::invalid_argument(
       "AsfStrategy does not support version " + to_string(*parsed.version)));
   }
+
+  StrategyParameters params = parseParameters(parsed.parameters);
+  m_retxSuppression = RetxSuppressionExponential::construct(params);
+  auto probingInterval = params.getOrDefault<time::milliseconds::rep>("probing-interval",
+                                                                      m_probing.getProbingInterval().count());
+  m_probing.setProbingInterval(time::milliseconds(probingInterval));
+  m_nMaxTimeouts = params.getOrDefault<size_t>("max-timeouts", m_nMaxTimeouts);
+
   this->setInstanceName(makeInstanceName(name, getStrategyName()));
 
+  NDN_LOG_DEBUG(*m_retxSuppression);
   NFD_LOG_DEBUG("probing-interval=" << m_probing.getProbingInterval()
                 << " max-timeouts=" << m_nMaxTimeouts);
 }
@@ -68,46 +67,6 @@
   return strategyName;
 }
 
-static uint64_t
-getParamValue(const std::string& param, const std::string& value)
-{
-  try {
-    if (!value.empty() && value[0] == '-')
-      NDN_THROW(boost::bad_lexical_cast());
-
-    return boost::lexical_cast<uint64_t>(value);
-  }
-  catch (const boost::bad_lexical_cast&) {
-    NDN_THROW(std::invalid_argument("Value of " + param + " must be a non-negative integer"));
-  }
-}
-
-void
-AsfStrategy::processParams(const PartialName& parsed)
-{
-  for (const auto& component : parsed) {
-    std::string parsedStr(reinterpret_cast<const char*>(component.value()), component.value_size());
-    auto n = parsedStr.find("~");
-    if (n == std::string::npos) {
-      NDN_THROW(std::invalid_argument("Format is <parameter>~<value>"));
-    }
-
-    auto f = parsedStr.substr(0, n);
-    auto s = parsedStr.substr(n + 1);
-    if (f == "probing-interval") {
-      m_probing.setProbingInterval(getParamValue(f, s));
-    }
-    else if (f == "max-timeouts") {
-      m_nMaxTimeouts = getParamValue(f, s);
-      if (m_nMaxTimeouts <= 0)
-        NDN_THROW(std::invalid_argument("max-timeouts should be greater than 0"));
-    }
-    else {
-      NDN_THROW(std::invalid_argument("Parameter should be probing-interval or max-timeouts"));
-    }
-  }
-}
-
 void
 AsfStrategy::afterReceiveInterest(const Interest& interest, const FaceEndpoint& ingress,
                                   const shared_ptr<pit::Entry>& pitEntry)
@@ -131,7 +90,7 @@
 
   auto* faceToUse = getBestFaceForForwarding(interest, ingress.face, fibEntry, pitEntry, false);
   if (faceToUse != nullptr) {
-    auto suppressResult = m_retxSuppression.decidePerUpstream(*pitEntry, *faceToUse);
+    auto suppressResult = m_retxSuppression->decidePerUpstream(*pitEntry, *faceToUse);
     if (suppressResult == RetxSuppressionResult::SUPPRESS) {
       // Cannot be sent on this face, interest was received within the suppression window
       NFD_LOG_DEBUG(interest << " retx-interest from=" << ingress
@@ -143,7 +102,7 @@
       NFD_LOG_DEBUG(interest << " retx-interest from=" << ingress << " forward-to=" << faceToUse->getId());
       auto* outRecord = forwardInterest(interest, *faceToUse, fibEntry, pitEntry);
       if (outRecord && suppressResult == RetxSuppressionResult::FORWARD) {
-        m_retxSuppression.incrementIntervalForOutRecord(*outRecord);
+        m_retxSuppression->incrementIntervalForOutRecord(*outRecord);
       }
     }
     return;
@@ -158,7 +117,7 @@
     return;
   }
   auto& outFace = it->getFace();
-  auto suppressResult = m_retxSuppression.decidePerUpstream(*pitEntry, outFace);
+  auto suppressResult = m_retxSuppression->decidePerUpstream(*pitEntry, outFace);
   if (suppressResult == RetxSuppressionResult::SUPPRESS) {
     NFD_LOG_DEBUG(interest << " retx-interest from=" << ingress
                   << " retry-to=" << outFace.getId() << " suppressed");
@@ -169,7 +128,7 @@
     // were already attached to this face in the previous forwarding
     auto* outRecord = sendInterest(interest, outFace, pitEntry);
     if (outRecord && suppressResult == RetxSuppressionResult::FORWARD) {
-      m_retxSuppression.incrementIntervalForOutRecord(*outRecord);
+      m_retxSuppression->incrementIntervalForOutRecord(*outRecord);
     }
   }
 }
diff --git a/daemon/fw/asf-strategy.hpp b/daemon/fw/asf-strategy.hpp
index af3e4b5..95d1d71 100644
--- a/daemon/fw/asf-strategy.hpp
+++ b/daemon/fw/asf-strategy.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2021,  Regents of the University of California,
+ * Copyright (c) 2014-2022,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -26,10 +26,10 @@
 #ifndef NFD_DAEMON_FW_ASF_STRATEGY_HPP
 #define NFD_DAEMON_FW_ASF_STRATEGY_HPP
 
+#include "strategy.hpp"
 #include "asf-measurements.hpp"
 #include "asf-probing-module.hpp"
-#include "fw/retx-suppression-exponential.hpp"
-#include "fw/strategy.hpp"
+#include "retx-suppression-exponential.hpp"
 
 namespace nfd {
 namespace fw {
@@ -65,9 +65,6 @@
                    const shared_ptr<pit::Entry>& pitEntry) override;
 
 private:
-  void
-  processParams(const PartialName& parsed);
-
   pit::OutRecord*
   forwardInterest(const Interest& interest, Face& outFace, const fib::Entry& fibEntry,
                   const shared_ptr<pit::Entry>& pitEntry);
@@ -89,12 +86,11 @@
 
 private:
   AsfMeasurements m_measurements;
-  ProbingModule m_probing;
-  RetxSuppressionExponential m_retxSuppression;
-  size_t m_nMaxTimeouts = 3;
 
-  static const time::milliseconds RETX_SUPPRESSION_INITIAL;
-  static const time::milliseconds RETX_SUPPRESSION_MAX;
+NFD_PUBLIC_WITH_TESTS_ELSE_PRIVATE:
+  std::unique_ptr<RetxSuppressionExponential> m_retxSuppression;
+  ProbingModule m_probing;
+  size_t m_nMaxTimeouts = 3;
 };
 
 } // namespace asf
diff --git a/daemon/fw/best-route-strategy.cpp b/daemon/fw/best-route-strategy.cpp
index eecc3b5..94f2e29 100644
--- a/daemon/fw/best-route-strategy.cpp
+++ b/daemon/fw/best-route-strategy.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2021,  Regents of the University of California,
+ * Copyright (c) 2014-2022,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -33,25 +33,22 @@
 NFD_LOG_INIT(BestRouteStrategy);
 NFD_REGISTER_STRATEGY(BestRouteStrategy);
 
-const time::milliseconds BestRouteStrategy::RETX_SUPPRESSION_INITIAL(10);
-const time::milliseconds BestRouteStrategy::RETX_SUPPRESSION_MAX(250);
-
 BestRouteStrategy::BestRouteStrategy(Forwarder& forwarder, const Name& name)
   : Strategy(forwarder)
   , ProcessNackTraits(this)
-  , m_retxSuppression(RETX_SUPPRESSION_INITIAL,
-                      RetxSuppressionExponential::DEFAULT_MULTIPLIER,
-                      RETX_SUPPRESSION_MAX)
 {
   ParsedInstanceName parsed = parseInstanceName(name);
-  if (!parsed.parameters.empty()) {
-    NDN_THROW(std::invalid_argument("BestRouteStrategy does not accept parameters"));
-  }
   if (parsed.version && *parsed.version != getStrategyName()[-1].toVersion()) {
     NDN_THROW(std::invalid_argument(
       "BestRouteStrategy does not support version " + to_string(*parsed.version)));
   }
+
+  StrategyParameters params = parseParameters(parsed.parameters);
+  m_retxSuppression = RetxSuppressionExponential::construct(params);
+
   this->setInstanceName(makeInstanceName(name, getStrategyName()));
+
+  NDN_LOG_DEBUG(*m_retxSuppression);
 }
 
 const Name&
@@ -65,7 +62,7 @@
 BestRouteStrategy::afterReceiveInterest(const Interest& interest, const FaceEndpoint& ingress,
                                         const shared_ptr<pit::Entry>& pitEntry)
 {
-  RetxSuppressionResult suppression = m_retxSuppression.decidePerPitEntry(*pitEntry);
+  RetxSuppressionResult suppression = m_retxSuppression->decidePerPitEntry(*pitEntry);
   if (suppression == RetxSuppressionResult::SUPPRESS) {
     NFD_LOG_DEBUG(interest << " from=" << ingress << " suppressed");
     return;
diff --git a/daemon/fw/best-route-strategy.hpp b/daemon/fw/best-route-strategy.hpp
index 3f11c7a..5b4e27d 100644
--- a/daemon/fw/best-route-strategy.hpp
+++ b/daemon/fw/best-route-strategy.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2021,  Regents of the University of California,
+ * Copyright (c) 2014-2022,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -70,9 +70,7 @@
                    const shared_ptr<pit::Entry>& pitEntry) override;
 
 NFD_PUBLIC_WITH_TESTS_ELSE_PRIVATE:
-  static const time::milliseconds RETX_SUPPRESSION_INITIAL;
-  static const time::milliseconds RETX_SUPPRESSION_MAX;
-  RetxSuppressionExponential m_retxSuppression;
+  std::unique_ptr<RetxSuppressionExponential> m_retxSuppression;
 
   friend ProcessNackTraits<BestRouteStrategy>;
 };
diff --git a/daemon/fw/multicast-strategy.cpp b/daemon/fw/multicast-strategy.cpp
index 090a560..f49f60a 100644
--- a/daemon/fw/multicast-strategy.cpp
+++ b/daemon/fw/multicast-strategy.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2021,  Regents of the University of California,
+ * Copyright (c) 2014-2022,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -34,24 +34,21 @@
 
 NFD_LOG_INIT(MulticastStrategy);
 
-const time::milliseconds MulticastStrategy::RETX_SUPPRESSION_INITIAL(10);
-const time::milliseconds MulticastStrategy::RETX_SUPPRESSION_MAX(250);
-
 MulticastStrategy::MulticastStrategy(Forwarder& forwarder, const Name& name)
   : Strategy(forwarder)
-  , m_retxSuppression(RETX_SUPPRESSION_INITIAL,
-                      RetxSuppressionExponential::DEFAULT_MULTIPLIER,
-                      RETX_SUPPRESSION_MAX)
 {
   ParsedInstanceName parsed = parseInstanceName(name);
-  if (!parsed.parameters.empty()) {
-    NDN_THROW(std::invalid_argument("MulticastStrategy does not accept parameters"));
-  }
   if (parsed.version && *parsed.version != getStrategyName()[-1].toVersion()) {
     NDN_THROW(std::invalid_argument(
       "MulticastStrategy does not support version " + to_string(*parsed.version)));
   }
+
+  StrategyParameters params = parseParameters(parsed.parameters);
+  m_retxSuppression = RetxSuppressionExponential::construct(params);
+
   this->setInstanceName(makeInstanceName(name, getStrategyName()));
+
+  NDN_LOG_DEBUG(*m_retxSuppression);
 }
 
 const Name&
@@ -71,7 +68,7 @@
   for (const auto& nexthop : nexthops) {
     Face& outFace = nexthop.getFace();
 
-    RetxSuppressionResult suppressResult = m_retxSuppression.decidePerUpstream(*pitEntry, outFace);
+    RetxSuppressionResult suppressResult = m_retxSuppression->decidePerUpstream(*pitEntry, outFace);
 
     if (suppressResult == RetxSuppressionResult::SUPPRESS) {
       NFD_LOG_DEBUG(interest << " from=" << ingress << " to=" << outFace.getId() << " suppressed");
@@ -85,7 +82,7 @@
     NFD_LOG_DEBUG(interest << " from=" << ingress << " pitEntry-to=" << outFace.getId());
     auto* sentOutRecord = this->sendInterest(interest, outFace, pitEntry);
     if (sentOutRecord && suppressResult == RetxSuppressionResult::FORWARD) {
-      m_retxSuppression.incrementIntervalForOutRecord(*sentOutRecord);
+      m_retxSuppression->incrementIntervalForOutRecord(*sentOutRecord);
     }
   }
 }
diff --git a/daemon/fw/multicast-strategy.hpp b/daemon/fw/multicast-strategy.hpp
index fe122a4..6ff301d 100644
--- a/daemon/fw/multicast-strategy.hpp
+++ b/daemon/fw/multicast-strategy.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2021,  Regents of the University of California,
+ * Copyright (c) 2014-2022,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -51,12 +51,8 @@
   void
   afterNewNextHop(const fib::NextHop& nextHop, const shared_ptr<pit::Entry>& pitEntry) override;
 
-private:
-  RetxSuppressionExponential m_retxSuppression;
-
 NFD_PUBLIC_WITH_TESTS_ELSE_PRIVATE:
-  static const time::milliseconds RETX_SUPPRESSION_INITIAL;
-  static const time::milliseconds RETX_SUPPRESSION_MAX;
+  std::unique_ptr<RetxSuppressionExponential> m_retxSuppression;
 };
 
 } // namespace fw
diff --git a/daemon/fw/retx-suppression-exponential.cpp b/daemon/fw/retx-suppression-exponential.cpp
index 7d0749e..114b491 100644
--- a/daemon/fw/retx-suppression-exponential.cpp
+++ b/daemon/fw/retx-suppression-exponential.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2021,  Regents of the University of California,
+ * Copyright (c) 2014-2022,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -28,11 +28,13 @@
 namespace nfd {
 namespace fw {
 
-const RetxSuppressionExponential::Duration RetxSuppressionExponential::DEFAULT_INITIAL_INTERVAL = 1_ms;
+const RetxSuppressionExponential::Duration RetxSuppressionExponential::DEFAULT_INITIAL_INTERVAL = 10_ms;
 const RetxSuppressionExponential::Duration RetxSuppressionExponential::DEFAULT_MAX_INTERVAL = 250_ms;
 const float RetxSuppressionExponential::DEFAULT_MULTIPLIER = 2.0f;
 
-class RetxSuppressionExponential::PitInfo final : public StrategyInfo
+namespace {
+
+class PitInfo final : public StrategyInfo
 {
 public:
   static constexpr int
@@ -42,7 +44,7 @@
   }
 
   explicit
-  PitInfo(const Duration& initialInterval)
+  PitInfo(const RetxSuppressionExponential::Duration& initialInterval)
     : suppressionInterval(initialInterval)
   {
   }
@@ -51,19 +53,27 @@
   /** \brief if last transmission occurred within suppressionInterval,
    *         retransmission will be suppressed
    */
-  Duration suppressionInterval;
+  RetxSuppressionExponential::Duration suppressionInterval;
 };
 
-RetxSuppressionExponential::RetxSuppressionExponential(const Duration& initialInterval,
-                                                       float multiplier,
-                                                       const Duration& maxInterval)
+} // namespace
+
+RetxSuppressionExponential::RetxSuppressionExponential(Duration initialInterval,
+                                                       Duration maxInterval,
+                                                       float multiplier)
   : m_initialInterval(initialInterval)
-  , m_multiplier(multiplier)
   , m_maxInterval(maxInterval)
+  , m_multiplier(multiplier)
 {
-  BOOST_ASSERT(initialInterval > 0_us);
-  BOOST_ASSERT(multiplier >= 1.0f);
-  BOOST_ASSERT(maxInterval >= initialInterval);
+  if (m_initialInterval <= 0_ns) {
+    NDN_THROW(std::invalid_argument("Retx suppression initial interval must be > 0"));
+  }
+  if (m_maxInterval < m_initialInterval) {
+    NDN_THROW(std::invalid_argument("Retx suppression max interval must be >= initial interval"));
+  }
+  if (m_multiplier < 1.0f) {
+    NDN_THROW(std::invalid_argument("Retx suppression multiplier must be >= 1"));
+  }
 }
 
 RetxSuppressionResult
@@ -123,5 +133,18 @@
     time::duration_cast<Duration>(pi->suppressionInterval * m_multiplier));
 }
 
+std::unique_ptr<RetxSuppressionExponential>
+RetxSuppressionExponential::construct(const StrategyParameters& params)
+{
+  auto init = params.getOrDefault<Duration::rep>("retx-suppression-initial",
+                                                 RetxSuppressionExponential::DEFAULT_INITIAL_INTERVAL.count());
+  auto max = params.getOrDefault<Duration::rep>("retx-suppression-max",
+                                                RetxSuppressionExponential::DEFAULT_MAX_INTERVAL.count());
+  auto mult = params.getOrDefault<float>("retx-suppression-multiplier",
+                                         RetxSuppressionExponential::DEFAULT_MULTIPLIER);
+
+  return make_unique<RetxSuppressionExponential>(Duration(init), Duration(max), mult);
+}
+
 } // namespace fw
 } // namespace nfd
diff --git a/daemon/fw/retx-suppression-exponential.hpp b/daemon/fw/retx-suppression-exponential.hpp
index 8455b5e..3c3be60 100644
--- a/daemon/fw/retx-suppression-exponential.hpp
+++ b/daemon/fw/retx-suppression-exponential.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2017,  Regents of the University of California,
+ * Copyright (c) 2014-2022,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -28,27 +28,30 @@
 
 #include "algorithm.hpp"
 #include "retx-suppression.hpp"
+#include "strategy.hpp"
 
 namespace nfd {
 namespace fw {
 
-/** \brief a retransmission suppression decision algorithm that
- *         suppresses retransmissions using exponential backoff
+/**
+ * \brief A retransmission suppression decision algorithm that suppresses
+ *        retransmissions using exponential backoff.
  *
- *  The i-th retransmission will be suppressed if the last transmission (out-record)
- *  occurred within MIN(initialInterval * multiplier^(i-1), maxInterval)
+ * The i-th retransmission will be suppressed if the last transmission (out-record)
+ * occurred within `MIN(initialInterval * multiplier^(i-1), maxInterval)`.
  */
 class RetxSuppressionExponential
 {
 public:
-  /** \brief time granularity
+  /**
+   * \brief Time granularity.
    */
-  typedef time::microseconds Duration;
+  using Duration = time::milliseconds;
 
   explicit
-  RetxSuppressionExponential(const Duration& initialInterval = DEFAULT_INITIAL_INTERVAL,
-                             float multiplier = DEFAULT_MULTIPLIER,
-                             const Duration& maxInterval = DEFAULT_MAX_INTERVAL);
+  RetxSuppressionExponential(Duration initialInterval = DEFAULT_INITIAL_INTERVAL,
+                             Duration maxInterval = DEFAULT_MAX_INTERVAL,
+                             float multiplier = DEFAULT_MULTIPLIER);
 
   /** \brief determines whether Interest is a retransmission per pit entry
    *         and if so, whether it shall be forwarded or suppressed
@@ -67,20 +70,27 @@
   void
   incrementIntervalForOutRecord(pit::OutRecord& outRecord);
 
-public:
-  /** \brief StrategyInfo on pit::Entry
-   */
-  class PitInfo;
+  static std::unique_ptr<RetxSuppressionExponential>
+  construct(const StrategyParameters& params);
+
+private: // non-member operators (hidden friends)
+  friend std::ostream&
+  operator<<(std::ostream& os, const RetxSuppressionExponential& retxSupp)
+  {
+    return os << "RetxSuppressionExponential initial-interval=" << retxSupp.m_initialInterval
+              << " max-interval=" << retxSupp.m_maxInterval
+              << " multiplier=" << retxSupp.m_multiplier;
+  }
 
 public:
   static const Duration DEFAULT_INITIAL_INTERVAL;
-  static const float DEFAULT_MULTIPLIER;
   static const Duration DEFAULT_MAX_INTERVAL;
+  static const float DEFAULT_MULTIPLIER;
 
-private:
+NFD_PUBLIC_WITH_TESTS_ELSE_PRIVATE:
   const Duration m_initialInterval;
-  const float m_multiplier;
   const Duration m_maxInterval;
+  const float m_multiplier;
 };
 
 } // namespace fw
diff --git a/daemon/fw/strategy.cpp b/daemon/fw/strategy.cpp
index 7212c52..fa948ee 100644
--- a/daemon/fw/strategy.cpp
+++ b/daemon/fw/strategy.cpp
@@ -140,6 +140,29 @@
   return hasVersion ? input : Name(input).append(strategyName.at(-1));
 }
 
+StrategyParameters
+Strategy::parseParameters(const PartialName& params)
+{
+  StrategyParameters parsed;
+
+  for (const auto& component : params) {
+    auto sep = std::find(component.value_begin(), component.value_end(), '~');
+    if (sep == component.value_end()) {
+      NDN_THROW(std::invalid_argument("Strategy parameters format is (<parameter>~<value>)*"));
+    }
+
+    std::string p(component.value_begin(), sep);
+    std::advance(sep, 1);
+    std::string v(sep, component.value_end());
+    if (p.empty() || v.empty()) {
+      NDN_THROW(std::invalid_argument("Strategy parameter name and value cannot be empty"));
+    }
+    parsed[std::move(p)] = std::move(v);
+  }
+
+  return parsed;
+}
+
 Strategy::Strategy(Forwarder& forwarder)
   : afterAddFace(forwarder.m_faceTable.afterAdd)
   , beforeRemoveFace(forwarder.m_faceTable.beforeRemove)
@@ -299,7 +322,7 @@
   for (const auto& delegation : fh) {
     fibEntry = &fib.findLongestPrefixMatch(delegation);
     if (fibEntry->hasNextHops()) {
-      if (fibEntry->getPrefix().size() == 0) {
+      if (fibEntry->getPrefix().empty()) {
         // in consumer region, return the default route
         NFD_LOG_TRACE("lookupFib inConsumerRegion found=" << fibEntry->getPrefix());
       }
@@ -309,9 +332,9 @@
       }
       return *fibEntry;
     }
-    BOOST_ASSERT(fibEntry->getPrefix().size() == 0); // only ndn:/ FIB entry can have zero nexthop
+    BOOST_ASSERT(fibEntry->getPrefix().empty()); // only ndn:/ FIB entry can have zero nexthop
   }
-  BOOST_ASSERT(fibEntry != nullptr && fibEntry->getPrefix().size() == 0);
+  BOOST_ASSERT(fibEntry != nullptr && fibEntry->getPrefix().empty());
   return *fibEntry; // only occurs if no delegation finds a FIB nexthop
 }
 
diff --git a/daemon/fw/strategy.hpp b/daemon/fw/strategy.hpp
index 93a154b..50c79dc 100644
--- a/daemon/fw/strategy.hpp
+++ b/daemon/fw/strategy.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2021,  Regents of the University of California,
+ * Copyright (c) 2014-2022,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -32,6 +32,8 @@
 namespace nfd {
 namespace fw {
 
+class StrategyParameters;
+
 /**
  * \brief Represents a forwarding strategy
  */
@@ -66,7 +68,7 @@
   canCreate(const Name& instanceName);
 
   /** \return A strategy instance created from \p instanceName
-   *  \retval nullptr if !canCreate(instanceName)
+   *  \retval nullptr if `canCreate(instanceName) == false`
    *  \throw std::invalid_argument strategy type constructor does not accept
    *                               specified version or parameters
    */
@@ -374,9 +376,9 @@
 protected: // instance name
   struct ParsedInstanceName
   {
-    Name strategyName; ///< strategy name without parameters
-    optional<uint64_t> version; ///< whether strategyName contains a version component
-    PartialName parameters; ///< parameter components
+    Name strategyName; ///< Strategy name without parameters
+    optional<uint64_t> version; ///< The strategy version number, if present
+    PartialName parameters; ///< Parameter components, may be empty
   };
 
   /** \brief Parse a strategy instance name
@@ -408,6 +410,15 @@
     m_name = name;
   }
 
+NFD_PUBLIC_WITH_TESTS_ELSE_PROTECTED:
+  /**
+   * \brief Parse strategy parameters encoded in a strategy instance name
+   * \param params encoded parameters, typically obtained from a call to parseInstanceName()
+   * \throw std::invalid_argument the encoding format is invalid or unsupported by this implementation
+   */
+  static StrategyParameters
+  parseParameters(const PartialName& params);
+
 private: // registry
   using CreateFunc = std::function<unique_ptr<Strategy>(Forwarder&, const Name& /*strategyName*/)>;
   using Registry = std::map<Name, CreateFunc>; // indexed by strategy name
@@ -428,6 +439,48 @@
   MeasurementsAccessor m_measurements;
 };
 
+class StrategyParameters : public std::map<std::string, std::string>
+{
+public:
+  // Note: only arithmetic types are supported by getOrDefault() for now
+
+  template<typename T>
+  std::enable_if_t<std::is_signed<T>::value, T>
+  getOrDefault(const key_type& key, const T& defaultVal) const
+  {
+    auto it = find(key);
+    if (it == end()) {
+      return defaultVal;
+    }
+
+    T val{};
+    if (!boost::conversion::try_lexical_convert(it->second, val)) {
+      NDN_THROW(std::invalid_argument(key + " value is malformed"));
+    }
+    return val;
+  }
+
+  template<typename T>
+  std::enable_if_t<std::is_unsigned<T>::value, T>
+  getOrDefault(const key_type& key, const T& defaultVal) const
+  {
+    auto it = find(key);
+    if (it == end()) {
+      return defaultVal;
+    }
+
+    if (it->second.find('-') != std::string::npos) {
+      NDN_THROW(std::invalid_argument(key + " cannot be negative"));
+    }
+
+    T val{};
+    if (!boost::conversion::try_lexical_convert(it->second, val)) {
+      NDN_THROW(std::invalid_argument(key + " value is malformed"));
+    }
+    return val;
+  }
+};
+
 } // namespace fw
 } // namespace nfd
 
diff --git a/docs/manpages/nfd-asf-strategy.rst b/docs/manpages/nfd-asf-strategy.rst
index d168b2a..0847144 100644
--- a/docs/manpages/nfd-asf-strategy.rst
+++ b/docs/manpages/nfd-asf-strategy.rst
@@ -40,13 +40,18 @@
     Use the default values for all parameters.
 
 nfdc strategy set prefix /ndn strategy /localhost/nfd/strategy/asf/v=4/probing-interval~30000
-    Set probing interval to 30 seconds.
+    Set the probing interval to 30 seconds.
 
 nfdc strategy set prefix /ndn strategy /localhost/nfd/strategy/asf/v=4/max-timeouts~5
-    Set max timeouts to 5.
+    Set the maximum number of timeouts to 5.
 
 nfdc strategy set prefix /ndn strategy /localhost/nfd/strategy/asf/v=4/probing-interval~30000/max-timeouts~2
-    Set probing interval to 30 seconds and max timeouts to 2.
+    Set the probing interval to 30 seconds and the maximum number of timeouts to 2.
+
+nfdc strategy set prefix /ndn strategy /localhost/nfd/strategy/asf/v=4/retx-suppression-multiplier~2.5/probing-interval~45000
+    Set the retransmission suppression multiplier to 2.5 and the probing interval
+    to 45 seconds. See :manpage:`nfdc-strategy(1)` for more information on the
+    retransmission suppression parameters.
 
 See also
 --------
diff --git a/docs/manpages/nfdc-strategy.rst b/docs/manpages/nfdc-strategy.rst
index f4a4041..93b0d45 100644
--- a/docs/manpages/nfdc-strategy.rst
+++ b/docs/manpages/nfdc-strategy.rst
@@ -42,6 +42,29 @@
     A name that identifies the forwarding strategy.
     Consult NFD Developer's Guide for a complete list of all implemented strategies.
 
+    For the ASF, BestRoute, and Multicast strategies, the following options may be supplied
+    to configure exponential retransmission suppression by appending them after the strategy
+    name and version number.
+
+    **retx-suppression-initial**
+        Starting duration of the suppression interval within which any retransmitted
+        Interests are considered for suppression. The default value is 10 ms.
+
+        Format: ``retx-suppression-initial~<milliseconds>``
+
+    **retx-suppression-max**
+        Maximum duration of the suppression interval. Must not be smaller than
+        **retx-suppression-initial**. The default value is 250 ms.
+
+        Format: ``retx-suppression-max~<milliseconds>``
+
+    **retx-suppression-multiplier**
+        The suppression interval is increased by this multiplier. The default value is 2.
+
+        Format: ``retx-suppression-multiplier~<float>``
+
+    See :manpage:`nfd-asf-strategy(7)` for details on additional parameters for ASF strategy.
+
 EXIT CODES
 ----------
 0: Success
@@ -66,6 +89,10 @@
 nfdc strategy set prefix /ndn strategy /localhost/nfd/strategy/multicast/v=4
     Set the strategy for the "/ndn" prefix to multicast, version 4.
 
+nfdc strategy set prefix /ndn strategy /localhost/nfd/strategy/multicast/v=4/retx-suppression-initial~20/retx-suppression-max~500
+    Set the strategy for the "/ndn" prefix to multicast, version 4, with retransmission
+    suppression initial interval set to 20 ms and maximum interval set to 500 ms.
+
 nfdc strategy unset prefix /ndn
     Clear the strategy choice for the "/ndn" prefix.
 
diff --git a/tests/daemon/fw/asf-strategy.t.cpp b/tests/daemon/fw/asf-strategy.t.cpp
index 1336b3e..f07e72a 100644
--- a/tests/daemon/fw/asf-strategy.t.cpp
+++ b/tests/daemon/fw/asf-strategy.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2021,  Regents of the University of California,
+ * Copyright (c) 2014-2022,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -541,39 +541,53 @@
   BOOST_CHECK_EQUAL(linkAC->getFace(nodeA).getCounters().nOutInterests, 1);
 }
 
-BOOST_AUTO_TEST_CASE(InstantiationWithParameters)
+BOOST_AUTO_TEST_CASE(Parameters)
 {
   FaceTable faceTable;
   Forwarder forwarder{faceTable};
 
-  auto checkValidity = [&] (std::string parameters, bool isCorrect) {
-    Name strategyName(Name(AsfStrategy::getStrategyName()).append(std::move(parameters)));
-    if (isCorrect) {
-      BOOST_CHECK_NO_THROW(make_unique<AsfStrategy>(forwarder, strategyName));
+  auto checkValidity = [&] (const std::string& parameters, bool isCorrect) {
+    Name strategyName(Name(AsfStrategy::getStrategyName()).append(parameters));
+    std::unique_ptr<AsfStrategy> strategy;
+    BOOST_TEST_CONTEXT(parameters) {
+      if (isCorrect) {
+        strategy = make_unique<AsfStrategy>(forwarder, strategyName);
+        BOOST_CHECK(strategy->m_retxSuppression != nullptr);
+      }
+      else {
+        BOOST_CHECK_THROW(make_unique<AsfStrategy>(forwarder, strategyName), std::invalid_argument);
+      }
     }
-    else {
-      BOOST_CHECK_THROW(make_unique<AsfStrategy>(forwarder, strategyName), std::invalid_argument);
-    }
+    return strategy;
   };
 
-  checkValidity("/probing-interval~30000/max-timeouts~5", true);
-  checkValidity("/max-timeouts~5/probing-interval~30000", true);
-  checkValidity("/probing-interval~30000", true);
-  checkValidity("/max-timeouts~5", true);
-  checkValidity("", true);
+  auto strategy = checkValidity("", true);
+  BOOST_TEST(strategy->m_probing.getProbingInterval() == 60_s);
+  BOOST_TEST(strategy->m_nMaxTimeouts == 3);
+  strategy = checkValidity("/probing-interval~30000/max-timeouts~5", true);
+  BOOST_TEST(strategy->m_probing.getProbingInterval() == 30_s);
+  BOOST_TEST(strategy->m_nMaxTimeouts == 5);
+  strategy = checkValidity("/max-timeouts~5/probing-interval~30000", true);
+  BOOST_TEST(strategy->m_probing.getProbingInterval() == 30_s);
+  BOOST_TEST(strategy->m_nMaxTimeouts == 5);
+  strategy = checkValidity("/probing-interval~1000", true);
+  BOOST_TEST(strategy->m_probing.getProbingInterval() == 1_s);
+  BOOST_TEST(strategy->m_nMaxTimeouts == 3);
+  strategy = checkValidity("/max-timeouts~0", true);
+  BOOST_TEST(strategy->m_probing.getProbingInterval() == 60_s);
+  BOOST_TEST(strategy->m_nMaxTimeouts == 0);
+  BOOST_TEST(strategy->m_retxSuppression->m_initialInterval == RetxSuppressionExponential::DEFAULT_INITIAL_INTERVAL);
+  BOOST_TEST(strategy->m_retxSuppression->m_maxInterval == RetxSuppressionExponential::DEFAULT_MAX_INTERVAL);
+  BOOST_TEST(strategy->m_retxSuppression->m_multiplier == RetxSuppressionExponential::DEFAULT_MULTIPLIER);
 
-  checkValidity("/probing-interval~500", false); // At least 1 seconds
+  checkValidity("/probing-interval~500", false); // minimum is 1 second
   checkValidity("/probing-interval~-5000", false);
-  checkValidity("/max-timeouts~0", false);
-  checkValidity("/max-timeouts~-5", false);
-  checkValidity("/max-timeouts~-5/probing-interval~-30000", false);
-  checkValidity("/max-timeouts", false);
-  checkValidity("/probing-interval~", false);
-  checkValidity("/~1000", false);
+  checkValidity("/max-timeouts~-1", false);
+  checkValidity("/max-timeouts~ -1", false);
+  checkValidity("/max-timeouts~1-0", false);
+  checkValidity("/max-timeouts~1/probing-interval~-30000", false);
   checkValidity("/probing-interval~foo", false);
   checkValidity("/max-timeouts~1~2", false);
-  checkValidity("/foo", false);
-  checkValidity("/foo~42", false);
 }
 
 BOOST_AUTO_TEST_SUITE_END() // TestAsfStrategy
diff --git a/tests/daemon/fw/best-route-strategy.t.cpp b/tests/daemon/fw/best-route-strategy.t.cpp
index 7293762..d829e98 100644
--- a/tests/daemon/fw/best-route-strategy.t.cpp
+++ b/tests/daemon/fw/best-route-strategy.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2021,  Regents of the University of California,
+ * Copyright (c) 2014-2022,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -83,7 +83,7 @@
   shared_ptr<pit::Entry> pitEntry = pit.insert(*interest).first;
 
   const auto TICK = time::duration_cast<time::nanoseconds>(
-                      BestRouteStrategy::RETX_SUPPRESSION_INITIAL * 0.1);
+                      RetxSuppressionExponential::DEFAULT_INITIAL_INTERVAL * 0.1);
 
   // first Interest goes to nexthop with lowest FIB cost,
   // however face1 is downstream so it cannot be used
@@ -114,7 +114,7 @@
     retxFrom4Evt = getScheduler().schedule(TICK * 5, periodicalRetxFrom4);
   };
   periodicalRetxFrom4();
-  this->advanceClocks(TICK, BestRouteStrategy::RETX_SUPPRESSION_MAX * 16);
+  this->advanceClocks(TICK, RetxSuppressionExponential::DEFAULT_MAX_INTERVAL * 16);
   retxFrom4Evt.cancel();
 
   // nexthops for accepted retransmissions: follow FIB cost,
@@ -130,7 +130,7 @@
 
   strategy.sendInterestHistory.clear();
   for (int i = 0; i < 3; ++i) {
-    this->advanceClocks(TICK, BestRouteStrategy::RETX_SUPPRESSION_MAX * 2);
+    this->advanceClocks(TICK, RetxSuppressionExponential::DEFAULT_MAX_INTERVAL * 2);
     pitEntry->insertOrUpdateInRecord(*face5, *interest);
     strategy.afterReceiveInterest(*interest, FaceEndpoint(*face5, 0), pitEntry);
   }
diff --git a/tests/daemon/fw/multicast-strategy.t.cpp b/tests/daemon/fw/multicast-strategy.t.cpp
index a1656e5..dbb8e52 100644
--- a/tests/daemon/fw/multicast-strategy.t.cpp
+++ b/tests/daemon/fw/multicast-strategy.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2021,  Regents of the University of California,
+ * Copyright (c) 2014-2022,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -170,7 +170,8 @@
   BOOST_TEST(didSendInterestTo(*face1));
   BOOST_TEST(didSendInterestTo(*face2));
 
-  const auto TICK = time::duration_cast<time::nanoseconds>(MulticastStrategy::RETX_SUPPRESSION_INITIAL) / 10;
+  const auto TICK = time::duration_cast<time::nanoseconds>(
+                    RetxSuppressionExponential::DEFAULT_INITIAL_INTERVAL) / 10;
 
   // downstream retransmits frequently, but the strategy should not send Interests
   // more often than DEFAULT_MIN_RETX_INTERVAL
@@ -195,7 +196,7 @@
     retxFrom4Evt = getScheduler().schedule(TICK * 5, periodicalRetxFrom4);
   };
   periodicalRetxFrom4();
-  this->advanceClocks(TICK, MulticastStrategy::RETX_SUPPRESSION_MAX * 16);
+  this->advanceClocks(TICK, RetxSuppressionExponential::DEFAULT_MAX_INTERVAL * 16);
   retxFrom4Evt.cancel();
 }
 
@@ -215,7 +216,7 @@
 
 BOOST_AUTO_TEST_CASE(RetxSuppression)
 {
-  const auto suppressPeriod = MulticastStrategy::RETX_SUPPRESSION_INITIAL;
+  const auto suppressPeriod = RetxSuppressionExponential::DEFAULT_INITIAL_INTERVAL;
   BOOST_ASSERT(suppressPeriod >= 8_ms);
 
   // Set up the FIB
diff --git a/tests/daemon/fw/retx-suppression.t.cpp b/tests/daemon/fw/retx-suppression.t.cpp
index 3522ace..3a2cf06 100644
--- a/tests/daemon/fw/retx-suppression.t.cpp
+++ b/tests/daemon/fw/retx-suppression.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-2022,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -95,7 +95,7 @@
   FaceTable faceTable;
   Forwarder forwarder(faceTable);
   Pit& pit = forwarder.getPit();
-  RetxSuppressionExponential rs(10_ms, 3.0, 100_ms);
+  RetxSuppressionExponential rs(10_ms, 100_ms, 3.0);
 
   shared_ptr<DummyFace> face1 = make_shared<DummyFace>();
   shared_ptr<DummyFace> face2 = make_shared<DummyFace>();
@@ -164,7 +164,7 @@
   FaceTable faceTable;
   Forwarder forwarder(faceTable);
   Pit& pit = forwarder.getPit();
-  RetxSuppressionExponential rs(10_ms, 3.0, 100_ms);
+  RetxSuppressionExponential rs(10_ms, 100_ms, 3.0);
 
   shared_ptr<DummyFace> face1 = make_shared<DummyFace>();
   shared_ptr<DummyFace> face2 = make_shared<DummyFace>();
diff --git a/tests/daemon/fw/strategy-instantiation.t.cpp b/tests/daemon/fw/strategy-instantiation.t.cpp
index 64991d3..3ce458a 100644
--- a/tests/daemon/fw/strategy-instantiation.t.cpp
+++ b/tests/daemon/fw/strategy-instantiation.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2021,  Regents of the University of California,
+ * Copyright (c) 2014-2022,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -74,8 +74,8 @@
 using Tests = boost::mpl::vector<
   Test<AccessStrategy, false, 1>,
   Test<AsfStrategy, true, 4>,
-  Test<BestRouteStrategy, false, 5>,
-  Test<MulticastStrategy, false, 4>,
+  Test<BestRouteStrategy, true, 5>,
+  Test<MulticastStrategy, true, 4>,
   Test<SelfLearningStrategy, false, 1>,
   Test<RandomStrategy, false, 1>
 >;
@@ -120,6 +120,85 @@
   }
 }
 
+template<typename S>
+class SuppressionParametersFixture
+{
+public:
+  std::unique_ptr<S>
+  checkValidity(const std::string& parameters, bool isCorrect)
+  {
+    Name strategyName(Name(S::getStrategyName()).append(parameters));
+    std::unique_ptr<S> strategy;
+    BOOST_TEST_CONTEXT(parameters) {
+      if (isCorrect) {
+        strategy = make_unique<S>(m_forwarder, strategyName);
+        BOOST_CHECK(strategy->m_retxSuppression != nullptr);
+      }
+      else {
+        BOOST_CHECK_THROW(make_unique<S>(m_forwarder, strategyName), std::invalid_argument);
+      }
+    }
+    return strategy;
+  }
+
+private:
+  FaceTable m_faceTable;
+  Forwarder m_forwarder{m_faceTable};
+};
+
+using StrategiesWithRetxSuppressionExponential = boost::mpl::vector<
+  AsfStrategy,
+  BestRouteStrategy,
+  MulticastStrategy
+>;
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(SuppressionParameters, S, StrategiesWithRetxSuppressionExponential,
+                                 SuppressionParametersFixture<S>)
+{
+  auto strategy = this->checkValidity("", true);
+  BOOST_TEST(strategy->m_retxSuppression->m_initialInterval == RetxSuppressionExponential::DEFAULT_INITIAL_INTERVAL);
+  BOOST_TEST(strategy->m_retxSuppression->m_maxInterval == RetxSuppressionExponential::DEFAULT_MAX_INTERVAL);
+  BOOST_TEST(strategy->m_retxSuppression->m_multiplier == RetxSuppressionExponential::DEFAULT_MULTIPLIER);
+
+  strategy = this->checkValidity("/retx-suppression-initial~20", true);
+  BOOST_TEST(strategy->m_retxSuppression->m_initialInterval == 20_ms);
+  BOOST_TEST(strategy->m_retxSuppression->m_maxInterval == RetxSuppressionExponential::DEFAULT_MAX_INTERVAL);
+  BOOST_TEST(strategy->m_retxSuppression->m_multiplier == RetxSuppressionExponential::DEFAULT_MULTIPLIER);
+  this->checkValidity("/retx-suppression-initial~0", false);
+  this->checkValidity("/retx-suppression-initial~20.5", false);
+  this->checkValidity("/retx-suppression-initial~-10", false);
+  this->checkValidity("/retx-suppression-initial~ -5", false);
+  this->checkValidity("/retx-suppression-initial~NaN", false);
+
+  strategy = this->checkValidity("/retx-suppression-max~1000", true);
+  BOOST_TEST(strategy->m_retxSuppression->m_initialInterval == RetxSuppressionExponential::DEFAULT_INITIAL_INTERVAL);
+  BOOST_TEST(strategy->m_retxSuppression->m_maxInterval == 1_s);
+  BOOST_TEST(strategy->m_retxSuppression->m_multiplier == RetxSuppressionExponential::DEFAULT_MULTIPLIER);
+  strategy = this->checkValidity("/retx-suppression-initial~40/retx-suppression-max~500", true);
+  BOOST_TEST(strategy->m_retxSuppression->m_initialInterval == 40_ms);
+  BOOST_TEST(strategy->m_retxSuppression->m_maxInterval == 500_ms);
+  this->checkValidity("/retx-suppression-initial~20/retx-suppression-max~10", false);
+  this->checkValidity("/retx-suppression-max~ 500", false);
+  this->checkValidity("/retx-suppression-max~521.5", false);
+
+  strategy = this->checkValidity("/retx-suppression-multiplier~2.25", true);
+  BOOST_TEST(strategy->m_retxSuppression->m_initialInterval == RetxSuppressionExponential::DEFAULT_INITIAL_INTERVAL);
+  BOOST_TEST(strategy->m_retxSuppression->m_maxInterval == RetxSuppressionExponential::DEFAULT_MAX_INTERVAL);
+  BOOST_TEST(strategy->m_retxSuppression->m_multiplier == 2.25);
+  this->checkValidity("/retx-suppression-multiplier~0", false);
+  this->checkValidity("/retx-suppression-multiplier~0.9", false);
+  this->checkValidity("/retx-suppression-multiplier~-2.1", false);
+  this->checkValidity("/retx-suppression-multiplier~foo", false);
+
+  strategy = this->checkValidity("/retx-suppression-initial~20/retx-suppression-max~500/retx-suppression-multiplier~3",
+                                 true);
+  BOOST_TEST(strategy->m_retxSuppression->m_initialInterval == 20_ms);
+  BOOST_TEST(strategy->m_retxSuppression->m_maxInterval == 500_ms);
+  BOOST_TEST(strategy->m_retxSuppression->m_multiplier == 3);
+
+  this->checkValidity("/foo~42", true); // unknown parameters are ignored
+}
+
 BOOST_AUTO_TEST_SUITE_END() // TestStrategyInstantiation
 BOOST_AUTO_TEST_SUITE_END() // Fw
 
diff --git a/tests/daemon/fw/strategy.t.cpp b/tests/daemon/fw/strategy.t.cpp
index 6f8e09b..82191f7 100644
--- a/tests/daemon/fw/strategy.t.cpp
+++ b/tests/daemon/fw/strategy.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2020,  Regents of the University of California,
+ * Copyright (c) 2014-2022,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -94,13 +94,33 @@
   FaceId id1 = face1->getId();
   FaceId id2 = face2->getId();
 
-  BOOST_CHECK(strategy.getLocalFaces() == std::vector<FaceId>{id2});
+  BOOST_TEST(strategy.getLocalFaces() == std::vector<FaceId>{id2}, boost::test_tools::per_element());
 
   face2->close();
   face1->close();
 
-  BOOST_CHECK((strategy.addedFaces   == std::vector<FaceId>{id1, id2}));
-  BOOST_CHECK((strategy.removedFaces == std::vector<FaceId>{id2, id1}));
+  BOOST_TEST((strategy.addedFaces   == std::vector<FaceId>{id1, id2}), boost::test_tools::per_element());
+  BOOST_TEST((strategy.removedFaces == std::vector<FaceId>{id2, id1}), boost::test_tools::per_element());
+}
+
+BOOST_AUTO_TEST_CASE(ParseParameters)
+{
+  BOOST_TEST(Strategy::parseParameters("").empty());
+  BOOST_TEST(Strategy::parseParameters("/").empty());
+  BOOST_CHECK_THROW(Strategy::parseParameters("/foo"), std::invalid_argument);
+  BOOST_CHECK_THROW(Strategy::parseParameters("/foo~"), std::invalid_argument);
+  BOOST_CHECK_THROW(Strategy::parseParameters("/~bar"), std::invalid_argument);
+  BOOST_CHECK_THROW(Strategy::parseParameters("/~"), std::invalid_argument);
+  BOOST_CHECK_THROW(Strategy::parseParameters("/~~"), std::invalid_argument);
+
+  StrategyParameters expected;
+  expected["foo"] = "bar";
+  BOOST_TEST(Strategy::parseParameters("/foo~bar") == expected);
+  BOOST_CHECK_THROW(Strategy::parseParameters("/foo~bar/42"), std::invalid_argument);
+  expected["the-answer"] = "42";
+  BOOST_TEST(Strategy::parseParameters("/foo~bar/the-answer~42") == expected);
+  expected["foo"] = "foo2";
+  BOOST_TEST(Strategy::parseParameters("/foo~bar/the-answer~42/foo~foo2") == expected);
 }
 
 BOOST_AUTO_TEST_SUITE_END() // TestStrategy
diff --git a/wscript b/wscript
index b08fbf6..647347d 100644
--- a/wscript
+++ b/wscript
@@ -28,8 +28,6 @@
 
 VERSION = '22.02'
 APPNAME = 'nfd'
-BUGREPORT = 'https://redmine.named-data.net/projects/nfd'
-URL = 'https://named-data.net/doc/NFD/'
 GIT_TAG_PREFIX = 'NFD-'
 
 def options(opt):