From 6e077a0b0989edeb999291531f20721ce42838cb Mon Sep 17 00:00:00 2001 From: Miguel Company Date: Mon, 1 Jun 2026 09:17:10 +0200 Subject: [PATCH] Assert liveliness with periodic heartbeats in secure participants (#6411) * Refs #24372. Assert liveliness with periodic heartbeats in secure participants. Signed-off-by: Juan Lopez Fernandez * Refs #24372. Added regression test. Signed-off-by: danipiza * Refs #24372. Revert change in returned value. Signed-off-by: Miguel Company * Refs #24372. Improve null pointer checks. Signed-off-by: Miguel Company * Refs #24372. Uncrustify. Signed-off-by: Miguel Company --------- Signed-off-by: Juan Lopez Fernandez Signed-off-by: danipiza Signed-off-by: Miguel Company Co-authored-by: Juan Lopez Fernandez Co-authored-by: danipiza (cherry picked from commit 910ac99079e9d140b7e77ab322d8a93bac62d48f) # Conflicts: # src/cpp/rtps/builtin/discovery/participant/PDPSimple.cpp --- .../discovery/participant/PDPSimple.cpp | 25 +++++ .../blackbox/common/BlackboxTestsSecurity.cpp | 92 +++++++++++++++++++ 2 files changed, 117 insertions(+) diff --git a/src/cpp/rtps/builtin/discovery/participant/PDPSimple.cpp b/src/cpp/rtps/builtin/discovery/participant/PDPSimple.cpp index 7fea56e8e44..596c80c79fa 100644 --- a/src/cpp/rtps/builtin/discovery/participant/PDPSimple.cpp +++ b/src/cpp/rtps/builtin/discovery/participant/PDPSimple.cpp @@ -285,12 +285,37 @@ void PDPSimple::announceParticipantState( #endif // HAVE_SECURITY auto endpoints = dynamic_cast(builtin_endpoints_.get()); +<<<<<<< HEAD +======= + assert(endpoints && endpoints->writer.history_); + if (!endpoints || !endpoints->writer.history_) + { + FASTDDS_UNREACHABLE(); // “cannot happen” invariant + } + +>>>>>>> 910ac9907 (Assert liveliness with periodic heartbeats in secure participants (#6411)) WriterHistory& history = *(endpoints->writer.history_); PDP::announceParticipantState(history, new_change, dispose, wp); if (!(dispose || new_change)) { endpoints->writer.writer_->send_periodic_announcement(); + +#if HAVE_SECURITY + if (mp_RTPSParticipant->is_secure()) + { + // PDP non-secure endpoints are unmatched after participant authentication succeeds (and secure PDP + // endpoints are matched), and since the secure ones are TRANSIENT_LOCAL, we send periodic heartbeats + // to assert liveliness on remote participants + auto secure = dynamic_cast(builtin_endpoints_.get()); + assert(secure && secure->secure_writer.writer_); + if (!secure || !secure->secure_writer.writer_) + { + FASTDDS_UNREACHABLE(); // “cannot happen” invariant + } + secure->secure_writer.writer_->send_periodic_heartbeat(true, true); + } +#endif // HAVE_SECURITY } } } diff --git a/test/blackbox/common/BlackboxTestsSecurity.cpp b/test/blackbox/common/BlackboxTestsSecurity.cpp index 74bb5bea836..fc1509d06eb 100644 --- a/test/blackbox/common/BlackboxTestsSecurity.cpp +++ b/test/blackbox/common/BlackboxTestsSecurity.cpp @@ -3115,6 +3115,98 @@ TEST_P(Security, RemoveParticipantProxyDataonSecurityManagerLeaseExpired_validat } +// Regression test for secure PDP liveliness maintenance without user traffic. +TEST(Security, SecureParticipantsDoNotLoseDiscoveryWithoutUserTraffic) +{ + PubSubReader reader("HelloWorldTopic_secure_participant_liveliness"); + PubSubWriter writer("HelloWorldTopic_secure_participant_liveliness"); + const auto idle_timeout = std::chrono::seconds(6); + const auto data_timeout = std::chrono::seconds(10); + // Keep the lease short so loss of secure PDP liveliness shows up quickly + const auto lease_duration = eprosima::fastdds::dds::Duration_t(3, 0); + const auto announcement_period = eprosima::fastdds::dds::Duration_t(1, 0); + // Use UDP transport to later block the fallback participant DATA(P) traffic + auto reader_transport = std::make_shared(); + auto writer_transport = std::make_shared(); + + const std::string governance_file("governance_helloworld_all_enable.smime"); + const std::string permissions_file("permissions_helloworld.smime"); + + CommonPermissionsConfigure(reader, writer, governance_file, permissions_file); + // Force the test through UDP to avoid local shortcuts masking the secure discovery behavior + reader.disable_builtin_transport().add_user_transport_to_pparams(reader_transport); + writer.disable_builtin_transport().add_user_transport_to_pparams(writer_transport); + + // Two secure participants that authenticate, discover each other, and exchange user data + reader.lease_duration(lease_duration, announcement_period) + .history_depth(10) + .reliability(eprosima::fastdds::dds::RELIABLE_RELIABILITY_QOS) + .init(); + ASSERT_TRUE(reader.isInitialized()); + + writer.lease_duration(lease_duration, announcement_period) + .history_depth(10) + .reliability(eprosima::fastdds::dds::RELIABLE_RELIABILITY_QOS) + .init(); + ASSERT_TRUE(writer.isInitialized()); + + reader.wait_authorized(); + writer.wait_authorized(); + + // Wait until both endpoints are matched + reader.wait_discovery(); + writer.wait_discovery(); + + // Verify discovery state stayed stable + auto assert_still_discovered = [&reader, &writer]() + { + ASSERT_TRUE(reader.is_matched()); + ASSERT_TRUE(writer.is_matched()); + ASSERT_EQ(reader.get_matched(), 1u); + ASSERT_EQ(writer.get_matched(), 1u); + + const auto reader_status = reader.get_subscription_matched_status(); + const auto writer_status = writer.get_publication_matched_status(); + ASSERT_EQ(reader_status.total_count, 1); + ASSERT_EQ(writer_status.total_count, 1); + }; + + // Check the secure participants must not be undiscovered just because user traffic stops + auto assert_idle_period_keeps_discovery = [&reader, &writer, &idle_timeout, &assert_still_discovered]() + { + ASSERT_FALSE(reader.wait_participant_undiscovery(idle_timeout)); + ASSERT_FALSE(writer.wait_participant_undiscovery(idle_timeout)); + + assert_still_discovered(); + }; + + // Discovery is only useful if data can still flow after the idle window + auto assert_data_flow = [&reader, &writer, &data_timeout, &assert_still_discovered]() + { + auto data = default_helloworld_data_generator(2); + + reader.startReception(data); + writer.send(data); + + ASSERT_TRUE(data.empty()); + ASSERT_EQ(reader.block_for_all(data_timeout), 2u); + assert_still_discovered(); + }; + + ASSERT_NO_FATAL_FAILURE(assert_still_discovered()); + + // After the secure PDP endpoints are established, stop the unprotected participant DATA(P) + // traffic, to depend on the secure liveliness maintenance path + reader_transport->test_transport_options->always_drop_participant_builtin_topic_data = true; + writer_transport->test_transport_options->always_drop_participant_builtin_topic_data = true; + + // Exercise the idle scenario twice to catch both immediate loss and unstable recovery/rematch behavior + ASSERT_NO_FATAL_FAILURE(assert_idle_period_keeps_discovery()); + ASSERT_NO_FATAL_FAILURE(assert_data_flow()); + ASSERT_NO_FATAL_FAILURE(assert_idle_period_keeps_discovery()); + ASSERT_NO_FATAL_FAILURE(assert_data_flow()); +} + TEST(Security, AllowUnauthenticatedParticipants_EntityCreationFailsIfRTPSProtectionIsNotNONE) { PubSubReader reader("HelloWorldTopic");