mgmt: handle rib/announce command

This commit adds logic to NFD so it can handle the new Prefix
Announcement protocol, which are commands sent to rib/announce
with Prefix Announcement object in the Application Parameters.

Refs: #4650
Change-Id: I2a306eb2c3eeb638cc789329d998bfa278880ca6
diff --git a/daemon/mgmt/rib-manager.cpp b/daemon/mgmt/rib-manager.cpp
index 6cc614a..ddc53cf 100644
--- a/daemon/mgmt/rib-manager.cpp
+++ b/daemon/mgmt/rib-manager.cpp
@@ -67,6 +67,9 @@
   registerCommandHandler<ndn::nfd::RibUnregisterCommand>([this] (auto&&, auto&&... args) {
     unregisterEntry(std::forward<decltype(args)>(args)...);
   });
+  registerCommandHandler<ndn::nfd::RibAnnounceCommand>([this] (auto&&, auto&&... args) {
+    announceEntry(std::forward<decltype(args)>(args)...);
+  });
   registerStatusDatasetHandler("list", [this] (auto&&, auto&&, auto&&... args) {
     listEntries(std::forward<decltype(args)>(args)...);
   });
@@ -266,6 +269,44 @@
 }
 
 void
+RibManager::announceEntry(const Interest& interest, const ndn::nfd::RibAnnounceParameters& parameters,
+                          const CommandContinuation& done)
+{
+  const auto& announcement = parameters.getPrefixAnnouncement();
+  if (announcement.getAnnouncedName().size() > Fib::getMaxDepth()) {
+    done(ControlResponse(414, "Route prefix cannot exceed " + std::to_string(Fib::getMaxDepth()) +
+                         " components"));
+    return;
+  }
+
+  // Prepare parameters for response
+  ControlParameters responseParams;
+  responseParams.setFaceId(0);
+  setFaceForSelfRegistration(interest, responseParams);
+
+  Route route(announcement, responseParams.getFaceId());
+
+  responseParams
+    .setName(announcement.getAnnouncedName())
+    .setOrigin(route.origin)
+    .setCost(route.cost)
+    .setFlags(route.flags)
+    .setExpirationPeriod(time::duration_cast<time::milliseconds>(route.annExpires - time::steady_clock::now()));
+
+  BOOST_ASSERT(announcement.getData());
+  m_paValidator.validate(*announcement.getData(),
+    [=, name = announcement.getAnnouncedName(), route = std::move(route)] (const Data&) {
+      // Respond since command is valid and authorized
+      done(ControlResponse(200, "Success").setBody(responseParams.wireEncode()));
+      beginAddRoute(name, std::move(route), std::nullopt, [] (RibUpdateResult) {});
+    },
+    [=] (const Data&, ndn::security::ValidationError err) {
+      done(ControlResponse(403, "Validation error: " + err.getInfo()));
+    }
+  );
+}
+
+void
 RibManager::listEntries(ndn::mgmt::StatusDatasetContext& context)
 {
   auto now = time::steady_clock::now();
@@ -311,7 +352,8 @@
                  const ndn::mgmt::AcceptContinuation& accept,
                  const ndn::mgmt::RejectContinuation& reject) {
     BOOST_ASSERT(params != nullptr);
-    BOOST_ASSERT(typeid(*params) == typeid(ndn::nfd::ControlParameters));
+    BOOST_ASSERT(typeid(*params) == typeid(ndn::nfd::ControlParameters) ||
+                 typeid(*params) == typeid(ndn::nfd::RibAnnounceParameters));
     BOOST_ASSERT(prefix == LOCALHOST_TOP_PREFIX || prefix == LOCALHOP_TOP_PREFIX);
 
     auto& validator = prefix == LOCALHOST_TOP_PREFIX ? m_localhostValidator : m_localhopValidator;
diff --git a/daemon/mgmt/rib-manager.hpp b/daemon/mgmt/rib-manager.hpp
index a7edc22..3b348a1 100644
--- a/daemon/mgmt/rib-manager.hpp
+++ b/daemon/mgmt/rib-manager.hpp
@@ -212,6 +212,13 @@
                   const CommandContinuation& done);
 
   /**
+   * \brief Serve `rib/announce` command.
+   */
+  void
+  announceEntry(const Interest& interest, const ndn::nfd::RibAnnounceParameters& parameters,
+                const CommandContinuation& done);
+
+  /**
    * \brief Serve `rib/list` dataset.
    */
   void
