fw: add FaceEndpoint parameter in Forwarding and Strategy API

refs: #4849

Change-Id: Ibe22557488fa83a555fd13d6eb8e03f8d81d0b2b
diff --git a/daemon/fw/access-strategy.cpp b/daemon/fw/access-strategy.cpp
index b07211b..e7e0904 100644
--- a/daemon/fw/access-strategy.cpp
+++ b/daemon/fw/access-strategy.cpp
@@ -56,19 +56,19 @@
 }
 
 void
-AccessStrategy::afterReceiveInterest(const Face& inFace, const Interest& interest,
+AccessStrategy::afterReceiveInterest(const FaceEndpoint& ingress, const Interest& interest,
                                      const shared_ptr<pit::Entry>& pitEntry)
 {
   RetxSuppressionResult suppressResult = m_retxSuppression.decidePerPitEntry(*pitEntry);
   switch (suppressResult) {
   case RetxSuppressionResult::NEW:
-    this->afterReceiveNewInterest(inFace, interest, pitEntry);
+    this->afterReceiveNewInterest(ingress.face, interest, pitEntry);
     break;
   case RetxSuppressionResult::FORWARD:
-    this->afterReceiveRetxInterest(inFace, interest, pitEntry);
+    this->afterReceiveRetxInterest(ingress.face, interest, pitEntry);
     break;
   case RetxSuppressionResult::SUPPRESS:
-    NFD_LOG_DEBUG(interest << " interestFrom " << inFace.getId() << " retx-suppress");
+    NFD_LOG_DEBUG(interest << " interestFrom " << ingress << " retx-suppress");
     break;
   default:
     BOOST_ASSERT(false);
@@ -87,8 +87,7 @@
 
   // has measurements for Interest Name?
   if (mi != nullptr) {
-    NFD_LOG_DEBUG(interest << " interestFrom " << inFace.getId() <<
-                  " new-interest mi=" << miName);
+    NFD_LOG_DEBUG(interest << " interestFrom " << inFace.getId() << " new-interest mi=" << miName);
 
     // send to last working nexthop
     bool isSentToLastNexthop = this->sendToLastNexthop(inFace, interest, pitEntry, *mi, fibEntry);
@@ -98,8 +97,7 @@
     }
   }
   else {
-    NFD_LOG_DEBUG(interest << " interestFrom " << inFace.getId() <<
-                  " new-interest no-mi");
+    NFD_LOG_DEBUG(interest << " interestFrom " << inFace.getId() << " new-interest no-mi");
   }
 
   // no measurements, or last working nexthop unavailable
@@ -148,10 +146,10 @@
   }
 
   RttEstimator::Duration rto = mi.rtt.computeRto();
-  NFD_LOG_DEBUG(pitEntry->getInterest() << " interestTo " << mi.lastNexthop <<
-                " last-nexthop rto=" << time::duration_cast<time::microseconds>(rto).count());
+  NFD_LOG_DEBUG(pitEntry->getInterest() << " interestTo " << mi.lastNexthop
+                << " last-nexthop rto=" << time::duration_cast<time::microseconds>(rto).count());
 
-  this->sendInterest(pitEntry, *outFace, interest);
+  this->sendInterest(pitEntry, FaceEndpoint(*outFace, 0), interest);
 
   // schedule RTO timeout
   PitInfo* pi = pitEntry->insertStrategyInfo<PitInfo>().first;
@@ -171,8 +169,8 @@
 
   Face* inFace = this->getFace(inFaceId);
   if (inFace == nullptr) {
-    NFD_LOG_DEBUG(pitEntry->getInterest() << " timeoutFrom " << firstOutFaceId <<
-                  " inFace-gone " << inFaceId);
+    NFD_LOG_DEBUG(pitEntry->getInterest() << " timeoutFrom " << firstOutFaceId
+                  << " inFace-gone " << inFaceId);
     return;
   }
 
@@ -185,8 +183,8 @@
   const Interest& interest = inRecord->getInterest();
   const fib::Entry& fibEntry = this->lookupFib(*pitEntry);
 
-  NFD_LOG_DEBUG(pitEntry->getInterest() << " timeoutFrom " << firstOutFaceId <<
-                " multicast-except " << firstOutFaceId);
+  NFD_LOG_DEBUG(pitEntry->getInterest() << " timeoutFrom " << firstOutFaceId
+                << " multicast-except " << firstOutFaceId);
   this->multicast(*inFace, interest, pitEntry, fibEntry, firstOutFaceId);
 }
 
@@ -202,9 +200,8 @@
         wouldViolateScope(inFace, interest, outFace)) {
       continue;
     }
-    NFD_LOG_DEBUG(pitEntry->getInterest() << " interestTo " << outFace.getId() <<
-                  " multicast");
-    this->sendInterest(pitEntry, outFace, interest);
+    NFD_LOG_DEBUG(pitEntry->getInterest() << " interestTo " << outFace.getId() << " multicast");
+    this->sendInterest(pitEntry, FaceEndpoint(outFace, 0), interest);
     ++nSent;
   }
   return nSent;
@@ -212,7 +209,7 @@
 
 void
 AccessStrategy::beforeSatisfyInterest(const shared_ptr<pit::Entry>& pitEntry,
-                                      const Face& inFace, const Data& data)
+                                      const FaceEndpoint& ingress, const Data& data)
 {
   PitInfo* pi = pitEntry->getStrategyInfo<PitInfo>();
   if (pi != nullptr) {
@@ -220,22 +217,20 @@
   }
 
   if (!pitEntry->hasInRecords()) { // already satisfied by another upstream
-    NFD_LOG_DEBUG(pitEntry->getInterest() << " dataFrom " << inFace.getId() <<
-                  " not-fastest");
+    NFD_LOG_DEBUG(pitEntry->getInterest() << " dataFrom " << ingress << " not-fastest");
     return;
   }
 
-  auto outRecord = pitEntry->getOutRecord(inFace, 0);
+  auto outRecord = pitEntry->getOutRecord(ingress.face, 0);
   if (outRecord == pitEntry->out_end()) { // no out-record
-    NFD_LOG_DEBUG(pitEntry->getInterest() << " dataFrom " << inFace.getId() <<
-                  " no-out-record");
+    NFD_LOG_DEBUG(pitEntry->getInterest() << " dataFrom " << ingress << " no-out-record");
     return;
   }
 
   auto rtt = time::steady_clock::now() - outRecord->getLastRenewed();
-  NFD_LOG_DEBUG(pitEntry->getInterest() << " dataFrom " << inFace.getId() <<
-                " rtt=" << time::duration_cast<time::microseconds>(rtt).count());
-  this->updateMeasurements(inFace, data, time::duration_cast<RttEstimator::Duration>(rtt));
+  NFD_LOG_DEBUG(pitEntry->getInterest() << " dataFrom " << ingress
+                << " rtt=" << time::duration_cast<time::microseconds>(rtt).count());
+  this->updateMeasurements(ingress.face, data, time::duration_cast<RttEstimator::Duration>(rtt));
 }
 
 void
diff --git a/daemon/fw/access-strategy.hpp b/daemon/fw/access-strategy.hpp
index cdb55e2..2dc1530 100644
--- a/daemon/fw/access-strategy.hpp
+++ b/daemon/fw/access-strategy.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-2019,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -43,6 +43,8 @@
  *     the granularity of this knowledge is the parent of Data Name.
  *  3. Forward subsequent Interests to the last working nexthop.
  *     If it doesn't respond, multicast again.
