Kea 3.1.8
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 // Recount the leases by class.
140 recountClassLeases<D>();
141 } else {
142 std::string const lease_db_access_string(isc::dhcp::CfgMgr::instance()
143 .getStagingCfg()
144 ->getCfgDbAccess()
145 ->getLeaseDbAccessString());
146 if ((lease_db_access_string.find("retry-on-startup=true") != std::string::npos) &&
147 ((lease_db_access_string.find("type=mysql") != std::string::npos) ||
148 (lease_db_access_string.find("type=postgresql") != std::string::npos))) {
150 } else {
153 }
154 }
155
156 return (0);
157 }
158
169 template <isc::util::DhcpSpace D>
174 return (0);
175 }
176
177 // Get the client classes.
179 handle.getArgument(isc::util::formatDhcpSpace<D>("query{}"), packet);
180 if (!packet) {
181 isc_throw(Unexpected, "null packet in LimitManager::pkt_receive");
182 }
183 auto const& classes(packet->getClasses());
184
185 // To be able to iterate over client classes and template classes, all
186 // regular classes must be added to a SubClassContainer with subclass
187 // name set to the same value as template class name.
188 auto const& relations = packet->getSubClassesRelations();
189
190 // Get the current time.
191 TimePoint const now(std::chrono::system_clock::now());
192
193 // Should contain client classes that are both in the packet and that are limited.
194 std::vector<isc::dhcp::ClientClass> common_client_classes;
195
196 // Lock a global-wide mutex for all client classes. It's not a necessary condition, but it's
197 // sufficient. Locking per client class, similarly to how it happens for subnet IDs would
198 // have been more efficient in a more heterogeneous class-to-packet distribution, but that
199 // situation is not guaranteed.
201
202 // Check if the rate limit is respected.
203 for (auto const& c : relations) {
204 auto const& class_def = isc::dhcp::CfgMgr::instance().getCurrentCfg()->getClientClassDictionary()->findClass(c.class_def_);
205 if (!class_def) {
206 continue;
207 }
208
209 auto const &limit_cfg = rate_limit_configuration_.parseUserContext(class_def->getContext());
210
211 if (!limit_cfg) {
212 continue;
213 }
214
215 // Get the limit.
216 RateLimit const& limit(limit_cfg->stringValue());
217
218 // Get the time series. Create a list on this class if not already added to the map.
219 TimeSeries& time_series(clocked_in_times_by_class_[c.class_]);
220
221 time_series.set_capacity(limit.allowed_packets_);
222
223 // Remove all expired times.
224 while (!time_series.empty()) {
225 if (time_series.back() + limit.time_unit_ < now) {
226 time_series.pop_back();
227 } else {
228 // This is an optimization. Job is finished at the first time point that is not
229 // expired. The rest are sorted and higher in value, thus not expired.
230 break;
231 }
232 }
233
234 // Effectively check the limit.
235 if (time_series.size() == limit.allowed_packets_) {
236 // Drop the packet.
238
241 .arg(classes.toText())
242 .arg(limit.text_)
243 .arg(c.class_);
244
245 // No need to check other client classes since packet is dropped.
246 break;
247 }
248
249 // Cache the set of client classes that are both in the packet and limited. If packet is
250 // not dropped, they will be iterated through one more time further below. As such, the
251 // configuration lookup for each client class at the beginning of this loop will no
252 // longer be required.
253 common_client_classes.push_back(c.class_);
254 }
255
259 stats_mgr.addValue("pkt4-limit-exceeded",
260 static_cast<int64_t>(1));
261 stats_mgr.addValue("pkt4-receive-drop",
262 static_cast<int64_t>(1));
263 } else {
264 stats_mgr.addValue("pkt6-limit-exceeded",
265 static_cast<int64_t>(1));
266 stats_mgr.addValue("pkt6-receive-drop",
267 static_cast<int64_t>(1));
268 }
269 } else {
270 // Honor the packet and keep track of it.
271 for (auto const& c : common_client_classes) {
272 TimeSeries& time_series(clocked_in_times_by_class_.at(c));
273 time_series.push_front(now);
274 }
275
276 // Only log that the packet is within the limit if this packet is being rate limited.
277 if (!common_client_classes.empty()) {
280 .arg(classes.toText());
281 }
282 }
283
284 return (0);
285 }
286
299 template <isc::util::DhcpSpace D>
304 return (0);
305 }
306
307 // Get the subnet ID.
309 handle.getArgument(isc::util::formatDhcpSpace<D>("subnet{}"), subnet);
310 if (!subnet) {
313 return (0);
314 }
315 isc::dhcp::SubnetID const subnet_id(subnet->getID());
316
317 RateLimit limit;
318 auto const& limit_cfg = subnetRateLimit<D>(subnet_id);
319 if (limit_cfg) {
320 // Get the limit.
321 limit = RateLimit(limit_cfg->stringValue());
322 } else {
323 return (0);
324 }
325
326 ProtectedTimeSeriesPtr time_series;
327
328 // Lock a global-wide mutex while configuration can be read-to and written-from.
329 {
331
332 // Create a time series on this subnet ID if not already added to the map.
333 ProtectedTimeSeriesPtr& time_series_ref(clocked_in_times_by_subnet_id_[subnet_id]);
334 if (!time_series_ref) {
335 time_series_ref = std::make_shared<ProtectedTimeSeries>();
336 }
337 time_series = time_series_ref;
338 }
339
340 // Get the current time.
341 TimePoint const now(std::chrono::system_clock::now());
342
343 // Lock the mutex on this subnet ID.
344 isc::util::MultiThreadingLock lock(time_series->mutex_);
345
346 time_series->time_points_.set_capacity(limit.allowed_packets_);
347
348 // Remove all expired times.
349 while (!time_series->time_points_.empty()) {
350 if (time_series->time_points_.back() + limit.time_unit_ < now) {
351 time_series->time_points_.pop_back();
352 } else {
353 // This is an optimization. Job is finished at the first time point that is not
354 // expired. The rest are sorted and higher in value, thus not expired.
355 break;
356 }
357 }
358
359 // Check the limit.
360 if (time_series->time_points_.size() < limit.allowed_packets_) {
361 // Honor the packet and keep track of it.
362 time_series->time_points_.push_front(now);
363 } else {
364 // Drop the packet.
366
369 .arg(subnet_id)
370 .arg(limit.text_);
371
374 stats_mgr.addValue("pkt4-limit-exceeded",
375 static_cast<int64_t>(1));
376 stats_mgr.addValue("pkt4-receive-drop",
377 static_cast<int64_t>(1));
378 } else {
379 stats_mgr.addValue("pkt6-limit-exceeded",
380 static_cast<int64_t>(1));
381 stats_mgr.addValue("pkt6-receive-drop",
382 static_cast<int64_t>(1));
383 }
384
385 return (0);
386 }
387
390 .arg(subnet_id)
391 .arg(limit.text_);
392
393 return (0);
394 }
395
411 template <isc::util::DhcpSpace D>
412 int lease_callout(isc::hooks::CalloutHandle& handle, bool lease_update = false) {
416 return (0);
417 }
418
419 // Get the lease.
421 handle.getArgument(isc::util::formatDhcpSpace<D>("lease{}"), lease);
422 if (!lease) {
423 isc_throw(Unexpected, "null lease in LimitManager::lease_callout");
424 }
425 isc::dhcp::SubnetID const subnet_id(lease->subnet_id_);
426
427 // Get the client classes.
429 handle.getArgument(isc::util::formatDhcpSpace<D>("query{}"), packet);
430 if (!packet) {
431 isc_throw(Unexpected, "null packet in LimitManager::lease_callout");
432 }
433 auto const& classes(packet->getClasses());
434
435 // Add client classes to the lease context.
436 addClientClassesToLeaseContext(classes, lease);
437
438 // Don't check limits for lease renewals.
439 if (lease_update) {
440 return (0);
441 }
442
443 // To be able to iterate over client classes and template classes, all
444 // regular classes must be added to a SubClassContainer with subclass
445 // name set to the same value as template class name.
446 auto const& relations(packet->getSubClassesRelations());
447
448 // Create parts of the limits context.
449 // Add client class limits to the context.
450 isc::data::ElementPtr client_class_limits = clientClassLimitsToElement<D>(relations, lease->getType());
451 // Add subnet limits to the context.
452 isc::data::ElementPtr subnet_limit = subnetLimitToElement<D>(subnet_id, lease->getType());
453
454 // Tie the limits together into an "ISC" context.
456 if (!client_class_limits->empty()) {
457 limits->set("client-classes", client_class_limits);
458 }
459 if (!subnet_limit->empty()) {
460 limits->set("subnet", subnet_limit);
461 }
462 if (limits->empty()) {
463 // No limits means we let the lease through by default. Exit here so that we don't check
464 // lease limits for nothing and so that we don't log details on non-limited leases.
465 return (0);
466 }
468 ISC->set("limits", limits);
470 context->set("ISC", ISC);
471
472 // Check lease limits.
473 std::string const limit_exceeded_text(checkLeaseLimits<D>(context));
474 if (limit_exceeded_text.empty()) {
476 .arg(lease->addr_);
477 } else {
478 // Prevent the lease from being allocated.
480
482 .arg(limit_exceeded_text);
483 }
484
485 return (0);
486 }
487
488private:
493 void addClientClassesToLeaseContext(isc::dhcp::ClientClasses const& classes,
494 isc::dhcp::LeasePtr const& lease);
495
507 template <isc::util::DhcpSpace D>
508 isc::data::ElementPtr clientClassLimitsToElement(isc::dhcp::SubClassRelationContainer const& classes,
509 isc::dhcp::Lease::Type const& lease_type);
510
521 template <isc::util::DhcpSpace D>
522 isc::data::ElementPtr subnetLimitToElement(isc::dhcp::SubnetID const subnet_id,
523 isc::dhcp::Lease::Type const& lease_type);
524
532 template <isc::util::DhcpSpace D>
533 isc::data::ConstElementPtr subnetRateLimit(isc::dhcp::SubnetID const subnet_id);
534
546 template <isc::util::DhcpSpace D>
547 std::string checkLeaseLimits(isc::data::ConstElementPtr const& context) const;
548
552 template <isc::util::DhcpSpace D>
553 void recountClassLeases() const;
554
557 std::unordered_map<isc::dhcp::ClientClass, TimeSeries> clocked_in_times_by_class_;
558
561 std::unordered_map<isc::dhcp::SubnetID, ProtectedTimeSeriesPtr> clocked_in_times_by_subnet_id_;
562
565 std::mutex mutex_;
566
568 AddressLimitConfiguration address_limit_configuration_;
569
571 PrefixLimitConfiguration prefix_limit_configuration_;
572
574 RateLimitConfiguration rate_limit_configuration_;
575
578 LimitManager() = default;
579 LimitManager(LimitManager const&) = delete;
580 LimitManager& operator=(LimitManager const&) = delete;
582};
583
584} // namespace limits
585} // namespace isc
586
587#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:354
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 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.
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:30
boost::shared_ptr< Element > ElementPtr
Definition data.h:29
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.