Kea  2.1.7-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  uint16_t code;
198  if (code_elem) {
199  int64_t value = code_elem->intValue();
200  int64_t max_code;
201  if (family == AF_INET) {
202  max_code = numeric_limits<uint8_t>::max();
203  } else {
204  max_code = numeric_limits<uint16_t>::max();
205  }
206  if ((value < 0) || (value > max_code)) {
207  isc_throw(OutOfRange, "invalid 'code' value " << value
208  << " not in [0.." << max_code << "]");
209  }
210  if (space == DHCP4_OPTION_SPACE) {
211  if (value == DHO_PAD) {
213  "invalid 'code' value 0: reserved for PAD");
214  } else if (value == DHO_END) {
216  "invalid 'code' value 255: reserved for END");
217  }
218  } else if (space == DHCP6_OPTION_SPACE) {
219  if (value == 0) {
220  isc_throw(BadValue, "invalid 'code' value 0: reserved");
221  }
222  }
223  code = static_cast<uint16_t>(value);
224  }
226  if (name_elem) {
227  string name = name_elem->stringValue();
228  if (name.empty()) {
229  isc_throw(BadValue, "'name' must not be empty");
230  }
231  def = LibDHCP::getOptionDef(space, name);
232  if (!def) {
233  def = LibDHCP::getRuntimeOptionDef(space, name);
234  }
235  if (!def) {
236  def = LibDHCP::getLastResortOptionDef(space, name);
237  }
238  if (!def) {
239  isc_throw(BadValue, "no known '" << name << "' option in '"
240  << space << "' space");
241  }
242  if (code_elem && (def->getCode() != code)) {
243  isc_throw(BadValue, "option '" << name << "' is defined as code: "
244  << def->getCode() << ", not the specified code: "
245  << code);
246  }
247  code = def->getCode();
248  }
249 
250  bool csv_format = false;
251  if (csv_format_elem) {
252  csv_format = csv_format_elem->boolValue();
253  }
254 
255  if (!csv_format && !sub_options) {
256  // No definition means no csv format.
257  if (def) {
258  def.reset();
259  }
260  } else if (!def) {
261  // Definition is required with csv format.
262  def = isc::dhcp::LibDHCP::getOptionDef(space, code);
263  if (!def) {
264  def = isc::dhcp::LibDHCP::getRuntimeOptionDef(space, code);
265  }
266  if (!def) {
268  }
269  if (!def && csv_format) {
270  isc_throw(BadValue, "no known option with code '" << code
271  << "' in '" << space << "' space");
272  }
273  }
274 
275  OptionConfigPtr opt_cfg(new OptionConfig(code, def));
276  if (class_elem) {
277  opt_cfg->setClass(class_elem->stringValue());
278  }
279 
280  // opt_cfg initial action is NONE.
281  if (sub_options) {
282  string action;
283  if (option->contains("add")) {
284  action = "add";
285  } else if (option->contains("supersede")) {
286  action = "supersede";
287  } else if (option->contains("remove")) {
288  action = "remove";
289  }
290  if (!action.empty()) {
291  isc_throw(BadValue, "'sub-options' and '" << action << "' are "
292  << "incompatible in the same entry");
293  }
294  parseSubOptions(sub_options, opt_cfg, universe);
295  } else {
296  parseAction(option, opt_cfg, universe,
297  "add", ADD, EvalContext::PARSER_STRING);
298  parseAction(option, opt_cfg, universe,
299  "supersede", SUPERSEDE, EvalContext::PARSER_STRING);
300  parseAction(option, opt_cfg, universe,
301  "remove", REMOVE, EvalContext::PARSER_BOOL);
302 
303  if (opt_cfg->getAction() == NONE) {
304  isc_throw(BadValue, "no action: " << option->str());
305  }
306 
307  // The [] operator creates the item if it does not exist before
308  // returning a reference to it.
309  OptionConfigList& opt_lst = option_config_map_[code];
310  opt_lst.push_back(opt_cfg);
311  }
312 }
313 
314 void
315 FlexOptionImpl::parseSubOptions(ConstElementPtr sub_options,
316  OptionConfigPtr opt_cfg,
317  Option::Universe universe) {
318  for (ConstElementPtr sub_option : sub_options->listValue()) {
319  parseSubOption(sub_option, opt_cfg, universe);
320  }
321 }
322 
323 void
324 FlexOptionImpl::parseSubOption(ConstElementPtr sub_option,
325  OptionConfigPtr opt_cfg,
326  Option::Universe universe) {
327  if (!sub_option) {
328  isc_throw(BadValue, "null sub-option element");
329  }
330  if (sub_option->getType() != Element::map) {
331  isc_throw(BadValue, "sub-option element is not a map");
332  }
333  // See SimpleParser::checkKeywords
334  for (auto entry : sub_option->mapValue()) {
335  if (SUB_OPTION_PARAMETERS.count(entry.first) == 0) {
336  isc_throw(BadValue, "unknown parameter '" << entry.first << "'");
337  }
338  Element::types expected = SUB_OPTION_PARAMETERS.at(entry.first);
339  if (entry.second->getType() == expected) {
340  continue;
341  }
342  isc_throw(BadValue, "'" << entry.first << "' must be "
343  << (expected == Element::integer ? "an " : "a ")
344  << Element::typeToName(expected)
345  << ": " << entry.second->str());
346  }
347  ConstElementPtr code_elem = sub_option->get("code");
348  ConstElementPtr name_elem = sub_option->get("name");
349  ConstElementPtr space_elem = sub_option->get("space");
350  ConstElementPtr csv_format_elem = sub_option->get("csv-format");
351  ConstElementPtr class_elem = sub_option->get("client-class");
352  if (!code_elem && !name_elem) {
353  isc_throw(BadValue, "'code' or 'name' must be specified: "
354  << sub_option->str());
355  }
356  string space;
357  if (space_elem) {
358  space = space_elem->stringValue();
359  if (!OptionSpace::validateName(space)) {
360  isc_throw(BadValue, "'" << space << "' is not a valid space name");
361  }
362  } else {
363  OptionDefinitionPtr opt_def = opt_cfg->getOptionDef();
364  if (!opt_def) {
365  isc_throw(BadValue, "container is not defined: can't get space");
366  }
367  space = opt_def->getEncapsulatedSpace();
368  if (space.empty()) {
369  isc_throw(BadValue, "container does not encapsulate a space");
370  }
371  }
372  uint32_t vendor_id = LibDHCP::optionSpaceToVendorId(space);
373  uint16_t code;
374  if (code_elem) {
375  int64_t value = code_elem->intValue();
376  int64_t max_code;
377  if (universe == Option::V4) {
378  max_code = numeric_limits<uint8_t>::max();
379  } else {
380  max_code = numeric_limits<uint16_t>::max();
381  }
382  if ((value < 0) || (value > max_code)) {
383  isc_throw(OutOfRange, "invalid 'code' value " << value
384  << " not in [0.." << max_code << "]");
385  }
386  code = static_cast<uint16_t>(value);
387  }
389  if (name_elem) {
390  string name = name_elem->stringValue();
391  if (name.empty()) {
392  isc_throw(BadValue, "'name' must not be empty");
393  }
394  def = LibDHCP::getOptionDef(space, name);
395  if (!def && vendor_id) {
396  def = LibDHCP::getVendorOptionDef(universe, vendor_id, name);
397  }
398  if (!def) {
399  def = LibDHCP::getRuntimeOptionDef(space, name);
400  }
401  if (!def) {
402  isc_throw(BadValue, "no known '" << name << "' sub-option in '"
403  << space << "' space");
404  }
405  if (code_elem && (def->getCode() != code)) {
406  isc_throw(BadValue, "sub-option '" << name
407  << "' is defined as code: " << def->getCode()
408  << ", not the specified code: " << code);
409  }
410  code = def->getCode();
411  }
412 
413  bool csv_format = false;
414  if (csv_format_elem) {
415  csv_format = csv_format_elem->boolValue();
416  }
417 
418  if (!csv_format) {
419  // No definition means no csv format.
420  if (def) {
421  def.reset();
422  }
423  } else if (!def) {
424  // Definition is required with csv format.
425  def = isc::dhcp::LibDHCP::getOptionDef(space, code);
426  if (!def && vendor_id) {
427  def = LibDHCP::getVendorOptionDef(universe, vendor_id, code);
428  }
429  if (!def) {
430  def = isc::dhcp::LibDHCP::getRuntimeOptionDef(space, code);
431  }
432  if (!def) {
433  isc_throw(BadValue, "no known sub-option with code '" << code
434  << "' in '" << space << "' space");
435  }
436  }
437 
438  SubOptionConfigPtr sub_cfg(new SubOptionConfig(code, def, opt_cfg));
439  if (vendor_id) {
440  if (((universe == Option::V4) &&
441  (opt_cfg->getCode() == DHO_VIVSO_SUBOPTIONS)) ||
442  ((universe == Option::V6) &&
443  (opt_cfg->getCode() == D6O_VENDOR_OPTS))) {
444  sub_cfg->setVendorId(vendor_id);
445  }
446  }
447  if (class_elem) {
448  sub_cfg->setClass(class_elem->stringValue());
449  }
450 
451  // sub_cfg initial action is NONE.
452  parseAction(sub_option, sub_cfg, universe,
453  "add", ADD, EvalContext::PARSER_STRING);
454  parseAction(sub_option, sub_cfg, universe,
455  "supersede", SUPERSEDE, EvalContext::PARSER_STRING);
456  parseAction(sub_option, sub_cfg, universe,
457  "remove", REMOVE, EvalContext::PARSER_BOOL);
458 
459  if (sub_cfg->getAction() == NONE) {
460  isc_throw(BadValue, "no action: " << sub_option->str());
461  }
462 
463  ConstElementPtr container_add = sub_option->get("container-add");
464  ConstElementPtr container_remove = sub_option->get("container-remove");
465  if ((sub_cfg->getAction() == ADD) || (sub_cfg->getAction() == SUPERSEDE)) {
466  sub_cfg->setContainerAction(ADD);
467  if (container_add && !container_add->boolValue()) {
468  sub_cfg->setContainerAction(NONE);
469  }
470  } else if (sub_cfg->getAction() == REMOVE) {
471  sub_cfg->setContainerAction(REMOVE);
472  if (container_remove && !container_remove->boolValue()) {
473  sub_cfg->setContainerAction(NONE);
474  }
475  }
476 
477  // The [] operator creates the item if it does not exist before
478  // returning a reference to it.
479  uint16_t opt_code = opt_cfg->getCode();
480  SubOptionConfigMap& sub_map = sub_option_config_map_[opt_code];
481  if (sub_map.count(code)) {
482  isc_throw(BadValue, "sub-option " << code << " of option " << opt_code
483  << " was already specified");
484  }
485  sub_map[code] = sub_cfg;
486 }
487 
488 void
489 FlexOptionImpl::logClass(const ClientClass& client_class, uint16_t code) {
492  .arg(client_class)
493  .arg(code);
494  return;
495 }
496 
497 void
498 FlexOptionImpl::logAction(Action action, uint16_t code,
499  const string& value) {
500  if (action == NONE) {
501  return;
502  }
503  if (action == REMOVE) {
506  .arg(code);
507  return;
508  }
509  ostringstream repr;
510  if (str::isPrintable(value)) {
511  repr << "'" << value << "'";
512  } else {
513  repr << "0x" << hex;
514  for (const char& ch : value) {
515  repr << setw(2) << setfill('0') << static_cast<unsigned>(ch);
516  }
517  }
518  if (action == SUPERSEDE) {
521  .arg(code)
522  .arg(repr.str());
523  } else {
526  .arg(code)
527  .arg(repr.str());
528  }
529 }
530 
531 void
532 FlexOptionImpl::logAction(Action action, uint16_t code,
533  uint32_t vendor_id) {
534  if (action == SUPERSEDE) {
537  .arg(code)
538  .arg(vendor_id);
539  } else {
542  .arg(code)
543  .arg(vendor_id);
544  }
545 }
546 
547 void
548 FlexOptionImpl::logSubClass(const ClientClass& client_class, uint16_t code,
549  uint16_t container_code) {
552  .arg(client_class)
553  .arg(code)
554  .arg(container_code);
555  return;
556 }
557 
558 void
559 FlexOptionImpl::logSubAction(Action action, uint16_t code,
560  uint16_t container_code,
561  const string& value) {
562  if (action == NONE) {
563  return;
564  }
565  if (action == REMOVE) {
568  .arg(code)
569  .arg(container_code);
570  return;
571  }
572  ostringstream repr;
573  if (str::isPrintable(value)) {
574  repr << "'" << value << "'";
575  } else {
576  repr << "0x" << hex;
577  for (const char& ch : value) {
578  repr << setw(2) << setfill('0') << static_cast<unsigned>(ch);
579  }
580  }
581  if (action == SUPERSEDE) {
584  .arg(code)
585  .arg(container_code)
586  .arg(repr.str());
587  } else {
590  .arg(code)
591  .arg(container_code)
592  .arg(repr.str());
593  }
594 }
595 
596 bool
597 FlexOptionImpl::checkVendor(OptionPtr opt, uint32_t vendor_id) {
598  OptionVendorPtr vendor = boost::dynamic_pointer_cast<OptionVendor>(opt);
599  bool ret = (!vendor || (vendor->getVendorId() == vendor_id));
600  if (!ret) {
603  .arg(opt->getType())
604  .arg(vendor->getVendorId())
605  .arg(vendor_id);
606  }
607  return (ret);
608 }
609 
610 } // end of namespace flex_option
611 } // 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:185
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:597
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:498
static OptionDefinitionPtr getOptionDef(const std::string &space, const uint16_t code)
Return the first option definition matching a particular option code.
Definition: libdhcp++.cc:122
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:548
#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:243
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:559
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:489
std::string ClientClass
Defines a single class name.
Definition: classify.h:40
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:366
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