Kea 3.1.4
limit_manager.h
Go to the documentation of this file.
1// Copyright (C) 2022-2025 Internet Systems Consortium, Inc. ("ISC")
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7#ifndef ISC_LIMITS_RATE_LIMIT_MANAGER_H
8#define ISC_LIMITS_RATE_LIMIT_MANAGER_H
9
11#include <dhcp/classify.h>
12#include <dhcp/pkt_template.h>
13#include <dhcpsrv/cfgmgr.h>
14#include <dhcpsrv/lease.h>
16#include <dhcpsrv/srv_config.h>
17#include <dhcpsrv/subnet.h>
18#include <dhcpsrv/subnet_id.h>
19#include <hooks/hooks.h>
22#include <stats/stats_mgr.h>
23#include <util/dhcp_space.h>
25
26#include <boost/circular_buffer.hpp>
27
28#include <chrono>
29#include <unordered_map>
30#include <vector>
31
32namespace isc {
33namespace limits {
34
36using TimePoint = std::chrono::time_point<std::chrono::system_clock>;
37
40using TimeSeries = boost::circular_buffer<TimePoint>;
41
51
53typedef std::shared_ptr<ProtectedTimeSeries> ProtectedTimeSeriesPtr;
54
56struct LimitManager {
60 static LimitManager& instance();
61
63 void clear();
64
72
80
93 template <isc::util::DhcpSpace D>
98 return (0);
99 }
100
102 handle.getArgument("audit_entries", audit_entries);
103 if (!audit_entries) {
104 isc_throw(Unexpected, "null audit_entries in LimitManager::cb_updated");
105 }
106
107 auto const& object_type_index(audit_entries->get<isc::db::AuditEntryObjectTypeTag>());
108 auto const& client_class_range(object_type_index.equal_range(
109 isc::util::formatDhcpSpace<D>("dhcp{}_client_class")));
110 auto const& subnet_range(object_type_index.equal_range(
111 isc::util::formatDhcpSpace<D>("dhcp{}_subnet")));
112
113 // If any client classes or subnets changed, reparse the entire configuration.
114 if (std::distance(client_class_range.first, client_class_range.second) ||
115 std::distance(subnet_range.first, subnet_range.second)) {
117 }
118
119 return (0);
120 }
121
130 template <isc::util::DhcpSpace D>
135 return (0);
136 }
137
139 // If lease limiting is used, make sure the database has JSON support.
140 if (!isc::dhcp::LeaseMgrFactory::instance().isJsonSupported()) {
141 const std::string error("The lease database you have configured "
142 "does not support JSON operations which are required for lease "
143 "limiting.");
144 handle.setArgument("error", error);
146 return (1);
147 }
148
149 // Recount the leases by class.
150 recountClassLeases<D>();
151 } else {
152 std::string const lease_db_access_string(isc::dhcp::CfgMgr::instance()
153 .getStagingCfg()
154 ->getCfgDbAccess()
155 ->getLeaseDbAccessString());
156 if (lease_db_access_string.find("retry-on-startup=true") &&
157 (lease_db_access_string.find("type=mysql") ||
158 lease_db_access_string.find("type=postgresql"))) {
160 } else {
163 }
164 }
165
166 return (0);
167 }
168
179 template <isc::util::DhcpSpace D>
184 return (0);
185 }
186
187 // Get the client classes.
189 handle.getArgument(isc::util::formatDhcpSpace<D>("query{}"), packet);
190 if (!packet) {
191 isc_throw(Unexpected, "null packet in LimitManager::pkt_receive");
192 }
193 auto const& classes(packet->getClasses());
194
195 // To be able to iterate over client classes and template classes, all
196 // regular classes must be added to a SubClassContainer with subclass
197 // name set to the same value as template class name.
198 auto const& relations = packet->getSubClassesRelations();
199
200 // Get the current time.
201 TimePoint const now(std::chrono::system_clock::now());
202
203 // Should contain client classes that are both in the packet and that are limited.
204 std::vector<isc::dhcp::ClientClass> common_client_classes;
205
206 // Lock a global-wide mutex for all client classes. It's not a necessary condition, but it's
207 // sufficient. Locking per client class, similarly to how it happens for subnet IDs would
208 // have been more efficient in a more heterogeneous class-to-packet distribution, but that
209 // situation is not guaranteed.
211
212 // Check if the rate limit is respected.
213 for (auto const& c : relations) {
214 auto const& class_def = isc::dhcp::CfgMgr::instance().getCurrentCfg()->getClientClassDictionary()->findClass(c.class_def_);
215 if (!class_def) {
216 continue;
217 }
218
219 auto const &limit_cfg = rate_limit_configuration_.parseUserContext(class_def->getContext());
220
221 if (!limit_cfg) {
222 continue;
223 }
224
225 // Get the limit.
226 RateLimit const& limit(limit_cfg->stringValue());
227
228 // Get the time series. Create a list on this class if not already added to the map.
229 TimeSeries& time_series(clocked_in_times_by_class_[c.class_]);
230
231 time_series.set_capacity(limit.allowed_packets_);
232
233 // Remove all expired times.
234 while (!time_series.empty()) {
235 if (time_series.back() + limit.time_unit_ < now) {
236 time_series.pop_back();
237 } else {
238 // This is an optimization. Job is finished at the first time point that is not
239 // expired. The rest are sorted and higher in value, thus not expired.
240 break;
241 }
242 }
243
244 // Effectively check the limit.
245 if (time_series.size() == limit.allowed_packets_) {
246 // Drop the packet.
248
251 .arg(classes.toText())
252 .arg(limit.text_)
253 .arg(c.class_);
254
255 // No need to check other client classes since packet is dropped.
256 break;
257 }
258
259 // Cache the set of client classes that are both in the packet and limited. If packet is
260 // not dropped, they will be iterated through one more time further below. As such, the
261 // configuration lookup for each client class at the beginning of this loop will no
262 // longer be required.
263 common_client_classes.push_back(c.class_);
264 }
265
269 stats_mgr.addValue("pkt4-limit-exceeded",
270 static_cast<int64_t>(1));
271 stats_mgr.addValue("pkt4-receive-drop",
272 static_cast<int64_t>(1));
273 } else {
274 stats_mgr.addValue("pkt6-limit-exceeded",
275 static_cast<int64_t>(1));
276 stats_mgr.addValue("pkt6-receive-drop",
277 static_cast<int64_t>(1));
278 }
279 } else {
280 // Honor the packet and keep track of it.
281 for (auto const& c : common_client_classes) {
282 TimeSeries& time_series(clocked_in_times_by_class_.at(c));
283 time_series.push_front(now);
284 }
285
286 // Only log that the packet is within the limit if this packet is being rate limited.
287 if (!common_client_classes.empty()) {
290 .arg(classes.toText());
291 }
292 }
293
294 return (0);
295 }
296
309 template <isc::util::DhcpSpace D>
314 return (0);
315 }
316
317 // Get the subnet ID.
319 handle.getArgument(isc::util::formatDhcpSpace<D>("subnet{}"), subnet);
320 if (!subnet) {
323 return (0);
324 }
325 isc::dhcp::SubnetID const subnet_id(subnet->getID());
326
327 RateLimit limit;
328 auto const& limit_cfg = subnetRateLimit<D>(subnet_id);
329 if (limit_cfg) {
330 // Get the limit.
331 limit = RateLimit(limit_cfg->stringValue());
332 } else {
333 return (0);
334 }
335
336 ProtectedTimeSeriesPtr time_series;
337
338 // Lock a global-wide mutex while configuration can be read-to and written-from.
339 {
341
342 // Create a time series on this subnet ID if not already added to the map.
343 ProtectedTimeSeriesPtr& time_series_ref(clocked_in_times_by_subnet_id_[subnet_id]);
344 if (!time_series_ref) {
345 time_series_ref = std::make_shared<ProtectedTimeSeries>();
346 }
347 time_series = time_series_ref;
348 }
349
350 // Get the current time.
351 TimePoint const now(std::chrono::system_clock::now());
352
353 // Lock the mutex on this subnet ID.
354 isc::util::MultiThreadingLock lock(time_series->mutex_);
355
356 time_series->time_points_.set_capacity(limit.allowed_packets_);
357
358 // Remove all expired times.
359 while (!time_series->time_points_.empty()) {
360 if (time_series->time_points_.back() + limit.time_unit_ < now) {
361 time_series->time_points_.pop_back();
362 } else {
363 // This is an optimization. Job is finished at the first time point that is not
364 // expired. The rest are sorted and higher in value, thus not expired.
365 break;
366 }
367 }
368
369 // Check the limit.
370 if (time_series->time_points_.size() < limit.allowed_packets_) {
371 // Honor the packet and keep track of it.
372 time_series->time_points_.push_front(now);
373 } else {
374 // Drop the packet.
376
379 .arg(subnet_id)
380 .arg(limit.text_);
381
384 stats_mgr.addValue("pkt4-limit-exceeded",
385 static_cast<int64_t>(1));
386 stats_mgr.addValue("pkt4-receive-drop",
387 static_cast<int64_t>(1));
388 } else {
389 stats_mgr.addValue("pkt6-limit-exceeded",
390 static_cast<int64_t>(1));
391 stats_mgr.addValue("pkt6-receive-drop",
392 static_cast<int64_t>(1));
393 }
394
395 return (0);
396 }
397
400 .arg(subnet_id)
401 .arg(limit.text_);
402
403 return (0);
404 }
405
421 template <isc::util::DhcpSpace D>
422 int lease_callout(isc::hooks::CalloutHandle& handle, bool lease_update = false) {
426 return (0);
427 }
428
429 // Get the lease.
431 handle.getArgument(isc::util::formatDhcpSpace<D>("lease{}"), lease);
432 if (!lease) {
433 isc_throw(Unexpected, "null lease in LimitManager::lease_callout");
434 }
435 isc::dhcp::SubnetID const subnet_id(lease->subnet_id_);
436
437 // Get the client classes.
439 handle.getArgument(isc::util::formatDhcpSpace<D>("query{}"), packet);
440 if (!packet) {
441 isc_throw(Unexpected, "null packet in LimitManager::lease_callout");
442 }
443 auto const& classes(packet->getClasses());
444
445 // Add client classes to the lease context.
446 addClientClassesToLeaseContext(classes, lease);
447
448 // Don't check limits for lease renewals.
449 if (lease_update) {
450 return (0);
451 }
452
453 // To be able to iterate over client classes and template classes, all
454 // regular classes must be added to a SubClassContainer with subclass
455 // name set to the same value as template class name.
456 auto const& relations(packet->getSubClassesRelations());
457
458 // Create parts of the limits context.
459 // Add client class limits to the context.
460 isc::data::ElementPtr client_class_limits = clientClassLimitsToElement<D>(relations, lease->getType());
461 // Add subnet limits to the context.
462 isc::data::ElementPtr subnet_limit = subnetLimitToElement<D>(subnet_id, lease->getType());
463
464 // Tie the limits together into an "ISC" context.
466 if (!client_class_limits->empty()) {
467 limits->set("client-classes", client_class_limits);
468 }
469 if (!subnet_limit->empty()) {
470 limits->set("subnet", subnet_limit);
471 }
472 if (limits->empty()) {
473 // No limits means we let the lease through by default. Exit here so that we don't check
474 // lease limits for nothing and so that we don't log details on non-limited leases.
475 return (0);
476 }
478 ISC->set("limits", limits);
480 context->set("ISC", ISC);
481
482 // Check lease limits.
483 std::string const limit_exceeded_text(checkLeaseLimits<D>(context));
484 if (limit_exceeded_text.empty()) {
486 .arg(lease->addr_);
487 } else {
488 // Prevent the lease from being allocated.
490
492 .arg(limit_exceeded_text);
493 }
494
495 return (0);
496 }
497
498private:
503 void addClientClassesToLeaseContext(isc::dhcp::ClientClasses const& classes,
504 isc::dhcp::LeasePtr const& lease);
505
517 template <isc::util::DhcpSpace D>
518 isc::data::ElementPtr clientClassLimitsToElement(isc::dhcp::SubClassRelationContainer const& classes,
519 isc::dhcp::Lease::Type const& lease_type);
520
531 template <isc::util::DhcpSpace D>
532 isc::data::ElementPtr subnetLimitToElement(isc::dhcp::SubnetID const subnet_id,
533 isc::dhcp::Lease::Type const& lease_type);
534
542 template <isc::util::DhcpSpace D>
543 isc::data::ConstElementPtr subnetRateLimit(isc::dhcp::SubnetID const subnet_id);
544
556 template <isc::util::DhcpSpace D>
557 std::string checkLeaseLimits(isc::data::ConstElementPtr const& context) const;
558
562 template <isc::util::DhcpSpace D>
563 void recountClassLeases() const;
564
567 std::unordered_map<isc::dhcp::ClientClass, TimeSeries> clocked_in_times_by_class_;
568
571 std::unordered_map<isc::dhcp::SubnetID, ProtectedTimeSeriesPtr> clocked_in_times_by_subnet_id_;
572
575 std::mutex mutex_;
576
578 AddressLimitConfiguration address_limit_configuration_;
579
581 PrefixLimitConfiguration prefix_limit_configuration_;
582
584 RateLimitConfiguration rate_limit_configuration_;
585
588 LimitManager() = default;
589 LimitManager(LimitManager const&) = delete;
590 LimitManager& operator=(LimitManager const&) = delete;
592};
593
594} // namespace limits
595} // namespace isc
596
597#endif // ISC_LIMITS_RATE_LIMIT_MANAGER_H
Defines elements for storing the names of client classes.
A generic exception that is thrown when an unexpected error condition occurs.
static ElementPtr createMap(const Position &pos=ZERO_POSITION())
Creates an empty MapElement type ElementPtr.
Definition data.cc:304
static CfgMgr & instance()
returns a single instance of Configuration Manager
Definition cfgmgr.cc:29
SrvConfigPtr getCurrentCfg()
Returns a pointer to the current configuration.
Definition cfgmgr.cc:116
Container for storing client class names.
Definition classify.h:110
static TrackingLeaseMgr & instance()
Return current lease manager.
static bool haveInstance()
Indicates if the lease manager has been instantiated.
Per-packet callout handle.
CalloutNextStep
Specifies allowed next steps.
@ NEXT_STEP_DROP
drop the packet
@ NEXT_STEP_SKIP
skip the next processing step
CalloutNextStep getStatus() const
Returns the next processing step.
void setStatus(const CalloutNextStep next)
Sets the next processing step.
void getArgument(const std::string &name, T &value) const
Get argument.
void setArgument(const std::string &name, T value)
Set argument.
Statistics Manager class.
static StatsMgr & instance()
Statistics Manager accessor method.
#define isc_throw(type, stream)
A shortcut macro to insert known values into exception arguments.
void addValue(const std::string &name, const int64_t value)
Records incremental integer observation.
#define LOG_ERROR(LOGGER, MESSAGE)
Macro to conveniently test error output and log it.
Definition macros.h:32
#define LOG_WARN(LOGGER, MESSAGE)
Macro to conveniently test warn output and log it.
Definition macros.h:26
#define LOG_DEBUG(LOGGER, LEVEL, MESSAGE)
Macro to conveniently test debug output and log it.
Definition macros.h:14
boost::shared_ptr< const Element > ConstElementPtr
Definition data.h:29
boost::shared_ptr< Element > ElementPtr
Definition data.h:28
@ error
Definition db_log.h:118
boost::shared_ptr< AuditEntryCollection > AuditEntryCollectionPtr
boost::shared_ptr< PktT< D > > PktTPtr
boost::shared_ptr< LeaseT< D > > LeaseTPtr
Definition lease.h:730
boost::shared_ptr< SrvConfig > SrvConfigPtr
Non-const pointer to the SrvConfig.
boost::shared_ptr< const SubnetT< D > > ConstSubnetTPtr
Definition subnet.h:1002
boost::multi_index_container< SubClassRelation, boost::multi_index::indexed_by< boost::multi_index::sequenced< boost::multi_index::tag< TemplateClassSequenceTag > >, boost::multi_index::hashed_unique< boost::multi_index::tag< TemplateClassNameTag >, boost::multi_index::member< SubClassRelation, ClientClass, &SubClassRelation::class_def_ > > > > SubClassRelationContainer
the subclass multi-index.
Definition classify.h:104
uint32_t SubnetID
Defines unique IPv4 or IPv6 subnet identifier.
Definition subnet_id.h:25
boost::shared_ptr< Lease > LeasePtr
Pointer to the lease object.
Definition lease.h:25
std::shared_ptr< ProtectedTimeSeries > ProtectedTimeSeriesPtr
Defines a smart pointer to a ProtectedTimeSeries.
const isc::log::MessageID LIMITS_CONFIGURATION_LEASE_BACKEND_NOT_AVAILABLE
std::chrono::time_point< std::chrono::system_clock > TimePoint
a point in time
const isc::log::MessageID LIMITS_PACKET_WITH_SUBNET_ID_RATE_LIMIT_HONORED
const isc::log::MessageID LIMITS_LEASE_LIMIT_EXCEEDED
const isc::log::MessageID LIMITS_PACKET_WITH_SUBNET_ID_RATE_LIMIT_DROPPED
const isc::log::MessageID LIMITS_LEASE_WITHIN_LIMITS
boost::circular_buffer< TimePoint > TimeSeries
Holds a number of time points, used in limiting by a single criterion.
const isc::log::MessageID LIMITS_PACKET_WITH_CLIENT_CLASSES_RATE_LIMIT_DROPPED
const isc::log::MessageID LIMITS_PACKET_WIIH_SUBNET_ID_RATE_NO_SUBNET
const isc::log::MessageID LIMITS_CONFIGURATION_LEASE_BACKEND_SHOULD_HAVE_BEEN_AVAILABLE
const isc::log::MessageID LIMITS_PACKET_WITH_CLIENT_CLASSES_RATE_LIMIT_HONORED
isc::log::Logger limits_logger("limits-hooks")
const int DBGLVL_TRACE_BASIC
Trace basic operations.
const int DBGLVL_TRACE_DETAIL_DATA
Trace data associated with detailed operations.
std::string formatDhcpSpace(char const *const format_string)
Replaces all occurrences of {} with 4 or 6 based on the templated DHCP space.
Definition dhcp_space.h:36
Defines the logger used by the top-level component of kea-lfc.
Tag used to access index by object type.
Type
Type of lease or pool.
Definition lease.h:46
the configuration manager for address limiting
Provides the capability to limit the number of leases or the response rate.
int cb_updated(isc::hooks::CalloutHandle &handle)
cbX_updated hook point
int dhcp_srv_configured(isc::hooks::CalloutHandle &handle)
dhcpX_srv_configured hook point
int subnet_select(isc::hooks::CalloutHandle &handle)
subnetX_select hook point
int pkt_receive(isc::hooks::CalloutHandle &handle)
pktX_receive hook point
void parse(isc::dhcp::SrvConfigPtr const &config)
Fetches limits from the given Kea configuration.
int lease_callout(isc::hooks::CalloutHandle &handle, bool lease_update=false)
leaseX_select hook point
void clear()
Clears the time series circular buffers in order to start over rate limiting.
void initialize(isc::dhcp::SrvConfigPtr const &config)
Reinitialize data structures required for limiting.
static LimitManager & instance()
singleton access function
the configuration manager for prefix limiting
Holds a number of time points, used in limiting by a single criterion, and a mutex to protect concurr...
std::mutex mutex_
Protects against races on the time points which can be edited at each hook callout.
TimeSeries time_points_
Holds the actual time points.
the configuration manager for rate limiting
a single rate-limiting entry configured as "rate-limit": "<n> packet[s] per <time-unit>"
std::string text_
a string representation of the rate limit as specified in the configuration used for logging purposes
std::chrono::seconds time_unit_
Seconds of one time unit's worth.
uint32_t allowed_packets_
the configured limit
RAII lock object to protect the code in the same scope with a mutex.