Kea  2.3.4-git
flex_option.cc
Go to the documentation of this file.
1 // Copyright (C) 2019-2022 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 
9 #include <flex_option.h>
10 #include <flex_option_log.h>
11 #include <util/strutil.h>
12 #include <cc/simple_parser.h>
13 #include <dhcp/dhcp4.h>
14 #include <dhcp/libdhcp++.h>
15 #include <dhcp/option_definition.h>
16 #include <dhcp/option_space.h>
17 #include <dhcp/option_vendor.h>
18 #include <dhcpsrv/cfgmgr.h>
19 #include <eval/eval_context.h>
20 
21 using namespace isc;
22 using namespace isc::data;
23 using namespace isc::dhcp;
24 using namespace isc::eval;
25 using namespace isc::flex_option;
26 using namespace isc::log;
27 using namespace isc::util;
28 using namespace std;
29 
30 namespace {
31 
42 void
43 parseAction(ConstElementPtr option,
45  Option::Universe universe,
46  const string& name,
48  EvalContext::ParserType parser_type) {
49  ConstElementPtr elem = option->get(name);
50  if (elem) {
51  string expr_text = elem->stringValue();
52  if (expr_text.empty()) {
53  isc_throw(BadValue, "'" << name << "' must not be empty");
54  }
55  if (opt_cfg->getAction() != FlexOptionImpl::NONE) {
56  isc_throw(BadValue, "multiple actions: " << option->str());
57  }
58  opt_cfg->setAction(action);
59  opt_cfg->setText(expr_text);
60  try {
61  EvalContext eval_ctx(universe);
62  eval_ctx.parseString(expr_text, parser_type);
63  ExpressionPtr expr(new Expression(eval_ctx.expression));
64  opt_cfg->setExpr(expr);
65  } catch (const std::exception& ex) {
66  isc_throw(BadValue, "can't parse " << name << " expression ["
67  << expr_text << "] error: " << ex.what());
68  }
69  }
70 }
71 
72 } // end of anonymous namespace
73 
74 namespace isc {
75 namespace flex_option {
76 
77 const SimpleKeywords FlexOptionImpl::OPTION_PARAMETERS = {
78  { "code", Element::integer },
79  { "name", Element::string },
80  { "space", Element::string },
81  { "csv-format", Element::boolean },
82  { "add", Element::string },
83  { "supersede", Element::string },
84  { "remove", Element::string },
85  { "sub-options", Element::list },
86  { "client-class", Element::string },
87  { "comment", Element::string }
88 };
89 
90 const SimpleKeywords FlexOptionImpl::SUB_OPTION_PARAMETERS = {
91  { "code", Element::integer },
92  { "name", Element::string },
93  { "space", Element::string },
94  { "csv-format", Element::boolean },
95  { "add", Element::string },
96  { "supersede", Element::string },
97  { "remove", Element::string },
98  { "container-add", Element::boolean },
99  { "container-remove", Element::boolean },
100  { "client-class", Element::string },
101  { "comment", Element::string }
102 };
103 
104 FlexOptionImpl::OptionConfig::OptionConfig(uint16_t code,
106  : code_(code), def_(def), action_(NONE), class_("") {
107 }
108 
110 }
111 
114  OptionConfigPtr container)
115  : OptionConfig(code, def), container_(container), vendor_id_(0),
116  container_action_(NONE) {
117  if (!container) {
118  isc_throw(Unexpected, "null container?");
119  }
120 }
121 
123 }
124 
126 }
127 
129  sub_option_config_map_.clear();
130  option_config_map_.clear();
131 }
132 
133 void
135  if (!options) {
136  isc_throw(BadValue, "'options' parameter is mandatory");
137  }
138  if (options->getType() != Element::list) {
139  isc_throw(BadValue, "'options' parameter must be a list");
140  }
141  if (options->empty()) {
142  return;
143  }
144  for (auto option : options->listValue()) {
145  parseOptionConfig(option);
146  }
147 }
148 
149 void
150 FlexOptionImpl::parseOptionConfig(ConstElementPtr option) {
151  uint16_t family = CfgMgr::instance().getFamily();
152  if (!option) {
153  isc_throw(BadValue, "null option element");
154  }
155  if (option->getType() != Element::map) {
156  isc_throw(BadValue, "option element is not a map");
157  }
158  // See SimpleParser::checkKeywords
159  for (auto entry : option->mapValue()) {
160  if (OPTION_PARAMETERS.count(entry.first) == 0) {
161  isc_throw(BadValue, "unknown parameter '" << entry.first << "'");
162  }
163  Element::types expected = OPTION_PARAMETERS.at(entry.first);
164  if (entry.second->getType() == expected) {
165  continue;
166  }
167  isc_throw(BadValue, "'" << entry.first << "' must be "
168  << (expected == Element::integer ? "an " : "a ")
169  << Element::typeToName(expected)
170  << ": " << entry.second->str());
171  }
172  ConstElementPtr code_elem = option->get("code");
173  ConstElementPtr name_elem = option->get("name");
174  ConstElementPtr space_elem = option->get("space");
175  ConstElementPtr csv_format_elem = option->get("csv-format");
176  ConstElementPtr class_elem = option->get("client-class");
177  ConstElementPtr sub_options = option->get("sub-options");
178  if (!code_elem && !name_elem) {
179  isc_throw(BadValue, "'code' or 'name' must be specified: "
180  << option->str());
181  }
182  string space;
183  Option::Universe universe;
184  if (family == AF_INET) {
185  space = DHCP4_OPTION_SPACE;
186  universe = Option::V4;
187  } else {
188  space = DHCP6_OPTION_SPACE;
189  universe = Option::V6;
190  }
191  if (space_elem) {
192  space = space_elem->stringValue();
193  if (!OptionSpace::validateName(space)) {
194  isc_throw(BadValue, "'" << space << "' is not a valid space name");
195  }
196  }
197  // The code is always set below but some compilers can't see that...
198  uint16_t code = 0;
199  if (code_elem) {
200  int64_t value = code_elem->intValue();
201  int64_t max_code;
202  if (family == AF_INET) {
203  max_code = numeric_limits<uint8_t>::max();
204  } else {
205  max_code = numeric_limits<uint16_t>::max();
206  }
207  if ((value < 0) || (value > max_code)) {
208  isc_throw(OutOfRange, "invalid 'code' value " << value
209  << " not in [0.." << max_code << "]");
210  }
211  if (space == DHCP4_OPTION_SPACE) {
212  if (value == DHO_PAD) {
214  "invalid 'code' value 0: reserved for PAD");
215  } else if (value == DHO_END) {
217  "invalid 'code' value 255: reserved for END");
218  }
219  } else if (space == DHCP6_OPTION_SPACE) {
220  if (value == 0) {
221  isc_throw(BadValue, "invalid 'code' value 0: reserved");
222  }
223  }
224  code = static_cast<uint16_t>(value);
225  }
227  if (name_elem) {
228  string name = name_elem->stringValue();
229  if (name.empty()) {
230  isc_throw(BadValue, "'name' must not be empty");
231  }
232  def = LibDHCP::getOptionDef(space, name);
233  if (!def) {
234  def = LibDHCP::getRuntimeOptionDef(space, name);
235  }
236  if (!def) {
237  def = LibDHCP::getLastResortOptionDef(space, name);
238  }
239  if (!def) {
240  isc_throw(BadValue, "no known '" << name << "' option in '"
241  << space << "' space");
242  }
243  if (code_elem && (def->getCode() != code)) {
244  isc_throw(BadValue, "option '" << name << "' is defined as code: "
245  << def->getCode() << ", not the specified code: "
246  << code);
247  }
248  code = def->getCode();
249  }
250 
251  bool csv_format = false;
252  if (csv_format_elem) {
253  csv_format = csv_format_elem->boolValue();
254  }
255 
256  if (!csv_format && !sub_options) {
257  // No definition means no csv format.
258  if (def) {
259  def.reset();
260  }
261  } else if (!def) {
262  // Definition is required with csv format.
263  def = isc::dhcp::LibDHCP::getOptionDef(space, code);
264  if (!def) {
265  def = isc::dhcp::LibDHCP::getRuntimeOptionDef(space, code);
266  }
267  if (!def) {
269  }
270  if (!def && csv_format) {
271  isc_throw(BadValue, "no known option with code '" << code
272  << "' in '" << space << "' space");
273  }
274  }
275 
276  OptionConfigPtr opt_cfg(new OptionConfig(code, def));
277  if (class_elem) {
278  opt_cfg->setClass(class_elem->stringValue());
279  }
280 
281  // opt_cfg initial action is NONE.
282  if (sub_options) {
283  string action;
284  if (option->contains("add")) {
285  action = "add";
286  } else if (option->contains("supersede")) {
287  action = "supersede";
288  } else if (option->contains("remove")) {
289  action = "remove";
290  }
291  if (!action.empty()) {
292  isc_throw(BadValue, "'sub-options' and '" << action << "' are "
293  << "incompatible in the same entry");
294  }
295  parseSubOptions(sub_options, opt_cfg, universe);
296  } else {
297  parseAction(option, opt_cfg, universe,
298  "add", ADD, EvalContext::PARSER_STRING);
299  parseAction(option, opt_cfg, universe,
300  "supersede", SUPERSEDE, EvalContext::PARSER_STRING);
301  parseAction(option, opt_cfg, universe,
302  "remove", REMOVE, EvalContext::PARSER_BOOL);
303 
304  if (opt_cfg->getAction() == NONE) {
305  isc_throw(BadValue, "no action: " << option->str());
306  }
307 
308  // The [] operator creates the item if it does not exist before
309  // returning a reference to it.
310  OptionConfigList& opt_lst = option_config_map_[code];
311  opt_lst.push_back(opt_cfg);
312  }
313 }
314 
315 void
316 FlexOptionImpl::parseSubOptions(ConstElementPtr sub_options,
317  OptionConfigPtr opt_cfg,
318  Option::Universe universe) {
319  for (ConstElementPtr sub_option : sub_options->listValue()) {
320  parseSubOption(sub_option, opt_cfg, universe);
321  }
322 }
323 
324 void
325 FlexOptionImpl::parseSubOption(ConstElementPtr sub_option,
326  OptionConfigPtr opt_cfg,
327  Option::Universe universe) {
328  if (!sub_option) {
329  isc_throw(BadValue, "null sub-option element");
330  }
331  if (sub_option->getType() != Element::map) {
332  isc_throw(BadValue, "sub-option element is not a map");
333  }
334  // See SimpleParser::checkKeywords
335  for (auto entry : sub_option->mapValue()) {
336  if (SUB_OPTION_PARAMETERS.count(entry.first) == 0) {
337  isc_throw(BadValue, "unknown parameter '" << entry.first << "'");
338  }
339  Element::types expected = SUB_OPTION_PARAMETERS.at(entry.first);
340  if (entry.second->getType() == expected) {
341  continue;
342  }
343  isc_throw(BadValue, "'" << entry.first << "' must be "
344  << (expected == Element::integer ? "an " : "a ")
345  << Element::typeToName(expected)
346  << ": " << entry.second->str());
347  }
348  ConstElementPtr code_elem = sub_option->get("code");
349  ConstElementPtr name_elem = sub_option->get("name");
350  ConstElementPtr space_elem = sub_option->get("space");
351  ConstElementPtr csv_format_elem = sub_option->get("csv-format");
352  ConstElementPtr class_elem = sub_option->get("client-class");
353  if (!code_elem && !name_elem) {
354  isc_throw(BadValue, "'code' or 'name' must be specified: "
355  << sub_option->str());
356  }
357  string space;
358  if (space_elem) {
359  space = space_elem->stringValue();
360  if (!OptionSpace::validateName(space)) {
361  isc_throw(BadValue, "'" << space << "' is not a valid space name");
362  }
363  } else {
364  OptionDefinitionPtr opt_def = opt_cfg->getOptionDef();
365  if (!opt_def) {
366  isc_throw(BadValue, "container is not defined: can't get space");
367  }
368  space = opt_def->getEncapsulatedSpace();
369  if (space.empty()) {
370  isc_throw(BadValue, "container does not encapsulate a space");
371  }
372  }
373  uint32_t vendor_id = LibDHCP::optionSpaceToVendorId(space);
374  uint16_t code;
375  if (code_elem) {
376  int64_t value = code_elem->intValue();
377  int64_t max_code;
378  if (universe == Option::V4) {
379  max_code = numeric_limits<uint8_t>::max();
380  } else {
381  max_code = numeric_limits<uint16_t>::max();
382  }
383  if ((value < 0) || (value > max_code)) {
384  isc_throw(OutOfRange, "invalid 'code' value " << value
385  << " not in [0.." << max_code << "]");
386  }
387  code = static_cast<uint16_t>(value);
388  }
390  if (name_elem) {
391  string name = name_elem->stringValue();
392  if (name.empty()) {
393  isc_throw(BadValue, "'name' must not be empty");
394  }
395  def = LibDHCP::getOptionDef(space, name);
396  if (!def && vendor_id) {
397  def = LibDHCP::getVendorOptionDef(universe, vendor_id, name);
398  }
399  if (!def) {
400  def = LibDHCP::getRuntimeOptionDef(space, name);
401  }
402  if (!def) {
403  isc_throw(BadValue, "no known '" << name << "' sub-option in '"
404  << space << "' space");
405  }
406  if (code_elem && (def->getCode() != code)) {
407  isc_throw(BadValue, "sub-option '" << name
408  << "' is defined as code: " << def->getCode()
409  << ", not the specified code: " << code);
410  }
411  code = def->getCode();
412  }
413 
414  bool csv_format = false;
415  if (csv_format_elem) {
416  csv_format = csv_format_elem->boolValue();
417  }
418 
419  if (!csv_format) {
420  // No definition means no csv format.
421  if (def) {
422  def.reset();
423  }
424  } else if (!def) {
425  // Definition is required with csv format.
426  def = isc::dhcp::LibDHCP::getOptionDef(space, code);
427  if (!def && vendor_id) {
428  def = LibDHCP::getVendorOptionDef(universe, vendor_id, code);
429  }
430  if (!def) {
431  def = isc::dhcp::LibDHCP::getRuntimeOptionDef(space, code);
432  }
433  if (!def) {
434  isc_throw(BadValue, "no known sub-option with code '" << code
435  << "' in '" << space << "' space");
436  }
437  }
438 
439  SubOptionConfigPtr sub_cfg(new SubOptionConfig(code, def, opt_cfg));
440  if (vendor_id) {
441  if (((universe == Option::V4) &&
442  (opt_cfg->getCode() == DHO_VIVSO_SUBOPTIONS)) ||
443  ((universe == Option::V6) &&
444  (opt_cfg->getCode() == D6O_VENDOR_OPTS))) {
445  sub_cfg->setVendorId(vendor_id);
446  }
447  }
448  if (class_elem) {
449  sub_cfg->setClass(class_elem->stringValue());
450  }
451 
452  // sub_cfg initial action is NONE.
453  parseAction(sub_option, sub_cfg, universe,
454  "add", ADD, EvalContext::PARSER_STRING);
455  parseAction(sub_option, sub_cfg, universe,
456  "supersede", SUPERSEDE, EvalContext::PARSER_STRING);
457  parseAction(sub_option, sub_cfg, universe,
458  "remove", REMOVE, EvalContext::PARSER_BOOL);
459 
460  if (sub_cfg->getAction() == NONE) {
461  isc_throw(BadValue, "no action: " << sub_option->str());
462  }
463 
464  ConstElementPtr container_add = sub_option->get("container-add");
465  ConstElementPtr container_remove = sub_option->get("container-remove");
466  if ((sub_cfg->getAction() == ADD) || (sub_cfg->getAction() == SUPERSEDE)) {
467  sub_cfg->setContainerAction(ADD);
468  if (container_add && !container_add->boolValue()) {
469  sub_cfg->setContainerAction(NONE);
470  }
471  } else if (sub_cfg->getAction() == REMOVE) {
472  sub_cfg->setContainerAction(REMOVE);
473  if (container_remove && !container_remove->boolValue()) {
474  sub_cfg->setContainerAction(NONE);
475  }
476  }
477 
478  // The [] operator creates the item if it does not exist before
479  // returning a reference to it.
480  uint16_t opt_code = opt_cfg->getCode();
481  SubOptionConfigMap& sub_map = sub_option_config_map_[opt_code];
482  if (sub_map.count(code)) {
483  isc_throw(BadValue, "sub-option " << code << " of option " << opt_code
484  << " was already specified");
485  }
486  sub_map[code] = sub_cfg;
487 }
488 
489 void
490 FlexOptionImpl::logClass(const ClientClass& client_class, uint16_t code) {
493  .arg(client_class)
494  .arg(code);
495  return;
496 }
497 
498 void
499 FlexOptionImpl::logAction(Action action, uint16_t code,
500  const string& value) {
501  if (action == NONE) {
502  return;
503  }
504  if (action == REMOVE) {
507  .arg(code);
508  return;
509  }
510  ostringstream repr;
511  if (str::isPrintable(value)) {
512  repr << "'" << value << "'";
513  } else {
514  repr << "0x" << hex;
515  for (const char& ch : value) {
516  repr << setw(2) << setfill('0') << static_cast<unsigned>(ch);
517  }
518  }
519  if (action == SUPERSEDE) {
522  .arg(code)
523  .arg(repr.str());
524  } else {
527  .arg(code)
528  .arg(repr.str());
529  }
530 }
531 
532 void
533 FlexOptionImpl::logAction(Action action, uint16_t code,
534  uint32_t vendor_id) {
535  if (action == SUPERSEDE) {
538  .arg(code)
539  .arg(vendor_id);
540  } else {
543  .arg(code)
544  .arg(vendor_id);
545  }
546 }
547 
548 void
549 FlexOptionImpl::logSubClass(const ClientClass& client_class, uint16_t code,
550  uint16_t container_code) {
553  .arg(client_class)
554  .arg(code)
555  .arg(container_code);
556  return;
557 }
558 
559 void
560 FlexOptionImpl::logSubAction(Action action, uint16_t code,
561  uint16_t container_code,
562  const string& value) {
563  if (action == NONE) {
564  return;
565  }
566  if (action == REMOVE) {
569  .arg(code)
570  .arg(container_code);
571  return;
572  }
573  ostringstream repr;
574  if (str::isPrintable(value)) {
575  repr << "'" << value << "'";
576  } else {
577  repr << "0x" << hex;
578  for (const char& ch : value) {
579  repr << setw(2) << setfill('0') << static_cast<unsigned>(ch);
580  }
581  }
582  if (action == SUPERSEDE) {
585  .arg(code)
586  .arg(container_code)
587  .arg(repr.str());
588  } else {
591  .arg(code)
592  .arg(container_code)
593  .arg(repr.str());
594  }
595 }
596 
597 bool
598 FlexOptionImpl::checkVendor(OptionPtr opt, uint32_t vendor_id) {
599  OptionVendorPtr vendor = boost::dynamic_pointer_cast<OptionVendor>(opt);
600  bool ret = (!vendor || (vendor->getVendorId() == vendor_id));
601  if (!ret) {
604  .arg(opt->getType())
605  .arg(vendor->getVendorId())
606  .arg(vendor_id);
607  }
608  return (ret);
609 }
610 
611 } // end of namespace flex_option
612 } // end of namespace isc
static OptionDefinitionPtr getRuntimeOptionDef(const std::string &space, const uint16_t code)
Returns runtime (non-standard) option definition by space and option code.
Definition: libdhcp++.cc:186
const isc::log::MessageID FLEX_OPTION_PROCESS_SUPERSEDE
const isc::log::MessageID FLEX_OPTION_PROCESS_VENDOR_ID_MISMATCH
const int DBGLVL_TRACE_BASIC
Trace basic operations.
Definition: log_dbglevels.h:69
isc::log::Logger flex_option_logger("flex-option-hooks")
std::map< std::string, isc::data::Element::types > SimpleKeywords
This specifies all accepted keywords with their types.
static bool checkVendor(isc::dhcp::OptionPtr opt, uint32_t vendor_id)
Check vendor option vendor id mismatch.
Definition: flex_option.cc:598
const isc::log::MessageID FLEX_OPTION_PROCESS_ADD
OptionConfig(uint16_t code, isc::dhcp::OptionDefinitionPtr def)
Constructor.
Definition: flex_option.cc:104
boost::shared_ptr< Option > OptionPtr
Definition: option.h:36
Universe
defines option universe DHCPv4 or DHCPv6
Definition: option.h:83
STL namespace.
boost::shared_ptr< OptionConfig > OptionConfigPtr
The type of shared pointers to option config.
Definition: flex_option.h:159
const isc::log::MessageID FLEX_OPTION_PROCESS_SUB_SUPERSEDE
std::map< uint16_t, SubOptionConfigPtr > SubOptionConfigMap
The type of the sub-option config map.
Definition: flex_option.h:248
virtual const char * what() const
Returns a C-style character string of the cause of the exception.
#define isc_throw(type, stream)
A shortcut macro to insert known values into exception arguments.
A generic exception that is thrown if a parameter given to a method is considered invalid in that con...
Definition: edns.h:19
void configure(isc::data::ConstElementPtr options)
Configure the Flex Option implementation.
Definition: flex_option.cc:134
const isc::log::MessageID FLEX_OPTION_PROCESS_SUB_ADD
A generic exception that is thrown when an unexpected error condition occurs.
SubOptionConfig(uint16_t code, isc::dhcp::OptionDefinitionPtr def, OptionConfigPtr container)
Constructor.
Definition: flex_option.cc:112
Evaluation context, an interface to the expression evaluation.
Definition: eval_context.h:34
boost::shared_ptr< const Element > ConstElementPtr
Definition: data.h:27
const isc::log::MessageID FLEX_OPTION_PROCESS_REMOVE
const isc::log::MessageID FLEX_OPTION_PROCESS_CLIENT_CLASS
static void logAction(Action action, uint16_t code, const std::string &value)
Log the action for option.
Definition: flex_option.cc:499
static OptionDefinitionPtr getOptionDef(const std::string &space, const uint16_t code)
Return the first option definition matching a particular option code.
Definition: libdhcp++.cc:123
Defines the logger used by the top-level component of kea-lfc.
uint16_t code_
std::vector< TokenPtr > Expression
This is a structure that holds an expression converted to RPN.
Definition: token.h:28
std::list< OptionConfigPtr > OptionConfigList
The type of lists of shared pointers to option config.
Definition: flex_option.h:162
static void logSubClass(const isc::dhcp::ClientClass &client_class, uint16_t code, uint16_t container_code)
Log the client class for sub-option.
Definition: flex_option.cc:549
#define DHCP6_OPTION_SPACE
const isc::log::MessageID FLEX_OPTION_PROCESS_SUB_CLIENT_CLASS
#define DHCP4_OPTION_SPACE
global std option spaces
boost::shared_ptr< SubOptionConfig > SubOptionConfigPtr
The type of shared pointers to sub-option config.
Definition: flex_option.h:244
#define LOG_DEBUG(LOGGER, LEVEL, MESSAGE)
Macro to conveniently test debug output and log it.
Definition: macros.h:14
ParserType
Specifies what type of expression the parser is expected to see.
Definition: eval_context.h:38
static OptionDefinitionPtr getLastResortOptionDef(const std::string &space, const uint16_t code)
Returns last resort option definition by space and option code.
Definition: libdhcp++.cc:244
static void logSubAction(Action action, uint16_t code, uint16_t container_code, const std::string &value)
Log the action for sub-option.
Definition: flex_option.cc:560
A generic exception that is thrown if a parameter given to a method would refer to or modify out-of-r...
boost::shared_ptr< OptionDefinition > OptionDefinitionPtr
Pointer to option definition object.
static void logClass(const isc::dhcp::ClientClass &client_class, uint16_t code)
Log the client class for option.
Definition: flex_option.cc:490
std::string ClientClass
Defines a single class name.
Definition: classify.h:42
boost::shared_ptr< OptionVendor > OptionVendorPtr
Pointer to a vendor option.
bool isPrintable(const std::string &content)
Check if a string is printable.
Definition: strutil.h:367
boost::shared_ptr< Expression > ExpressionPtr
Definition: token.h:30
This class represents vendor-specific information option.
Definition: option_vendor.h:30
const isc::log::MessageID FLEX_OPTION_PROCESS_SUB_REMOVE