]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#4282] Checkpoint: added idle timer
authorFrancis Dupont <fdupont@isc.org>
Mon, 29 Dec 2025 08:45:07 +0000 (09:45 +0100)
committerFrancis Dupont <fdupont@isc.org>
Tue, 20 Jan 2026 09:31:09 +0000 (10:31 +0100)
src/hooks/dhcp/radius/radius_access.cc
src/hooks/dhcp/radius/radius_access.h
src/hooks/dhcp/radius/radius_accounting.cc
src/hooks/dhcp/radius/radius_accounting.h
src/hooks/dhcp/radius/radius_parsers.cc
src/hooks/dhcp/radius/radius_request.cc
src/hooks/dhcp/radius/radius_service.cc
src/hooks/dhcp/radius/radius_service.h
src/hooks/dhcp/radius/radius_status.cc
src/hooks/dhcp/radius/tests/config_unittests.cc

index fc8201388e6c14fc890bd3ac3ed8ff7aba1db847..e7ed9203b13620afd3c63a94546d1132db5e39c1 100644 (file)
@@ -13,6 +13,7 @@
 #include <dhcpsrv/host_mgr.h>
 #include <radius_access.h>
 #include <radius_log.h>
+#include <radius_status.h>
 #include <radius_utils.h>
 #include <util/multi_threading_mgr.h>
 #include <stdio.h>
@@ -1012,5 +1013,30 @@ RadiusAccess::terminate6(RadiusAuthEnv env, int result,
     }
 }
 
+void
+RadiusAccess::setIdleTimer() {
+    MultiThreadingLock lock(idle_timer_mutex_);
+    cancelIdleTimer();
+    if (idle_timer_interval_ <= 0) {
+        return;
+    }
+    // Cope to one day.
+    long secs = idle_timer_interval_;
+    if (secs > 24*60*60) {
+        secs = 24*60*60;
+    }
+    idle_timer_.reset(new IntervalTimer(RadiusImpl::instance().getIOContext()));
+    idle_timer_->setup(RadiusAccess::IdleTimerCallback,
+                       secs * 1000, IntervalTimer::REPEATING);
+}
+
+void
+RadiusAccess::IdleTimerCallback() {
+    AttributesPtr send_attrs;
+    RadiusAuthStatusPtr handler(new RadiusAuthStatus(send_attrs, 0));
+    RadiusImpl::instance().registerExchange(handler->getExchange());
+    handler->start();
+}
+
 } // end of namespace isc::radius
 } // end of namespace isc
index 41a89297db8e220efca07404408eafe324a353b9..7291175366a79eb6c1eeb8ccedc71c1584ef26f9 100644 (file)
@@ -328,6 +328,14 @@ public:
     /// @brief Pending RADIUS access requests - IPv6.
     RadiusAuthPendingRequests<dhcp::Pkt6Ptr> requests6_;
 
+    /// @brief Set idle timer.
+    ///
+    /// @note: The caller must hold the idle timer mutex.
+    void setIdleTimer();
+
+    /// @brief Idle timer callback.
+    static void IdleTimerCallback();
+
 };
 
 } // end of namespace isc::radius
index 71b9dcff3c71131406d2fe0706d2f36d2a5a27cc..7c17a65f064e53f6cb734a38f481fcced8b74125 100644 (file)
@@ -13,6 +13,7 @@
 #include <dhcpsrv/subnet.h>
 #include <radius_accounting.h>
 #include <radius_log.h>
+#include <radius_status.h>
 #include <radius_utils.h>
 #include <util/multi_threading_mgr.h>
 #include <stdio.h>
@@ -67,8 +68,8 @@ RadiusAcctHandler::RadiusAcctHandler(RadiusAcctEnv env,
                                      const CallbackAcct& callback)
     : env_(env), acct_() {
     acct_.reset(new RadiusAsyncAcct(env_.subnet_id_, env_.send_attrs_, callback));
-    MultiThreadingLock lock(mutex_);
     RadiusImpl::instance().registerExchange(acct_->getExchange());
+    MultiThreadingLock lock(mutex_);
     ++counter_;
 }
 
@@ -988,5 +989,30 @@ RadiusAccounting::storeToFile() {
     record_count_ = 0;
 }
 
+void
+RadiusAccounting::setIdleTimer() {
+    MultiThreadingLock lock(idle_timer_mutex_);
+    cancelIdleTimer();
+    if (idle_timer_interval_ <= 0) {
+        return;
+    }
+    // Cope to one day.
+    long secs = idle_timer_interval_;
+    if (secs > 24*60*60) {
+        secs = 24*60*60;
+    }
+    idle_timer_.reset(new IntervalTimer(RadiusImpl::instance().getIOContext()));
+    idle_timer_->setup(RadiusAccounting::IdleTimerCallback,
+                       secs * 1000, IntervalTimer::REPEATING);
+}
+
+void
+RadiusAccounting::IdleTimerCallback() {
+    AttributesPtr send_attrs;
+    RadiusAcctStatusPtr handler(new RadiusAcctStatus(send_attrs, 0));
+    RadiusImpl::instance().registerExchange(handler->getExchange());
+    handler->start();
+}
+
 } // end of namespace isc::radius
 } // end of namespace isc