diff --git a/daemon/rib/route.cpp b/daemon/rib/route.cpp
index e7c825f..5011290 100644
--- a/daemon/rib/route.cpp
+++ b/daemon/rib/route.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2023,  Regents of the University of California,
+ * Copyright (c) 2014-2025,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -29,8 +29,6 @@
 
 namespace nfd::rib {
 
-constexpr uint64_t PA_ROUTE_COST = 2048; // cost of route created by prefix announcement
-
 Route::Route() = default;
 
 static time::steady_clock::time_point
diff --git a/daemon/rib/route.hpp b/daemon/rib/route.hpp
index 2c275df..43f520d 100644
--- a/daemon/rib/route.hpp
+++ b/daemon/rib/route.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2024,  Regents of the University of California,
+ * Copyright (c) 2014-2025,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -90,6 +90,9 @@
   }
 
 public:
+  /// Cost of route created by prefix announcement.
+  static constexpr uint64_t PA_ROUTE_COST = 2048;
+
   uint64_t faceId = 0;
   ndn::nfd::RouteOrigin origin = ndn::nfd::ROUTE_ORIGIN_APP;
   uint64_t cost = 0;
diff --git a/tests/daemon/mgmt/manager-common-fixture.cpp b/tests/daemon/mgmt/manager-common-fixture.cpp
index 3b98dd2..e918cdb 100644
--- a/tests/daemon/mgmt/manager-common-fixture.cpp
+++ b/tests/daemon/mgmt/manager-common-fixture.cpp
@@ -52,6 +52,20 @@
   NDN_CXX_UNREACHABLE;
 }
 
+Interest
+InterestSignerFixture::makeControlCommandRequest(Name commandName,
+                                                 const ndn::PrefixAnnouncement& prefixAnnouncement,
+                                                 const Name& identity)
+{
+  const Block& paBlock = prefixAnnouncement.getData().value().wireEncode();
+
+  Interest interest(commandName);
+  interest.setApplicationParameters(paBlock);
+  m_signer.makeSignedInterest(interest, ndn::security::signingByIdentity(identity));
+
+  return interest;
+}
+
 void
 ManagerCommonFixture::setTopPrefix()
 {
diff --git a/tests/daemon/mgmt/manager-common-fixture.hpp b/tests/daemon/mgmt/manager-common-fixture.hpp
index c8f5498..22d9120 100644
--- a/tests/daemon/mgmt/manager-common-fixture.hpp
+++ b/tests/daemon/mgmt/manager-common-fixture.hpp
@@ -59,6 +59,19 @@
                             ndn::security::SignedInterestFormat format = ndn::security::SignedInterestFormat::V03,
                             const Name& identity = DEFAULT_COMMAND_SIGNER_IDENTITY);
 
+  /**
+   * \brief Create a ControlCommand request for a Prefix Announcement.
+   * \param commandName Command name including prefix, such as `/localhost/nfd/rib/announce`
+   * \param prefixAnnouncement Prefix Announcement object
+   * \param identity Signing identity
+   *
+   * Per specification, Prefix Announcements use Signed Interest v0.3 only.
+   */
+  Interest
+  makeControlCommandRequest(Name commandName,
+                            const ndn::PrefixAnnouncement& prefixAnnouncement,
+                            const Name& identity = DEFAULT_COMMAND_SIGNER_IDENTITY);
+
 protected:
   static inline const Name DEFAULT_COMMAND_SIGNER_IDENTITY{"/InterestSignerFixture-identity"};
 
