]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#4143] Checkpoint: doc todo
authorFrancis Dupont <fdupont@isc.org>
Wed, 22 Jul 2026 23:31:35 +0000 (01:31 +0200)
committerFrancis Dupont <fdupont@isc.org>
Thu, 23 Jul 2026 14:24:26 +0000 (16:24 +0200)
src/hooks/dhcp/flex_option/flex_option.cc
src/hooks/dhcp/flex_option/flex_option.dox
src/hooks/dhcp/flex_option/flex_option_messages.cc
src/hooks/dhcp/flex_option/flex_option_messages.h
src/hooks/dhcp/flex_option/flex_option_messages.mes
src/hooks/dhcp/flex_option/tests/flex_option_unittests.cc
src/hooks/dhcp/flex_option/tests/meson.build
src/hooks/dhcp/flex_option/tests/sub_option_unittests.cc

index b7cb445b28125432c3ca6bd999f53d846e95b597..f7d9c05a468c83b99127c357038d4f8a6d58508e 100644 (file)
@@ -324,16 +324,6 @@ FlexOptionImpl::parseOptionConfig(ConstElementPtr option) {
             .arg(code)
             .arg(opt_cfg->getClass());
     }
-    if (!opt_cfg->getDestination() && opt_cfg->getExpr()) {
-        for (auto const& tok : *opt_cfg->getExpr()) {
-            if (boost::dynamic_pointer_cast<TokenMember>(tok)) {
-                LOG_WARN(flex_option_logger, FLEX_OPTION_CONFIG_USELESS_MEMBER)
-                    .arg(code)
-                    .arg(opt_cfg->getText());
-                break;
-            }
-        }
-    }
 
     // opt_cfg initial action is NONE.
     if (sub_options) {
@@ -368,6 +358,19 @@ FlexOptionImpl::parseOptionConfig(ConstElementPtr option) {
         opt_lst.push_back(opt_cfg);
     }
 
+    // Not working as expected: the destination is the query and
+    // TokenMember is used.
+    if (!opt_cfg->getDestination() && opt_cfg->getExpr()) {
+        for (auto const& tok : *opt_cfg->getExpr()) {
+            if (boost::dynamic_pointer_cast<TokenMember>(tok)) {
+                LOG_WARN(flex_option_logger, FLEX_OPTION_CONFIG_USELESS_MEMBER)
+                    .arg(code)
+                    .arg(opt_cfg->getText());
+                break;
+            }
+        }
+    }
+
     // Check if we have to copy classes from the query to the response.
     if (need_copy_classes_to_response_ ||
         opt_cfg->getSource() ||
@@ -523,6 +526,15 @@ FlexOptionImpl::parseSubOption(ConstElementPtr sub_option,
     sub_cfg->setSource(opt_cfg->getSource());
     sub_cfg->setDestination(opt_cfg->getDestination());
 
+    // Not working as expected: the destination is the query and classes
+    // are used.
+    if (!opt_cfg->getDestination() && !sub_cfg->getClass().empty()) {
+        LOG_WARN(flex_option_logger, FLEX_OPTION_CONFIG_SUB_USELESS_CLASS)
+            .arg(code)
+            .arg(opt_cfg->getCode())
+            .arg(sub_cfg->getClass());
+    }
+
     // sub_cfg initial action is NONE.
     parseAction(sub_option, sub_cfg, universe,
                 "add", ADD, EvalContext::PARSER_STRING);
@@ -535,6 +547,21 @@ FlexOptionImpl::parseSubOption(ConstElementPtr sub_option,
         isc_throw(BadValue, "no action: " << sub_option->str());
     }
 
+    // Not working as expected: the destination is the query and
+    // TokenMember is used.
+    if (!opt_cfg->getDestination() && sub_cfg->getExpr()) {
+        for (auto const& tok : *sub_cfg->getExpr()) {
+            if (boost::dynamic_pointer_cast<TokenMember>(tok)) {
+                LOG_WARN(flex_option_logger,
+                         FLEX_OPTION_CONFIG_SUB_USELESS_MEMBER)
+                    .arg(code)
+                    .arg(opt_cfg->getCode())
+                    .arg(sub_cfg->getText());
+                break;
+            }
+        }
+    }
+
     ConstElementPtr container_add = sub_option->get("container-add");
     ConstElementPtr container_remove = sub_option->get("container-remove");
     if ((sub_cfg->getAction() == ADD) || (sub_cfg->getAction() == SUPERSEDE)) {
@@ -558,6 +585,20 @@ FlexOptionImpl::parseSubOption(ConstElementPtr sub_option,
                   << " was already specified");
     }
     sub_map[code] = sub_cfg;
+
+    // Check if we have to copy classes from the query to the response.
+    if (need_copy_classes_to_response_ ||
+        opt_cfg->getSource() ||
+        !sub_cfg->getExpr()) {
+        return;
+    }
+    for (auto const& tok : *sub_cfg->getExpr()) {
+        if (boost::dynamic_pointer_cast<TokenMember>(tok)) {
+            need_copy_classes_to_response_ = true;
+            break;
+        }
+    }
+
 }
 
 void
index 0436b791a5445b2955e8ba6d2d01e870b18c6924..d458f499bd5a1fffeb5b1331ad716b73ebfe02d4 100644 (file)
@@ -99,6 +99,16 @@ The sole parameter is a options list of options with:
    definition.
  - @b client-class - Specifies the guard i.e. the client class the query
    must belong to.
+ - @b source - Specifies what is the source for expression evaluation.
+   The default is the query but this can be changed to the response.
+   Note that if an expression includes a member clause the query classes
+   are copied to the response to make this to work.
+ - @b destination - Specifies what is the destination of option changes.
+   The default is the response but this can be changed to the query.
+   Note the change is performed before the classification so
+   @ client-class or a member clause in expression is very likely
+   to not work as expected. A warning is emitted when this situation
+   is detected when the configuration is parsed.
 
 Note for the rare options which can be empty this mechanism does not work.
 The proposed solution in this case is to use a client class to set the
@@ -121,7 +131,9 @@ decodes the option configurations. @ref unload() free the configuration.
 
 Kea engine checks if the library has functions that match known hook point
 names. This library has two such functions: @ref pkt4_send and @ref pkt6_send,
-all located in flex_option_callouts.cc.
+all located in flex_option_callouts.cc. The @b destination feature added
+@ref pkt4_receive and @ref pkt6_receive, of course only the query is passed
+when they are called.
 
 kea-dhcp4 server calls @ref pkt4_send (and kea-dhcp6 @ref pkt6_send) with
 the query and response packets. For each configured option and sub-option
index 0f5cc6a69b2332bcb7663e8f1cea24a6c80d7d7e..c1d5fc88d61b1b5da48d2c595876427c6f9a4ebf 100644 (file)
@@ -4,6 +4,8 @@
 #include <log/message_types.h>
 #include <log/message_initializer.h>
 
+extern const isc::log::MessageID FLEX_OPTION_CONFIG_SUB_USELESS_CLASS = "FLEX_OPTION_CONFIG_SUB_USELESS_CLASS";
+extern const isc::log::MessageID FLEX_OPTION_CONFIG_SUB_USELESS_MEMBER = "FLEX_OPTION_CONFIG_SUB_USELESS_MEMBER";
 extern const isc::log::MessageID FLEX_OPTION_CONFIG_USELESS_CLASS = "FLEX_OPTION_CONFIG_USELESS_CLASS";
 extern const isc::log::MessageID FLEX_OPTION_CONFIG_USELESS_MEMBER = "FLEX_OPTION_CONFIG_USELESS_MEMBER";
 extern const isc::log::MessageID FLEX_OPTION_LOAD_ERROR = "FLEX_OPTION_LOAD_ERROR";
@@ -22,8 +24,10 @@ extern const isc::log::MessageID FLEX_OPTION_UNLOAD = "FLEX_OPTION_UNLOAD";
 namespace {
 
 const char* values[] = {
-    "FLEX_OPTION_CONFIG_USELESS_CLASS", "For the option '%1' the client class '%2' is required before classification for a 'query' destination",
-    "FLEX_OPTION_CONFIG_USELESS_MEMBER", "For the option '%1' the  member expression '%2' is evaluated before classification for a 'query' destination",
+    "FLEX_OPTION_CONFIG_SUB_USELESS_CLASS", "For the sub-option code %1 in option code %2 the client class %3 is required before classification for a query destination",
+    "FLEX_OPTION_CONFIG_SUB_USELESS_MEMBER", "For the sub-option code %1 in option code %2 the member expression %3 is evaluated before classification for a query destination",
+    "FLEX_OPTION_CONFIG_USELESS_CLASS", "For the option code %1 the client class %2 is required before classification for a query destination",
+    "FLEX_OPTION_CONFIG_USELESS_MEMBER", "For the option code %1 the member expression %2 is evaluated before classification for a query destination",
     "FLEX_OPTION_LOAD_ERROR", "loading Flex Option hooks library failed: %1",
     "FLEX_OPTION_PROCESS_ADD", "Added the option code %1 with value %2",
     "FLEX_OPTION_PROCESS_CLIENT_CLASS", "Skip processing of the option code %1 for class '%2'",
index 67950a2cb3889fc7946c1f6c4f3fb08f0f4aa5ed..599d59c30d1ad05f0485d2835d5a059c86a4bf11 100644 (file)
@@ -5,6 +5,8 @@
 
 #include <log/message_types.h>
 
+extern const isc::log::MessageID FLEX_OPTION_CONFIG_SUB_USELESS_CLASS;
+extern const isc::log::MessageID FLEX_OPTION_CONFIG_SUB_USELESS_MEMBER;
 extern const isc::log::MessageID FLEX_OPTION_CONFIG_USELESS_CLASS;
 extern const isc::log::MessageID FLEX_OPTION_CONFIG_USELESS_MEMBER;
 extern const isc::log::MessageID FLEX_OPTION_LOAD_ERROR;
index 18da2940d444d22caaacf1758d5e9147ad311c40..7402f50c0ee53ef48dda93255a22d77b7925b110 100644 (file)
@@ -9,13 +9,27 @@ This error message indicates an error during loading the Flex Option
 hooks library. The details of the error are provided as argument of
 the log message.
 
-% FLEX_OPTION_CONFIG_USELESS_CLASS For the option '%1' the client class '%2' is required before classification for a 'query' destination
+% FLEX_OPTION_CONFIG_SUB_USELESS_CLASS For the sub-option code %1 in option code %2 the client class %3 is required before classification for a query destination
 This warning message indicates the config specifies a class requirement
 when the destination is the query but the callout point for patching
 queries is before the classification so it will very likely not work
-as expected. The code of the option and the client class are displayed.
+as expected. The codes of the sub-option and option, and the client
+class name are displayed.
 
-% FLEX_OPTION_CONFIG_USELESS_MEMBER For the option '%1' the  member expression '%2' is evaluated before classification for a 'query' destination
+% FLEX_OPTION_CONFIG_SUB_USELESS_MEMBER For the sub-option code %1 in option code %2 the member expression %3 is evaluated before classification for a query destination
+This warning message indicates the config specifies an expression
+with a member clause when the destination is the query but the callout point
+for patching queries is before the classification so it will very likely
+not work as expected. The code of the sub-option and option, and the
+expression are displayed.
+
+% FLEX_OPTION_CONFIG_USELESS_CLASS For the option code %1 the client class %2 is required before classification for a query destination
+This warning message indicates the config specifies a class requirement
+when the destination is the query but the callout point for patching
+queries is before the classification so it will very likely not work
+as expected. The code of the option and the client class name are displayed.
+
+% FLEX_OPTION_CONFIG_USELESS_MEMBER For the option code %1 the member expression %2 is evaluated before classification for a query destination
 This warning message indicates the config specifies an expression
 with a member clause when the destination is the query but the callout point
 for patching queries is before the classification so it will very likely
index 92d56f0f2e6f05190790ac2167bf7b02dad86149..b180181f0ef0c4ad2622469ad3a995b86b08b3d4 100644 (file)
@@ -16,6 +16,7 @@
 #include <eval/eval_context.h>
 #include <hooks/callout_manager.h>
 #include <hooks/hooks.h>
+#include <testutils/log_utils.h>
 
 #include <tests/test_flex_option.h>
 #include <gtest/gtest.h>
@@ -25,6 +26,7 @@ using namespace std;
 using namespace isc;
 using namespace isc::data;
 using namespace isc::dhcp;
+using namespace isc::dhcp::test;
 using namespace isc::eval;
 using namespace isc::hooks;
 using namespace isc::flex_option;
@@ -1881,4 +1883,151 @@ TEST_F(FlexOptionTest, processQuery) {
     EXPECT_EQ(0U, buffer_fqdn[12]);
 }
 
+// Verify that the source is followed: derived from processFullTest:
+//  - the source is set to response
+//  - the trigger option is added to the response
+TEST_F(FlexOptionTest, processSource) {
+    ElementPtr options = Element::createList();
+    ElementPtr option = Element::createMap();
+    options->add(option);
+    ElementPtr code = Element::create(DHO_BOOT_FILE_NAME);
+    option->set("code", code);
+    string expr = "ifelse(option[host-name].exists,";
+    expr += "concat(option[host-name].text,'.boot'),'')";
+    ElementPtr add = Element::create(expr);
+    option->set("add", add);
+    ElementPtr source = Element::create(string("response"));
+    option->set("source", source);
+    EXPECT_NO_THROW(impl_->testConfigure(options));
+    EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg();
+
+    Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 12345));
+    Pkt4Ptr response(new Pkt4(DHCPOFFER, 12345));
+    OptionStringPtr str(new OptionString(Option::V4, DHO_HOST_NAME, "foo"));
+    response->addOption(str);
+    EXPECT_FALSE(query->getOption(DHO_HOST_NAME));
+    EXPECT_FALSE(response->getOption(DHO_BOOT_FILE_NAME));
+
+    EXPECT_NO_THROW(impl_->process<Pkt4Ptr>(Option::V4, query, response));
+
+    OptionPtr opt = response->getOption(DHO_BOOT_FILE_NAME);
+    ASSERT_TRUE(opt);
+    EXPECT_EQ(DHO_BOOT_FILE_NAME, opt->getType());
+    const OptionBuffer& buffer = opt->getData();
+    ASSERT_EQ(8U, buffer.size());
+    EXPECT_EQ(0, memcmp(&buffer[0], "foo.boot", 8));
+}
+
+// Verify that a member in the expression works with the response source.
+TEST_F(FlexOptionTest, processMemberSource) {
+    CfgMgr::instance().setFamily(AF_INET6);
+
+    ElementPtr options = Element::createList();
+    ElementPtr option = Element::createMap();
+    options->add(option);
+    ElementPtr code = Element::create(D6O_BOOTFILE_URL);
+    option->set("code", code);
+    ElementPtr remove = Element::create(string("member('foobar')"));
+    option->set("remove", remove);
+    ElementPtr source = Element::create(string("response"));
+    option->set("source", source);
+    EXPECT_NO_THROW(impl_->testConfigure(options));
+    EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg();
+
+    Pkt6Ptr query(new Pkt6(DHCPV6_SOLICIT, 12345));
+    query->addClass("foobar");
+    Pkt6Ptr response(new Pkt6(DHCPV6_ADVERTISE, 12345));
+    string response_txt = response->toText();
+    OptionStringPtr str(new OptionString(Option::V6, D6O_BOOTFILE_URL, "http"));
+    response->addOption(str);
+
+    EXPECT_NO_THROW(impl_->process<Pkt6Ptr>(Option::V6, query, response));
+
+    EXPECT_EQ(response_txt, response->toText());
+    EXPECT_FALSE(response->getOption(D6O_BOOTFILE_URL));
+    // No magic: we simply copied classes from query to response...
+    EXPECT_TRUE(response->inClass("foobar"));
+}
+
+/// @brief Test fixture for testing warnings from the Flex Option library.
+class FlexOptionLogTest : public LogContentTest {
+public:
+    /// @brief Constructor.
+    FlexOptionLogTest() {
+        impl_.reset(new TestFlexOptionImpl());
+        CfgMgr::instance().setFamily(AF_INET);
+    }
+
+    /// @brief Destructor.
+    virtual ~FlexOptionLogTest() {
+        LibDHCP::clearRuntimeOptionDefs();
+        CfgMgr::instance().setFamily(AF_INET);
+        impl_.reset();
+    }
+
+    /// @brief Flex Option implementation.
+    TestFlexOptionImplPtr impl_;
+};
+
+// Verify that client-classes does not trigger a warning by default.
+TEST_F(FlexOptionLogTest, noWarning) {
+    ElementPtr options = Element::createList();
+    ElementPtr option = Element::createMap();
+    options->add(option);
+    ElementPtr add = Element::create(string("'abc'"));
+    option->set("add", add);
+    ElementPtr code = Element::create(DHO_HOST_NAME);
+    option->set("code", code);
+    option->set("client-class", Element::create(string("foobar")));
+    EXPECT_NO_THROW(impl_->testConfigure(options));
+    EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg();
+
+    EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_USELESS_CLASS"));
+    EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_USELESS_MEMBER"));
+}
+
+// Verify that client-classes triggers a warning with query destination.
+TEST_F(FlexOptionLogTest, classWarning) {
+    ElementPtr options = Element::createList();
+    ElementPtr option = Element::createMap();
+    options->add(option);
+    ElementPtr add = Element::create(string("'abc'"));
+    option->set("add", add);
+    ElementPtr code = Element::create(DHO_HOST_NAME);
+    option->set("code", code);
+    option->set("client-class", Element::create(string("foobar")));
+    ElementPtr dest = Element::create(string("query"));
+    option->set("destination", dest);
+    EXPECT_NO_THROW(impl_->testConfigure(options));
+    EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg();
+
+    string expected = "FLEX_OPTION_CONFIG_USELESS_CLASS ";
+    expected += "For the option code 12 the client class foobar is ";
+    expected += "required before classification for a query destination";
+    EXPECT_EQ(1U, countFile(expected));
+    EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_USELESS_MEMBER"));
+}
+
+// Verify that client-classes does not trigger a warning by default.
+TEST_F(FlexOptionLogTest, memberWarning) {
+    ElementPtr options = Element::createList();
+    ElementPtr option = Element::createMap();
+    options->add(option);
+    ElementPtr remove = Element::create(string("member('foobar')"));
+    option->set("remove", remove);
+    ElementPtr code = Element::create(DHO_HOST_NAME);
+    option->set("code", code);
+    ElementPtr dest = Element::create(string("query"));
+    option->set("destination", dest);
+    EXPECT_NO_THROW(impl_->testConfigure(options));
+    EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg();
+
+    EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_USELESS_CLASS"));
+    string expected = "FLEX_OPTION_CONFIG_USELESS_MEMBER ";
+    expected += "For the option code 12 the member expression ";
+    expected += "member('foobar') is ";
+    expected += "evaluated before classification for a query destination";
+    EXPECT_EQ(1U, countFile(expected));
+}
+
 } // end of anonymous namespace
index da0c58c43e718e0d374585f725ae627768e1962a..f1715be07422239771c6d0fd6b816413b9437ff2 100644 (file)
@@ -12,6 +12,6 @@ dhcp_flex_option_lib_tests = executable(
     ],
     dependencies: [GTEST_DEP, CRYPTO_DEP],
     include_directories: [include_directories('.'), include_directories('..')] + INCLUDES,
-    link_with: [dhcp_flex_option_archive] + LIBS_BUILT_SO_FAR,
+    link_with: [dhcp_flex_option_archive, kea_testutils_lib] + LIBS_BUILT_SO_FAR,
 )
 test('dhcp-flex-option-lib-tests', dhcp_flex_option_lib_tests, protocol: 'gtest')