index 9a826c4213ce7ffd24792cc7b6ecb9a0ad9f0e16..36d09a82b82248a7a2907298123f1c4372551bf0 100644 (file)
@@ -286,6 +286,14 @@ public:
     /// in the increasing timestamp order.
     void storeToFile();
 
+    /// @brief Set idle timer.
+    ///
+    /// @note: The caller must hold the idle timer mutex.
+    void setIdleTimer();
+
+    /// @brief Idle timer callback.
+    static void IdleTimerCallback();
+
 protected:
 
     /// @brief Create timestamps file name.
index d7e9702ebb8b2a765fbd10f69a1eeb439b414100..104636ca4e964aa4b24c5f9d0a7065c88503693f 100644 (file)
@@ -312,7 +312,8 @@ RadiusConfigParser::parse(ElementPtr& config) {
 /// @brief Keywords for service configuration.
 const set<string>
 RadiusServiceParser::SERVICE_KEYWORDS = {
-    "servers", "attributes", "peer-updates", "max-pending-requests"
+    "servers", "attributes", "peer-updates", "max-pending-requests",
+    "idle-timer-interval"
 };
 
 void
@@ -390,6 +391,25 @@ RadiusServiceParser::parse(const RadiusServicePtr& service,
             }
             service->max_pending_requests_ = max_pending_requests->intValue();
         }