diff --git a/tests/daemon/mgmt/rib-manager.t.cpp b/tests/daemon/mgmt/rib-manager.t.cpp
index 43c69df..679f40c 100644
--- a/tests/daemon/mgmt/rib-manager.t.cpp
+++ b/tests/daemon/mgmt/rib-manager.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2024,  Regents of the University of California,
+ * Copyright (c) 2014-2025,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -117,6 +117,14 @@
   return makeSection(config);
 }
 
+static ConfigSection
+getPaValidatorConfigSection()
+{
+  ConfigSection section;
+  section.put("trust-anchor.type", "any");
+  return section;
+}
+
 class RibManagerFixture : public ManagerCommonFixture
 {
 public:
@@ -151,6 +159,8 @@
       m_manager.disableLocalhop();
     }
 
+    m_manager.applyPaConfig(getPaValidatorConfigSection(), "testPa");
+
     registerWithNfd();
 
     if (shouldClearRib) {
@@ -442,6 +452,7 @@
   }
   auto params = makeRegisterParameters(prefix, 2899);
   auto command = makeControlCommandRequest("/localhost/nfd/rib/register", params);
+
   receiveInterest(command);
 
   BOOST_REQUIRE_EQUAL(m_responses.size(), 1);
@@ -455,6 +466,107 @@
 
 BOOST_AUTO_TEST_SUITE_END() // RegisterUnregister
 