+ *
+ *  \note This strategy is not EndpointId-aware.
  */
 class AccessStrategy : public Strategy
 {
@@ -55,12 +57,12 @@
 
 public: // triggers
   void
-  afterReceiveInterest(const Face& inFace, const Interest& interest,
+  afterReceiveInterest(const FaceEndpoint& ingress, const Interest& interest,
                        const shared_ptr<pit::Entry>& pitEntry) override;
 
   void
   beforeSatisfyInterest(const shared_ptr<pit::Entry>& pitEntry,
-                        const Face& inFace, const Data& data) override;
+                        const FaceEndpoint& ingress, const Data& data) override;
 
 private: // StrategyInfo
   /** \brief StrategyInfo on PIT entry
diff --git a/daemon/fw/asf-strategy.cpp b/daemon/fw/asf-strategy.cpp
index 67d6944..779daee 100644
--- a/daemon/fw/asf-strategy.cpp
+++ b/daemon/fw/asf-strategy.cpp
@@ -107,7 +107,7 @@
 }
 
 void
-AsfStrategy::afterReceiveInterest(const Face& inFace, const Interest& interest,
+AsfStrategy::afterReceiveInterest(const FaceEndpoint& ingress, const Interest& interest,
                                   const shared_ptr<pit::Entry>& pitEntry)
 {
   // Should the Interest be suppressed?
@@ -118,7 +118,7 @@
   case RetxSuppressionResult::FORWARD:
     break;
   case RetxSuppressionResult::SUPPRESS:
-    NFD_LOG_DEBUG(interest << " from=" << inFace.getId() << " suppressed");
+    NFD_LOG_DEBUG(interest << " from=" << ingress << " suppressed");
     return;
   }
 
@@ -126,15 +126,15 @@
   const fib::NextHopList& nexthops = fibEntry.getNextHops();
 
   if (nexthops.size() == 0) {
-    sendNoRouteNack(inFace, interest, pitEntry);
+    sendNoRouteNack(ingress, interest, pitEntry);
     this->rejectPendingInterest(pitEntry);
     return;
   }
 
-  Face* faceToUse = getBestFaceForForwarding(fibEntry, interest, inFace);
+  Face* faceToUse = getBestFaceForForwarding(fibEntry, interest, ingress.face);
 
   if (faceToUse == nullptr) {
-    sendNoRouteNack(inFace, interest, pitEntry);
+    sendNoRouteNack(ingress, interest, pitEntry);
     this->rejectPendingInterest(pitEntry);
     return;
   }
@@ -145,7 +145,7 @@
 
   // If necessary, send probe
   if (m_probing.isProbingNeeded(fibEntry, interest)) {
-    Face* faceToProbe = m_probing.getFaceToProbe(inFace, interest, fibEntry, *faceToUse);
+    Face* faceToProbe = m_probing.getFaceToProbe(ingress.face, interest, fibEntry, *faceToUse);
 
     if (faceToProbe != nullptr) {
       bool wantNewNonce = true;
@@ -157,7 +157,7 @@
 
 void
 AsfStrategy::beforeSatisfyInterest(const shared_ptr<pit::Entry>& pitEntry,
-                                   const Face& inFace, const Data& data)
+                                   const FaceEndpoint& ingress, const Data& data)
 {
   NamespaceInfo* namespaceInfo = m_measurements.getNamespaceInfo(pitEntry->getName());
 
@@ -167,14 +167,14 @@
   }
 
   // Record the RTT between the Interest out to Data in
-  FaceInfo* faceInfo = namespaceInfo->get(inFace.getId());
+  FaceInfo* faceInfo = namespaceInfo->get(ingress.face.getId());
   if (faceInfo == nullptr) {
     return;
   }
-  faceInfo->recordRtt(pitEntry, inFace);
+  faceInfo->recordRtt(pitEntry, ingress.face);
 
   // Extend lifetime for measurements associated with Face
-  namespaceInfo->extendFaceInfoLifetime(*faceInfo, inFace.getId());
+  namespaceInfo->extendFaceInfoLifetime(*faceInfo, ingress.face.getId());
 
   if (faceInfo->isTimeoutScheduled()) {
     faceInfo->cancelTimeoutEvent(data.getName());
@@ -182,11 +182,11 @@
 }
 
 void
-AsfStrategy::afterReceiveNack(const Face& inFace, const lp::Nack& nack,
+AsfStrategy::afterReceiveNack(const FaceEndpoint& ingress, const lp::Nack& nack,
                               const shared_ptr<pit::Entry>& pitEntry)
 {
-  NFD_LOG_DEBUG("Nack for " << nack.getInterest() << " from=" << inFace.getId() << ": " << nack.getReason());
-  onTimeout(pitEntry->getName(), inFace.getId());
+  NFD_LOG_DEBUG("Nack for " << nack.getInterest() << " from=" << ingress << ": reason=" << nack.getReason());
+  onTimeout(pitEntry->getName(), ingress.face.getId());
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -199,34 +199,35 @@
                              Face& outFace,
                              bool wantNewNonce)
 {
+  auto egress = FaceEndpoint(outFace, 0);
+
   if (wantNewNonce) {
     //Send probe: interest with new Nonce
     Interest probeInterest(interest);
     probeInterest.refreshNonce();
     NFD_LOG_TRACE("Sending probe for " << probeInterest << probeInterest.getNonce()
-                                       << " to FaceId: " << outFace.getId());
-    this->sendInterest(pitEntry, outFace, probeInterest);
+                  << " to: " << egress);
+    this->sendInterest(pitEntry, egress, probeInterest);
   }
   else {
-    this->sendInterest(pitEntry, outFace, interest);
+    this->sendInterest(pitEntry, egress, interest);
   }
 
-  FaceInfo& faceInfo = m_measurements.getOrCreateFaceInfo(fibEntry, interest, outFace.getId());
+  FaceInfo& faceInfo = m_measurements.getOrCreateFaceInfo(fibEntry, interest, egress.face.getId());
 
   // Refresh measurements since Face is being used for forwarding
   NamespaceInfo& namespaceInfo = m_measurements.getOrCreateNamespaceInfo(fibEntry, interest);
-  namespaceInfo.extendFaceInfoLifetime(faceInfo, outFace.getId());
+  namespaceInfo.extendFaceInfoLifetime(faceInfo, egress.face.getId());
 
   if (!faceInfo.isTimeoutScheduled()) {
     // Estimate and schedule timeout
     RttEstimator::Duration timeout = faceInfo.computeRto();
 
-    NFD_LOG_TRACE("Scheduling timeout for " << fibEntry.getPrefix()
-                                            << " FaceId: " << outFace.getId()
-                                            << " in " << time::duration_cast<time::milliseconds>(timeout) << " ms");
+    NFD_LOG_TRACE("Scheduling timeout for " << fibEntry.getPrefix() << " to: " << egress
+                  << " in " << time::duration_cast<time::milliseconds>(timeout) << " ms");
 
     scheduler::EventId id = scheduler::schedule(timeout,
-        bind(&AsfStrategy::onTimeout, this, interest.getName(), outFace.getId()));
+        bind(&AsfStrategy::onTimeout, this, interest.getName(), egress.face.getId()));
 
     faceInfo.setTimeoutEvent(id, interest.getName());
   }
@@ -356,14 +357,14 @@
 }
 
 void
-AsfStrategy::sendNoRouteNack(const Face& inFace, const Interest& interest,
+AsfStrategy::sendNoRouteNack(const FaceEndpoint& ingress, const Interest& interest,
                              const shared_ptr<pit::Entry>& pitEntry)
 {
-  NFD_LOG_DEBUG(interest << " from=" << inFace.getId() << " noNextHop");
+  NFD_LOG_DEBUG(interest << " from=" << ingress << " noNextHop");
 
   lp::NackHeader nackHeader;
   nackHeader.setReason(lp::NackReason::NO_ROUTE);
-  this->sendNack(pitEntry, inFace, nackHeader);
+  this->sendNack(pitEntry, ingress, nackHeader);
 }
 
 } // namespace asf
diff --git a/daemon/fw/asf-strategy.hpp b/daemon/fw/asf-strategy.hpp
index b58e656..2155d94 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-2018,  Regents of the University of California,
+ * Copyright (c) 2014-2019,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -40,6 +40,8 @@
  *  \see Vince Lehman, Ashlesh Gawande, Rodrigo Aldecoa, Dmitri Krioukov, Beichuan Zhang, Lixia Zhang, and Lan Wang,
  *       "An Experimental Investigation of Hyperbolic Routing with a Smart Forwarding Plane in NDN,"
  *       NDN Technical Report NDN-0042, 2016. http://named-data.net/techreports.html
+ *
+ *  \note This strategy is not EndpointId-aware.
  */
 class AsfStrategy : public Strategy
 {
@@ -52,15 +54,15 @@
 
 public: // triggers
   void
-  afterReceiveInterest(const Face& inFace, const Interest& interest,
+  afterReceiveInterest(const FaceEndpoint& ingress, const Interest& interest,
                        const shared_ptr<pit::Entry>& pitEntry) override;
 
   void
   beforeSatisfyInterest(const shared_ptr<pit::Entry>& pitEntry,
-                        const Face& inFace, const Data& data) override;
+                        const FaceEndpoint& ingress, const Data& data) override;
 
   void
-  afterReceiveNack(const Face& inFace, const lp::Nack& nack,
+  afterReceiveNack(const FaceEndpoint& ingress, const lp::Nack& nack,
                    const shared_ptr<pit::Entry>& pitEntry) override;
 
 private:
@@ -78,7 +80,7 @@
   onTimeout(const Name& interestName, const FaceId faceId);
 
   void
-  sendNoRouteNack(const Face& inFace, const Interest& interest, const shared_ptr<pit::Entry>& pitEntry);
+  sendNoRouteNack(const FaceEndpoint& ingress, const Interest& interest, const shared_ptr<pit::Entry>& pitEntry);
 
   void
   processParams(const PartialName& parsed);
diff --git a/daemon/fw/best-route-strategy.cpp b/daemon/fw/best-route-strategy.cpp
index dbf410b..658a17a 100644
--- a/daemon/fw/best-route-strategy.cpp
+++ b/daemon/fw/best-route-strategy.cpp
@@ -35,7 +35,7 @@
 }
 
 void
-BestRouteStrategyBase::afterReceiveInterest(const Face& inFace, const Interest& interest,
+BestRouteStrategyBase::afterReceiveInterest(const FaceEndpoint& ingress, const Interest& interest,
                                             const shared_ptr<pit::Entry>& pitEntry)
 {
   if (hasPendingOutRecords(*pitEntry)) {
@@ -46,9 +46,9 @@
   const fib::Entry& fibEntry = this->lookupFib(*pitEntry);
   for (const auto& nexthop : fibEntry.getNextHops()) {
     Face& outFace = nexthop.getFace();
-    if (!wouldViolateScope(inFace, interest, outFace) &&
+    if (!wouldViolateScope(ingress.face, interest, outFace) &&
         canForwardToLegacy(*pitEntry, outFace)) {
-      this->sendInterest(pitEntry, outFace, interest);
+      this->sendInterest(pitEntry, FaceEndpoint(outFace, 0), interest);
       return;
     }
   }
diff --git a/daemon/fw/best-route-strategy.hpp b/daemon/fw/best-route-strategy.hpp
index 3cef907..e3cc60c 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-2016,  Regents of the University of California,
+/*
+ * Copyright (c) 2014-2019,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -35,7 +35,7 @@
 {
 public:
   void
-  afterReceiveInterest(const Face& inFace, const Interest& interest,
+  afterReceiveInterest(const FaceEndpoint& ingress, const Interest& interest,
                        const shared_ptr<pit::Entry>& pitEntry) override;
 
 protected:
@@ -52,6 +52,8 @@
  *  \note This strategy is superceded by Best Route strategy version 2,
  *        which allows consumer retransmissions. This version is kept for
  *        comparison purposes and is not recommended for general usage.
+ *
+ *  \note This strategy is not EndpointId-aware.
  */
 class BestRouteStrategy : public BestRouteStrategyBase
 {
diff --git a/daemon/fw/best-route-strategy2.cpp b/daemon/fw/best-route-strategy2.cpp
index 7a1f063..ebab2ef 100644
--- a/daemon/fw/best-route-strategy2.cpp
+++ b/daemon/fw/best-route-strategy2.cpp
@@ -124,13 +124,12 @@
 }
 
 void
-BestRouteStrategy2::afterReceiveInterest(const Face& inFace, const Interest& interest,
+BestRouteStrategy2::afterReceiveInterest(const FaceEndpoint& ingress, const Interest& interest,
                                          const shared_ptr<pit::Entry>& pitEntry)
 {
   RetxSuppressionResult suppression = m_retxSuppression.decidePerPitEntry(*pitEntry);
   if (suppression == RetxSuppressionResult::SUPPRESS) {
-    NFD_LOG_DEBUG(interest << " from=" << inFace.getId()
-                           << " suppressed");
+    NFD_LOG_DEBUG(interest << " from=" << ingress << " suppressed");
     return;
   }
 
@@ -141,58 +140,55 @@
   if (suppression == RetxSuppressionResult::NEW) {
     // forward to nexthop with lowest cost except downstream
     it = std::find_if(nexthops.begin(), nexthops.end(), [&] (const auto& nexthop) {
-      return isNextHopEligible(inFace, interest, nexthop, pitEntry);
+      return isNextHopEligible(ingress.face, interest, nexthop, pitEntry);
     });
 
     if (it == nexthops.end()) {
-      NFD_LOG_DEBUG(interest << " from=" << inFace.getId() << " noNextHop");
+      NFD_LOG_DEBUG(interest << " from=" << ingress << " noNextHop");
 
       lp::NackHeader nackHeader;
       nackHeader.setReason(lp::NackReason::NO_ROUTE);
-      this->sendNack(pitEntry, inFace, nackHeader);
+      this->sendNack(pitEntry, ingress, nackHeader);
 
       this->rejectPendingInterest(pitEntry);
       return;
     }
 
-    Face& outFace = it->getFace();
-    this->sendInterest(pitEntry, outFace, interest);
-    NFD_LOG_DEBUG(interest << " from=" << inFace.getId()
-                           << " newPitEntry-to=" << outFace.getId());
+    auto egress = FaceEndpoint(it->getFace(), 0);
+    this->sendInterest(pitEntry, egress, interest);
+    NFD_LOG_DEBUG(interest << " from=" << ingress << " newPitEntry-to=" << egress);
     return;
   }
 
   // find an unused upstream with lowest cost except downstream
   it = std::find_if(nexthops.begin(), nexthops.end(), [&] (const auto& nexthop) {
-    return isNextHopEligible(inFace, interest, nexthop, pitEntry, true, time::steady_clock::now());
+    return isNextHopEligible(ingress.face, interest, nexthop, pitEntry, true, time::steady_clock::now());
   });
 
   if (it != nexthops.end()) {
-    Face& outFace = it->getFace();
-    this->sendInterest(pitEntry, outFace, interest);
-    NFD_LOG_DEBUG(interest << " from=" << inFace.getId()
-                           << " retransmit-unused-to=" << outFace.getId());
+    auto egress = FaceEndpoint(it->getFace(), 0);
+    this->sendInterest(pitEntry, egress, interest);
+    NFD_LOG_DEBUG(interest << " from=" << ingress << " retransmit-unused-to=" << egress);
     return;
   }
 
   // find an eligible upstream that is used earliest
-  it = findEligibleNextHopWithEarliestOutRecord(inFace, interest, nexthops, pitEntry);
+  it = findEligibleNextHopWithEarliestOutRecord(ingress.face, interest, nexthops, pitEntry);
   if (it == nexthops.end()) {
-    NFD_LOG_DEBUG(interest << " from=" << inFace.getId() << " retransmitNoNextHop");
+    NFD_LOG_DEBUG(interest << " from=" << ingress << " retransmitNoNextHop");
   }
   else {
-    Face& outFace = it->getFace();
-    this->sendInterest(pitEntry, outFace, interest);
-    NFD_LOG_DEBUG(interest << " from=" << inFace.getId()
-                           << " retransmit-retry-to=" << outFace.getId());
+    auto egress = FaceEndpoint(it->getFace(), 0);
+    this->sendInterest(pitEntry, egress, interest);
+    NFD_LOG_DEBUG(interest << " from=" << ingress << " retransmit-retry-to=" << egress);
   }
 }
 
 void
-BestRouteStrategy2::afterReceiveNack(const Face& inFace, const lp::Nack& nack,
+BestRouteStrategy2::afterReceiveNack(const FaceEndpoint& ingress, const lp::Nack& nack,
                                      const shared_ptr<pit::Entry>& pitEntry)
 {
-  this->processNack(inFace, nack, pitEntry);
+  this->processNack(ingress.face, nack, pitEntry);
 }
 
 } // namespace fw
diff --git a/daemon/fw/best-route-strategy2.hpp b/daemon/fw/best-route-strategy2.hpp
index 8d1f144..598469f 100644
--- a/daemon/fw/best-route-strategy2.hpp
+++ b/daemon/fw/best-route-strategy2.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-2019,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -49,6 +49,8 @@
  *
  *  This strategy returns Nack to all downstreams if all upstreams have returned Nacks.
  *  The reason of the sent Nack equals the least severe reason among received Nacks.
+ *
+ *  \note This strategy is not EndpointId-aware.
  */
 class BestRouteStrategy2 : public Strategy
                          , public ProcessNackTraits<BestRouteStrategy2>
@@ -61,11 +63,11 @@
   getStrategyName();
 
   void
-  afterReceiveInterest(const Face& inFace, const Interest& interest,
+  afterReceiveInterest(const FaceEndpoint& ingress, const Interest& interest,
                        const shared_ptr<pit::Entry>& pitEntry) override;
 
   void
-  afterReceiveNack(const Face& inFace, const lp::Nack& nack,
+  afterReceiveNack(const FaceEndpoint& ingress, const lp::Nack& nack,
                    const shared_ptr<pit::Entry>& pitEntry) override;
 
 PUBLIC_WITH_TESTS_ELSE_PRIVATE:
diff --git a/daemon/fw/face-endpoint.hpp b/daemon/fw/face-endpoint.hpp
new file mode 100644
index 0000000..acfbc2e
--- /dev/null
+++ b/daemon/fw/face-endpoint.hpp
@@ -0,0 +1,57 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2014-2019,  Regents of the University of California,
+ *                           Arizona Board of Regents,
+ *                           Colorado State University,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * NFD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef NFD_DAEMON_FW_FACE_ENDPOINT_HPP
+#define NFD_DAEMON_FW_FACE_ENDPOINT_HPP
+
+#include "face/face.hpp"
+
+namespace nfd {
+
+/** \brief Represents a face-endpoint pair in the forwarder
+ */
+class FaceEndpoint
+{
+public:
+  FaceEndpoint(const Face& face, EndpointId endpoint)
+    : face(const_cast<Face&>(face))
+    , endpoint(endpoint)
+  {
+  }
+
+public:
+  Face& face;
+  const EndpointId endpoint;
+};
+
+inline std::ostream&
+operator<<(std::ostream& os, const FaceEndpoint& fe)
+{
+  return os << "(" << fe.face.getId() << "," << fe.endpoint << ")";
+}
+
+} // namespace nfd
+
+#endif // NFD_DAEMON_FW_FACE_ENDPOINT_HPP
\ No newline at end of file
diff --git a/daemon/fw/forwarder.cpp b/daemon/fw/forwarder.cpp
index f4e5ac9..508a890 100644
--- a/daemon/fw/forwarder.cpp
+++ b/daemon/fw/forwarder.cpp
@@ -53,19 +53,19 @@
   m_faceTable.afterAdd.connect([this] (Face& face) {
     face.afterReceiveInterest.connect(
       [this, &face] (const Interest& interest) {
-        this->startProcessInterest(face, interest);
+        this->startProcessInterest(FaceEndpoint(face, 0), interest);
       });
     face.afterReceiveData.connect(
       [this, &face] (const Data& data) {
-        this->startProcessData(face, data);
+        this->startProcessData(FaceEndpoint(face, 0), data);
       });
     face.afterReceiveNack.connect(
       [this, &face] (const lp::Nack& nack) {
-        this->startProcessNack(face, nack);
+        this->startProcessNack(FaceEndpoint(face, 0), nack);
       });
     face.onDroppedInterest.connect(
       [this, &face] (const Interest& interest) {
-        this->onDroppedInterest(face, interest);
+        this->onDroppedInterest(FaceEndpoint(face, 0), interest);
       });
   });
 
@@ -79,20 +79,19 @@
 Forwarder::~Forwarder() = default;
 
 void
-Forwarder::onIncomingInterest(Face& inFace, const Interest& interest)
+Forwarder::onIncomingInterest(const FaceEndpoint& ingress, const Interest& interest)
 {
   // receive Interest
-  NFD_LOG_DEBUG("onIncomingInterest face=" << inFace.getId() <<
-                " interest=" << interest.getName());
-  interest.setTag(make_shared<lp::IncomingFaceIdTag>(inFace.getId()));
+  NFD_LOG_DEBUG("onIncomingInterest in=" << ingress << " interest=" << interest.getName());
+  interest.setTag(make_shared<lp::IncomingFaceIdTag>(ingress.face.getId()));
   ++m_counters.nInInterests;
 
   // /localhost scope control
-  bool isViolatingLocalhost = inFace.getScope() == ndn::nfd::FACE_SCOPE_NON_LOCAL &&
+  bool isViolatingLocalhost = ingress.face.getScope() == ndn::nfd::FACE_SCOPE_NON_LOCAL &&
                               scope_prefix::LOCALHOST.isPrefixOf(interest.getName());
   if (isViolatingLocalhost) {
-    NFD_LOG_DEBUG("onIncomingInterest face=" << inFace.getId() <<
-                  " interest=" << interest.getName() << " violates /localhost");
+    NFD_LOG_DEBUG("onIncomingInterest in=" << ingress
+                  << " interest=" << interest.getName() << " violates /localhost");
     // (drop)
     return;
   }
@@ -101,15 +100,15 @@
   bool hasDuplicateNonceInDnl = m_deadNonceList.has(interest.getName(), interest.getNonce());
   if (hasDuplicateNonceInDnl) {
     // goto Interest loop pipeline
-    this->onInterestLoop(inFace, interest);
+    this->onInterestLoop(ingress, interest);
     return;
   }
 
   // strip forwarding hint if Interest has reached producer region
   if (!interest.getForwardingHint().empty() &&
       m_networkRegionTable.isInProducerRegion(interest.getForwardingHint())) {
-    NFD_LOG_DEBUG("onIncomingInterest face=" << inFace.getId() <<
-                  " interest=" << interest.getName() << " reaching-producer-region");
+    NFD_LOG_DEBUG("onIncomingInterest in=" << ingress
+                  << " interest=" << interest.getName() << " reaching-producer-region");
     const_cast<Interest&>(interest).setForwardingHint({});
   }
 
@@ -117,49 +116,47 @@
   shared_ptr<pit::Entry> pitEntry = m_pit.insert(interest).first;
 
   // detect duplicate Nonce in PIT entry
-  int dnw = fw::findDuplicateNonce(*pitEntry, interest.getNonce(), inFace);
+  int dnw = fw::findDuplicateNonce(*pitEntry, interest.getNonce(), ingress.face);
   bool hasDuplicateNonceInPit = dnw != fw::DUPLICATE_NONCE_NONE;
-  if (inFace.getLinkType() == ndn::nfd::LINK_TYPE_POINT_TO_POINT) {
+  if (ingress.face.getLinkType() == ndn::nfd::LINK_TYPE_POINT_TO_POINT) {
     // for p2p face: duplicate Nonce from same incoming face is not loop
     hasDuplicateNonceInPit = hasDuplicateNonceInPit && !(dnw & fw::DUPLICATE_NONCE_IN_SAME);
   }
   if (hasDuplicateNonceInPit) {
     // goto Interest loop pipeline
-    this->onInterestLoop(inFace, interest);
+    this->onInterestLoop(ingress, interest);
     return;
   }
 
   // is pending?
   if (!pitEntry->hasInRecords()) {
     m_cs.find(interest,
-              bind(&Forwarder::onContentStoreHit, this, std::ref(inFace), pitEntry, _1, _2),
-              bind(&Forwarder::onContentStoreMiss, this, std::ref(inFace), pitEntry, _1));
+              bind(&Forwarder::onContentStoreHit, this, ingress, pitEntry, _1, _2),
+              bind(&Forwarder::onContentStoreMiss, this, ingress, pitEntry, _1));
   }
   else {
-    this->onContentStoreMiss(inFace, pitEntry, interest);
+    this->onContentStoreMiss(ingress, pitEntry, interest);
   }
 }
 
 void
-Forwarder::onInterestLoop(Face& inFace, const Interest& interest)
+Forwarder::onInterestLoop(const FaceEndpoint& ingress, const Interest& interest)
 {
   // if multi-access or ad hoc face, drop
-  if (inFace.getLinkType() != ndn::nfd::LINK_TYPE_POINT_TO_POINT) {
-    NFD_LOG_DEBUG("onInterestLoop face=" << inFace.getId() <<
-                  " interest=" << interest.getName() <<
-                  " drop");
+  if (ingress.face.getLinkType() != ndn::nfd::LINK_TYPE_POINT_TO_POINT) {
+    NFD_LOG_DEBUG("onInterestLoop in=" << ingress
+                  << " interest=" << interest.getName() << " drop");
     return;
   }
 
-  NFD_LOG_DEBUG("onInterestLoop face=" << inFace.getId() <<
-                " interest=" << interest.getName() <<
-                " send-Nack-duplicate");
+  NFD_LOG_DEBUG("onInterestLoop in=" << ingress << " interest=" << interest.getName()
+                << " send-Nack-duplicate");
 
   // send Nack with reason=DUPLICATE
   // note: Don't enter outgoing Nack pipeline because it needs an in-record.
   lp::Nack nack(interest);
   nack.setReason(lp::NackReason::DUPLICATE);
-  inFace.sendNack(nack);
+  ingress.face.sendNack(nack);
 }
 
 static inline bool
@@ -169,14 +166,14 @@
 }
 
 void
-Forwarder::onContentStoreMiss(const Face& inFace, const shared_ptr<pit::Entry>& pitEntry,
-                              const Interest& interest)
+Forwarder::onContentStoreMiss(const FaceEndpoint& ingress,
+                              const shared_ptr<pit::Entry>& pitEntry, const Interest& interest)
 {
   NFD_LOG_DEBUG("onContentStoreMiss interest=" << interest.getName());
   ++m_counters.nCsMisses;
 
   // insert in-record
-  pitEntry->insertOrUpdateInRecord(const_cast<Face&>(inFace), 0, interest);
+  pitEntry->insertOrUpdateInRecord(ingress.face, ingress.endpoint, interest);
 
   // set PIT expiry timer to the time that the last PIT in-record expires
   auto lastExpiring = std::max_element(pitEntry->in_begin(), pitEntry->in_end(), &compare_InRecord_expiry);
@@ -192,18 +189,18 @@
       NFD_LOG_DEBUG("onContentStoreMiss interest=" << interest.getName() << " nexthop-faceid=" << nextHopFace->getId());
       // go to outgoing Interest pipeline
       // scope control is unnecessary, because privileged app explicitly wants to forward
-      this->onOutgoingInterest(pitEntry, *nextHopFace, interest);
+      this->onOutgoingInterest(pitEntry, FaceEndpoint(*nextHopFace, 0), interest);
     }
     return;
   }
 
   // dispatch to strategy: after incoming Interest
   this->dispatchToStrategy(*pitEntry,
-    [&] (fw::Strategy& strategy) { strategy.afterReceiveInterest(inFace, interest, pitEntry); });
+    [&] (fw::Strategy& strategy) { strategy.afterReceiveInterest(ingress, interest, pitEntry); });
 }
 
 void
-Forwarder::onContentStoreHit(const Face& inFace, const shared_ptr<pit::Entry>& pitEntry,
+Forwarder::onContentStoreHit(const FaceEndpoint& ingress, const shared_ptr<pit::Entry>& pitEntry,
                              const Interest& interest, const Data& data)
 {
   NFD_LOG_DEBUG("onContentStoreHit interest=" << interest.getName());
@@ -220,28 +217,28 @@
 
   // dispatch to strategy: after Content Store hit
   this->dispatchToStrategy(*pitEntry,
-    [&] (fw::Strategy& strategy) { strategy.afterContentStoreHit(pitEntry, inFace, data); });
+    [&] (fw::Strategy& strategy) { strategy.afterContentStoreHit(pitEntry, ingress, data); });
 }
 
 void
-Forwarder::onOutgoingInterest(const shared_ptr<pit::Entry>& pitEntry, Face& outFace, const Interest& interest)
+Forwarder::onOutgoingInterest(const shared_ptr<pit::Entry>& pitEntry,
+                              const FaceEndpoint& egress, const Interest& interest)
 {
-  NFD_LOG_DEBUG("onOutgoingInterest face=" << outFace.getId() <<
-                " interest=" << pitEntry->getName());
+  NFD_LOG_DEBUG("onOutgoingInterest out=" << egress << " interest=" << pitEntry->getName());
 
   // insert out-record
-  pitEntry->insertOrUpdateOutRecord(outFace, 0, interest);
+  pitEntry->insertOrUpdateOutRecord(egress.face, egress.endpoint, interest);
 
   // send Interest
-  outFace.sendInterest(interest);
+  egress.face.sendInterest(interest);
   ++m_counters.nOutInterests;
 }
 
 void
 Forwarder::onInterestFinalize(const shared_ptr<pit::Entry>& pitEntry)
 {
-  NFD_LOG_DEBUG("onInterestFinalize interest=" << pitEntry->getName() <<
-                (pitEntry->isSatisfied ? " satisfied" : " unsatisfied"));
+  NFD_LOG_DEBUG("onInterestFinalize interest=" << pitEntry->getName()
+                << (pitEntry->isSatisfied ? " satisfied" : " unsatisfied"));
 
   // Dead Nonce List insert if necessary
   this->insertDeadNonceList(*pitEntry, 0);
@@ -260,19 +257,18 @@
 }
 
 void
-Forwarder::onIncomingData(Face& inFace, const Data& data)
+Forwarder::onIncomingData(const FaceEndpoint& ingress, const Data& data)
 {
   // receive Data
-  NFD_LOG_DEBUG("onIncomingData face=" << inFace.getId() << " data=" << data.getName());
-  data.setTag(make_shared<lp::IncomingFaceIdTag>(inFace.getId()));
+  NFD_LOG_DEBUG("onIncomingData in=" << ingress << " data=" << data.getName());
+  data.setTag(make_shared<lp::IncomingFaceIdTag>(ingress.face.getId()));
   ++m_counters.nInData;
 
   // /localhost scope control
-  bool isViolatingLocalhost = inFace.getScope() == ndn::nfd::FACE_SCOPE_NON_LOCAL &&
+  bool isViolatingLocalhost = ingress.face.getScope() == ndn::nfd::FACE_SCOPE_NON_LOCAL &&
                               scope_prefix::LOCALHOST.isPrefixOf(data.getName());
   if (isViolatingLocalhost) {
-    NFD_LOG_DEBUG("onIncomingData face=" << inFace.getId() <<
-                  " data=" << data.getName() << " violates /localhost");
+    NFD_LOG_DEBUG("onIncomingData in=" << ingress << " data=" << data.getName() << " violates /localhost");
     // (drop)
     return;
   }
@@ -281,7 +277,7 @@
   pit::DataMatchResult pitMatches = m_pit.findAllDataMatches(data);
   if (pitMatches.size() == 0) {
     // goto Data unsolicited pipeline
-    this->onDataUnsolicited(inFace, data);
+    this->onDataUnsolicited(ingress, data);
     return;
   }
 
@@ -299,22 +295,22 @@
 
     // trigger strategy: after receive Data
     this->dispatchToStrategy(*pitEntry,
-      [&] (fw::Strategy& strategy) { strategy.afterReceiveData(pitEntry, inFace, data); });
+      [&] (fw::Strategy& strategy) { strategy.afterReceiveData(pitEntry, ingress, data); });
 
     // mark PIT satisfied
     pitEntry->isSatisfied = true;
     pitEntry->dataFreshnessPeriod = data.getFreshnessPeriod();
 
     // Dead Nonce List insert if necessary (for out-record of inFace)
-    this->insertDeadNonceList(*pitEntry, &inFace);
+    this->insertDeadNonceList(*pitEntry, &ingress.face);
 
     // delete PIT entry's out-record
-    pitEntry->deleteOutRecord(inFace, 0);
+    pitEntry->deleteOutRecord(ingress.face, ingress.endpoint);
   }
   // when more than one PIT entry is matched, trigger strategy: before satisfy Interest,
   // and send Data to all matched out faces
   else {
-    std::set<Face*> pendingDownstreams;
+    std::set<std::pair<Face*, EndpointId>> pendingDownstreams;
     auto now = time::steady_clock::now();
 
     for (const shared_ptr<pit::Entry>& pitEntry : pitMatches) {
@@ -323,7 +319,7 @@
       // remember pending downstreams
       for (const pit::InRecord& inRecord : pitEntry->getInRecords()) {
         if (inRecord.getExpiry() > now) {
-          pendingDownstreams.insert(&inRecord.getFace());
+          pendingDownstreams.emplace(&inRecord.getFace(), inRecord.getEndpointId());
         }
       }
 
@@ -332,62 +328,60 @@
 
       // invoke PIT satisfy callback
       this->dispatchToStrategy(*pitEntry,
-        [&] (fw::Strategy& strategy) { strategy.beforeSatisfyInterest(pitEntry, inFace, data); });
+        [&] (fw::Strategy& strategy) { strategy.beforeSatisfyInterest(pitEntry, ingress, data); });
 
       // mark PIT satisfied
       pitEntry->isSatisfied = true;
       pitEntry->dataFreshnessPeriod = data.getFreshnessPeriod();
 
       // Dead Nonce List insert if necessary (for out-record of inFace)
-      this->insertDeadNonceList(*pitEntry, &inFace);
+      this->insertDeadNonceList(*pitEntry, &ingress.face);
 
       // clear PIT entry's in and out records
       pitEntry->clearInRecords();
-      pitEntry->deleteOutRecord(inFace, 0);
+      pitEntry->deleteOutRecord(ingress.face, ingress.endpoint);
     }
 
     // foreach pending downstream
-    for (Face* pendingDownstream : pendingDownstreams) {
-      if (pendingDownstream->getId() == inFace.getId() &&
-          pendingDownstream->getLinkType() != ndn::nfd::LINK_TYPE_AD_HOC) {
+    for (const auto& pendingDownstream : pendingDownstreams) {
+      if (pendingDownstream.first->getId() == ingress.face.getId() &&
+          pendingDownstream.second == ingress.endpoint &&
+          pendingDownstream.first->getLinkType() != ndn::nfd::LINK_TYPE_AD_HOC) {
         continue;
       }
       // goto outgoing Data pipeline
-      this->onOutgoingData(data, *pendingDownstream);
+      this->onOutgoingData(data, FaceEndpoint(*pendingDownstream.first, pendingDownstream.second));
     }
   }
 }
 
 void
-Forwarder::onDataUnsolicited(Face& inFace, const Data& data)
+Forwarder::onDataUnsolicited(const FaceEndpoint& ingress, const Data& data)
 {
   // accept to cache?
-  fw::UnsolicitedDataDecision decision = m_unsolicitedDataPolicy->decide(inFace, data);
+  fw::UnsolicitedDataDecision decision = m_unsolicitedDataPolicy->decide(ingress.face, data);
   if (decision == fw::UnsolicitedDataDecision::CACHE) {
     // CS insert
     m_cs.insert(data, true);
   }
 
-  NFD_LOG_DEBUG("onDataUnsolicited face=" << inFace.getId() <<
-                " data=" << data.getName() <<
-                " decision=" << decision);
+  NFD_LOG_DEBUG("onDataUnsolicited in=" << ingress << " data=" << data.getName() << " decision=" << decision);
 }
 
 void
-Forwarder::onOutgoingData(const Data& data, Face& outFace)
+Forwarder::onOutgoingData(const Data& data, FaceEndpoint egress)
 {
-  if (outFace.getId() == face::INVALID_FACEID) {
-    NFD_LOG_WARN("onOutgoingData face=invalid data=" << data.getName());
+  if (egress.face.getId() == face::INVALID_FACEID) {
+    NFD_LOG_WARN("onOutgoingData out=(invalid) data=" << data.getName());
     return;
   }
-  NFD_LOG_DEBUG("onOutgoingData face=" << outFace.getId() << " data=" << data.getName());
+  NFD_LOG_DEBUG("onOutgoingData out=" << egress << " data=" << data.getName());
 
   // /localhost scope control
-  bool isViolatingLocalhost = outFace.getScope() == ndn::nfd::FACE_SCOPE_NON_LOCAL &&
+  bool isViolatingLocalhost = egress.face.getScope() == ndn::nfd::FACE_SCOPE_NON_LOCAL &&
                               scope_prefix::LOCALHOST.isPrefixOf(data.getName());
   if (isViolatingLocalhost) {
-    NFD_LOG_DEBUG("onOutgoingData face=" << outFace.getId() <<
-                  " data=" << data.getName() << " violates /localhost");
+    NFD_LOG_DEBUG("onOutgoingData out=" << egress << " data=" << data.getName() << " violates /localhost");
     // (drop)
     return;
   }
@@ -395,22 +389,21 @@
   // TODO traffic manager
 
   // send Data
-  outFace.sendData(data);
+  egress.face.sendData(data);
   ++m_counters.nOutData;
 }
 
 void
-Forwarder::onIncomingNack(Face& inFace, const lp::Nack& nack)
+Forwarder::onIncomingNack(const FaceEndpoint& ingress, const lp::Nack& nack)
 {
   // receive Nack
-  nack.setTag(make_shared<lp::IncomingFaceIdTag>(inFace.getId()));
+  nack.setTag(make_shared<lp::IncomingFaceIdTag>(ingress.face.getId()));
   ++m_counters.nInNacks;
 
   // if multi-access or ad hoc face, drop
-  if (inFace.getLinkType() != ndn::nfd::LINK_TYPE_POINT_TO_POINT) {
-    NFD_LOG_DEBUG("onIncomingNack face=" << inFace.getId() <<
-                  " nack=" << nack.getInterest().getName() <<
-                  "~" << nack.getReason() << " face-is-multi-access");
+  if (ingress.face.getLinkType() != ndn::nfd::LINK_TYPE_POINT_TO_POINT) {
+    NFD_LOG_DEBUG("onIncomingNack in=" << ingress << " nack=" << nack.getInterest().getName()
+                  << "~" << nack.getReason() << " face-is-multi-access");
     return;
   }
 
@@ -418,34 +411,30 @@
   shared_ptr<pit::Entry> pitEntry = m_pit.find(nack.getInterest());
   // if no PIT entry found, drop
   if (pitEntry == nullptr) {
-    NFD_LOG_DEBUG("onIncomingNack face=" << inFace.getId() <<
-                  " nack=" << nack.getInterest().getName() <<
-                  "~" << nack.getReason() << " no-PIT-entry");
+    NFD_LOG_DEBUG("onIncomingNack in=" << ingress << " nack=" << nack.getInterest().getName()
+                  << "~" << nack.getReason() << " no-PIT-entry");
     return;
   }
 
   // has out-record?
-  pit::OutRecordCollection::iterator outRecord = pitEntry->getOutRecord(inFace, 0);
+  pit::OutRecordCollection::iterator outRecord = pitEntry->getOutRecord(ingress.face, ingress.endpoint);
   // if no out-record found, drop
   if (outRecord == pitEntry->out_end()) {
-    NFD_LOG_DEBUG("onIncomingNack face=" << inFace.getId() <<
-                  " nack=" << nack.getInterest().getName() <<
-                  "~" << nack.getReason() << " no-out-record");
+    NFD_LOG_DEBUG("onIncomingNack in=" << ingress << " nack=" << nack.getInterest().getName()
+                  << "~" << nack.getReason() << " no-out-record");
     return;
   }
 
   // if out-record has different Nonce, drop
   if (nack.getInterest().getNonce() != outRecord->getLastNonce()) {
-    NFD_LOG_DEBUG("onIncomingNack face=" << inFace.getId() <<
-                  " nack=" << nack.getInterest().getName() <<
-                  "~" << nack.getReason() << " wrong-Nonce " <<
-                  nack.getInterest().getNonce() << "!=" << outRecord->getLastNonce());
+    NFD_LOG_DEBUG("onIncomingNack in=" << ingress << " nack=" << nack.getInterest().getName()
+                  << "~" << nack.getReason() << " wrong-Nonce " << nack.getInterest().getNonce()
+                  << "!=" << outRecord->getLastNonce());
     return;
   }
 
-  NFD_LOG_DEBUG("onIncomingNack face=" << inFace.getId() <<
-                " nack=" << nack.getInterest().getName() <<
-                "~" << nack.getReason() << " OK");
+  NFD_LOG_DEBUG("onIncomingNack in=" << ingress << " nack=" << nack.getInterest().getName()
+                << "~" << nack.getReason() << " OK");
 
   // record Nack on out-record
   outRecord->setIncomingNack(nack);
@@ -457,59 +446,59 @@
 
   // trigger strategy: after receive NACK
   this->dispatchToStrategy(*pitEntry,
-    [&] (fw::Strategy& strategy) { strategy.afterReceiveNack(inFace, nack, pitEntry); });
+    [&] (fw::Strategy& strategy) { strategy.afterReceiveNack(ingress, nack, pitEntry); });
 }
 
 void
-Forwarder::onOutgoingNack(const shared_ptr<pit::Entry>& pitEntry, const Face& outFace,
-                          const lp::NackHeader& nack)
+Forwarder::onOutgoingNack(const shared_ptr<pit::Entry>& pitEntry,
+                          const FaceEndpoint& egress, const lp::NackHeader& nack)
 {
-  if (outFace.getId() == face::INVALID_FACEID) {
-    NFD_LOG_WARN("onOutgoingNack face=invalid" <<
-                  " nack=" << pitEntry->getInterest().getName() <<
-                  "~" << nack.getReason() << " no-in-record");
+  if (egress.face.getId() == face::INVALID_FACEID) {
+    NFD_LOG_WARN("onOutgoingNack out=(invalid)"
+                 << " nack=" << pitEntry->getInterest().getName()
+                 << "~" << nack.getReason() << " no-in-record");
     return;
   }
 
   // has in-record?
-  pit::InRecordCollection::iterator inRecord = pitEntry->getInRecord(outFace, 0);
+  pit::InRecordCollection::iterator inRecord = pitEntry->getInRecord(egress.face, egress.endpoint);
 
   // if no in-record found, drop
   if (inRecord == pitEntry->in_end()) {
-    NFD_LOG_DEBUG("onOutgoingNack face=" << outFace.getId() <<
-                  " nack=" << pitEntry->getInterest().getName() <<
-                  "~" << nack.getReason() << " no-in-record");
+    NFD_LOG_DEBUG("onOutgoingNack out=" << egress
+                  << " nack=" << pitEntry->getInterest().getName()
+                  << "~" << nack.getReason() << " no-in-record");
     return;
   }
 
   // if multi-access or ad hoc face, drop
-  if (outFace.getLinkType() != ndn::nfd::LINK_TYPE_POINT_TO_POINT) {
-    NFD_LOG_DEBUG("onOutgoingNack face=" << outFace.getId() <<
-                  " nack=" << pitEntry->getInterest().getName() <<
-                  "~" << nack.getReason() << " face-is-multi-access");
+  if (egress.face.getLinkType() != ndn::nfd::LINK_TYPE_POINT_TO_POINT) {
+    NFD_LOG_DEBUG("onOutgoingNack out=" << egress
+                  << " nack=" << pitEntry->getInterest().getName()
+                  << "~" << nack.getReason() << " face-is-multi-access");
     return;
   }
 
-  NFD_LOG_DEBUG("onOutgoingNack face=" << outFace.getId() <<
-                " nack=" << pitEntry->getInterest().getName() <<
-                "~" << nack.getReason() << " OK");
+  NFD_LOG_DEBUG("onOutgoingNack out=" << egress
+                << " nack=" << pitEntry->getInterest().getName()
+                << "~" << nack.getReason() << " OK");
 
   // create Nack packet with the Interest from in-record
   lp::Nack nackPkt(inRecord->getInterest());
   nackPkt.setHeader(nack);
 
   // erase in-record
-  pitEntry->deleteInRecord(outFace, 0);
+  pitEntry->deleteInRecord(egress.face, egress.endpoint);
 
   // send Nack on face
-  const_cast<Face&>(outFace).sendNack(nackPkt);
+  egress.face.sendNack(nackPkt);
   ++m_counters.nOutNacks;
 }
 
 void
-Forwarder::onDroppedInterest(Face& outFace, const Interest& interest)
+Forwarder::onDroppedInterest(const FaceEndpoint& egress, const Interest& interest)
 {
-  m_strategyChoice.findEffectiveStrategy(interest.getName()).onDroppedInterest(outFace, interest);
+  m_strategyChoice.findEffectiveStrategy(interest.getName()).onDroppedInterest(egress, interest);
 }
 
 void
diff --git a/daemon/fw/forwarder.hpp b/daemon/fw/forwarder.hpp
index bcb3caf..ecc4d72 100644
--- a/daemon/fw/forwarder.hpp
+++ b/daemon/fw/forwarder.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-2019,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -30,6 +30,7 @@
 #include "core/scheduler.hpp"
 #include "forwarder-counters.hpp"
 #include "face-table.hpp"
+#include "face-endpoint.hpp"
 #include "unsolicited-data-policy.hpp"
 #include "table/fib.hpp"
 #include "table/pit.hpp"
@@ -105,33 +106,33 @@
 
 public: // forwarding entrypoints and tables
   /** \brief start incoming Interest processing
-   *  \param face face on which Interest is received
+   *  \param ingress face on which Interest is received and endpoint of the sender
    *  \param interest the incoming Interest, must be well-formed and created with make_shared
    */
   void
-  startProcessInterest(Face& face, const Interest& interest)
+  startProcessInterest(const FaceEndpoint& ingress, const Interest& interest)
   {
-    this->onIncomingInterest(face, interest);
+    this->onIncomingInterest(ingress, interest);
   }
 
   /** \brief start incoming Data processing
-   *  \param face face on which Data is received
+   *  \param ingress face on which Data is received and endpoint of the sender
    *  \param data the incoming Data, must be well-formed and created with make_shared
    */
   void
-  startProcessData(Face& face, const Data& data)
+  startProcessData(const FaceEndpoint& ingress, const Data& data)
   {
-    this->onIncomingData(face, data);
+    this->onIncomingData(ingress, data);
   }
 
   /** \brief start incoming Nack processing
-   *  \param face face on which Nack is received
+   *  \param ingress face on which Nack is received and endpoint of the sender
    *  \param nack the incoming Nack, must be well-formed
    */
   void
-  startProcessNack(Face& face, const lp::Nack& nack)
+  startProcessNack(const FaceEndpoint& ingress, const lp::Nack& nack)
   {
-    this->onIncomingNack(face, nack);
+    this->onIncomingNack(ingress, nack);
   }
 
   NameTree&
@@ -186,28 +187,30 @@
   /** \brief incoming Interest pipeline
    */
   VIRTUAL_WITH_TESTS void
-  onIncomingInterest(Face& inFace, const Interest& interest);
+  onIncomingInterest(const FaceEndpoint& ingress, const Interest& interest);
 
   /** \brief Interest loop pipeline
    */
   VIRTUAL_WITH_TESTS void
-  onInterestLoop(Face& inFace, const Interest& interest);
+  onInterestLoop(const FaceEndpoint& ingress, const Interest& interest);
 
   /** \brief Content Store miss pipeline
   */
   VIRTUAL_WITH_TESTS void
-  onContentStoreMiss(const Face& inFace, const shared_ptr<pit::Entry>& pitEntry, const Interest& interest);
+  onContentStoreMiss(const FaceEndpoint& ingress,
+                     const shared_ptr<pit::Entry>& pitEntry, const Interest& interest);
 
   /** \brief Content Store hit pipeline
   */
   VIRTUAL_WITH_TESTS void
-  onContentStoreHit(const Face& inFace, const shared_ptr<pit::Entry>& pitEntry,
+  onContentStoreHit(const FaceEndpoint& ingress, const shared_ptr<pit::Entry>& pitEntry,
                     const Interest& interest, const Data& data);
 
   /** \brief outgoing Interest pipeline
    */
   VIRTUAL_WITH_TESTS void
-  onOutgoingInterest(const shared_ptr<pit::Entry>& pitEntry, Face& outFace, const Interest& interest);
+  onOutgoingInterest(const shared_ptr<pit::Entry>& pitEntry,
+                     const FaceEndpoint& egress, const Interest& interest);
 
   /** \brief Interest finalize pipeline
    */
@@ -217,30 +220,31 @@
   /** \brief incoming Data pipeline
    */
   VIRTUAL_WITH_TESTS void
-  onIncomingData(Face& inFace, const Data& data);
+  onIncomingData(const FaceEndpoint& ingress, const Data& data);
 
   /** \brief Data unsolicited pipeline
    */
   VIRTUAL_WITH_TESTS void
-  onDataUnsolicited(Face& inFace, const Data& data);
+  onDataUnsolicited(const FaceEndpoint& ingress, const Data& data);
 
   /** \brief outgoing Data pipeline
    */
   VIRTUAL_WITH_TESTS void
-  onOutgoingData(const Data& data, Face& outFace);
+  onOutgoingData(const Data& data, FaceEndpoint egress);
 
   /** \brief incoming Nack pipeline
    */
   VIRTUAL_WITH_TESTS void
-  onIncomingNack(Face& inFace, const lp::Nack& nack);
+  onIncomingNack(const FaceEndpoint& ingress, const lp::Nack& nack);
 
   /** \brief outgoing Nack pipeline
    */
   VIRTUAL_WITH_TESTS void
-  onOutgoingNack(const shared_ptr<pit::Entry>& pitEntry, const Face& outFace, const lp::NackHeader& nack);
+  onOutgoingNack(const shared_ptr<pit::Entry>& pitEntry,
+                 const FaceEndpoint& egress, const lp::NackHeader& nack);
 
   VIRTUAL_WITH_TESTS void
-  onDroppedInterest(Face& outFace, const Interest& interest);
+  onDroppedInterest(const FaceEndpoint& egress, const Interest& interest);
 
 PROTECTED_WITH_TESTS_ELSE_PRIVATE:
   /** \brief set a new expiry timer (now + \p duration) on a PIT entry
diff --git a/daemon/fw/multicast-strategy.cpp b/daemon/fw/multicast-strategy.cpp
index 7732e09..f22514a 100644
--- a/daemon/fw/multicast-strategy.cpp
+++ b/daemon/fw/multicast-strategy.cpp
@@ -63,7 +63,7 @@
 }
 
 void
-MulticastStrategy::afterReceiveInterest(const Face& inFace, const Interest& interest,
+MulticastStrategy::afterReceiveInterest(const FaceEndpoint& ingress, const Interest& interest,
                                         const shared_ptr<pit::Entry>& pitEntry)
 {
   const fib::Entry& fibEntry = this->lookupFib(*pitEntry);
@@ -79,20 +79,18 @@
     RetxSuppressionResult suppressResult = m_retxSuppression.decidePerUpstream(*pitEntry, outFace);
 
     if (suppressResult == RetxSuppressionResult::SUPPRESS) {
-      NFD_LOG_DEBUG(interest << " from=" << inFace.getId()
-                    << "to=" << outFace.getId() << " suppressed");
+      NFD_LOG_DEBUG(interest << " from=" << ingress << " to=" << outFace.getId() << " suppressed");
       isSuppressed = true;
       continue;
     }
 
-    if ((outFace.getId() == inFace.getId() && outFace.getLinkType() != ndn::nfd::LINK_TYPE_AD_HOC) ||
-        wouldViolateScope(inFace, interest, outFace)) {
+    if ((outFace.getId() == ingress.face.getId() && outFace.getLinkType() != ndn::nfd::LINK_TYPE_AD_HOC) ||
+        wouldViolateScope(ingress.face, interest, outFace)) {
       continue;
     }
 
-    this->sendInterest(pitEntry, outFace, interest);
-    NFD_LOG_DEBUG(interest << " from=" << inFace.getId()
-                           << " pitEntry-to=" << outFace.getId());
+    this->sendInterest(pitEntry, FaceEndpoint(outFace, 0), interest);
+    NFD_LOG_DEBUG(interest << " from=" << ingress << " pitEntry-to=" << outFace.getId());
 
     if (suppressResult == RetxSuppressionResult::FORWARD) {
       m_retxSuppression.incrementIntervalForOutRecord(*pitEntry->getOutRecord(outFace, 0));
@@ -101,21 +99,21 @@
   }
 
   if (nEligibleNextHops == 0 && !isSuppressed) {
-    NFD_LOG_DEBUG(interest << " from=" << inFace.getId() << " noNextHop");
+    NFD_LOG_DEBUG(interest << " from=" << ingress << " noNextHop");
 
     lp::NackHeader nackHeader;
     nackHeader.setReason(lp::NackReason::NO_ROUTE);
-    this->sendNack(pitEntry, inFace, nackHeader);
+    this->sendNack(pitEntry, ingress, nackHeader);
 
     this->rejectPendingInterest(pitEntry);
   }
 }
 
 void
-MulticastStrategy::afterReceiveNack(const Face& inFace, const lp::Nack& nack,
+MulticastStrategy::afterReceiveNack(const FaceEndpoint& ingress, const lp::Nack& nack,
                                     const shared_ptr<pit::Entry>& pitEntry)
 {
-  this->processNack(inFace, nack, pitEntry);
+  this->processNack(ingress.face, nack, pitEntry);
 }
 
 } // namespace fw
diff --git a/daemon/fw/multicast-strategy.hpp b/daemon/fw/multicast-strategy.hpp
index d75e3a1..15f0d0c 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-2017,  Regents of the University of California,
+/*
+ * Copyright (c) 2014-2019,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -34,6 +34,8 @@
 namespace fw {
 
 /** \brief a forwarding strategy that forwards Interest to all FIB nexthops
+ *
+ *  \note This strategy is not EndpointId-aware.
  */
 class MulticastStrategy : public Strategy
                         , public ProcessNackTraits<MulticastStrategy>
@@ -46,11 +48,11 @@
   getStrategyName();
 
   void
-  afterReceiveInterest(const Face& inFace, const Interest& interest,
+  afterReceiveInterest(const FaceEndpoint& ingress, const Interest& interest,
                        const shared_ptr<pit::Entry>& pitEntry) override;
 
   void
-  afterReceiveNack(const Face& inFace, const lp::Nack& nack,
+  afterReceiveNack(const FaceEndpoint& ingress, const lp::Nack& nack,
                    const shared_ptr<pit::Entry>& pitEntry) override;
 
 private:
diff --git a/daemon/fw/ncc-strategy.cpp b/daemon/fw/ncc-strategy.cpp
index 778ed2b..a6c1e99 100644
--- a/daemon/fw/ncc-strategy.cpp
+++ b/daemon/fw/ncc-strategy.cpp
@@ -59,7 +59,7 @@
 }
 
 void
-NccStrategy::afterReceiveInterest(const Face& inFace, const Interest& interest,
+NccStrategy::afterReceiveInterest(const FaceEndpoint& ingress, const Interest& interest,
                                   const shared_ptr<pit::Entry>& pitEntry)
 {
   const fib::Entry& fibEntry = this->lookupFib(*pitEntry);
@@ -83,13 +83,13 @@
 
   shared_ptr<Face> bestFace = meInfo.getBestFace();
   if (bestFace != nullptr && fibEntry.hasNextHop(*bestFace, 0) &&
-      !wouldViolateScope(inFace, interest, *bestFace) &&
+      !wouldViolateScope(ingress.face, interest, *bestFace) &&
       canForwardToLegacy(*pitEntry, *bestFace)) {
     // TODO Should we use `randlow = 100 + nrand48(h->seed) % 4096U;` ?
     deferFirst = meInfo.prediction;
     deferRange = time::microseconds((deferFirst.count() + 1) / 2);
     --nUpstreams;
-    this->sendInterest(pitEntry, *bestFace, interest);
+    this->sendInterest(pitEntry, FaceEndpoint(*bestFace, 0), interest);
     pitEntryInfo->bestFaceTimeout = scheduler::schedule(
       meInfo.prediction,
       bind(&NccStrategy::timeoutOnBestFace, this, weak_ptr<pit::Entry>(pitEntry)));
@@ -99,11 +99,11 @@
     auto firstEligibleNexthop = std::find_if(nexthops.begin(), nexthops.end(),
         [&] (const fib::NextHop& nexthop) {
           Face& outFace = nexthop.getFace();
-          return !wouldViolateScope(inFace, interest, outFace) &&
+          return !wouldViolateScope(ingress.face, interest, outFace) &&
                  canForwardToLegacy(*pitEntry, outFace);
         });
     if (firstEligibleNexthop != nexthops.end()) {
-      this->sendInterest(pitEntry, firstEligibleNexthop->getFace(), interest);
+      this->sendInterest(pitEntry, FaceEndpoint(firstEligibleNexthop->getFace(), 0), interest);
     }
     else {
       this->rejectPendingInterest(pitEntry);
@@ -113,7 +113,7 @@
 
   shared_ptr<Face> previousFace = meInfo.previousFace.lock();
   if (previousFace != nullptr && fibEntry.hasNextHop(*previousFace, 0) &&
-      !wouldViolateScope(inFace, interest, *previousFace) &&
+      !wouldViolateScope(ingress.face, interest, *previousFace) &&
       canForwardToLegacy(*pitEntry, *previousFace)) {
     --nUpstreams;
   }
@@ -129,7 +129,7 @@
     pitEntryInfo->maxInterval = deferFirst;
   }
   pitEntryInfo->propagateTimer = scheduler::schedule(deferFirst,
-    bind(&NccStrategy::doPropagate, this, inFace.getId(), weak_ptr<pit::Entry>(pitEntry)));
+    bind(&NccStrategy::doPropagate, this, ingress.face.getId(), weak_ptr<pit::Entry>(pitEntry)));
 }
 
 void
@@ -161,7 +161,7 @@
   if (previousFace != nullptr && fibEntry.hasNextHop(*previousFace, 0) &&
       !wouldViolateScope(*inFace, interest, *previousFace) &&
       canForwardToLegacy(*pitEntry, *previousFace)) {
-    this->sendInterest(pitEntry, *previousFace, interest);
+    this->sendInterest(pitEntry, FaceEndpoint(*previousFace, 0), interest);
   }
 
   bool isForwarded = false;
@@ -170,7 +170,7 @@
     if (!wouldViolateScope(*inFace, interest, face) &&
         canForwardToLegacy(*pitEntry, face)) {
       isForwarded = true;
-      this->sendInterest(pitEntry, face, interest);
+      this->sendInterest(pitEntry, FaceEndpoint(face, 0), interest);
       break;
     }
   }
@@ -208,7 +208,7 @@
 
 void
 NccStrategy::beforeSatisfyInterest(const shared_ptr<pit::Entry>& pitEntry,
-                                   const Face& inFace, const Data& data)
+                                   const FaceEndpoint& ingress, const Data& data)
 {
   if (!pitEntry->hasInRecords()) {
     // PIT entry has already been satisfied (and is now waiting for straggler timer to expire)
@@ -226,7 +226,7 @@
     this->getMeasurements().extendLifetime(*measurementsEntry, MEASUREMENTS_LIFETIME);
 
     MeasurementsEntryInfo& meInfo = this->getMeasurementsEntryInfo(measurementsEntry);
-    meInfo.updateBestFace(inFace);
+    meInfo.updateBestFace(ingress.face);
 
     measurementsEntry = this->getMeasurements().getParent(*measurementsEntry);
   }
@@ -239,7 +239,7 @@
     MeasurementsEntryInfo& meInfo = this->getMeasurementsEntryInfo(pitEntry);
     shared_ptr<Face> bestFace = meInfo.getBestFace();
 
-    if (bestFace.get() == &inFace)
+    if (bestFace.get() == &ingress.face)
       scheduler::cancel(pitEntryInfo->bestFaceTimeout);
   }
 }
diff --git a/daemon/fw/ncc-strategy.hpp b/daemon/fw/ncc-strategy.hpp
index 7e63333..473a87d 100644
--- a/daemon/fw/ncc-strategy.hpp
+++ b/daemon/fw/ncc-strategy.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-2019,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -32,6 +32,8 @@
 namespace fw {
 
 /** \brief a forwarding strategy similar to CCNx 0.7.2
+ *
+ *  \note This strategy is not EndpointId-aware.
  */
 class NccStrategy : public Strategy
 {
@@ -42,13 +44,13 @@
   static const Name&
   getStrategyName();
 
-  virtual void
-  afterReceiveInterest(const Face& inFace, const Interest& interest,
+  void
+  afterReceiveInterest(const FaceEndpoint& ingress, const Interest& interest,
                        const shared_ptr<pit::Entry>& pitEntry) override;
 
-  virtual void
+  void
   beforeSatisfyInterest(const shared_ptr<pit::Entry>& pitEntry,
-                        const Face& inFace, const Data& data) override;
+                        const FaceEndpoint& ingress, const Data& data) override;
 
 PUBLIC_WITH_TESTS_ELSE_PROTECTED:
   /// StrategyInfo on measurements::Entry
@@ -104,7 +106,6 @@
       return 1001;
     }
 
-    virtual
     ~PitEntryInfo() override;
 
   public:
diff --git a/daemon/fw/process-nack-traits.hpp b/daemon/fw/process-nack-traits.hpp
index fe70183..86e31e1 100644
--- a/daemon/fw/process-nack-traits.hpp
+++ b/daemon/fw/process-nack-traits.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-2019,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -81,7 +81,7 @@
   sendNackForProcessNackTraits(const shared_ptr<pit::Entry>& pitEntry, const Face& outFace,
                                const lp::NackHeader& header) override
   {
-    m_strategy->sendNack(pitEntry, outFace, header);
+    m_strategy->sendNack(pitEntry, FaceEndpoint(outFace, 0), header);
   }
 
   void
diff --git a/daemon/fw/self-learning-strategy.cpp b/daemon/fw/self-learning-strategy.cpp
index 92f9ae3..12e6176 100644
--- a/daemon/fw/self-learning-strategy.cpp
+++ b/daemon/fw/self-learning-strategy.cpp
@@ -66,73 +66,74 @@
 }
 
 void
-SelfLearningStrategy::afterReceiveInterest(const Face& inFace, const Interest& interest,
+SelfLearningStrategy::afterReceiveInterest(const FaceEndpoint& ingress, const Interest& interest,
                                            const shared_ptr<pit::Entry>& pitEntry)
 {
   const fib::Entry& fibEntry = this->lookupFib(*pitEntry);
   const fib::NextHopList& nexthops = fibEntry.getNextHops();
 
   bool isNonDiscovery = interest.getTag<lp::NonDiscoveryTag>() != nullptr;
-  auto inRecordInfo = pitEntry->getInRecord(inFace, 0)->insertStrategyInfo<InRecordInfo>().first;
+  auto inRecordInfo = pitEntry->getInRecord(ingress.face, ingress.endpoint)->insertStrategyInfo<InRecordInfo>().first;
   if (isNonDiscovery) { // "non-discovery" Interest
     inRecordInfo->isNonDiscoveryInterest = true;
     if (nexthops.empty()) { // return NACK if no matching FIB entry exists
-      NFD_LOG_DEBUG("NACK non-discovery Interest=" << interest << " from=" << inFace.getId() << " noNextHop");
+      NFD_LOG_DEBUG("NACK non-discovery Interest=" << interest << " from=" << ingress << " noNextHop");
       lp::NackHeader nackHeader;
       nackHeader.setReason(lp::NackReason::NO_ROUTE);
-      this->sendNack(pitEntry, inFace, nackHeader);
+      this->sendNack(pitEntry, ingress, nackHeader);
       this->rejectPendingInterest(pitEntry);
     }
     else { // multicast it if matching FIB entry exists
-      multicastInterest(interest, inFace, pitEntry, nexthops);
+      multicastInterest(interest, ingress.face, pitEntry, nexthops);
     }
   }
   else { // "discovery" Interest
     inRecordInfo->isNonDiscoveryInterest = false;
     if (nexthops.empty()) { // broadcast it if no matching FIB entry exists
-      broadcastInterest(interest, inFace, pitEntry);
+      broadcastInterest(interest, ingress.face, pitEntry);
     }
     else { // multicast it with "non-discovery" mark if matching FIB entry exists
       interest.setTag(make_shared<lp::NonDiscoveryTag>(lp::EmptyValue{}));
-      multicastInterest(interest, inFace, pitEntry, nexthops);
+      multicastInterest(interest, ingress.face, pitEntry, nexthops);
     }
   }
 }
 
 void
 SelfLearningStrategy::afterReceiveData(const shared_ptr<pit::Entry>& pitEntry,
-                                       const Face& inFace, const Data& data)
+                                       const FaceEndpoint& ingress, const Data& data)
 {
-  OutRecordInfo* outRecordInfo = pitEntry->getOutRecord(inFace, 0)->getStrategyInfo<OutRecordInfo>();
+  OutRecordInfo* outRecordInfo = pitEntry->getOutRecord(ingress.face, ingress.endpoint)->getStrategyInfo<OutRecordInfo>();
   if (outRecordInfo && outRecordInfo->isNonDiscoveryInterest) { // outgoing Interest was non-discovery
     if (!needPrefixAnn(pitEntry)) { // no need to attach a PA (common cases)
-      sendDataToAll(pitEntry, inFace, data);
+      sendDataToAll(pitEntry, ingress, data);
     }
     else { // needs a PA (to respond discovery Interest)
-      asyncProcessData(pitEntry, inFace, data);
+      asyncProcessData(pitEntry, ingress.face, data);
     }
   }
   else { // outgoing Interest was discovery
     auto paTag = data.getTag<lp::PrefixAnnouncementTag>();
     if (paTag != nullptr) {
-      addRoute(pitEntry, inFace, data, *paTag->get().getPrefixAnn());
+      addRoute(pitEntry, ingress.face, data, *paTag->get().getPrefixAnn());
     }
     else { // Data contains no PrefixAnnouncement, upstreams do not support self-learning
     }
-    sendDataToAll(pitEntry, inFace, data);
+    sendDataToAll(pitEntry, ingress, data);
   }
 }
 
 void
-SelfLearningStrategy::afterReceiveNack(const Face& inFace, const lp::Nack& nack,
+SelfLearningStrategy::afterReceiveNack(const FaceEndpoint& ingress, const lp::Nack& nack,
                                        const shared_ptr<pit::Entry>& pitEntry)
 {
-  NFD_LOG_DEBUG("Nack for " << nack.getInterest() << " from=" << inFace.getId() << ": " << nack.getReason());
+  NFD_LOG_DEBUG("Nack for " << nack.getInterest() << " from=" << ingress
+                << " reason=" << nack.getReason());
   if (nack.getReason() == lp::NackReason::NO_ROUTE) { // remove FIB entries
     BOOST_ASSERT(this->lookupFib(*pitEntry).hasNextHops());
     NFD_LOG_DEBUG("Send NACK to all downstreams");
     this->sendNacks(pitEntry, nack.getHeader());
-    renewRoute(nack.getInterest().getName(), inFace.getId(), 0_ms);
+    renewRoute(nack.getInterest().getName(), ingress.face.getId(), 0_ms);
   }
 }
 
@@ -145,7 +146,7 @@
         wouldViolateScope(inFace, interest, outFace) || outFace.getScope() == ndn::nfd::FACE_SCOPE_LOCAL) {
       continue;
     }
-    this->sendInterest(pitEntry, outFace, interest);
+    this->sendInterest(pitEntry, FaceEndpoint(outFace, 0), interest);
     pitEntry->getOutRecord(outFace, 0)->insertStrategyInfo<OutRecordInfo>().first->isNonDiscoveryInterest = false;
     NFD_LOG_DEBUG("send discovery Interest=" << interest << " from="
                   << inFace.getId() << " to=" << outFace.getId());
@@ -163,7 +164,7 @@
         wouldViolateScope(inFace, interest, outFace)) {
       continue;
     }
-    this->sendInterest(pitEntry, outFace, interest);
+    this->sendInterest(pitEntry, FaceEndpoint(outFace, 0), interest);
     pitEntry->getOutRecord(outFace, 0)->insertStrategyInfo<OutRecordInfo>().first->isNonDiscoveryInterest = true;
     NFD_LOG_DEBUG("send non-discovery Interest=" << interest << " from="
                   << inFace.getId() << " to=" << outFace.getId());
@@ -188,7 +189,7 @@
             if (pitEntry && inFace) {
               NFD_LOG_DEBUG("found PrefixAnnouncement=" << pa.getAnnouncedName());
               data.setTag(make_shared<lp::PrefixAnnouncementTag>(lp::PrefixAnnouncementHeader(pa)));
-              this->sendDataToAll(pitEntry, *inFace, data);
+              this->sendDataToAll(pitEntry, FaceEndpoint(*inFace, 0), data);
               this->setExpiryTimer(pitEntry, 0_ms);
             }
             else {
diff --git a/daemon/fw/self-learning-strategy.hpp b/daemon/fw/self-learning-strategy.hpp
index e0c3962..7265a9e 100644
--- a/daemon/fw/self-learning-strategy.hpp
+++ b/daemon/fw/self-learning-strategy.hpp
@@ -39,6 +39,8 @@
  *  then unicasts subsequent Interests along the learned path
  *
  *  \see https://redmine.named-data.net/attachments/864/Self-learning-strategy-v1.pdf
+ *
+ *  \note This strategy is not EndpointId-aware
  */
 class SelfLearningStrategy : public Strategy
 {
@@ -79,15 +81,15 @@
 
 public: // triggers
   void
-  afterReceiveInterest(const Face& inFace, const Interest& interest,
+  afterReceiveInterest(const FaceEndpoint& ingress, const Interest& interest,
                        const shared_ptr<pit::Entry>& pitEntry) override;
 
   void
   afterReceiveData(const shared_ptr<pit::Entry>& pitEntry,
-                   const Face& inFace, const Data& data) override;
+                   const FaceEndpoint& ingress, const Data& data) override;
 
   void
-  afterReceiveNack(const Face& inFace, const lp::Nack& nack,
+  afterReceiveNack(const FaceEndpoint& ingress, const lp::Nack& nack,
                    const shared_ptr<pit::Entry>& pitEntry) override;
 
 private: // operations
diff --git a/daemon/fw/strategy.cpp b/daemon/fw/strategy.cpp
index 378e554..54574d7 100644
--- a/daemon/fw/strategy.cpp
+++ b/daemon/fw/strategy.cpp
@@ -96,8 +96,8 @@
   }
 
   unique_ptr<Strategy> instance = found->second(forwarder, instanceName);
-  NFD_LOG_DEBUG("create " << instanceName << " found=" << found->first <<
-                " created=" << instance->getInstanceName());
+  NFD_LOG_DEBUG("create " << instanceName << " found=" << found->first
+                << " created=" << instance->getInstanceName());
   BOOST_ASSERT(!instance->getInstanceName().empty());
   return instance;
 }
@@ -150,100 +150,103 @@
 
 void
 Strategy::beforeSatisfyInterest(const shared_ptr<pit::Entry>& pitEntry,
-                                const Face& inFace, const Data& data)
+                                const FaceEndpoint& ingress, const Data& data)
 {
-  NFD_LOG_DEBUG("beforeSatisfyInterest pitEntry=" << pitEntry->getName() <<
-                " inFace=" << inFace.getId() << " data=" << data.getName());
+  NFD_LOG_DEBUG("beforeSatisfyInterest pitEntry=" << pitEntry->getName()
+                << " in=" << ingress << " data=" << data.getName());
 }
 
 void
 Strategy::afterContentStoreHit(const shared_ptr<pit::Entry>& pitEntry,
-                               const Face& inFace, const Data& data)
+                               const FaceEndpoint& ingress, const Data& data)
 {
-  NFD_LOG_DEBUG("afterContentStoreHit pitEntry=" << pitEntry->getName() <<
-                " inFace=" << inFace.getId() << " data=" << data.getName());
+  NFD_LOG_DEBUG("afterContentStoreHit pitEntry=" << pitEntry->getName()
+                << " in=" << ingress << " data=" << data.getName());
 
-  this->sendData(pitEntry, data, inFace);
+  this->sendData(pitEntry, data, ingress);
 }
 
 void
 Strategy::afterReceiveData(const shared_ptr<pit::Entry>& pitEntry,
-                           const Face& inFace, const Data& data)
+                           const FaceEndpoint& ingress, const Data& data)
 {
-  NFD_LOG_DEBUG("afterReceiveData pitEntry=" << pitEntry->getName() <<
-                " inFace=" << inFace.getId() << " data=" << data.getName());
+  NFD_LOG_DEBUG("afterReceiveData pitEntry=" << pitEntry->getName()
+                << " in=" << ingress << " data=" << data.getName());
 
-  this->beforeSatisfyInterest(pitEntry, inFace, data);
+  this->beforeSatisfyInterest(pitEntry, ingress, data);
 
-  this->sendDataToAll(pitEntry, inFace, data);
+  this->sendDataToAll(pitEntry, ingress, data);
 }
 
 void
-Strategy::afterReceiveNack(const Face& inFace, const lp::Nack& nack,
+Strategy::afterReceiveNack(const FaceEndpoint& ingress, const lp::Nack& nack,
                            const shared_ptr<pit::Entry>& pitEntry)
 {
-  NFD_LOG_DEBUG("afterReceiveNack inFace=" << inFace.getId() <<
-                " pitEntry=" << pitEntry->getName());
+  NFD_LOG_DEBUG("afterReceiveNack in=" << ingress << " pitEntry=" << pitEntry->getName());
 }
 
 void
-Strategy::onDroppedInterest(const Face& outFace, const Interest& interest)
+Strategy::onDroppedInterest(const FaceEndpoint& egress, const Interest& interest)
 {
-  NFD_LOG_DEBUG("onDroppedInterest outFace=" << outFace.getId() << " name=" << interest.getName());
+  NFD_LOG_DEBUG("onDroppedInterest out=" << egress << " name=" << interest.getName());
 }
 
 void
-Strategy::sendData(const shared_ptr<pit::Entry>& pitEntry, const Data& data, const Face& outFace)
+Strategy::sendData(const shared_ptr<pit::Entry>& pitEntry, const Data& data,
+                   const FaceEndpoint& egress)
 {
   BOOST_ASSERT(pitEntry->getInterest().matchesData(data));
 
-  // delete the PIT entry's in-record based on outFace,
-  // since Data is sent to outFace from which the Interest was received
-  pitEntry->deleteInRecord(outFace, 0);
+  // delete the PIT entry's in-record based on egress,
+  // since Data is sent to face and endpoint from which the Interest was received
+  pitEntry->deleteInRecord(egress.face, egress.endpoint);
 
-  m_forwarder.onOutgoingData(data, *const_pointer_cast<Face>(outFace.shared_from_this()));
+  m_forwarder.onOutgoingData(data, egress);
 }
 
 void
-Strategy::sendDataToAll(const shared_ptr<pit::Entry>& pitEntry, const Face& inFace, const Data& data)
+Strategy::sendDataToAll(const shared_ptr<pit::Entry>& pitEntry,
+                        const FaceEndpoint& ingress, const Data& data)
 {
-  std::set<Face*> pendingDownstreams;
+  std::set<std::pair<Face*, EndpointId>> pendingDownstreams;
   auto now = time::steady_clock::now();
 
   // remember pending downstreams
   for (const pit::InRecord& inRecord : pitEntry->getInRecords()) {
     if (inRecord.getExpiry() > now) {
-      if (inRecord.getFace().getId() == inFace.getId() &&
+      if (inRecord.getFace().getId() == ingress.face.getId() &&
+          inRecord.getEndpointId() == ingress.endpoint &&
           inRecord.getFace().getLinkType() != ndn::nfd::LINK_TYPE_AD_HOC) {
         continue;
       }
-      pendingDownstreams.insert(&inRecord.getFace());
+      pendingDownstreams.emplace(&inRecord.getFace(), inRecord.getEndpointId());
     }
   }
 
-  for (const Face* pendingDownstream : pendingDownstreams) {
-    this->sendData(pitEntry, data, *pendingDownstream);
+  for (const auto& pendingDownstream : pendingDownstreams) {
+    this->sendData(pitEntry, data, FaceEndpoint(*pendingDownstream.first, pendingDownstream.second));
   }
 }
 
 void
 Strategy::sendNacks(const shared_ptr<pit::Entry>& pitEntry, const lp::NackHeader& header,
-                    std::initializer_list<const Face*> exceptFaces)
+                    std::initializer_list<FaceEndpoint> exceptFaceEndpoints)
 {
   // populate downstreams with all downstreams faces
-  std::unordered_set<const Face*> downstreams;
+  std::set<std::pair<Face*, EndpointId>> downstreams;
   std::transform(pitEntry->in_begin(), pitEntry->in_end(), std::inserter(downstreams, downstreams.end()),
-                 [] (const pit::InRecord& inR) { return &inR.getFace(); });
+                 [] (const pit::InRecord& inR) {
+                  return std::make_pair(&inR.getFace(), inR.getEndpointId());
+                 });
 
   // delete excluded faces
-  // .erase in a loop is more efficient than std::set_difference because that requires sorted range
-  for (const Face* exceptFace : exceptFaces) {
-    downstreams.erase(exceptFace);
+  for (const auto& exceptFaceEndpoint : exceptFaceEndpoints) {
+    downstreams.erase({&exceptFaceEndpoint.face, exceptFaceEndpoint.endpoint});
   }
 
   // send Nacks
-  for (const Face* downstream : downstreams) {
-    this->sendNack(pitEntry, *downstream, header);
+  for (const auto& downstream : downstreams) {
+    this->sendNack(pitEntry, FaceEndpoint(*downstream.first, downstream.second), header);
   }
   // warning: don't loop on pitEntry->getInRecords(), because in-record is deleted when sending Nack
 }
diff --git a/daemon/fw/strategy.hpp b/daemon/fw/strategy.hpp
index 0df65b9..bb50556 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-2018,  Regents of the University of California,
+ * Copyright (c) 2014-2019,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -138,7 +138,7 @@
    *           may occur. However, the strategy is allowed to store weak_ptr<pit::Entry>.
    */
   virtual void
-  afterReceiveInterest(const Face& inFace, const Interest& interest,
+  afterReceiveInterest(const FaceEndpoint& ingress, const Interest& interest,
                        const shared_ptr<pit::Entry>& pitEntry) = 0;
 
   /** \brief trigger before PIT entry is satisfied
@@ -162,15 +162,15 @@
    */
   virtual void
   beforeSatisfyInterest(const shared_ptr<pit::Entry>& pitEntry,
-                        const Face& inFace, const Data& data);
+                        const FaceEndpoint& ingress, const Data& data);
 
   /** \brief trigger after a Data is matched in CS
    *
-   *  In the base class this method sends \p data to \p inFace
+   *  In the base class this method sends \p data to \p ingress
    */
   virtual void
   afterContentStoreHit(const shared_ptr<pit::Entry>& pitEntry,
-                       const Face& inFace, const Data& data);
+                       const FaceEndpoint& ingress, const Data& data);
 
   /** \brief trigger after Data is received
    *
@@ -197,7 +197,7 @@
    */
   virtual void
   afterReceiveData(const shared_ptr<pit::Entry>& pitEntry,
-                   const Face& inFace, const Data& data);
+                   const FaceEndpoint& ingress, const Data& data);
 
   /** \brief trigger after Nack is received
    *
@@ -221,7 +221,7 @@
    *           may occur. However, the strategy is allowed to store weak_ptr<pit::Entry>.
    */
   virtual void
-  afterReceiveNack(const Face& inFace, const lp::Nack& nack,
+  afterReceiveNack(const FaceEndpoint& ingress, const lp::Nack& nack,
                    const shared_ptr<pit::Entry>& pitEntry);
 
   /** \brief trigger after Interest dropped for exceeding allowed retransmissions
@@ -229,39 +229,40 @@
    *  In the base class this method does nothing.
    */
   virtual void
-  onDroppedInterest(const Face& outFace, const Interest& interest);
+  onDroppedInterest(const FaceEndpoint& egress, const Interest& interest);
 
 protected: // actions
-  /** \brief send Interest to outFace
+  /** \brief send Interest to egress
    *  \param pitEntry PIT entry
-   *  \param outFace face through which to send out the Interest
+   *  \param egress face through which to send out the Interest and destination endpoint
    *  \param interest the Interest packet
    */
   VIRTUAL_WITH_TESTS void
-  sendInterest(const shared_ptr<pit::Entry>& pitEntry, Face& outFace,
-               const Interest& interest)
+  sendInterest(const shared_ptr<pit::Entry>& pitEntry,
+               const FaceEndpoint& egress, const Interest& interest)
   {
-    m_forwarder.onOutgoingInterest(pitEntry, outFace, interest);
+    m_forwarder.onOutgoingInterest(pitEntry, egress, interest);
   }
 
-  /** \brief send \p data to \p outFace
+  /** \brief send \p data to \p egress
    *  \param pitEntry PIT entry
    *  \param data the Data packet
-   *  \param outFace face through which to send out the Data
+   *  \param egress face through which to send out the Data and destination endpoint
    */
   VIRTUAL_WITH_TESTS void
-  sendData(const shared_ptr<pit::Entry>& pitEntry, const Data& data, const Face& outFace);
+  sendData(const shared_ptr<pit::Entry>& pitEntry, const Data& data, const FaceEndpoint& egress);
 
-  /** \brief send \p data to all matched and qualified faces
+  /** \brief send \p data to all matched and qualified face-endpoint pairs
    *
-   *  A matched face is qualified if it is ad-hoc or it is NOT \p inFace
+   *  A matched face is qualified if it is ad-hoc or it is NOT \p ingress
    *
    *  \param pitEntry PIT entry
-   *  \param inFace face through which the Data comes from
+   *  \param ingress face through which the Data comes from and endpoint of the sender
    *  \param data the Data packet
    */
   VIRTUAL_WITH_TESTS void
-  sendDataToAll(const shared_ptr<pit::Entry>& pitEntry, const Face& inFace, const Data& data);
+  sendDataToAll(const shared_ptr<pit::Entry>& pitEntry,
+                const FaceEndpoint& ingress, const Data& data);
 
   /** \brief schedule the PIT entry for immediate deletion
    *
@@ -275,30 +276,29 @@
     this->setExpiryTimer(pitEntry, 0_ms);
   }
 
-  /** \brief send Nack to outFace
+  /** \brief send Nack to egress
    *  \param pitEntry PIT entry
-   *  \param outFace face through which to send out the Nack
+   *  \param egress face through which to send out the Nack and destination endpoint
    *  \param header Nack header
    *
-   *  The outFace must have a PIT in-record, otherwise this method has no effect.
+   *  The egress must have a PIT in-record, otherwise this method has no effect.
    */
   VIRTUAL_WITH_TESTS void
-  sendNack(const shared_ptr<pit::Entry>& pitEntry, const Face& outFace,
-           const lp::NackHeader& header)
+  sendNack(const shared_ptr<pit::Entry>& pitEntry,
+           const FaceEndpoint& egress, const lp::NackHeader& header)
   {
-    m_forwarder.onOutgoingNack(pitEntry, outFace, header);
+    m_forwarder.onOutgoingNack(pitEntry, egress, header);
   }
 
-  /** \brief send Nack to every face that has an in-record,
-   *         except those in \p exceptFaces
+  /** \brief send Nack to every face-endpoint pair that has an in-record, except those in \p exceptFaceEndpoints
    *  \param pitEntry PIT entry
    *  \param header NACK header
-   *  \param exceptFaces list of faces that should be excluded from sending Nacks
+   *  \param exceptFaceEndpoints list of face-endpoint pairs that should be excluded from sending Nacks
    *  \note This is not an action, but a helper that invokes the sendNack action.
    */
   void
   sendNacks(const shared_ptr<pit::Entry>& pitEntry, const lp::NackHeader& header,
-            std::initializer_list<const Face*> exceptFaces = std::initializer_list<const Face*>());
+            std::initializer_list<FaceEndpoint> exceptFaceEndpoints = {});
 
   /** \brief Schedule the PIT entry to be erased after \p duration
    */