Kea 3.1.1
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 <util/dhcp_space.h>
24
25#include <boost/circular_buffer.hpp>
26
27#include <chrono>
28#include <unordered_map>
29#include <vector>
30
31namespace isc {
32namespace limits {
33
35using TimePoint = std::chrono::time_point<std::chrono::system_clock>;
36
39using TimeSeries = boost::circular_buffer<TimePoint>;
40
50
52typedef std::shared_ptr<ProtectedTimeSeries> ProtectedTimeSeriesPtr;
53
55struct LimitManager {
59 static LimitManager& instance();
60
62 void clear();
63
71
79
92 template <isc::util::DhcpSpace D>
97 return (0);
98 }
99
101 handle.getArgument("audit_entries", audit_entries);
102 if (!audit_entries) {
103 isc_throw(Unexpected, "null audit_entries in LimitManager::cb_updated");
104 }
105
106 auto const& object_type_index(audit_entries->get<isc::db::AuditEntryObjectTypeTag>());
107 auto const& client_class_range(object_type_index.equal_range(
108 isc::util::formatDhcpSpace<D>("dhcp{}_client_class")));
109 auto const& subnet_range(object_type_index.equal_range(
110 isc::util::formatDhcpSpace<D>("dhcp{}_subnet")));
111
112 // If any client classes or subnets changed, reparse the entire configuration.
113 if (std::distance(client_class_range.first, client_class_range.second) ||
114 std::distance(subnet_range.first, subnet_range.second)) {
116 }
117
118 return (0);
119 }
120
129 template <isc::util::DhcpSpace D>
134 return (0);
135 }
136
138 // If lease limiting is used, make sure the database has JSON support.
139 if (!isc::dhcp::LeaseMgrFactory::instance().isJsonSupported()) {
140 const std::string error("The lease database you have configured "
141 "does not support JSON operations which are required for lease "
142 "limiting.");
143 handle.setArgument("error", error);
145 return (1);
146 }
147
148 // Recount the leases by class.
149 recountClassLeases<D>();
150 } else {
151 std::string const lease_db_access_string(isc::dhcp::CfgMgr::instance()
152 .getStagingCfg()
153 ->getCfgDbAccess()
154 ->getLeaseDbAccessString());
155 if (lease_db_access_string.find("retry-on-startup=true") &&
156 (lease_db_access_string.find("type=mysql") ||
157 lease_db_access_string.find("type=postgresql"))) {
159 } else {
162 }
163 }
164
165 return (0);
166 }
167
178 template <isc::util::DhcpSpace D>
183 return (0);
184 }
185
186 // Get the client classes.
188 handle.getArgument(isc::util::formatDhcpSpace<D>("query{}"), packet);
189 if (!packet) {
190 isc_throw(Unexpected, "null packet in LimitManager::pkt_receive");
191 }
192 auto const& classes(packet->getClasses());
193
194 // To be able to iterate over client classes and template classes, all
195 // regular classes must be added to a SubClassContainer with subclass
196 // name set to the same value as template class name.
197 auto const& relations = packet->getSubClassesRelations();
198
199 // Get the current time.
200 TimePoint const now(std::chrono::system_clock::now());
201
202 // Should contain client classes that are both in the packet and that are limited.
203 std::vector<isc::dhcp::ClientClass> common_client_classes;
204
205 // Lock a global-wide mutex for all client classes. It's not a necessary condition, but it's
206 // sufficient. Locking per client class, similarly to how it happens for subnet IDs would
207 // have been more efficient in a more heterogeneous class-to-packet distribution, but that
208 // situation is not guaranteed.
210
211 // Check if the rate limit is respected.
212 for (auto const& c : relations) {
213 auto const& class_def = isc::dhcp::CfgMgr::instance().getCurrentCfg()->getClientClassDictionary()->findClass(c.class_def_);
214 if (!class_def) {
215 continue;
216 }
217
218 auto const &limit_cfg = rate_limit_configuration_.parseUserContext(class_def->getContext());
219
220 if (!limit_cfg) {
221 continue;
222 }
223
224 // Get the limit.
225 RateLimit const& limit(limit_cfg->stringValue());
226
227 // Get the time series. Create a list on this class if not already added to the map.
228 TimeSeries& time_series(clocked_in_times_by_class_[c.class_]);
229
230 time_series.set_capacity(limit.allowed_packets_);
231
232 // Remove all expired times.
233 while (!time_series.empty()) {
234 if (time_series.back() + limit.time_unit_ < now) {
235 time_series.pop_back();
236 } else {
237 // This is an optimization. Job is finished at the first time point that is not
238 // expired. The rest are sorted and higher in value, thus not expired.
239 break;
240 }
241 }
242
243 // Effectively check the limit.
244 if (time_series.size() == limit.allowed_packets_) {
245 // Drop the packet.
247
250 .arg(classes.toText())
251 .arg(limit.text_)
252 .arg(c.class_);
253
254 // No need to check other client classes since packet is dropped.
255 break;
256 }
257
258 // Cache the set of client classes that are both in the packet and limited. If packet is
259 // not dropped, they will be iterated through one more time further below. As such, the
260 // configuration lookup for each client class at the beginning of this loop will no
261 // longer be required.
262 common_client_classes.push_back(c.class_);
263 }
264
266 // Honor the packet and keep track of it.
267 for (auto const& c : common_client_classes) {
268 TimeSeries& time_series(clocked_in_times_by_class_.at(c));
269 time_series.push_front(now);
270 }
271
272 // Only log that the packet is within the limit if this packet is being rate limited.
273 if (!common_client_classes.empty()) {
276 .arg(classes.toText());
277 }
278 }
279
280 return (0);
281 }
282
295 template <isc::util::DhcpSpace D>
300 return (0);
301 }
302
303 // Get the subnet ID.
305 handle.getArgument(isc::util::formatDhcpSpace<D>("subnet{}"), subnet);
306 if (!subnet) {
309 return (0);
310 }
311 isc::dhcp::SubnetID const subnet_id(subnet->getID());
312
313 RateLimit limit;
314 auto const& limit_cfg = subnetRateLimit<D>(subnet_id);
315 if (limit_cfg) {
316 // Get the limit.
317 limit = RateLimit(limit_cfg->stringValue());
318 } else {
319 return (0);
320 }
321
322 ProtectedTimeSeriesPtr time_series;
323
324 // Lock a global-wide mutex while configuration can be read-to and written-from.
325 {
327
328 // Create a time series on this subnet ID if not already added to the map.
329 ProtectedTimeSeriesPtr& time_series_ref(clocked_in_times_by_subnet_id_[subnet_id]);
330 if (!time_series_ref) {
331 time_series_ref = std::make_shared<ProtectedTimeSeries>();
332 }
333 time_series = time_series_ref;
334 }
335
336 // Get the current time.
337 TimePoint const now(std::chrono::system_clock::now());
338
339 // Lock the mutex on this subnet ID.
340 isc::util::MultiThreadingLock lock(time_series->mutex_);
341
342 time_series->time_points_.set_capacity(limit.allowed_packets_);
343
344 // Remove all expired times.
345 while (!time_series->time_points_.empty()) {
346 if (time_series->time_points_.back() + limit.time_unit_ < now) {
347 time_series->time_points_.pop_back();
348 } else {
349 // This is an optimization. Job is finished at the first time point that is not
350 // expired. The rest are sorted and higher in value, thus not expired.
351 break;
352 }
353 }
354
355 // Check the limit.
356 if (time_series->time_points_.size() < limit.allowed_packets_) {
357 // Honor the packet and keep track of it.
358 time_series->time_points_.push_front(now);
359 } else {
360 // Drop the packet.
362
365 .arg(subnet_id)
366 .arg(limit.text_);
367
368 return (0);
369 }
370
373 .arg(subnet_id)
374 .arg(limit.text_);
375
376 return (0);
377 }
378
394 template <isc::util::DhcpSpace D>
395 int lease_callout(isc::hooks::CalloutHandle& handle, bool lease_update = false) {
399 return (0);
400 }
401
402 // Get the lease.
404 handle.getArgument(isc::util::formatDhcpSpace<D>("lease{}"), lease);
405 if (!lease) {
406 isc_throw(Unexpected, "null lease in LimitManager::lease_callout");
407 }
408 isc::dhcp::SubnetID const subnet_id(lease->subnet_id_);
409
410 // Get the client classes.
412 handle.getArgument(isc::util::formatDhcpSpace<D>("query{}"), packet);
413 if (!packet) {
414 isc_throw(Unexpected, "null packet in LimitManager::lease_callout");
415 }
416 auto const& classes(packet->getClasses());
417
418 // Add client classes to the lease context.
419 addClientClassesToLeaseContext(classes, lease);
420
421 // Don't check limits for lease renewals.
422 if (lease_update) {
423 return (0);
424 }
425
426 // To be able to iterate over client classes and template classes, all
427 // regular classes must be added to a SubClassContainer with subclass
428 // name set to the same value as template class name.
429 auto const& relations(packet->getSubClassesRelations());
430
431 // Create parts of the limits context.
432 // Add client class limits to the context.
433 isc::data::ElementPtr client_class_limits = clientClassLimitsToElement<D>(relations, lease->getType());
434 // Add subnet limits to the context.
435 isc::data::ElementPtr subnet_limit = subnetLimitToElement<D>(subnet_id, lease->getType());
436
437 // Tie the limits together into an "ISC" context.
439 if (!client_class_limits->empty()) {
440 limits->set("client-classes", client_class_limits);
441 }
442 if (!subnet_limit->empty()) {
443 limits->set("subnet", subnet_limit);
444 }
445 if (limits->empty()) {
446 // No limits means we let the lease through by default. Exit here so that we don't check
447 // lease limits for nothing and so that we don't log details on non-limited leases.
448 return (0);
449 }
451 ISC->set("limits", limits);
453 context->set("ISC", ISC);
454
455 // Check lease limits.
456 std::string const limit_exceeded_text(checkLeaseLimits<D>(context));
457 if (limit_exceeded_text.empty()) {
459 .arg(lease->addr_);
460 } else {
461 // Prevent the lease from being allocated.
463
465 .arg(limit_exceeded_text);
466 }
467
468 return (0);
469 }
470
471private:
476 void addClientClassesToLeaseContext(isc::dhcp::ClientClasses const& classes,
477 isc::dhcp::LeasePtr const& lease);
478
490 template <isc::util::DhcpSpace D>
491 isc::data::ElementPtr clientClassLimitsToElement(isc::dhcp::SubClassRelationContainer const& classes,
492 isc::dhcp::Lease::Type const& lease_type);
493
504 template <isc::util::DhcpSpace D>
505 isc::data::ElementPtr subnetLimitToElement(isc::dhcp::SubnetID const subnet_id,
506 isc::dhcp::Lease::Type const& lease_type);
507
515 template <isc::util::DhcpSpace D>
516 isc::data::ConstElementPtr subnetRateLimit(isc::dhcp::SubnetID const subnet_id);
517
529 template <isc::util::DhcpSpace D>
530 std::string checkLeaseLimits(isc::data::ConstElementPtr const& context) const;
531
535 template <isc::util::DhcpSpace D>
536 void recountClassLeases() const;
537
540 std::unordered_map<isc::dhcp::ClientClass, TimeSeries> clocked_in_times_by_class_;
541
544 std::unordered_map<isc::dhcp::SubnetID, ProtectedTimeSeriesPtr> clocked_in_times_by_subnet_id_;
545
548 std::mutex mutex_;
549
551 AddressLimitConfiguration address_limit_configuration_;
552
554 PrefixLimitConfiguration prefix_limit_configuration_;
555
557 RateLimitConfiguration rate_limit_configuration_;
558
561 LimitManager() = default;
562 LimitManager(LimitManager const&) = delete;
563 LimitManager& operator=(LimitManager const&) = delete;
565};
566
567} // namespace limits
568} // namespace isc
569
570#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.
#define isc_throw(type, stream)
A shortcut macro to insert known values into exception arguments.
#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.