+BOOST_FIXTURE_TEST_SUITE(PrefixAnnounce, LocalhostAuthorizedRibManagerFixture)
+
+BOOST_AUTO_TEST_CASE(Basic)
+{
+  const uint64_t announceFaceId = 1234;
+
+  ndn::PrefixAnnouncement pa = signPrefixAnn(makePrefixAnn("/test-prefix-announce", 10_s), m_keyChain);
+  auto commandAnnounce = makeControlCommandRequest("/localhost/nfd/rib/announce", pa);
+  commandAnnounce.setTag(make_shared<lp::IncomingFaceIdTag>(announceFaceId));
+
+  auto paramsUnregister = makeUnregisterParameters("/test-prefix-announce");
+  paramsUnregister.setOrigin(ndn::nfd::ROUTE_ORIGIN_PREFIXANN);
+  BOOST_CHECK_EQUAL(paramsUnregister.getFaceId(), 0);
+  auto commandUnregister = makeControlCommandRequest("/localhost/nfd/rib/unregister", paramsUnregister);
+  commandUnregister.setTag(make_shared<lp::IncomingFaceIdTag>(announceFaceId)); // same incoming face
+
+  receiveInterest(commandAnnounce);
+  receiveInterest(commandUnregister);
+
+  ControlParameters paramsAnnounceResponse;
+  paramsAnnounceResponse.setName("/test-prefix-announce")
+    .setFaceId(announceFaceId)
+    .setOrigin(ndn::nfd::ROUTE_ORIGIN_PREFIXANN)
+    .setCost(rib::Route::PA_ROUTE_COST)
+    .setFlags(ndn::nfd::ROUTE_FLAG_CHILD_INHERIT)
+    .setExpirationPeriod(10_s);
+  paramsUnregister.setFaceId(announceFaceId);
+
+  BOOST_REQUIRE_EQUAL(m_responses.size(), 2);
+  BOOST_CHECK_EQUAL(checkResponse(0, commandAnnounce.getName(), makeResponse(200, "Success", paramsAnnounceResponse)),
+                    CheckResponseResult::OK);
+  BOOST_CHECK_EQUAL(checkResponse(1, commandUnregister.getName(), makeResponse(200, "Success", paramsUnregister)),
+                    CheckResponseResult::OK);
+
+  BOOST_REQUIRE_EQUAL(m_fibUpdater.updates.size(), 2);
+  BOOST_CHECK_EQUAL(m_fibUpdater.updates.front(),
+                    rib::FibUpdate::createAddUpdate("/test-prefix-announce", announceFaceId, rib::Route::PA_ROUTE_COST));
+  BOOST_CHECK_EQUAL(m_fibUpdater.updates.back(),
+                    rib::FibUpdate::createRemoveUpdate("/test-prefix-announce", announceFaceId));
+}
+
+BOOST_AUTO_TEST_CASE(UnregisterFromDifferentFace)
+{
+  const uint64_t announceFaceId = 1234;
+
+  ndn::PrefixAnnouncement pa = signPrefixAnn(makePrefixAnn("/test-prefix-announce", 10_s), m_keyChain);
+  auto commandAnnounce = makeControlCommandRequest("/localhost/nfd/rib/announce", pa);
+  commandAnnounce.setTag(make_shared<lp::IncomingFaceIdTag>(announceFaceId));
+
+  auto paramsUnregister = makeUnregisterParameters("/test-prefix-announce", announceFaceId);
+  paramsUnregister.setOrigin(ndn::nfd::ROUTE_ORIGIN_PREFIXANN);
+  auto commandUnregister = makeControlCommandRequest("/localhost/nfd/rib/unregister", paramsUnregister);
+  commandUnregister.setTag(make_shared<lp::IncomingFaceIdTag>(999)); // unregister from different face
+
+  receiveInterest(commandAnnounce);
+  receiveInterest(commandUnregister);
+
+  ControlParameters paramsAnnounceResponse;
+  paramsAnnounceResponse.setName("/test-prefix-announce")
+    .setFaceId(announceFaceId)
+    .setOrigin(ndn::nfd::ROUTE_ORIGIN_PREFIXANN)
+    .setCost(rib::Route::PA_ROUTE_COST)
+    .setFlags(ndn::nfd::ROUTE_FLAG_CHILD_INHERIT)
+    .setExpirationPeriod(10_s);
+
+  BOOST_REQUIRE_EQUAL(m_responses.size(), 2);
+  BOOST_CHECK_EQUAL(checkResponse(0, commandAnnounce.getName(), makeResponse(200, "Success", paramsAnnounceResponse)),
+                    CheckResponseResult::OK);
+  BOOST_CHECK_EQUAL(checkResponse(1, commandUnregister.getName(), makeResponse(200, "Success", paramsUnregister)),
+                    CheckResponseResult::OK);
+
+  BOOST_REQUIRE_EQUAL(m_fibUpdater.updates.size(), 2);
+  BOOST_CHECK_EQUAL(m_fibUpdater.updates.front(),
+                    rib::FibUpdate::createAddUpdate("/test-prefix-announce", announceFaceId, rib::Route::PA_ROUTE_COST));
+  BOOST_CHECK_EQUAL(m_fibUpdater.updates.back(),
+                    rib::FibUpdate::createRemoveUpdate("/test-prefix-announce", announceFaceId));
+}
+
+BOOST_AUTO_TEST_CASE(NameTooLong)
+{
+  Name prefix;
+  while (prefix.size() <= Fib::getMaxDepth()) {
+    prefix.append("A");
+  }
+  ndn::PrefixAnnouncement pa = signPrefixAnn(makePrefixAnn(prefix, 10_s), m_keyChain);
+  auto command = makeControlCommandRequest("/localhost/nfd/rib/announce", pa);
+  command.setTag(make_shared<lp::IncomingFaceIdTag>(333));
+
+  receiveInterest(command);
+
+  BOOST_REQUIRE_EQUAL(m_responses.size(), 1);
+  BOOST_CHECK_EQUAL(checkResponse(0, command.getName(),
+                                  ControlResponse(414, "Route prefix cannot exceed " +
+                                                  std::to_string(Fib::getMaxDepth()) + " components")),
+                    CheckResponseResult::OK);
+
+  BOOST_CHECK_EQUAL(m_fibUpdater.updates.size(), 0);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // PrefixAnnounce
+
 BOOST_FIXTURE_TEST_CASE(RibDataset, UnauthorizedRibManagerFixture)
 {
   uint64_t faceId = 0;