]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#2227] add support for long options RFC3396
authorRazvan Becheriu <razvan@isc.org>
Mon, 18 Apr 2022 21:20:54 +0000 (00:20 +0300)
committerRazvan Becheriu <razvan@isc.org>
Thu, 19 May 2022 17:29:57 +0000 (17:29 +0000)
src/bin/dhcp4/tests/inform_unittest.cc
src/lib/dhcp/libdhcp++.cc
src/lib/dhcp/libdhcp++.h
src/lib/dhcp/option.cc
src/lib/dhcp/option.h
src/lib/dhcp/pkt4.cc
src/lib/dhcp/tests/libdhcp++_unittest.cc

index f6d83fd360715e1533613196e75ba4d67f449965..a382b763edfe0ec1f76704416abfcfc10a109653 100644 (file)
@@ -49,6 +49,28 @@ namespace {
 ///     - next-server = 10.0.0.7
 ///     - server name = "some-name.example.org"
 ///     - boot-file-name = "bootfile.efi"
+///
+/// - Configuration 3:
+///   - This configuration provides reservations for big options
+///     server-hostname and boot-file-name value.
+///   - 1 subnet: 192.0.2.0/24
+///   - 1 reservation for this subnet:
+///     - Client's HW address: aa:bb:cc:dd:ee:ff
+///     - option 240 = data
+///            -010203040506070809-010203040506070809-010203040506070809-010203040506070809
+///            -010203040506070809-010203040506070809-010203040506070809-010203040506070809
+///            -010203040506070809-010203040506070809-010203040506070809-010203040506070809
+///            -data
+///     - tftp-server-name = some-name
+///            -010203040506070809-010203040506070809-010203040506070809-010203040506070809
+///            -010203040506070809-010203040506070809-010203040506070809-010203040506070809
+///            -010203040506070809-010203040506070809-010203040506070809-010203040506070809
+///            .example.org
+///     - boot-file-name = bootfile
+///            -010203040506070809-010203040506070809-010203040506070809-010203040506070809
+///            -010203040506070809-010203040506070809-010203040506070809-010203040506070809
+///            -010203040506070809-010203040506070809-010203040506070809-010203040506070809
+///            .efi
 const char* INFORM_CONFIGS[] = {
 // Configuration 0
     "{ \"interfaces-config\": {"
@@ -123,6 +145,60 @@ const char* INFORM_CONFIGS[] = {
         "    ]"
         "} ]"
     "}",
+
+// Configuration 3
+    "{ \"interfaces-config\": {"
+        "      \"interfaces\": [ \"*\" ]"
+        "},"
+        "\"valid-lifetime\": 600,"
+        "\"next-server\": \"10.0.0.1\","
+        "\"server-hostname\": \"nohost\","
+        "\"boot-file-name\": \"nofile\","
+        "\"option-def\": ["
+        "    {"
+        "        \"array\": false,"
+        "        \"code\": 240,"
+        "        \"encapsulate\": \"\","
+        "        \"name\": \"my-option\","
+        "        \"record-types\": \"string\","
+        "        \"space\": \"dhcp\","
+        "        \"type\": \"record\""
+        "    }"
+        "],"
+        "\"option-data\": ["
+        "    {"
+        "        \"always-send\": false,"
+        "        \"code\": 240,"
+        "        \"name\": \"my-option\","
+        "        \"csv-format\": true,"
+        "        \"data\": \"data"
+        "-010203040506070809-010203040506070809-010203040506070809-010203040506070809"
+        "-010203040506070809-010203040506070809-010203040506070809-010203040506070809"
+        "-010203040506070809-010203040506070809-010203040506070809-010203040506070809"
+        "-data\","
+        "        \"space\": \"dhcp\","
+        "        \"type\": \"record\""
+        "    }"
+        "],"
+        "\"subnet4\": [ { "
+        "    \"subnet\": \"192.0.2.0/24\", "
+        "    \"reservations\": [ "
+        "       {"
+        "         \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+        "         \"tftp-server-name\": \"some-name"
+        "-010203040506070809-010203040506070809-010203040506070809-010203040506070809"
+        "-010203040506070809-010203040506070809-010203040506070809-010203040506070809"
+        "-010203040506070809-010203040506070809-010203040506070809-010203040506070809"
+        ".example.org\","
+        "         \"boot-file-name\": \"bootfile"
+        "-090807060504030201-090807060504030201-090807060504030201-090807060504030201"
+        "-090807060504030201-090807060504030201-090807060504030201-090807060504030201"
+        "-090807060504030201-090807060504030201-090807060504030201-090807060504030201"
+        ".efi\""
+        "       }"
+        "    ]"
+        "} ]"
+    "}",
 };
 
 /// @brief Test fixture class for testing DHCPINFORM.
@@ -429,6 +505,68 @@ TEST_F(InformTest, messageFieldsReservations) {
     EXPECT_EQ("bootfile.efi", client.config_.boot_file_name_);
 }
 
+// This test verifies that the server assigns and splits long options within
+// DHCPv4 message.
+TEST_F(InformTest, messageFieldsLongOptions) {
+    // Client has a reservation.
+    Dhcp4Client client(Dhcp4Client::SELECTING);
+    // Message is relayed.
+    client.useRelay();
+    // Set explicit HW address so as it matches the reservation in the
+    // configuration used below.
+    client.setHWAddress("aa:bb:cc:dd:ee:ff");
+    // Configure DHCP server.
+    configure(INFORM_CONFIGS[2], *client.getServer());
+    // Client requests big option
+    client.requestOption(240);
+    // Client sends DHCPINFORM and should receive reserved fields.
+    ASSERT_NO_THROW(client.doInform());
+    // Make sure that the server responded.
+    ASSERT_TRUE(client.getContext().response_);
+    Pkt4Ptr resp = client.getContext().response_;
+    // Make sure that the server has responded with DHCPACK.
+    ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+    // Check that the reserved and requested values have been assigned.
+    string expected =
+        "-010203040506070809-010203040506070809-010203040506070809-010203040506070809"
+        "-010203040506070809-010203040506070809-010203040506070809-010203040506070809"
+        "-010203040506070809-010203040506070809-010203040506070809-010203040506070809";
+    uint32_t count = 0;
+    string value = "";
+    for (auto const& option : resp->options_) {
+        if (option.second->getType() == 240) {
+            value += string(reinterpret_cast<const char*>(&option.second->getData()[0]),
+                            option.second->getData().size());
+        }
+        count++;
+    }
+    EXPECT_EQ(count, 2);
+    EXPECT_EQ(value, string("data") + expected + string("-data"));
+    count = 0;
+    value = "";
+    for (auto const& option : resp->options_) {
+        if (option.second->getType() == DHO_TFTP_SERVER_NAME) {
+            value += string(reinterpret_cast<const char*>(&option.second->getData()[0]),
+                            option.second->getData().size());
+        }
+        count++;
+    }
+    EXPECT_EQ(count, 2);
+    EXPECT_EQ(value, string("some-name") + expected + string(".example.org"));
+    count = 0;
+    value = "";
+    for (auto const& option : resp->options_) {
+        if (option.second->getType() == DHO_BOOT_FILE_NAME) {
+            value += string(reinterpret_cast<const char*>(&option.second->getData()[0]),
+                            option.second->getData().size());
+        }
+        count++;
+    }
+    EXPECT_EQ(count, 2);
+    EXPECT_EQ(value, string("bootfile") + expected + string(".efi"));
+}
+
 /// This test verifies that after a client completes its INFORM exchange,
 /// appropriate statistics are updated.
 TEST_F(InformTest, statisticsInform) {
index 9b8099a388206461fe26b935eeec714addeaaf9e..a320204122715633b03b87588feab2edb0ba9ab5 100644 (file)
@@ -93,7 +93,7 @@ bool LibDHCP::initialized_ = LibDHCP::initOptionDefs();
 
 const OptionDefContainerPtr
 LibDHCP::getOptionDefs(const std::string& space) {
-    OptionDefContainers::const_iterator container = option_defs_.find(space);
+    auto const& container = option_defs_.find(space);
     if (container != option_defs_.end()) {
         return (container->second);
     }
@@ -214,12 +214,10 @@ void
 LibDHCP::setRuntimeOptionDefs(const OptionDefSpaceContainer& defs) {
     OptionDefSpaceContainer defs_copy;
     std::list<std::string> option_space_names = defs.getOptionSpaceNames();
-    for (std::list<std::string>::const_iterator name = option_space_names.begin();
-         name != option_space_names.end(); ++name) {
-        OptionDefContainerPtr container = defs.getItems(*name);
-        for (OptionDefContainer::const_iterator def = container->begin();
-             def != container->end(); ++def) {
-            OptionDefinitionPtr def_copy(new OptionDefinition(**def));
+    for (auto const& name : option_space_names) {
+        OptionDefContainerPtr container = defs.getItems(name);
+        for (auto const& def : *container) {
+            OptionDefinitionPtr def_copy(new OptionDefinition(*def));
             defs_copy.addItem(def_copy);
         }
     }
@@ -812,8 +810,8 @@ LibDHCP::unpackVendorOptions4(const uint32_t vendor_id, const OptionBuffer& buf,
 
 void
 LibDHCP::packOptions4(isc::util::OutputBuffer& buf,
-                     const OptionCollection& options,
-                     bool top /* = false */) {
+                      const OptionCollection& options,
+                      bool top /* = false */) {
     OptionPtr agent;
     OptionPtr end;
 
@@ -828,42 +826,111 @@ LibDHCP::packOptions4(isc::util::OutputBuffer& buf,
         }
     }
 
-    for (OptionCollection::const_iterator it = options.begin();
-         it != options.end(); ++it) {
+    for (auto const& option : options) {
 
         // TYPE is already done, RAI and END options must be last.
-        switch (it->first) {
+        switch (option.first) {
             case DHO_DHCP_MESSAGE_TYPE:
                 break;
             case DHO_DHCP_AGENT_OPTIONS:
-                agent = it->second;
+                agent = option.second;
                 break;
             case DHO_END:
-                end = it->second;
+                end = option.second;
                 break;
             default:
-                it->second->pack(buf);
+                option.second->pack(buf);
                 break;
         }
     }
 
     // Add the RAI option if it exists.
     if (agent) {
-       agent->pack(buf);
+        agent->pack(buf);
     }
 
     // And at the end the END option.
     if (end)  {
-       end->pack(buf);
+        end->pack(buf);
+    }
+}
+
+void
+LibDHCP::splitOptions4(OptionCollection& options) {
+    // We need to loop until all options have been split.
+    for (;;) {
+        bool found = false;
+        // Iterate over all options in the container.
+        for (auto const& option : options) {
+            OptionPtr candidate = option.second;
+            OptionCollection& sub_options = candidate->getMutableOptions();
+            // Split suboptions recursively, if any.
+            if (sub_options.size()) {
+                LibDHCP::splitOptions4(sub_options);
+            }
+            uint32_t header_len = candidate->getHeaderLen();
+            // Maximum option buffer size is 255 - header size.
+            uint8_t len = 255 - header_len;
+            // Current option size after split is the sum of the data, the
+            // suboptions and the header size. The header is duplicated in all
+            // new options, but the rest of the data must be serialized.
+            uint32_t size = candidate->len() - header_len;
+            // Only split if data does not fit in the current option.
+            if (size > len) {
+                // Make a copy of the options so we can safely iterate over the
+                // old container.
+                OptionCollection copy = options;
+                // Erase the old option from the new container so that only new
+                // options are present.
+                copy.erase(option.first);
+                uint32_t offset = 0;
+                // Drain the option buffer in multiple new options until all
+                // data is serialized.
+                for (; offset != size;) {
+                    // Adjust the data length of the new option if remaining
+                    // data is less than the 255 - header size (for the last
+                    // option).
+                    if (size - offset < len) {
+                        len = size - offset;
+                    }
+                    // Create new option with data starting from offset and
+                    // containing truncated length.
+                    OptionPtr new_option(new Option(candidate->getUniverse(),
+                                                    candidate->getType(),
+                                                    OptionBuffer(candidate->getData()[offset],
+                                                                 len)));
+                    // If this is the first option, also add (if necessary,
+                    // already split) suboptions, if any.
+                    if (!offset) {
+                        new_option->getMutableOptions() = candidate->getOptions();
+                    }
+                    // Adjust the offset for remaining data to be written to the
+                    // next new option.
+                    offset += len;
+                    // Add the new option to the new container.
+                    copy.insert(make_pair(candidate->getType(), new_option));
+                }
+                // After all new options have been split and added, update the
+                // option container with the new container.
+                options = copy;
+                // Other options might need splitting, so we need to iterate
+                // again until no option needs splitting.
+                found = true;
+                break;
+            }
+        }
+        // No option needs splitting, so we can exit the loop.
+        if (!found) {
+            break;
+        }
     }
 }
 
 void
 LibDHCP::packOptions6(isc::util::OutputBuffer& buf,
                       const OptionCollection& options) {
-    for (OptionCollection::const_iterator it = options.begin();
-         it != options.end(); ++it) {
-        it->second->pack(buf);
+    for (auto const& option : options) {
+        option.second->pack(buf);
     }
 }
 
index 43891275473a9b33fd4ef1da8bbbd9ccdecfa202..315a1a0256a3055cfcc439fc46519a24822b2855 100644 (file)
@@ -194,6 +194,13 @@ public:
                              const isc::dhcp::OptionCollection& options,
                              bool top = false);
 
+    /// @brief Split long options in multiple suboptions with the same option
+    /// code (RFC3396).
+    ///
+    /// @param options The option container which needs to be updated with split
+    /// options.
+    static void splitOptions4(isc::dhcp::OptionCollection& options);
+
     /// @brief Stores DHCPv6 options in a buffer.
     ///
     /// Stores all options defined in options containers in a on-wire
index 52a6da7ecf8024e2309d6f5b49a182f30791cbed..e2928869582699dfbf02dc53b11bbd35b2b9a6ee 100644 (file)
@@ -36,7 +36,6 @@ Option::factory(Option::Universe u,
     return(LibDHCP::optionFactory(u, type, buf));
 }
 
-
 Option::Option(Universe u, uint16_t type)
     :universe_(u), type_(type) {
     check();
@@ -89,7 +88,7 @@ Option::clone() const {
 
 void
 Option::check() const {
-    if ( (universe_ != V4) && (universe_ != V6) ) {
+    if ((universe_ != V4) && (universe_ != V6)) {
         isc_throw(BadValue, "Invalid universe type specified. "
                   << "Only V4 and V6 are allowed.");
     }
@@ -99,11 +98,6 @@ Option::check() const {
         if (type_ > 255) {
             isc_throw(OutOfRange, "DHCPv4 Option type " << type_ << " is too big. "
                       << "For DHCPv4 allowed type range is 0..255");
-        } else if (data_.size() > 255) {
-            isc_throw(OutOfRange, "DHCPv4 Option " << type_ << " is too big.");
-            /// TODO Larger options can be stored as separate instances
-            /// of DHCPv4 options. Clients MUST concatenate them.
-            /// Fortunately, there are no such large options used today.
         }
     }
 
@@ -186,10 +180,8 @@ uint16_t Option::len() const {
     size_t length = getHeaderLen() + data_.size();
 
     // ... and sum of lengths of all suboptions
-    for (OptionCollection::const_iterator it = options_.begin();
-         it != options_.end();
-         ++it) {
-        length += (*it).second->len();
+    for (auto const& option : options_) {
+        length += option.second->len();
     }
 
     // note that this is not equal to length field. This value denotes
@@ -209,10 +201,9 @@ Option::valid() const {
 }
 
 OptionPtr Option::getOption(uint16_t opt_type) const {
-    isc::dhcp::OptionCollection::const_iterator x =
-        options_.find(opt_type);
-    if ( x != options_.end() ) {
-        return (*x).second;
+    auto const& x = options_.find(opt_type);
+    if (x != options_.end()) {
+        return (x->second);
     }
     return OptionPtr(); // NULL
 }
@@ -220,11 +211,9 @@ OptionPtr Option::getOption(uint16_t opt_type) const {
 void
 Option::getOptionsCopy(OptionCollection& options_copy) const {
     OptionCollection local_options;
-    for (OptionCollection::const_iterator it = options_.begin();
-         it != options_.end(); ++it) {
-        OptionPtr copy = it->second->clone();
-        local_options.insert(std::make_pair(it->second->getType(),
-                                            copy));
+    for (auto const& option : options_) {
+        OptionPtr copy = option.second->clone();
+        local_options.insert(std::make_pair(option.second->getType(), copy));
     }
     // All options copied successfully, so assign them to the output
     // parameter.
@@ -232,15 +221,14 @@ Option::getOptionsCopy(OptionCollection& options_copy) const {
 }
 
 bool Option::delOption(uint16_t opt_type) {
-    isc::dhcp::OptionCollection::iterator x = options_.find(opt_type);
-    if ( x != options_.end() ) {
+    auto const& x = options_.find(opt_type);
+    if (x != options_.end()) {
         options_.erase(x);
-        return true; // delete successful
+        return (true); // delete successful
     }
     return (false); // option not found, can't delete
 }
 
-
 std::string Option::toText(int indent) const {
     std::stringstream output;
     output << headerToText(indent) << ": ";
@@ -325,9 +313,8 @@ Option::suboptionsToText(const int indent) const {
 
     if (!options_.empty()) {
         output << "," << std::endl << "options:";
-        for (OptionCollection::const_iterator opt = options_.begin();
-             opt != options_.end(); ++opt) {
-            output << std::endl << (*opt).second->toText(indent);
+        for (auto const& opt : options_) {
+            output << std::endl << opt.second->toText(indent);
         }
     }
 
index 1c4b97d6a21fb27ba859cfd4e9ed3c2451a719f1..453c00c554d3f36d97361aeb0129f1cfab681205 100644 (file)
@@ -230,7 +230,7 @@ public:
     /// @brief returns option universe (V4 or V6)
     ///
     /// @return universe type
-    Universe  getUniverse() const { return universe_; };
+    Universe getUniverse() const { return universe_; };
 
     /// @brief Writes option in wire-format to a buffer.
     ///
@@ -340,6 +340,15 @@ public:
         return (options_);
     }
 
+    /// @brief Returns all encapsulated options.
+    ///
+    /// @warning This function returns a reference to the container holding
+    /// encapsulated options, which is valid as long as the object which
+    /// returned it exists.
+    OptionCollection& getMutableOptions() {
+        return (options_);
+    }
+
     /// @brief Performs deep copy of suboptions.
     ///
     /// This method calls @ref clone method to deep copy each option.
index 9c733652b8e2b02719d216560d1c83de46f454aa..e7cad2fe56fdeb5d318d62d98704cc217a161d85 100644 (file)
@@ -117,6 +117,10 @@ Pkt4::pack() {
         // write DHCP magic cookie
         buffer_out_.writeUint32(DHCP_OPTIONS_COOKIE);
 
+        // The RFC3396 adds support for long options split over multiple options
+        // using the same code.
+        LibDHCP::splitOptions4(options_);
+
         // Call packOptions4() with parameter,"top", true. This invokes
         // logic to emit the message type option first.
         LibDHCP::packOptions4(buffer_out_, options_, true);
index 12cf9983b5406e6907981c8fb06239e4057483fb..8ca9d768eda6c8fa3a0f6051b291d5fba1904c22 100644 (file)
@@ -669,6 +669,37 @@ static uint8_t v4_opts[] = {
     0x01, 0x02, 0x03, 0x00 // Vendor Specific Information continued
 };
 
+// This test verifies that split options for v4 is working correctly.
+TEST_F(LibDhcpTest, longOption) {
+    OptionDefinition opt_def("option-foo", 231, "my-space", "binary",
+                             "option-foo-space");
+
+    // Create a buffer holding some binary data. This data will be
+    // used as reference when we read back the data from a created
+    // option.
+    OptionBuffer buf_in(2560);
+    for (unsigned i = 0; i < 2560; ++i) {
+        buf_in[i] = i;
+    }
+
+    // Use scoped pointer because it allows to declare the option
+    // in the function scope and initialize it under ASSERT.
+    boost::shared_ptr<OptionCustom> option;
+    // Custom option may throw exception if the provided buffer is
+    // malformed.
+    ASSERT_NO_THROW(
+        option.reset(new OptionCustom(opt_def, Option::V4, buf_in));
+    );
+    ASSERT_TRUE(option);
+    isc::util::OutputBuffer buf(0);
+    OptionCollection col;
+    col.insert(std::make_pair(213, option));
+    LibDHCP::splitOptions4(col);
+    LibDHCP::packOptions4(buf, col, true);
+
+    EXPECT_EQ(11, col.size());
+}
+
 // This test verifies that pack options for v4 is working correctly.
 TEST_F(LibDhcpTest, packOptions4) {