+
+        // idle-timer-interval.
+        const ConstElementPtr& idle_timer_interval =
+            srv_cfg->get("idle-timer-interval");
+        if (idle_timer_interval) {
+            if (idle_timer_interval->getType() != Element::integer) {
+                isc_throw(BadValue, "expected idle-timer-interval to be "
+                          << "integer, but got "
+                          << Element::typeToName(idle_timer_interval->getType())
+                          << " instead");
+            }
+            if (idle_timer_interval->intValue() < 0) {
+                isc_throw(BadValue, "expected idle-timer-interval to be "
+                          << "positive, but got "
+                          << idle_timer_interval->intValue()
+                          << " instead");
+            }
+            service->idle_timer_interval_ = idle_timer_interval->intValue();
+        }
     } catch (const std::exception& ex) {
         isc_throw(ConfigError, ex.what() << " (parsing "
                   << service->name_ << ")");
index 8eec4bf6350afae0ff8349cb5782d678a77d90cf..3ab877599548daad21323a2df38fe6ffabccaa8c 100644 (file)
@@ -86,10 +86,12 @@ RadiusSyncAuth::start() {
         LOG_DEBUG(radius_logger, RADIUS_DBG_TRACE,
                   RADIUS_AUTHENTICATION_SYNC_ACCEPTED)
             .arg(recv_attrs ? recv_attrs->toText() : "no attributes");
+        RadiusImpl::instance().auth_->setIdleTimer();
     } else if (result == REJECT_RC) {
         LOG_DEBUG(radius_logger, RADIUS_DBG_TRACE,
                   RADIUS_AUTHENTICATION_SYNC_REJECTED)
             .arg(recv_attrs ? recv_attrs->toText() : "no attributes");
+        RadiusImpl::instance().auth_->setIdleTimer();
     } else {
         LOG_DEBUG(radius_logger, RADIUS_DBG_TRACE,
                   RADIUS_AUTHENTICATION_SYNC_FAILED)
@@ -145,10 +147,12 @@ RadiusAsyncAuth::invokeCallback(const CallbackAuth& callback,
         LOG_DEBUG(radius_logger, RADIUS_DBG_TRACE,
                   RADIUS_AUTHENTICATION_ASYNC_ACCEPTED)
             .arg(recv_attrs ? recv_attrs->toText() : "no attributes");
+        RadiusImpl::instance().auth_->setIdleTimer();
     } else if (result == REJECT_RC) {
         LOG_DEBUG(radius_logger, RADIUS_DBG_TRACE,
                   RADIUS_AUTHENTICATION_ASYNC_REJECTED)
             .arg(recv_attrs ? recv_attrs->toText() : "no attributes");
+        RadiusImpl::instance().auth_->setIdleTimer();
     } else {
         LOG_DEBUG(radius_logger, RADIUS_DBG_TRACE,
                   RADIUS_AUTHENTICATION_ASYNC_FAILED)
@@ -183,6 +187,7 @@ RadiusSyncAcct::start() {
     if (result == OK_RC) {
         LOG_DEBUG(radius_logger, RADIUS_DBG_TRACE,
                   RADIUS_ACCOUNTING_SYNC_SUCCEED);
+        RadiusImpl::instance().acct_->setIdleTimer();
     } else {
         LOG_DEBUG(radius_logger, RADIUS_DBG_TRACE,
                   RADIUS_ACCOUNTING_SYNC_FAILED)
@@ -232,6 +237,7 @@ RadiusAsyncAcct::invokeCallback(const CallbackAcct& callback,
     if (result == OK_RC) {
         LOG_DEBUG(radius_logger, RADIUS_DBG_TRACE,
                   RADIUS_ACCOUNTING_ASYNC_SUCCEED);
+        RadiusImpl::instance().acct_->setIdleTimer();
     } else {
         LOG_DEBUG(radius_logger, RADIUS_DBG_TRACE,
                   RADIUS_ACCOUNTING_ASYNC_FAILED)
index 6b1f51ba1134a249c4c7494ff98f7cebce4e4244..d9c0b3c6d4ce3506fc5978ff70773e0dc15f4c9d 100644 (file)
@@ -7,17 +7,24 @@
 #include <config.h>
 
 #include <radius_service.h>
+#include <util/multi_threading_mgr.h>
 
 using namespace std;
 using namespace isc;
 using namespace isc::data;
+using namespace isc::util;
 
 namespace isc {
 namespace radius {
 
 RadiusService::RadiusService(const std::string& name)
     : name_(name), enabled_(false), peer_updates_(true),
-      max_pending_requests_(0) {
+      max_pending_requests_(0), idle_timer_interval_(), idle_timer_() {
+}
+
+RadiusService::~RadiusService() {
+    MultiThreadingLock lock(idle_timer_mutex_);
+    cancelIdleTimer();
 }
 
 ElementPtr
@@ -38,8 +45,19 @@ RadiusService::toElement() const {
     // attributes.
     result->set("attributes", attributes_.toElement());
 
+    // idle-timer-interval.
+    result->set("idle-timer-interval", Element::create(idle_timer_interval_));
+
     return (result);
 }
 
+void
+RadiusService::cancelIdleTimer() {
+    if (idle_timer_) {
+        idle_timer_->cancel();
+        idle_timer_.reset();
+    }
+}
+
 } // end of namespace isc::radius
 } // end of namespace isc
index 20e0ebfe6f9e182ebf0a4e87930a451c9e874db9..ea931020c52cb30fe09b451ee4101f9f79e8bd46 100644 (file)
@@ -7,12 +7,15 @@
 #ifndef RADIUS_SERVICE_H
 #define RADIUS_SERVICE_H
 
-#include <cc/cfg_to_element.h>
-#include <cc/data.h>
 #include <client_server.h>
 #include <cfg_attribute.h>
+#include <asiolink/asio_wrapper.h>
+#include <asiolink/interval_timer.h>
+#include <cc/cfg_to_element.h>
+#include <cc/data.h>
 #include <boost/noncopyable.hpp>
 #include <boost/shared_ptr.hpp>
+#include <mutex>
 
 namespace isc {
 namespace radius {
@@ -29,8 +32,8 @@ public:
     /// @param name service name.
     explicit RadiusService(const std::string& name);
 
-    /// @brief Default destructor.
-    virtual ~RadiusService() = default;
+    /// @brief Destructor.
+    virtual ~RadiusService();
 
     /// @brief Unparse service configuration.
     ///
@@ -54,6 +57,20 @@ public:
 
     /// @brief Maximum number of pending requests.
     size_t max_pending_requests_;
+
+    /// @brief Idle timer interval in seconds.
+    long idle_timer_interval_;
+
+    /// @brief Idle timer.
+    asiolink::IntervalTimerPtr idle_timer_;
+
+    /// @brief Idle timer mutex.
+    std::mutex idle_timer_mutex_;
+
+    /// @brief Cancel idle timer.
+    ///
+    /// @note: The caller must hold the idle timer mutex.
+    void cancelIdleTimer();
 };
 
 /// @brief Type of pointers to Radius service.
index 36a7922e7f870676c16525b94dd865d65d2ad699..fa295948379d7158197e52a377f91822e3799e33 100644 (file)
@@ -64,6 +64,7 @@ RadiusAuthStatus::invokeCallback(const CallbackAcct& callback,
     if (result == OK_RC) {
         LOG_DEBUG(radius_logger, RADIUS_DBG_TRACE,
                   RADIUS_AUTHENTICATION_STATUS_SUCCEED);
+        RadiusImpl::instance().auth_->setIdleTimer();
     } else {
         LOG_DEBUG(radius_logger, RADIUS_DBG_TRACE,
                   RADIUS_AUTHENTICATION_STATUS_FAILED)
@@ -124,6 +125,7 @@ RadiusAcctStatus::invokeCallback(const CallbackAcct& callback,
     if (result == OK_RC) {
         LOG_DEBUG(radius_logger, RADIUS_DBG_TRACE,
                   RADIUS_ACCOUNTING_STATUS_SUCCEED);
+        RadiusImpl::instance().acct_->setIdleTimer();
     } else {
         LOG_DEBUG(radius_logger, RADIUS_DBG_TRACE,
                   RADIUS_ACCOUNTING_STATUS_FAILED)
index 1c67a9bb7dfe1afea87c960952053b2a8c670419..b9bd5d16ce53b7a2fe13bb1ecc7384519053ed6b 100644 (file)
@@ -97,10 +97,12 @@ TEST_F(ConfigTest, defaults) {
     EXPECT_NO_THROW(impl_.init(config));
     string expected = "{ "
         "\"access\": {"
-        "   \"attributes\": [ ]"
+        "   \"attributes\": [ ],"
+        "   \"idle-timer-interval\": 0"
         "}, "
         "\"accounting\": {"
-        "   \"attributes\": [ ]"
+        "   \"attributes\": [ ],"
+        "   \"idle-timer-interval\": 0"
         "}, "
         "\"bindaddr\": \"*\", "
         "\"canonical-mac-address\": false, "
@@ -142,10 +144,12 @@ TEST_F(ConfigTest, global) {
     EXPECT_NO_THROW(impl_.init(config));
     string expected = "{ "
         "\"access\": {"
-        "   \"attributes\": [ ]"
+        "   \"attributes\": [ ],"
+        "   \"idle-timer-interval\": 0"
         "}, "
         "\"accounting\": {"
-        "   \"attributes\": [ ]"
+        "   \"attributes\": [ ],"
+        "   \"idle-timer-interval\": 0"
         "}, "
         "\"bindaddr\": \"127.0.0.1\", "
         "\"canonical-mac-address\": true, "
@@ -624,7 +628,9 @@ TEST_F(ConfigTest, services) {
         "   \"name\": \"User-Name\", "
         "   \"type\": 1, "
         "   \"data\": \"foobar\" "
-        "} ] }";
+        "} ],"
+        "\"idle-timer-interval\": 0"
+        "}";
     runToElementTest<RadiusService>(expected, *impl_.auth_);
 
     // Needs a server to be enabled.
@@ -665,6 +671,7 @@ TEST_F(ConfigTest, services) {
 
     expected = "{ "
         "\"attributes\": [ ], "
+        "\"idle-timer-interval\": 0,"
         "\"servers\": [ {"
         "   \"deadtime\": 0, "
         "   \"local-address\": \"127.0.0.1\", "
@@ -723,6 +730,7 @@ TEST_F(ConfigTest, services) {
 
     expected = "{ "
         "\"attributes\": [ ], "
+        "\"idle-timer-interval\": 0,"
         "\"servers\": [ {"
         "   \"deadtime\": 0, "
         "   \"local-address\": \"127.0.0.1\", "
@@ -1432,4 +1440,44 @@ TEST_F(ConfigTest, maxPendingRequests) {
     EXPECT_NO_THROW_LOG(impl_.reset());
 }
 
+// Verify that idle-timer-interval can be configured and that proper errors
+// are reported in negative cases.
+TEST_F(ConfigTest, idleTimerInterval) {
+    ElementPtr config;
+
+    config = Element::fromJSON(R"({
+        "access": {
+            "idle-timer-interval": -1
+        }
+    })");
+    EXPECT_THROW_MSG(impl_.init(config), ConfigError,
+                     "expected idle-timer-interval to be positive, but got "
+                     "-1 instead (parsing access)");
+    EXPECT_NO_THROW_LOG(impl_.reset());
+
+    config = Element::fromJSON(R"({
+        "accounting": {
+            "idle-timer-interval": false
+        }
+    })");
+    EXPECT_THROW_MSG(impl_.init(config), ConfigError,
+                     "expected idle-timer-interval to be integer, but got "
+                     "boolean instead (parsing accounting)");
+    EXPECT_NO_THROW_LOG(impl_.reset());
+
+    config = Element::fromJSON(R"({
+        "access": {
+            "idle-timer-interval": 10
+        }
+    })");
+    EXPECT_NO_THROW_LOG(impl_.init(config));
+    EXPECT_EQ(10, impl_.auth_->idle_timer_interval_);
+    EXPECT_NO_THROW_LOG(impl_.reset());
+
+    config = Element::createMap();
+    EXPECT_NO_THROW_LOG(impl_.init(config));
+    EXPECT_EQ(0, impl_.auth_->idle_timer_interval_);
+    EXPECT_NO_THROW_LOG(impl_.reset());
+}
+
 } // end of anonymous namespace