Kea 3.3.1
client_class_def_parser.cc
Go to the documentation of this file.
1// Copyright (C) 2015-2026 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#include <config.h>
8#include <dhcp/libdhcp++.h>
9#include <dhcpsrv/cfgmgr.h>
11#include <dhcpsrv/dhcpsrv_log.h>
16#include <eval/eval_context.h>
17#include <asiolink/io_address.h>
18#include <asiolink/io_error.h>
19
20#include <algorithm>
21#include <sstream>
22
23using namespace isc::data;
24using namespace isc::asiolink;
25using namespace isc::util;
26using namespace std;
27
31
32namespace isc {
33namespace dhcp {
34
35// ********************** ExpressionParser ****************************
36
37void
39 ConstElementPtr expression_cfg,
40 uint16_t family,
41 EvalContext::CheckDefined check_defined,
42 EvalContext::ParserType parser_type) {
43 if (expression_cfg->getType() != Element::string) {
44 isc_throw(DhcpConfigError, "expression ["
45 << expression_cfg->str() << "] must be a string, at ("
46 << expression_cfg->getPosition() << ")");
47 }
48
49 // Get the expression's text via getValue() as the text returned
50 // by str() enclosed in quotes.
51 std::string value;
52 expression_cfg->getValue(value);
53
54 if (parser_type == EvalContext::PARSER_STRING && value.empty()) {
55 isc_throw(DhcpConfigError, "expression can not be empty at ("
56 << expression_cfg->getPosition() << ")");
57 }
58
59 try {
60 EvalContext eval_ctx(family == AF_INET ? Option::V4 : Option::V6,
61 check_defined);
62 eval_ctx.parseString(value, parser_type);
63 expression.reset(new Expression());
64 *expression = eval_ctx.expression_;
65 } catch (const std::exception& ex) {
66 // Append position if there is a failure.
68 "expression: [" << value
69 << "] error: " << ex.what() << " at ("
70 << expression_cfg->getPosition() << ")");
71 }
72}
73
74// ********************** ClientClassDefParser ****************************
75
76void
78 ConstElementPtr class_def_cfg,
79 uint16_t family,
80 bool append_error_position,
81 bool check_dependencies) {
82 // name is now mandatory, so let's deal with it first.
83 std::string name = getString(class_def_cfg, "name");
84 if (name.empty()) {
86 "not empty parameter 'name' is required "
87 << getPosition("name", class_def_cfg) << ")");
88 }
89 // Note that the escape character is not problematic so is not escaped.
90 std::string escaped = ClientClasses::escape(name, false);
91 if (escaped != name) {
92 // Two possible long term behaviors: reject the name or sanitize it.
94 .arg(name)
95 .arg(escaped);
96 }
97
99
100 // Let's try to parse the template-test expression
101 bool is_template = false;
102
103 // Parse matching expression
104 ExpressionPtr match_expr;
105 ConstElementPtr test_cfg = class_def_cfg->get("test");
106 ConstElementPtr template_test_cfg = class_def_cfg->get("template-test");
107 if (test_cfg && template_test_cfg) {
108 isc_throw(DhcpConfigError, "can not use both 'test' and 'template-test' ("
109 << test_cfg->getPosition() << ") and ("
110 << template_test_cfg->getPosition() << ")");
111 }
112 std::string test;
113 bool depend_on_known = false;
115 if (template_test_cfg) {
116 test_cfg = template_test_cfg;
117 parser_type = EvalContext::PARSER_STRING;
118 is_template = true;
119 // We need to check for dependency on KNOWN, other than that we do not require
120 // classes in a template expression to be defined. This permits them to
121 // reference spawned classes. In other words, one template can depend on
122 // membership in the spawn of another template.
123 check_defined = [&depend_on_known](const ClientClass& cclass) {
124 // Check direct dependency on [UN]KNOWN
125 if ((cclass == "KNOWN") || (cclass == "UNKNOWN")) {
126 depend_on_known = true;
127 }
128
129 return (true);
130 };
131 } else {
132 check_defined =
133 [&class_dictionary, &depend_on_known, check_dependencies](const ClientClass& cclass) {
134 return (!check_dependencies || isClientClassDefined(class_dictionary,
135 depend_on_known, cclass));
136 };
137 }
138
139 if (test_cfg) {
140 ExpressionParser parser;
141 parser.parse(match_expr, test_cfg, family, check_defined, parser_type);
142 test = test_cfg->stringValue();
143 }
144
145 // Parse option def
146 CfgOptionDefPtr defs(new CfgOptionDef());
147 ConstElementPtr option_defs = class_def_cfg->get("option-def");
148 if (option_defs) {
149 // Apply defaults
151 family == AF_INET ?
154
155 OptionDefParser parser(family);
156 for (auto const& option_def : option_defs->listValue()) {
157 OptionDefinitionPtr def = parser.parse(option_def);
158
159 // Verify if the definition is for an option which is in a deferred
160 // processing list.
161 if (!LibDHCP::shouldDeferOptionUnpack(def->getOptionSpaceName(),
162 def->getCode())) {
164 "Not allowed option definition for code '"
165 << def->getCode() << "' in space '"
166 << def->getOptionSpaceName() << "' at ("
167 << option_def->getPosition() << ")");
168 }
169 try {
170 defs->add(def);
171 } catch (const std::exception& ex) {
172 // Sanity check: it should never happen
173 isc_throw(DhcpConfigError, ex.what() << " ("
174 << option_def->getPosition() << ")");
175 }
176 }
177 }
178
179 // Parse option data
180 CfgOptionPtr options(new CfgOption());
181 ConstElementPtr option_data = class_def_cfg->get("option-data");
182 if (option_data) {
183 auto opts_parser = createOptionDataListParser(family, defs);
184 opts_parser->parse(options, option_data);
185 }
186
187 // Parse user context
188 ConstElementPtr user_context = class_def_cfg->get("user-context");
189 if (user_context) {
190 if (user_context->getType() != Element::map) {
191 isc_throw(isc::dhcp::DhcpConfigError, "User context has to be a map ("
192 << user_context->getPosition() << ")");
193 }
194 }
195
196 // Let's try to parse the only-in-additional-list/only-if-required flag
197 auto required_elem = class_def_cfg->get("only-if-required");
198 auto additional_elem = class_def_cfg->get("only-in-additional-list");
199 if (required_elem) {
200 if (!additional_elem) {
202 additional_elem = required_elem;
203 } else {
205 "cannot specify both 'only-if-required' and "
206 "'only-in-additional-list'. Use only the latter.");
207 }
208 }
209
210 bool additional = false;
211 if (additional_elem) {
212 if (additional_elem->getType() == Element::boolean) {
213 additional = additional_elem->boolValue();
214 } else {
216 "'only-in-additional-list' must be boolean"
217 << additional_elem->getPosition());
218 }
219 }
220
221 // Let's try to parse the next-server field
222 IOAddress next_server("0.0.0.0");
223 if (class_def_cfg->contains("next-server")) {
224 std::string next_server_txt = getString(class_def_cfg, "next-server");
225 try {
226 next_server = IOAddress(next_server_txt);
227 } catch (const IOError& ex) {
229 "Invalid next-server value specified: '"
230 << next_server_txt << "' ("
231 << getPosition("next-server", class_def_cfg) << ")");
232 }
233
234 if (next_server.getFamily() != AF_INET) {
235 isc_throw(DhcpConfigError, "Invalid next-server value: '"
236 << next_server_txt << "', must be IPv4 address ("
237 << getPosition("next-server", class_def_cfg) << ")");
238 }
239
240 if (next_server.isV4Bcast()) {
241 isc_throw(DhcpConfigError, "Invalid next-server value: '"
242 << next_server_txt << "', must not be a broadcast ("
243 << getPosition("next-server", class_def_cfg) << ")");
244 }
245 }
246
247 // Let's try to parse server-hostname
248 std::string sname;
249 if (class_def_cfg->contains("server-hostname")) {
250 sname = getString(class_def_cfg, "server-hostname");
251
252 if (sname.length() >= Pkt4::MAX_SNAME_LEN) {
253 isc_throw(DhcpConfigError, "server-hostname must be at most "
254 << Pkt4::MAX_SNAME_LEN - 1 << " bytes long, it is "
255 << sname.length() << " ("
256 << getPosition("server-hostname", class_def_cfg) << ")");
257 }
258 }
259
260 // Let's try to parse boot-file-name
261 std::string filename;
262 if (class_def_cfg->contains("boot-file-name")) {
263 filename = getString(class_def_cfg, "boot-file-name");
264
265 if (filename.length() > Pkt4::MAX_FILE_LEN) {
266 isc_throw(DhcpConfigError, "boot-file-name must be at most "
267 << Pkt4::MAX_FILE_LEN - 1 << " bytes long, it is "
268 << filename.length() << " ("
269 << getPosition("boot-file-name", class_def_cfg) << ")");
270 }
271 }
272
273 Optional<uint32_t> offer_lft;
274 if (class_def_cfg->contains("offer-lifetime")) {
275 auto value = getInteger(class_def_cfg, "offer-lifetime");
276 if (value < 0) {
277 isc_throw(DhcpConfigError, "the value of offer-lifetime '"
278 << value << "' must be a positive number ("
279 << getPosition("offer-lifetime", class_def_cfg) << ")");
280 }
281
282 offer_lft = value;
283 }
284
285 // Parse valid lifetime triplet.
286 Triplet<uint32_t> valid_lft = parseIntTriplet(class_def_cfg, "valid-lifetime");
287
288 Triplet<uint32_t> preferred_lft;
289 if (family != AF_INET) {
290 // Parse preferred lifetime triplet.
291 preferred_lft = parseIntTriplet(class_def_cfg, "preferred-lifetime");
292 }
293
294 // Sanity checks on built-in classes
295 for (auto const& bn : builtinNames) {
296 if (name == bn) {
297 if (additional) {
298 isc_throw(DhcpConfigError, "built-in class '" << name
299 << "' only-in-additional-list flag must be false");
300 }
301 if (!test.empty()) {
302 isc_throw(DhcpConfigError, "built-in class '" << name
303 << "' test expression must be empty");
304 }
305 }
306 }
307
308 // Sanity checks on DROP and REJECT
309 if ((name == "DROP") || (name == "REJECT")) {
310 if (additional) {
311 isc_throw(DhcpConfigError, "special class '" << name
312 << "' only-in-additional-list flag must be false");
313 }
314 // depend_on_known is now allowed
315 }
316
317 if (additional &&
318 (!valid_lft.unspecified() ||
319 !preferred_lft.unspecified() ||
320 !offer_lft.unspecified())) {
322 .arg(name);
323 }
324
325 // Add the client class definition
326 try {
327 class_dictionary->addClass(name, match_expr, test, additional,
328 depend_on_known, options, defs,
329 user_context, next_server, sname, filename,
330 valid_lft, preferred_lft, is_template, offer_lft);
331 } catch (const std::exception& ex) {
332 std::ostringstream s;
333 s << "Can't add class: " << ex.what();
334 // Append position of the error in JSON string if required.
335 if (append_error_position) {
336 s << " (" << class_def_cfg->getPosition() << ")";
337 }
338 isc_throw(DhcpConfigError, s.str());
339 }
340}
341
342void
344 const uint16_t family) {
345 // Make sure that the client class definition is stored in a map.
346 if (!class_def_cfg || (class_def_cfg->getType() != Element::map)) {
347 isc_throw(DhcpConfigError, "client class definition is not a map");
348 }
349
350 // Common v4 and v6 parameters supported for the client class.
351 static std::set<std::string> supported_params = { "name",
352 "test",
353 "option-data",
354 "user-context",
355 "only-if-required", // deprecated
356 "only-in-additional-list",
357 "valid-lifetime",
358 "min-valid-lifetime",
359 "max-valid-lifetime",
360 "template-test"};
361
362 // The v4 client class supports additional parameters.
363 static std::set<std::string> supported_params_v4 = { "option-def",
364 "next-server",
365 "server-hostname",
366 "boot-file-name" };
367
368 // The v6 client class supports additional parameters.
369 static std::set<std::string> supported_params_v6 = { "preferred-lifetime",
370 "min-preferred-lifetime",
371 "max-preferred-lifetime" };
372
373 // Iterate over the specified parameters and check if they are all supported.
374 for (auto const& name_value_pair : class_def_cfg->mapValue()) {
375 if ((supported_params.count(name_value_pair.first) > 0) ||
376 ((family == AF_INET) && (supported_params_v4.count(name_value_pair.first) > 0)) ||
377 ((family != AF_INET) && (supported_params_v6.count(name_value_pair.first) > 0))) {
378 continue;
379 } else {
380 isc_throw(DhcpConfigError, "unsupported client class parameter '"
381 << name_value_pair.first << "'");
382 }
383 }
384}
385
386boost::shared_ptr<OptionDataListParser>
388 CfgOptionDefPtr cfg_option_def) const {
389 auto parser = boost::make_shared<OptionDataListParser>(address_family, cfg_option_def);
390 return (parser);
391}
392
393// ****************** ClientClassDefListParser ************************
394
397 uint16_t family, bool check_dependencies) {
399 for (auto const& client_class_def : client_class_def_list->listValue()) {
401 parser.parse(dictionary, client_class_def, family, true, check_dependencies);
402 }
403 return (dictionary);
404}
405
406} // end of namespace isc::dhcp
407} // end of namespace isc
static size_t setListDefaults(isc::data::ConstElementPtr list, const SimpleDefaults &default_values)
Sets the default values for all entries in a list.
static const data::Element::Position & getPosition(const std::string &name, const data::ConstElementPtr parent)
Utility method that returns position of an element.
static std::string getString(isc::data::ConstElementPtr scope, const std::string &name)
Returns a string parameter from a scope.
const isc::util::Triplet< uint32_t > parseIntTriplet(const data::ConstElementPtr &scope, const std::string &name)
Parses an integer triplet.
static int64_t getInteger(isc::data::ConstElementPtr scope, const std::string &name)
Returns an integer parameter from a scope.
Represents option definitions used by the DHCP server.
Represents option data configuration for the DHCP server.
Definition cfg_option.h:405
ClientClassDictionaryPtr parse(isc::data::ConstElementPtr class_def_list, uint16_t family, bool check_dependencies=true)
Parse configuration entries.
Parser for a single client class definition.
virtual boost::shared_ptr< OptionDataListParser > createOptionDataListParser(const uint16_t address_family, CfgOptionDefPtr cfg_option_def) const
Returns an instance of the OptionDataListParser to be used in parsing the option-data structure.
void parse(ClientClassDictionaryPtr &class_dictionary, isc::data::ConstElementPtr client_class_def, uint16_t family, bool append_error_position=true, bool check_dependencies=true)
Parses an entry that describes single client class definition.
void checkParametersSupported(const isc::data::ConstElementPtr &class_def_cfg, const uint16_t family)
Iterates over class parameters and checks if they are supported.
Maintains a list of ClientClassDef's.
static std::string escape(const std::string &name, bool escape_escape=true)
Escape a client class name.
Definition classify.cc:164
To be removed. Please use ConfigError instead.
Parser for a logical expression.
void parse(ExpressionPtr &expression, isc::data::ConstElementPtr expression_cfg, uint16_t family, isc::eval::EvalContext::CheckDefined check_defined=isc::eval::EvalContext::acceptAll, isc::eval::EvalContext::ParserType parser_type=isc::eval::EvalContext::PARSER_BOOL)
Parses an expression configuration element into an Expression.
static bool shouldDeferOptionUnpack(const std::string &space, const uint16_t code)
Checks if an option unpacking has to be deferred.
Definition libdhcp++.cc:286
Parser for a single option definition.
OptionDefinitionPtr parse(isc::data::ConstElementPtr option_def)
Parses an entry that describes single option definition.
static const size_t MAX_SNAME_LEN
length of the SNAME field in DHCPv4 message
Definition pkt4.h:44
static const size_t MAX_FILE_LEN
length of the FILE field in DHCPv4 message
Definition pkt4.h:47
static const isc::data::SimpleDefaults OPTION4_DEF_DEFAULTS
This table defines default values for option definitions in DHCPv4.
static const isc::data::SimpleDefaults OPTION6_DEF_DEFAULTS
This table defines default values for option definitions in DHCPv6.
Evaluation context, an interface to the expression evaluation.
std::function< bool(const ClientClass &)> CheckDefined
Type of the check defined function.
bool parseString(const std::string &str, ParserType type=PARSER_BOOL)
Run the parser on the string specified.
ParserType
Specifies what type of expression the parser is expected to see.
@ PARSER_BOOL
expression is expected to evaluate to bool
@ PARSER_STRING
expression is expected to evaluate to string
static bool acceptAll(const ClientClass &client_class)
Accept all client class names.
isc::dhcp::Expression expression_
Parsed expression (output tokens are stored here).
A template representing an optional value.
Definition optional.h:37
void unspecified(bool unspecified)
Modifies the flag that indicates whether the value is specified or unspecified.
Definition optional.h:145
This template specifies a parameter value.
Definition triplet.h:37
Defines classes for storing client class definitions.
Parsers for client class definitions.
#define isc_throw(type, stream)
A shortcut macro to insert known values into exception arguments.
#define LOG_WARN(LOGGER, MESSAGE)
Macro to conveniently test warn output and log it.
Definition macros.h:26
boost::shared_ptr< const Element > ConstElementPtr
Definition data.h:30
isc::log::Logger dhcpsrv_logger("dhcpsrv")
DHCP server library Logger.
Definition dhcpsrv_log.h:56
std::string ClientClass
Defines a single class name.
Definition classify.h:45
boost::shared_ptr< CfgOption > CfgOptionPtr
Non-const pointer.
Definition cfg_option.h:983
boost::shared_ptr< CfgOptionDef > CfgOptionDefPtr
Non-const pointer.
const isc::log::MessageID DHCPSRV_CLASS_WITH_ADDITIONAL_AND_LIFETIMES
const isc::log::MessageID DHCPSRV_CLASS_BAD_NAME
boost::shared_ptr< OptionDefinition > OptionDefinitionPtr
Pointer to option definition object.
boost::shared_ptr< Expression > ExpressionPtr
Definition token.h:31
boost::shared_ptr< ClientClassDictionary > ClientClassDictionaryPtr
Defines a pointer to a ClientClassDictionary.
bool isClientClassDefined(ClientClassDictionaryPtr &class_dictionary, bool &depend_on_known, const ClientClass &client_class)
Check if a client class name is already defined, i.e.
const isc::log::MessageID DHCPSRV_ONLY_IF_REQUIRED_DEPRECATED
std::vector< TokenPtr > Expression
This is a structure that holds an expression converted to RPN.
Definition token.h:29
std::list< std::string > builtinNames
List of classes for which test expressions cannot be defined.
Defines the logger used by the top-level component of kea-lfc.