index 85b4c58a4172556053809d91f411c8822d1632c8..57de021a004cc44feedabe81fe4f20bbbe9faab7 100644 (file)
@@ -19,6 +19,7 @@
 #include <eval/eval_context.h>
 #include <hooks/callout_manager.h>
 #include <hooks/hooks.h>
+#include <testutils/log_utils.h>
 
 #include <tests/test_flex_option.h>
 #include <gtest/gtest.h>
@@ -29,6 +30,7 @@ using namespace isc;
 using namespace isc::asiolink;
 using namespace isc::data;
 using namespace isc::dhcp;
+using namespace isc::dhcp::test;
 using namespace isc::eval;
 using namespace isc::hooks;
 using namespace isc::flex_option;
@@ -1080,6 +1082,43 @@ TEST_F(FlexSubOptionTest, subOptionConfigComplex) {
 
 // Empty sub-option config list doing nothing is the same as empty option list.
 
+// Verify that response processing does nothing with no response.
+TEST_F(FlexSubOptionTest, subProcessNoResponse) {
+    OptionDefSpaceContainer defs;
+    OptionDefinitionPtr def(new OptionDefinition("my-container", 222,
+                                                 DHCP4_OPTION_SPACE, "empty",
+                                                 "my-space"));
+    defs.addItem(def);
+    OptionDefinitionPtr sdef(new OptionDefinition("my-option", 1, "my-space",
+                                                  "fqdn", true));
+    defs.addItem(sdef);
+    EXPECT_NO_THROW(LibDHCP::setRuntimeOptionDefs(defs));
+
+    ElementPtr options = Element::createList();
+    ElementPtr option = Element::createMap();
+    options->add(option);
+    ElementPtr code = Element::create(222);
+    option->set("code", code);
+    ElementPtr sub_options = Element::createList();
+    option->set("sub-options", sub_options);
+    ElementPtr sub_option = Element::createMap();
+    sub_options->add(sub_option);
+    ElementPtr space = Element::create(string("my-space"));
+    sub_option->set("space", space);
+    ElementPtr add = Element::create(string("'example.com'"));
+    sub_option->set("add", add);
+    ElementPtr name = Element::create(string("my-option"));
+    sub_option->set("name", name);
+    sub_option->set("csv-format", Element::create(true));
+
+    EXPECT_NO_THROW(impl_->testConfigure(options));
+    EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg();
+
+    Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 12345));
+
+    EXPECT_NO_THROW(impl_->process<Pkt4Ptr>(Option::V4, query, Pkt4Ptr()));
+}
+
 // Verify that NONE action really does nothing.
 TEST_F(FlexSubOptionTest, subProcessNone) {
     auto rai_def = LibDHCP::getOptionDef(DHCP4_OPTION_SPACE,
@@ -3170,4 +3209,327 @@ TEST_F(FlexSubOptionTest, subOptionConfigGuardSubOptiondMatch) {
     EXPECT_EQ(0, memcmp(&buffer[0], "abc", 3));
 }
 
+// Verify that ADD action adds the specified sub-option in an already
+// existing container option with a query destination.
+TEST_F(FlexSubOptionTest, subProcessQuery) {
+    OptionDefSpaceContainer defs;
+    OptionDefinitionPtr def(new OptionDefinition("my-container", 222,
+                                                 DHCP4_OPTION_SPACE, "empty",
+                                                 "my-space"));
+    defs.addItem(def);
+    OptionDefinitionPtr sdef(new OptionDefinition("my-option", 1, "my-space",
+                                                  "string"));
+    defs.addItem(sdef);
+    EXPECT_NO_THROW(LibDHCP::setRuntimeOptionDefs(defs));
+
+    ElementPtr options = Element::createList();
+    ElementPtr option = Element::createMap();
+    options->add(option);
+    ElementPtr code = Element::create(222);
+    option->set("code", code);
+    ElementPtr sub_options = Element::createList();
+    option->set("sub-options", sub_options);
+    ElementPtr dest = Element::create(string("query"));
+    option->set("destination", dest);
+    ElementPtr sub_option = Element::createMap();
+    sub_options->add(sub_option);
+    ElementPtr space = Element::create(string("my-space"));
+    sub_option->set("space", space);
+    ElementPtr add = Element::create(string("'abc'"));
+    sub_option->set("add", add);
+    ElementPtr name = Element::create(string("my-option"));
+    sub_option->set("name", name);
+
+    EXPECT_NO_THROW(impl_->testConfigure(options));
+    EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg();
+
+    Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 12345));
+    OptionPtr container(new Option(Option::V4, 222));
+    query->addOption(container);
+    EXPECT_TRUE(query->getOption(222));
+
+    EXPECT_NO_THROW(impl_->process<Pkt4Ptr>(Option::V4, query, Pkt4Ptr()));
+
+    // Only one option with code 222.
+    EXPECT_EQ(1U, query->options_.count(222));
+
+    OptionPtr opt = query->getOption(222);
+    ASSERT_TRUE(opt);
+    EXPECT_EQ(222U, opt->getType());
+    OptionPtr sub = opt->getOption(1);
+    ASSERT_TRUE(sub);
+    EXPECT_EQ(1U, sub->getType());
+    const OptionBuffer& buffer = sub->getData();
+    ASSERT_EQ(3U, buffer.size());
+    EXPECT_EQ(0, memcmp(&buffer[0], "abc", 3));
+
+    // Only one sub-option.
+    auto const& opts = opt->getOptions();
+    EXPECT_EQ(1U, opts.size());
+}
+
+// Verify that ADD action adds the specified sub-option in an already
+// existing container option with a query destination.
+TEST_F(FlexSubOptionTest, subProcessSource) {
+    OptionDefSpaceContainer defs;
+    OptionDefinitionPtr def(new OptionDefinition("my-container", 222,
+                                                 DHCP4_OPTION_SPACE, "empty",
+                                                 "my-space"));
+    defs.addItem(def);
+    OptionDefinitionPtr sdef(new OptionDefinition("my-option", 1, "my-space",
+                                                  "string"));
+    defs.addItem(sdef);
+    EXPECT_NO_THROW(LibDHCP::setRuntimeOptionDefs(defs));
+
+    ElementPtr options = Element::createList();
+    ElementPtr option = Element::createMap();
+    options->add(option);
+    ElementPtr code = Element::create(222);
+    option->set("code", code);
+    ElementPtr sub_options = Element::createList();
+    option->set("sub-options", sub_options);
+    ElementPtr sub_option = Element::createMap();
+    sub_options->add(sub_option);
+    ElementPtr space = Element::create(string("my-space"));
+    sub_option->set("space", space);
+    string expr = "ifelse(option[host-name].exists,";
+    expr += "concat(option[host-name].text,'.boot'),'')";
+    ElementPtr add = Element::create(expr);
+    sub_option->set("add", add);
+    ElementPtr name = Element::create(string("my-option"));
+    sub_option->set("name", name);
+    ElementPtr source = Element::create(string("response"));
+    option->set("source", source);
+
+    EXPECT_NO_THROW(impl_->testConfigure(options));
+    EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg();
+
+    Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 12345));
+    Pkt4Ptr response(new Pkt4(DHCPOFFER, 12345));
+    OptionStringPtr str(new OptionString(Option::V4, DHO_HOST_NAME, "foo"));
+    response->addOption(str);
+    OptionPtr container(new Option(Option::V4, 222));
+    response->addOption(container);
+    EXPECT_TRUE(response->getOption(222));
+
+    EXPECT_NO_THROW(impl_->process<Pkt4Ptr>(Option::V4, query, response));
+
+    // Only one option with code 222.
+    EXPECT_EQ(1U, response->options_.count(222));
+
+    OptionPtr opt = response->getOption(222);
+    ASSERT_TRUE(opt);
+    EXPECT_EQ(222U, opt->getType());
+    OptionPtr sub = opt->getOption(1);
+    ASSERT_TRUE(sub);
+    EXPECT_EQ(1U, sub->getType());
+    const OptionBuffer& buffer = sub->getData();
+    ASSERT_EQ(8U, buffer.size());
+    EXPECT_EQ(0, memcmp(&buffer[0], "foo.boot", 3));
+
+    // Only one sub-option.
+    auto const& opts = opt->getOptions();
+    EXPECT_EQ(1U, opts.size());
+}
+
+// Verify that a member in the expression works with the response source.
+TEST_F(FlexSubOptionTest, subProcessMemberSource) {
+    OptionDefSpaceContainer defs;
+    OptionDefinitionPtr def(new OptionDefinition("my-container", 222,
+                                                 DHCP4_OPTION_SPACE, "empty",
+                                                 "my-space"));
+    defs.addItem(def);
+    OptionDefinitionPtr sdef(new OptionDefinition("my-option", 1, "my-space",
+                                                  "string"));
+    defs.addItem(sdef);
+    EXPECT_NO_THROW(LibDHCP::setRuntimeOptionDefs(defs));
+
+    ElementPtr options = Element::createList();
+    ElementPtr option = Element::createMap();
+    options->add(option);
+    ElementPtr code = Element::create(222);
+    option->set("code", code);
+    ElementPtr sub_options = Element::createList();
+    option->set("sub-options", sub_options);
+    ElementPtr source = Element::create(string("response"));
+    option->set("source", source);
+
+    ElementPtr sub_option = Element::createMap();
+    sub_options->add(sub_option);
+    ElementPtr space = Element::create(string("my-space"));
+    sub_option->set("space", space);
+    ElementPtr remove = Element::create(string("member('foobar')"));
+    sub_option->set("remove", remove);
+    ElementPtr name = Element::create(string("my-option"));
+    sub_option->set("name", name);
+
+    EXPECT_NO_THROW(impl_->testConfigure(options));
+    EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg();
+
+    Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 12345));
+    query->addClass("foobar");
+    Pkt4Ptr response(new Pkt4(DHCPOFFER, 12345));
+    string response_txt = response->toText();
+    OptionPtr container(new Option(Option::V4, 222));
+    response->addOption(container);
+    EXPECT_TRUE(response->getOption(222));
+    OptionStringPtr str(new OptionString(Option::V4, 1, "xyzt"));
+    container->addOption(str);
+
+    EXPECT_NO_THROW(impl_->process<Pkt4Ptr>(Option::V4, query, response));
+
+    EXPECT_EQ(response_txt, response->toText());
+    EXPECT_FALSE(response->getOption(222));
+    // No magic: we simply copied classes from query to response...
+    EXPECT_TRUE(response->inClass("foobar"));
+}
+
+/// @brief Test fixture for testing warnings from the Flex Option library.
+class FlexSubOptionLogTest : public LogContentTest {
+public:
+    /// @brief Constructor.
+    FlexSubOptionLogTest() {
+        impl_.reset(new TestFlexOptionImpl());
+        CfgMgr::instance().setFamily(AF_INET);
+    }
+
+    /// @brief Destructor.
+    virtual ~FlexSubOptionLogTest() {
+        LibDHCP::clearRuntimeOptionDefs();
+        CfgMgr::instance().setFamily(AF_INET);
+        impl_.reset();
+    }
+
+    /// @brief Flex Option implementation.
+    TestFlexOptionImplPtr impl_;
+};
+
+// Verify that client-classes does not trigger a warning by default.
+TEST_F(FlexSubOptionLogTest, noWarning) {
+    OptionDefSpaceContainer defs;
+    OptionDefinitionPtr def(new OptionDefinition("my-container", 222,
+                                                 DHCP4_OPTION_SPACE, "empty",
+                                                 "my-space"));
+    defs.addItem(def);
+    OptionDefinitionPtr sdef(new OptionDefinition("my-option", 1, "my-space",
+                                                  "string"));
+    defs.addItem(sdef);
+    EXPECT_NO_THROW(LibDHCP::setRuntimeOptionDefs(defs));
+
+    ElementPtr options = Element::createList();
+    ElementPtr option = Element::createMap();
+    options->add(option);
+    ElementPtr code = Element::create(222);
+    option->set("code", code);
+
+    ElementPtr sub_options = Element::createList();
+    option->set("sub-options", sub_options);
+    ElementPtr sub_option = Element::createMap();
+    sub_options->add(sub_option);
+    ElementPtr space = Element::create(string("my-space"));
+    sub_option->set("space", space);
+    ElementPtr add = Element::create(string("'abc'"));
+    sub_option->set("add", add);
+    ElementPtr name = Element::create(string("my-option"));
+    sub_option->set("name", name);
+    sub_option->set("client-class", Element::create(string("foobar")));
+
+    EXPECT_NO_THROW(impl_->testConfigure(options));
+    EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg();
+
+    EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_USELESS_CLASS"));
+    EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_USELESS_MEMBER"));
+    EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_SUB_USELESS_CLASS"));
+    EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_SUB_USELESS_MEMBER"));
+}
+
+// Verify that client-classes triggers a warning with query destination.
+TEST_F(FlexSubOptionLogTest, classWarning) {
+    OptionDefSpaceContainer defs;
+    OptionDefinitionPtr def(new OptionDefinition("my-container", 222,
+                                                 DHCP4_OPTION_SPACE, "empty",
+                                                 "my-space"));
+    defs.addItem(def);
+    OptionDefinitionPtr sdef(new OptionDefinition("my-option", 1, "my-space",
+                                                  "string"));
+    defs.addItem(sdef);
+    EXPECT_NO_THROW(LibDHCP::setRuntimeOptionDefs(defs));
+
+    ElementPtr options = Element::createList();
+    ElementPtr option = Element::createMap();
+    options->add(option);
+    ElementPtr code = Element::create(222);
+    option->set("code", code);
+    ElementPtr sub_options = Element::createList();
+    option->set("sub-options", sub_options);
+    ElementPtr dest = Element::create(string("query"));
+    option->set("destination", dest);
+
+    ElementPtr sub_option = Element::createMap();
+    sub_options->add(sub_option);
+    ElementPtr space = Element::create(string("my-space"));
+    sub_option->set("space", space);
+    ElementPtr add = Element::create(string("'abc'"));
+    sub_option->set("add", add);
+    ElementPtr name = Element::create(string("my-option"));
+    sub_option->set("name", name);
+    sub_option->set("client-class", Element::create(string("foobar")));
+
+    EXPECT_NO_THROW(impl_->testConfigure(options));
+    EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg();
+
+    EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_USELESS_CLASS"));
+    EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_USELESS_MEMBER"));
+    string expected = "FLEX_OPTION_CONFIG_SUB_USELESS_CLASS ";
+    expected += "For the sub-option code 1 in option code 222 ";
+    expected += "the client class foobar is ";
+    expected += "required before classification for a query destination";
+    EXPECT_EQ(1U, countFile(expected));
+    EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_SUB_USELESS_MEMBER"));
+}
+
+// Verify that client-classes does not trigger a warning by default.
+TEST_F(FlexSubOptionLogTest, memberWarning) {
+    OptionDefSpaceContainer defs;
+    OptionDefinitionPtr def(new OptionDefinition("my-container", 222,
+                                                 DHCP4_OPTION_SPACE, "empty",
+                                                 "my-space"));
+    defs.addItem(def);
+    OptionDefinitionPtr sdef(new OptionDefinition("my-option", 1, "my-space",
+                                                  "string"));
+    defs.addItem(sdef);
+    EXPECT_NO_THROW(LibDHCP::setRuntimeOptionDefs(defs));
+
+    ElementPtr options = Element::createList();
+    ElementPtr option = Element::createMap();
+    options->add(option);
+    ElementPtr code = Element::create(222);
+    option->set("code", code);
+    ElementPtr sub_options = Element::createList();
+    option->set("sub-options", sub_options);
+    ElementPtr dest = Element::create(string("query"));
+    option->set("destination", dest);
+
+    ElementPtr sub_option = Element::createMap();
+    sub_options->add(sub_option);
+    ElementPtr space = Element::create(string("my-space"));
+    sub_option->set("space", space);
+    ElementPtr remove = Element::create(string("member('foobar')"));
+    sub_option->set("remove", remove);
+    ElementPtr name = Element::create(string("my-option"));
+    sub_option->set("name", name);
+
+    EXPECT_NO_THROW(impl_->testConfigure(options));
+    EXPECT_TRUE(impl_->getErrMsg().empty()) << impl_->getErrMsg();
+
+    EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_USELESS_CLASS"));
+    EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_USELESS_MEMBER"));
+    EXPECT_EQ(0U, countFile("FLEX_OPTION_CONFIG_SUB_USELESS_CLASS"));
+    string expected = "FLEX_OPTION_CONFIG_SUB_USELESS_MEMBER ";
+    expected += "For the sub-option code 1 in option code 222 ";
+    expected += "the member expression member('foobar') is ";
+    expected += "evaluated before classification for a query destination";
+    EXPECT_EQ(1U, countFile(expected));
+}
+
 } // end of anonymous namespace