Browse Source

[master] Merge branch 'trac3316'

Marcin Siodelski 11 years ago
parent
commit
1e61d7db5b

+ 23 - 21
doc/guide/bind10-guide.xml

@@ -4431,15 +4431,16 @@ Dhcp4/subnet4	[]	list	(default)
         extensions will be using hooks extensions.
       </para>
       </note>
-      <para>In certain cases it is useful to differentiate between different types
-      of clients and treat them differently. The process of doing classification
-      is conducted in two steps. The first step is to assess incoming packet and
-      assign it to zero or more classes. This classification is currently simple,
-      but is expected to grow in capability soon. Currently the server checks whether
-      incoming packet has vendor class identifier option (60). If it has, content
-      of that option is interpreted as a class. For example, modern cable modems
-      will send this option with value &quot;docsis3.0&quot; and as a result the
-      packet will belong to class &quot;docsis3.0&quot;.
+      <para>In certain cases it is useful to differentiate between different
+      types of clients and treat them differently. The process of doing
+      classification is conducted in two steps. The first step is to assess
+      incoming packet and assign it to zero or more classes. This classification
+      is currently simple, but is expected to grow in capability soon. Currently
+      the server checks whether incoming packet has vendor class identifier
+      option (60). If it has, content of that option is prepended with
+      &quot;VENDOR_CLASS_&quot; then is interpreted as a class. For example,
+      modern cable modems will send this option with value &quot;docsis3.0&quot;
+      and as a result the packet will belong to class &quot;VENDOR_CLASS_docsis3.0&quot;.
       </para>
 
       <para>It is envisaged that the client classification will be used for changing
@@ -4450,12 +4451,12 @@ Dhcp4/subnet4	[]	list	(default)
       subnet selection.</para>
 
       <para>
-        For clients that belong to the docsis3.0 class, the siaddr field is set to
-        the value of next-server (if specified in a subnet). If there is
-        boot-file-name option specified, its value is also set in the file field
-        in the DHCPv4 packet. For eRouter1.0 class, the siaddr is always set to
-        0.0.0.0. That capability is expected to be moved to external hook
-        library that will be dedicated to cable modems.
+        For clients that belong to the VENDOR_CLASS_docsis3.0 class, the siaddr
+        field is set to the value of next-server (if specified in a subnet). If
+        there is boot-file-name option specified, its value is also set in the
+        file field in the DHCPv4 packet. For eRouter1.0 class, the siaddr is
+        always set to 0.0.0.0. That capability is expected to be moved to
+        external hook library that will be dedicated to cable modems.
       </para>
 
       <para>
@@ -4483,13 +4484,13 @@ Dhcp4/subnet4	[]	list	(default)
         the 192.0.2.0/24 prefix. The Administrator of that network has decided
         that addresses from range 192.0.2.10 to 192.0.2.20 are going to be
         managed by the Dhcp4 server. Only clients belonging to client class
-        docsis3.0 are allowed to use this subnet. Such a configuration can be
-        achieved in the following way:
+        VENDOR_CLASS_docsis3.0 are allowed to use this subnet. Such a
+        configuration can be achieved in the following way:
         <screen>
 &gt; <userinput>config add Dhcp4/subnet4</userinput>
 &gt; <userinput>config set Dhcp4/subnet4[0]/subnet "192.0.2.0/24"</userinput>
 &gt; <userinput>config set Dhcp4/subnet4[0]/pool [ "192.0.2.10 - 192.0.2.20" ]</userinput>
-&gt; <userinput>config set Dhcp4/subnet4[0]/client-class "docsis3.0"</userinput>
+&gt; <userinput>config set Dhcp4/subnet4[0]/client-class "VENDOR_CLASS_docsis3.0"</userinput>
 &gt; <userinput>config commit</userinput></screen>
       </para>
 
@@ -5558,9 +5559,10 @@ should include options from the isc option space:
       assign it to zero or more classes. This classification is currently simple,
       but is expected to grow in capability soon. Currently the server checks whether
       incoming packet has vendor class option (16). If it has, content
-      of that option is interpreted as a class. For example, modern cable modems
-      will send this option with value &quot;docsis3.0&quot; and as a result the
-      packet will belong to class &quot;docsis3.0&quot;.
+      of that option is prepended with &quot;VENDOR_CLASS_&quot; interpreted as a
+      class. For example, modern cable modems will send this option with value
+      &quot;docsis3.0&quot; and as a result the packet will belong to class
+      &quot;VENDOR_CLASS_docsis3.0&quot;.
       </para>
 
       <para>It is envisaged that the client classification will be used for changing

+ 9 - 7
src/bin/dhcp4/dhcp4_srv.cc

@@ -80,6 +80,8 @@ Dhcp4Hooks Hooks;
 namespace isc {
 namespace dhcp {
 
+const std::string Dhcpv4Srv::VENDOR_CLASS_PREFIX("VENDOR_CLASS_");
+
 Dhcpv4Srv::Dhcpv4Srv(uint16_t port, const char* dbconfig, const bool use_bcast,
                      const bool direct_response_desired)
 : shutdown_(true), alloc_engine_(), port_(port),
@@ -1809,15 +1811,15 @@ void Dhcpv4Srv::classifyPacket(const Pkt4Ptr& pkt) {
     // quals subscriber-id option that was inserted by the relay (CMTS).
     // This kind of logic will appear here soon.
     if (vendor_class->getValue().find(DOCSIS3_CLASS_MODEM) != std::string::npos) {
-        pkt->addClass(DOCSIS3_CLASS_MODEM);
-        classes += string(DOCSIS3_CLASS_MODEM) + " ";
+        pkt->addClass(VENDOR_CLASS_PREFIX + DOCSIS3_CLASS_MODEM);
+        classes += string(VENDOR_CLASS_PREFIX + DOCSIS3_CLASS_MODEM) + " ";
     } else
     if (vendor_class->getValue().find(DOCSIS3_CLASS_EROUTER) != std::string::npos) {
-        pkt->addClass(DOCSIS3_CLASS_EROUTER);
-        classes += string(DOCSIS3_CLASS_EROUTER) + " ";
+        pkt->addClass(VENDOR_CLASS_PREFIX + DOCSIS3_CLASS_EROUTER);
+        classes += string(VENDOR_CLASS_PREFIX + DOCSIS3_CLASS_EROUTER) + " ";
     } else {
-        classes += vendor_class->getValue();
-        pkt->addClass(vendor_class->getValue());
+        classes += VENDOR_CLASS_PREFIX + vendor_class->getValue();
+        pkt->addClass(VENDOR_CLASS_PREFIX + vendor_class->getValue());
     }
 
     if (!classes.empty()) {
@@ -1833,7 +1835,7 @@ bool Dhcpv4Srv::classSpecificProcessing(const Pkt4Ptr& query, const Pkt4Ptr& rsp
         return (true);
     }
 
-    if (query->inClass(DOCSIS3_CLASS_MODEM)) {
+    if (query->inClass(VENDOR_CLASS_PREFIX + DOCSIS3_CLASS_MODEM)) {
 
         // Set next-server. This is TFTP server address. Cable modems will
         // download their configuration from that server.

+ 8 - 0
src/bin/dhcp4/dhcp4_srv.h

@@ -430,6 +430,14 @@ protected:
     /// @param [out] answer A response message to be sent to a client.
     void processClientName(const Pkt4Ptr& query, Pkt4Ptr& answer);
 
+    /// @brief this is a prefix added to the contend of vendor-class option
+    ///
+    /// If incoming packet has a vendor class option, its content is
+    /// prepended with this prefix and then interpreted as a class.
+    /// For example, a packet that sends vendor class with value of "FOO"
+    /// will cause the packet to be assigned to class VENDOR_CLASS_FOO.
+    static const std::string VENDOR_CLASS_PREFIX;
+
 private:
     /// @brief Process Client FQDN %Option sent by a client.
     ///

+ 4 - 4
src/bin/dhcp4/tests/dhcp4_srv_unittest.cc

@@ -3294,8 +3294,8 @@ TEST_F(Dhcpv4SrvTest, clientClassification) {
 
     srv.classifyPacket(dis1);
 
-    EXPECT_TRUE(dis1->inClass("docsis3.0"));
-    EXPECT_FALSE(dis1->inClass("eRouter1.0"));
+    EXPECT_TRUE(dis1->inClass(srv.VENDOR_CLASS_PREFIX + "docsis3.0"));
+    EXPECT_FALSE(dis1->inClass(srv.VENDOR_CLASS_PREFIX + "eRouter1.0"));
 
     // Let's create a relayed DISCOVER. This particular relayed DISCOVER has
     // vendor-class set to eRouter1.0
@@ -3305,8 +3305,8 @@ TEST_F(Dhcpv4SrvTest, clientClassification) {
 
     srv.classifyPacket(dis2);
 
-    EXPECT_TRUE(dis2->inClass("eRouter1.0"));
-    EXPECT_FALSE(dis2->inClass("docsis3.0"));
+    EXPECT_TRUE(dis2->inClass(srv.VENDOR_CLASS_PREFIX + "eRouter1.0"));
+    EXPECT_FALSE(dis2->inClass(srv.VENDOR_CLASS_PREFIX + "docsis3.0"));
 }
 
 // Checks if the client-class field is indeed used for subnet selection.

+ 1 - 0
src/bin/dhcp4/tests/dhcp4_test_utils.h

@@ -199,6 +199,7 @@ public:
     using Dhcpv4Srv::accept;
     using Dhcpv4Srv::acceptMessageType;
     using Dhcpv4Srv::selectSubnet;
+    using Dhcpv4Srv::VENDOR_CLASS_PREFIX;
 };
 
 class Dhcpv4SrvTest : public ::testing::Test {

+ 20 - 22
src/bin/dhcp6/dhcp6_srv.cc

@@ -28,6 +28,7 @@
 #include <dhcp/option6_iaprefix.h>
 #include <dhcp/option_custom.h>
 #include <dhcp/option_vendor.h>
+#include <dhcp/option_vendor_class.h>
 #include <dhcp/option_int_array.h>
 #include <dhcp/pkt6.h>
 #include <dhcp6/dhcp6_log.h>
@@ -54,6 +55,7 @@
 #include <time.h>
 #include <iomanip>
 #include <fstream>
+#include <sstream>
 
 using namespace isc;
 using namespace isc::asiolink;
@@ -98,6 +100,8 @@ Dhcp6Hooks Hooks;
 namespace isc {
 namespace dhcp {
 
+const std::string Dhcpv6Srv::VENDOR_CLASS_PREFIX("VENDOR_CLASS_");
+
 namespace {
 
 // The following constants describe server's behavior with respect to the
@@ -2440,36 +2444,30 @@ Dhcpv6Srv::ifaceMgrSocket6ErrorHandler(const std::string& errmsg) {
 }
 
 void Dhcpv6Srv::classifyPacket(const Pkt6Ptr& pkt) {
+    OptionVendorClassPtr vclass = boost::dynamic_pointer_cast<
+        OptionVendorClass>(pkt->getOption(D6O_VENDOR_CLASS));
 
-    boost::shared_ptr<OptionCustom> vclass =
-        boost::dynamic_pointer_cast<OptionCustom>(pkt->getOption(D6O_VENDOR_CLASS));
-
-    if (!vclass) {
+    if (!vclass || vclass->getTuplesNum() == 0) {
         return;
     }
 
-    string classes = "";
+    std::ostringstream classes;
+    if (vclass->hasTuple(DOCSIS3_CLASS_MODEM)) {
+        classes << VENDOR_CLASS_PREFIX << DOCSIS3_CLASS_MODEM;
+
+    } else if (vclass->hasTuple(DOCSIS3_CLASS_EROUTER)) {
+        classes << VENDOR_CLASS_PREFIX << DOCSIS3_CLASS_EROUTER;
+
+    } else {
+        classes << vclass->getTuple(0).getText();
 
-    // DOCSIS specific section
-    if (vclass->readString(VENDOR_CLASS_STRING_INDEX)
-        .find(DOCSIS3_CLASS_MODEM) != std::string::npos) {
-        pkt->addClass(DOCSIS3_CLASS_MODEM);
-        classes += string(DOCSIS3_CLASS_MODEM) + " ";
-    } else
-    if (vclass->readString(VENDOR_CLASS_STRING_INDEX)
-        .find(DOCSIS3_CLASS_EROUTER) != std::string::npos) {
-        pkt->addClass(DOCSIS3_CLASS_EROUTER);
-        classes += string(DOCSIS3_CLASS_EROUTER) + " ";
-    }else
-    {
-        // Otherwise use the string as is
-        classes += vclass->readString(VENDOR_CLASS_STRING_INDEX);
-        pkt->addClass(vclass->readString(VENDOR_CLASS_STRING_INDEX));
     }
 
-    if (!classes.empty()) {
+    // If there is no class identified, leave.
+    if (!classes.str().empty()) {
+        pkt->addClass(classes.str());
         LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_CLASS_ASSIGNED)
-            .arg(classes);
+            .arg(classes.str());
     }
 }
 

+ 9 - 0
src/bin/dhcp6/dhcp6_srv.h

@@ -536,6 +536,15 @@ protected:
     /// @param pkt packet to be classified
     void classifyPacket(const Pkt6Ptr& pkt);
 
+
+    /// @brief this is a prefix added to the contend of vendor-class option
+    ///
+    /// If incoming packet has a vendor class option, its content is
+    /// prepended with this prefix and then interpreted as a class.
+    /// For example, a packet that sends vendor class with value of "FOO"
+    /// will cause the packet to be assigned to class VENDOR_CLASS_FOO.
+    static const std::string VENDOR_CLASS_PREFIX;
+
 private:
 
     /// @brief Implements the error handler for socket open failure.

+ 28 - 3
src/bin/dhcp6/tests/dhcp6_srv_unittest.cc

@@ -1725,7 +1725,7 @@ TEST_F(Dhcpv6SrvTest, clientClassification) {
     srv.classifyPacket(sol1);
 
     // It should belong to docsis3.0 class. It should not belong to eRouter1.0
-    EXPECT_TRUE(sol1->inClass("docsis3.0"));
+    EXPECT_TRUE(sol1->inClass("VENDOR_CLASS_docsis3.0"));
     EXPECT_FALSE(sol1->inClass("eRouter1.0"));
 
     // Let's get a relayed SOLICIT. This particular relayed SOLICIT has
@@ -1736,8 +1736,8 @@ TEST_F(Dhcpv6SrvTest, clientClassification) {
 
     srv.classifyPacket(sol2);
 
-    EXPECT_TRUE(sol2->inClass("eRouter1.0"));
-    EXPECT_FALSE(sol2->inClass("docsis3.0"));
+    EXPECT_TRUE(sol2->inClass(srv.VENDOR_CLASS_PREFIX + "eRouter1.0"));
+    EXPECT_FALSE(sol2->inClass(srv.VENDOR_CLASS_PREFIX + "docsis3.0"));
 }
 
 // Checks if the client-class field is indeed used for subnet selection.
@@ -1806,6 +1806,31 @@ TEST_F(Dhcpv6SrvTest, clientClassify2) {
     EXPECT_TRUE(srv.selectSubnet(sol));
 }
 
+// This test checks that the server will handle a Solicit with the Vendor Class
+// having a length of 4 (enterprise-id only).
+TEST_F(Dhcpv6SrvTest, cableLabsShortVendorClass) {
+    NakedDhcpv6Srv srv(0);
+
+    // Create a simple Solicit with the 4-byte long vendor class option.
+    Pkt6Ptr sol = captureCableLabsShortVendorClass();
+
+    // Simulate that we have received that traffic
+    srv.fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive6(), it will read all packets from the list set by
+    // fakeReceive()
+    srv.run();
+
+    // Get Advertise...
+    ASSERT_FALSE(srv.fake_sent_.empty());
+    Pkt6Ptr adv = srv.fake_sent_.front();
+    ASSERT_TRUE(adv);
+
+    // This is sent back to client, so port is 546
+    EXPECT_EQ(DHCP6_CLIENT_PORT, adv->getRemotePort());
+
+}
 
 /// @todo: Add more negative tests for processX(), e.g. extend sanityCheck() test
 /// to call processX() methods.

+ 2 - 0
src/bin/dhcp6/tests/dhcp6_test_utils.h

@@ -110,6 +110,7 @@ public:
     using Dhcpv6Srv::writeServerID;
     using Dhcpv6Srv::unpackOptions;
     using Dhcpv6Srv::name_change_reqs_;
+    using Dhcpv6Srv::VENDOR_CLASS_PREFIX;
 
     /// @brief packets we pretend to receive
     ///
@@ -508,6 +509,7 @@ public:
     isc::dhcp::Pkt6Ptr captureRelayedSolicit();
     isc::dhcp::Pkt6Ptr captureDocsisRelayedSolicit();
     isc::dhcp::Pkt6Ptr captureeRouterRelayedSolicit();
+    isc::dhcp::Pkt6Ptr captureCableLabsShortVendorClass();
 
     /// @brief Auxiliary method that sets Pkt6 fields
     ///

+ 25 - 1
src/bin/dhcp6/tests/wireshark.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -301,5 +301,29 @@ DHCPv6
     return (pkt);
 }
 
+Pkt6Ptr isc::test::Dhcpv6SrvTest::captureCableLabsShortVendorClass() {
+    // This is a simple non-relayed Solicit:
+    // - client-identifier
+    // - IA_NA
+    // - Vendor Class (4 bytes)
+    //   - enterprise-id 4491
+    // - Vendor-specific Information
+    //   - enterprise-id 4491
+    std::string hex_string =
+        "01671cb90001000e0001000152ea903a08002758f1e80003000c00004bd10000000000"
+        "000000001000040000118b0011000a0000118b000100020020";
+
+    std::vector<uint8_t> bin;
+
+    // Decode the hex string and store it in bin (which happens
+    // to be OptionBuffer format)
+    isc::util::encode::decodeHex(hex_string, bin);
+
+    Pkt6Ptr pkt(new Pkt6(&bin[0], bin.size()));
+    captureSetDefaultFields(pkt);
+    return (pkt);
+
+}
+
 }; // end of isc::test namespace
 }; // end of isc namespace

+ 2 - 0
src/lib/dhcp/Makefile.am

@@ -23,6 +23,7 @@ libb10_dhcp___la_SOURCES += iface_mgr_error_handler.h
 libb10_dhcp___la_SOURCES += iface_mgr_linux.cc
 libb10_dhcp___la_SOURCES += iface_mgr_sun.cc
 libb10_dhcp___la_SOURCES += libdhcp++.cc libdhcp++.h
+libb10_dhcp___la_SOURCES += opaque_data_tuple.cc opaque_data_tuple.h
 libb10_dhcp___la_SOURCES += option4_addrlst.cc option4_addrlst.h
 libb10_dhcp___la_SOURCES += option4_client_fqdn.cc option4_client_fqdn.h
 libb10_dhcp___la_SOURCES += option6_ia.cc option6_ia.h
@@ -31,6 +32,7 @@ libb10_dhcp___la_SOURCES += option6_iaprefix.cc option6_iaprefix.h
 libb10_dhcp___la_SOURCES += option6_addrlst.cc option6_addrlst.h
 libb10_dhcp___la_SOURCES += option6_client_fqdn.cc option6_client_fqdn.h
 libb10_dhcp___la_SOURCES += option_vendor.cc option_vendor.h
+libb10_dhcp___la_SOURCES += option_vendor_class.cc option_vendor_class.h
 libb10_dhcp___la_SOURCES += option_int.h
 libb10_dhcp___la_SOURCES += option_int_array.h
 libb10_dhcp___la_SOURCES += option.cc option.h

+ 123 - 0
src/lib/dhcp/opaque_data_tuple.cc

@@ -0,0 +1,123 @@
+// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <dhcp/opaque_data_tuple.h>
+
+namespace isc {
+namespace dhcp {
+
+OpaqueDataTuple::OpaqueDataTuple(LengthFieldType length_field_type)
+    : length_field_type_(length_field_type) {
+}
+
+void
+OpaqueDataTuple::append(const std::string& text) {
+    // Don't do anything if text is empty.
+    if (!text.empty()) {
+        append(&text[0], text.size());
+    }
+}
+
+void
+OpaqueDataTuple::assign(const std::string& text) {
+    // If empty string is being assigned, reset the buffer.
+    if (text.empty()) {
+        clear();
+    } else {
+        assign(&text[0], text.size());
+    }
+}
+
+void
+OpaqueDataTuple::clear() {
+    data_.clear();
+}
+
+bool
+OpaqueDataTuple::equals(const std::string& other) const {
+    return (getText() == other);
+}
+
+std::string
+OpaqueDataTuple::getText() const {
+    // Convert the data carried in the buffer to a string.
+    return (std::string(data_.begin(), data_.end()));
+}
+
+void
+OpaqueDataTuple::pack(isc::util::OutputBuffer& buf) const {
+    if (getLength() == 0) {
+        isc_throw(OpaqueDataTupleError, "failed to create on-wire format of the"
+                  " opaque data field, because the field appears to be empty");
+    } else if ((1 << (getDataFieldSize() * 8)) <= getLength()) {
+        isc_throw(OpaqueDataTupleError, "failed to create on-wire format of the"
+                  " opaque data field, because current data length "
+                  << getLength() << " exceeds the maximum size for the length"
+                  << " field size " << getDataFieldSize());
+    }
+
+    if (getDataFieldSize() == 1) {
+        buf.writeUint8(static_cast<uint8_t>(getLength()));
+    } else {
+        buf.writeUint16(getLength());
+    }
+
+    buf.writeData(&getData()[0], getLength());
+}
+
+int
+OpaqueDataTuple::getDataFieldSize() const {
+    return (length_field_type_ == LENGTH_1_BYTE ? 1 : 2);
+}
+
+OpaqueDataTuple&
+OpaqueDataTuple::operator=(const std::string& other) {
+    // Replace existing data with the new data converted from a string.
+    assign(&other[0], other.length());
+    return (*this);
+}
+
+bool
+OpaqueDataTuple::operator==(const std::string& other) const {
+    return (equals(other));
+}
+
+bool
+OpaqueDataTuple::operator!=(const std::string& other) {
+    return (!equals(other));
+}
+
+std::ostream& operator<<(std::ostream& os, const OpaqueDataTuple& tuple) {
+    os << tuple.getText();
+    return (os);
+}
+
+std::istream& operator>>(std::istream& is, OpaqueDataTuple& tuple) {
+    // We will replace the current data with new data.
+    tuple.clear();
+    char buf[256];
+    // Read chunks of data as long as we haven't reached the end of the stream.
+    while (!is.eof()) {
+        is.read(buf, sizeof(buf));
+        // Append the chunk of data we have just read. It is fine if the
+        // gcount is 0, because append() function will check that.
+        tuple.append(buf, is.gcount());
+    }
+    // Clear eof flag, so as the stream can be read again.
+    is.clear();
+    return (is);
+}
+
+}
+}

+ 319 - 0
src/lib/dhcp/opaque_data_tuple.h

@@ -0,0 +1,319 @@
+// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef OPAQUE_DATA_TUPLE_H
+#define OPAQUE_DATA_TUPLE_H
+
+#include <util/buffer.h>
+#include <util/io_utilities.h>
+#include <iostream>
+#include <iterator>
+#include <string>
+#include <vector>
+
+namespace isc {
+namespace dhcp {
+
+/// @brief Exception to be thrown when the operation on @c OpaqueDataTuple
+/// object results in an error.
+class OpaqueDataTupleError : public Exception {
+public:
+    OpaqueDataTupleError(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
+
+/// @brief Represents a single instance of the opaque data preceded by length.
+///
+/// Some of the DHCP options, such as Vendor Class option (16) in DHCPv6 or
+/// V-I Vendor Class option (124) in DHCPv4 may carry multiple pairs of
+/// opaque-data preceded by its length. Such pairs are called tuples. This class
+/// represents a single instance of the tuple in the DHCPv4 or DHCPv6 option.
+///
+/// Although, the primary purpose of this class is to represent data tuples in
+/// Vendor Class options, there may be other options defined in the future that
+/// may have similar structure and this class can be used to represent the data
+/// tuples in these new options too.
+///
+/// This class exposes a set of convenience methods to assign and retrieve the
+/// opaque data from the tuple. It also implements a method to render the tuple
+/// data into a wire format, as well as a method to create an instance of the
+/// tuple from the wire format.
+class OpaqueDataTuple {
+public:
+
+    /// @brief Size of the length field in the tuple.
+    ///
+    /// In the wire format, the tuple consists of the two fields: one holding
+    /// a length of the opaque data size, second holding opaque data. The first
+    /// field's size may be equal to 1 or 2 bytes. Usually, the tuples carried
+    /// in the DHCPv6 options have 2 byte long length fields, the tuples carried
+    /// in DHCPv4 options have 1 byte long length fields.
+    enum LengthFieldType {
+        LENGTH_1_BYTE,
+        LENGTH_2_BYTES
+    };
+
+    /// @brief Defines a type of the data buffer used to hold the opaque data.
+    typedef std::vector<uint8_t> Buffer;
+
+    /// @brief Default constructor.
+    ///
+    /// @param length_field_type Indicates a length of the field which holds
+    /// the size of the tuple.
+    OpaqueDataTuple(LengthFieldType length_field_type);
+
+    /// @brief Constructor
+    ///
+    /// Creates a tuple from on-wire data. It calls @c OpaqueDataTuple::unpack
+    /// internally.
+    ///
+    /// @param length_field_type Indicates the length of the field holding the
+    /// opaque data size.
+    /// @param begin Iterator pointing to the beginning of the buffer holding
+    /// wire data.
+    /// @param end Iterator pointing to the end of the buffer holding wire data.
+    /// @tparam InputIterator Type of the iterators passed to this function.
+    /// @throw It may throw an exception if the @c unpack throws.
+    template<typename InputIterator>
+    OpaqueDataTuple(LengthFieldType length_field_type, InputIterator begin,
+                    InputIterator end)
+        : length_field_type_(length_field_type) {
+        unpack(begin, end);
+    }
+
+    /// @brief Appends data to the tuple.
+    ///
+    /// This function appends the data of the specified length to the tuple.
+    /// If the specified buffer length is greater than the size of the buffer,
+    /// the behavior of this function is undefined.
+    ///
+    /// @param data Iterator pointing to the beginning of the buffer being
+    /// appended. The source buffer may be an STL object or an array of
+    /// characters. In the latter case, the pointer to the beginning of this
+    /// array should be passed.
+    /// @param len Length of the source buffer.
+    /// @tparam InputIterator Type of the iterator pointing to the beginning of
+    /// the source buffer.
+    template<typename InputIterator>
+    void append(InputIterator data, const size_t len) {
+        data_.insert(data_.end(), data, data + len);
+    }
+
+    /// @brief Appends string to the tuple.
+    ///
+    /// In most cases, the tuple will carry a string. This function appends the
+    /// string to the tuple.
+    ///
+    /// @param text String to be appended in the tuple.
+    void append(const std::string& text);
+
+    /// @brief Assigns data to the tuple.
+    ///
+    /// This function replaces existing data in the tuple with the new data.
+    /// If the speficified buffer length is greater than the size of the buffer,
+    /// the behavior of this function is undefined.
+    /// @param data Iterator pointing to the beginning of the buffer being
+    /// assigned. The source buffer may be an STL object or an array of
+    /// characters. In the latter case, the pointer to the beginning of this
+    /// array should be passed.
+    /// @param len Length of the source buffer.
+    /// @tparam InputIterator Type of the iterator pointing to the beginning of
+    /// the source buffer.
+    template<typename InputIterator>
+    void assign(InputIterator data, const size_t len) {
+        data_.assign(data, data + len);
+    }
+
+    /// @brief Assigns string data to the tuple.
+    ///
+    /// In most cases, the tuple will carry a string. This function sets the
+    /// string to the tuple.
+    ///
+    /// @param text String to be assigned to the tuple.
+    void assign(const std::string& text);
+
+    /// @brief Removes the contents of the tuple.
+    void clear();
+
+    /// @brief Checks if the data carried in the tuple match the string.
+    ///
+    /// @param other String to compare tuple data against.
+    bool equals(const std::string& other) const;
+
+    /// @brief Returns tuple length data field type.
+    LengthFieldType getLengthFieldType() const {
+        return (length_field_type_);
+    }
+
+    /// @brief Returns the length of the data in the tuple.
+    size_t getLength() const {
+        return (data_.size());
+    }
+
+    /// @brief Returns a total size of the tuple, including length field.
+    size_t getTotalLength() const {
+        return (getDataFieldSize() + getLength());
+    }
+
+    /// @brief Returns a reference to the buffer holding tuple data.
+    ///
+    /// @warning The returned reference is valid only within the lifetime
+    /// of the object which returned it. The use of the returned reference
+    /// after the object has been destroyed yelds undefined behavior.
+    const Buffer& getData() const {
+        return (data_);
+    }
+
+    /// @brief Return the tuple data in the textual format.
+    std::string getText() const;
+
+    /// @brief Renders the tuple to a buffer in the wire format.
+    ///
+    /// This function creates the following wire representation of the tuple:
+    /// - 1 or 2 bytes holding a length of the data.
+    /// - variable number of bytes holding data.
+    /// and writes it to the specified buffer. The new are appended to the
+    /// buffer, so as data existing in the buffer is preserved.
+    ///
+    /// The tuple is considered malformed if one of the follwing occurs:
+    /// - the size of the data is 0 (tuple is empty),
+    /// - the size of the data is greater than 255 and the size of the length
+    /// field is 1 byte (see @c LengthFieldType).
+    /// - the size of the data is greater than 65535 and the size of the length
+    /// field is 2 bytes (see @c LengthFieldType).
+    ///
+    /// Function will throw an exception if trying to render malformed tuple.
+    ///
+    /// @param [out] buf Buffer to which the data is rendered.
+    ///
+    /// @throw OpaqueDataTupleError if failed to render the data to the
+    /// buffer because the tuple is malformed.
+    void pack(isc::util::OutputBuffer& buf) const;
+
+    /// @brief Parses wire data and creates a tuple from it.
+    ///
+    /// This function parses on-wire data stored in the provided buffer and
+    /// stores it in the tuple object. The wire data must include at least the
+    /// data field of the length matching the specified @c LengthFieldType.
+    /// The remaining buffer length (excluding the length field) must be equal
+    /// or greater than the length carried in the length field. If any of these
+    /// two conditions is not met, an exception is thrown.
+    ///
+    /// This function allows opaque data with the length of 0.
+    ///
+    /// @param begin Iterator pointing to the beginning of the buffer holding
+    /// wire data.
+    /// @param end Iterator pointing to the end of the buffer holding wire data.
+    /// @tparam InputIterator Type of the iterators passed to this function.
+    template<typename InputIterator>
+    void unpack(InputIterator begin, InputIterator end) {
+        Buffer buf(begin, end);
+        // The buffer must at least hold the size of the data.
+        if (std::distance(begin, end) < getDataFieldSize()) {
+            isc_throw(OpaqueDataTupleError,
+                      "unable to parse the opaque data tuple, the buffer"
+                      " length is " << std::distance(begin, end)
+                      << ", expected at least " << getDataFieldSize());
+        }
+        // Read the data length from the length field, depending on the
+        // size of the data field (1 or 2 bytes).
+        size_t len = getDataFieldSize() == 1 ? *begin :
+            isc::util::readUint16(&(*begin), std::distance(begin, end));
+        // Now that we have the expected data size, let's check that the
+        // reminder of the buffer is long enough.
+        begin += getDataFieldSize();
+        if (std::distance(begin, end) < len) {
+            isc_throw(OpaqueDataTupleError,
+                      "unable to parse the opaque data tuple, the buffer"
+                      " length is " << std::distance(begin, end)
+                      << ", but the length of the tuple in the length field"
+                      " is " << len);
+        }
+        // The buffer length is long enough to read the desired amount of data.
+        assign(begin, len);
+    }
+
+    /// @name Assignment and comparison operators.
+    //{@
+
+    /// @brief Assignment operator.
+    ///
+    /// This operator assigns the string data to the tuple.
+    ///
+    /// @param other string to be assigned to the tuple.
+    /// @return Tuple object after assignment.
+    OpaqueDataTuple& operator=(const std::string& other);
+
+    /// @brief Equality operator.
+    ///
+    /// This operator compares the string given as an argument to the data
+    /// carried in the tuple in the textual format.
+    ///
+    /// @param other String to compare the tuple against.
+    /// @return true if data carried in the tuple is equal to the string.
+    bool operator==(const std::string& other) const;
+
+    /// @brief Inequality operator.
+    ///
+    /// This operator compares the string given as an argument to the data
+    /// carried in the tuple for inequality.
+    ///
+    /// @param other String to compare the tuple against.
+    /// @return true if the data carried in the tuple is unequal the given
+    /// string.
+    bool operator!=(const std::string& other);
+    //@}
+
+    /// @brief Returns the size of the tuple length field.
+    ///
+    /// The returned value depends on the @c LengthFieldType set for the tuple.
+    int getDataFieldSize() const;
+
+private:
+
+    /// @brief Buffer which holds the opaque tuple data.
+    Buffer data_;
+    /// @brief Holds a type of tuple size field (1 byte long or 2 bytes long).
+    LengthFieldType length_field_type_;
+};
+
+/// @brief Pointer to the @c OpaqueDataTuple object.
+typedef boost::shared_ptr<OpaqueDataTuple> OpaqueDataTuplePtr;
+
+/// @brief Inserts the @c OpaqueDataTuple as a string into stream.
+///
+/// This operator gets the tuple data in the textual format and inserts it
+/// into the output stream.
+///
+/// @param os Stream object on which insertion is performed.
+/// @param tuple Object encapsulating a tuple which data in the textual format
+/// is inserted into the stream.
+/// @return Reference to the same stream but after insertion operation.
+std::ostream& operator<<(std::ostream& os, const OpaqueDataTuple& tuple);
+
+/// @brief Inserts data carried in the stream into the tuple.
+///
+/// this operator inserts data carried in the input stream and inserts it to
+/// the @c OpaqueDataTuple object. The existing data is replaced with new data.
+///
+/// @param is Input stream from which the data will be inserted.
+/// @param tuple @c OpaqueDataTuple object to which the data will be inserted.
+/// @return Input stream after insertion to the tuple is performed.
+std::istream& operator>>(std::istream& is, OpaqueDataTuple& tuple);
+
+} // namespace isc::dhcp
+} // namespace isc
+
+#endif

+ 26 - 3
src/lib/dhcp/option_definition.cc

@@ -28,6 +28,7 @@
 #include <dhcp/option_space.h>
 #include <dhcp/option_string.h>
 #include <dhcp/option_vendor.h>
+#include <dhcp/option_vendor_class.h>
 #include <util/encode/hex.h>
 #include <util/strutil.h>
 #include <boost/algorithm/string/classification.hpp>
@@ -400,6 +401,22 @@ OptionDefinition::haveVendor6Format() const {
 }
 
 bool
+OptionDefinition::haveVendorClass4Format() const {
+    return (haveType(OPT_RECORD_TYPE) &&
+            (record_fields_.size() == 2) &&
+            (record_fields_[0] == OPT_UINT32_TYPE) &&
+            (record_fields_[1] == OPT_BINARY_TYPE));
+}
+
+bool
+OptionDefinition::haveVendorClass6Format() const {
+    return (haveType(OPT_RECORD_TYPE) &&
+            (record_fields_.size() == 2) &&
+            (record_fields_[0] == OPT_UINT32_TYPE) &&
+            (record_fields_[1] == OPT_BINARY_TYPE));
+}
+
+bool
 OptionDefinition::convertToBool(const std::string& value_str) const {
     // Case insensitve check that the input is one of: "true" or "false".
     if (boost::iequals(value_str, "true")) {
@@ -651,15 +668,21 @@ OptionDefinition::factorySpecialFormatOption(Option::Universe u,
             // a specialized class to handle it.
             return (OptionPtr(new Option6ClientFqdn(begin, end)));
         } else if (getCode() == D6O_VENDOR_OPTS && haveVendor6Format()) {
-            // Vendor-Specific Information.
+            // Vendor-Specific Information (option code 17)
             return (OptionPtr(new OptionVendor(Option::V6, begin, end)));
+        } else if (getCode() == D6O_VENDOR_CLASS && haveVendorClass6Format()) {
+            // Vendor Class (option code 16).
+            return (OptionPtr(new OptionVendorClass(Option::V6, begin, end)));
         }
     } else {
         if ((getCode() == DHO_FQDN) && haveFqdn4Format()) {
             return (OptionPtr(new Option4ClientFqdn(begin, end)));
-
+        } else if ((getCode() == DHO_VIVCO_SUBOPTIONS) &&
+                   haveVendorClass4Format()) {
+            // V-I Vendor Class (option code 124).
+            return (OptionPtr(new OptionVendorClass(Option::V4, begin, end)));
         } else if (getCode() == DHO_VIVSO_SUBOPTIONS && haveVendor4Format()) {
-            // Vendor-Specific Information.
+            // Vendor-Specific Information (option code 125).
             return (OptionPtr(new OptionVendor(Option::V4, begin, end)));
 
         }

+ 11 - 1
src/lib/dhcp/option_definition.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2014 Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -324,6 +324,16 @@ public:
     /// Vendor-Specific Information %Option.
     bool haveVendor6Format() const;
 
+    /// @brief Check if the option has format of DHCPv4 V-I Vendor Class option.
+    ///
+    /// @return true if the option has the format of DHCPv4 Vendor Class option.
+    bool haveVendorClass4Format() const;
+
+    /// @brief Check if the option has format of DHCPv6 Vendor Class option.
+    ///
+    /// @return true if option has the format of DHCPv6 Vendor Class option.
+    bool haveVendorClass6Format() const;
+
     /// @brief Option factory.
     ///
     /// This function creates an instance of DHCP option using

+ 191 - 0
src/lib/dhcp/option_vendor_class.cc

@@ -0,0 +1,191 @@
+// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <exceptions/exceptions.h>
+#include <dhcp/opaque_data_tuple.h>
+#include <dhcp/option_vendor_class.h>
+#include <sstream>
+
+namespace isc {
+namespace dhcp {
+
+OptionVendorClass::OptionVendorClass(Option::Universe u,
+                                     const uint32_t vendor_id)
+    : Option(u, getOptionCode(u)), vendor_id_(vendor_id) {
+    if (u == Option::V4) {
+        addTuple(OpaqueDataTuple(OpaqueDataTuple::LENGTH_1_BYTE));
+    }
+}
+
+    OptionVendorClass::OptionVendorClass(Option::Universe u,
+                                         OptionBufferConstIter begin,
+                                         OptionBufferConstIter end)
+    : Option(u, getOptionCode(u)) {
+    unpack(begin, end);
+}
+
+void
+OptionVendorClass::pack(isc::util::OutputBuffer& buf) {
+    packHeader(buf);
+
+    buf.writeUint32(getVendorId());
+
+    for (TuplesCollection::const_iterator it = tuples_.begin();
+         it != tuples_.end(); ++it) {
+        // For DHCPv4 V-I Vendor Class option, there is enterprise id before
+        // every tuple.
+        if ((getUniverse() == V4) && (it != tuples_.begin())) {
+            buf.writeUint32(getVendorId());
+        }
+        it->pack(buf);
+
+    }
+
+}
+
+void
+OptionVendorClass::unpack(OptionBufferConstIter begin,
+                          OptionBufferConstIter end) {
+    if (std::distance(begin, end) < getMinimalLength() - getHeaderLen()) {
+        isc_throw(OutOfRange, "parsed Vendor Class option data truncated to"
+                  " size " << std::distance(begin, end));
+    }
+    // Option must contain at least one enterprise id. It is ok to read 4-byte
+    // value here because we have checked that the buffer he minimal length.
+    vendor_id_ = isc::util::readUint32(&(*begin), distance(begin, end));
+    begin += sizeof(vendor_id_);
+
+    // Start reading opaque data.
+    size_t offset = 0;
+    while (offset < std::distance(begin, end)) {
+        // Parse a tuple.
+        OpaqueDataTuple tuple(getLengthFieldType(), begin + offset, end);
+        addTuple(tuple);
+        // The tuple has been parsed correctly which implies that it is safe to
+        // advance the offset by its total length.
+        offset += tuple.getTotalLength();
+        // For DHCPv4 option, there is enterprise id before every opaque data
+        // tuple. Let's read it, unless we have already reached the end of
+        // buffer.
+        if ((getUniverse() == V4) && (begin + offset != end)) {
+            // Advance the offset by the size of enterprise id.
+            offset += sizeof(vendor_id_);
+            // If the offset already ran over the buffer length or there is
+            // no space left for the empty tuple (thus we add 1), we have
+            // to signal the option truncation.
+            if (offset + 1 >= std::distance(begin, end)) {
+                isc_throw(isc::OutOfRange, "truncated DHCPv4 V-I Vendor Class"
+                          " option - it should contain enterprise id followed"
+                          " by opaque data field tuple");
+            }
+        }
+    }
+}
+
+void
+OptionVendorClass::addTuple(const OpaqueDataTuple& tuple) {
+    if (tuple.getLengthFieldType() != getLengthFieldType()) {
+        isc_throw(isc::BadValue, "attempted to add opaque data tuple having"
+                  " invalid size of the length field "
+                  << tuple.getDataFieldSize() << " to Vendor Class option");
+    }
+
+    tuples_.push_back(tuple);
+}
+
+
+void
+OptionVendorClass::setTuple(const size_t at, const OpaqueDataTuple& tuple) {
+    if (at >= getTuplesNum()) {
+        isc_throw(isc::OutOfRange, "attempted to set an opaque data for the"
+                  " vendor option at position " << at << " which is out of"
+                  " range");
+
+    } else if (tuple.getLengthFieldType() != getLengthFieldType()) {
+        isc_throw(isc::BadValue, "attempted to set opaque data tuple having"
+                  " invalid size of the length field "
+                  << tuple.getDataFieldSize() << " to Vendor Class option");
+    }
+
+    tuples_[at] = tuple;
+}
+
+OpaqueDataTuple
+OptionVendorClass::getTuple(const size_t at) const {
+    if (at >= getTuplesNum()) {
+        isc_throw(isc::OutOfRange, "attempted to get an opaque data for the"
+                  " vendor option at position " << at << " which is out of"
+                  " range. There are only " << getTuplesNum() << " tuples");
+    }
+    return (tuples_[at]);
+}
+
+bool
+OptionVendorClass::hasTuple(const std::string& tuple_str) const {
+    // Iterate over existing tuples (there shouldn't be many of them),
+    // and try to match the searched one.
+    for (TuplesCollection::const_iterator it = tuples_.begin();
+         it != tuples_.end(); ++it) {
+        if (*it == tuple_str) {
+            return (true);
+        }
+    }
+    return (false);
+}
+
+
+uint16_t
+OptionVendorClass::len() {
+    // The option starts with the header and enterprise id.
+    uint16_t length = getHeaderLen() + sizeof(uint32_t);
+    // Now iterate over existing tuples and add their size.
+    for (TuplesCollection::const_iterator it = tuples_.begin();
+         it != tuples_.end(); ++it) {
+        // For DHCPv4 V-I Vendor Class option, there is enterprise id before
+        // every tuple.
+        if ((getUniverse() == V4) && (it != tuples_.begin())) {
+            length += sizeof(uint32_t);
+        }
+        length += it->getTotalLength();
+
+    }
+
+    return (length);
+}
+
+std::string
+OptionVendorClass::toText(int indent) {
+    std::ostringstream s;
+
+    // Apply indentation
+    s << std::string(indent, ' ');
+    // Print type, length and first occurence of enterprise id.
+    s << "type=" << getType() << ", len=" << len() - getHeaderLen() << ", "
+        " enterprise id=0x" << std::hex << getVendorId() << std::dec;
+    // Iterate over all tuples and print their size and contents.
+    for (int i = 0; i < getTuplesNum(); ++i) {
+        // The DHCPv4 V-I Vendor Class has enterprise id before every tuple.
+        if ((getUniverse() == V4) && (i > 0)) {
+            s << ", enterprise id=0x" << std::hex << getVendorId() << std::dec;
+        }
+        // Print the tuple.
+        s << ", data-len" << i << "=" << getTuple(i).getLength();
+        s << ", vendor-class-data" << i << "='" << getTuple(i) << "'";
+    }
+
+    return (s.str());
+}
+
+} // namespace isc::dhcp
+} // namespace isc

+ 197 - 0
src/lib/dhcp/option_vendor_class.h

@@ -0,0 +1,197 @@
+// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef OPTION_VENDOR_CLASS_H
+#define OPTION_VENDOR_CLASS_H
+
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/opaque_data_tuple.h>
+#include <dhcp/option.h>
+#include <util/buffer.h>
+#include <boost/shared_ptr.hpp>
+#include <stdint.h>
+
+namespace isc {
+namespace dhcp {
+
+/// @brief This class encapsulates DHCPv6 Vendor Class and DHCPv4 V-I Vendor
+/// Class options.
+///
+/// The format of DHCPv6 Vendor Class option (16) is described in section 22.16
+/// of RFC3315 and the format of the DHCPv4 V-I Vendor Class option (124) is
+/// described in section 3 of RFC3925. Each of these options carries enterprise
+/// id followed by the collection of tuples carring opaque data. A single tuple
+/// consists of the field holding opaque data length and the actual data.
+/// In case of the DHCPv4 V-I Vendor Class each tuple is preceded by the
+/// 4-byte long enterprise id. Also, the field which carries the length of
+/// the tuple is 1-byte long for DHCPv4 V-I Vendor Class and 2-bytes long
+/// for the DHCPv6 Vendor Class option.
+///
+/// Whether the encapsulated format is DHCPv4 V-I Vendor Class or DHCPv6
+/// Vendor Class option is controlled by the @c u (universe) parameter passed
+/// to the constructor.
+///
+/// @todo Currently, the enterprise id field is set to a value of the first
+/// enterprise id occurrence in the parsed option. At some point we should
+/// be able to differentiate between enterprise ids.
+class OptionVendorClass : public Option {
+public:
+
+    /// @brief Collection of opaque data tuples carried by the option.
+    typedef std::vector<OpaqueDataTuple> TuplesCollection;
+
+    /// @brief Constructor.
+    ///
+    /// This constructor instance of the DHCPv4 V-I Vendor Class option (124)
+    /// or DHCPv6 Vendor Class option (16), depending on universe specified.
+    /// If the universe is v4, the constructor adds one empty tuple to the
+    /// option, as per RFC3925, section 3. the complete option must hold at
+    /// least one data-len field for opaque data. If the specified universe
+    /// is v6, the constructor adds no tuples.
+    ///
+    /// @param u universe (v4 or v6).
+    /// @param vendor_id vendor enterprise id (unique 32-bit integer).
+    OptionVendorClass(Option::Universe u, const uint32_t vendor_id);
+
+    /// @brief Constructor.
+    ///
+    /// This constructor creates an instance of the option using a buffer with
+    /// on-wire data. It may throw an exception if the @c unpack method throws.
+    ///
+    /// @param u universe (v4 or v6)
+    /// @param begin Iterator pointing to the beginning of the buffer holding an
+    /// option.
+    /// @param end Iterator pointing to the end of the buffer holding an option.
+    OptionVendorClass(Option::Universe u, OptionBufferConstIter begin,
+                      OptionBufferConstIter end);
+
+    /// @brief Renders option into the buffer in the wire format.
+    ///
+    /// @param [out] buf Buffer to which the option is rendered.
+    virtual void pack(isc::util::OutputBuffer& buf);
+
+    /// @brief Parses buffer holding an option.
+    ///
+    /// This function parses the buffer holding an option and initializes option
+    /// properties: enterprise ids and the collection of tuples.
+    ///
+    /// @param begin Iterator pointing to the beginning of the buffer holding an
+    /// option.
+    /// @param end Iterator pointing to the end of the buffer holding an option.
+    virtual void unpack(OptionBufferConstIter begin, OptionBufferConstIter end);
+
+    /// @brief Returns enterprise id.
+    uint32_t getVendorId() const {
+        return (vendor_id_);
+    }
+
+    /// @brief Adds a new opaque data tuple to the option.
+    ///
+    /// @param tuple Tuple to be added.
+    /// @throw isc::BadValue if the type of the tuple doesn't match the
+    /// universe this option belongs to.
+    void addTuple(const OpaqueDataTuple& tuple);
+
+    /// @brief Replaces tuple at the specified index with a new tuple.
+    ///
+    /// This function replaces an opaque data tuple at the specified position
+    /// with the new tuple. If the specified index is out of range an exception
+    /// is thrown.
+    ///
+    /// @param at Index at which the tuple should be replaced.
+    /// @param tuple Tuple to be set.
+    /// @throw isc::OutOfRange if the tuple position is out of range.
+    /// @throw isc::BadValue if the type of the tuple doesn't match the
+    /// universe this option belongs to.
+    void setTuple(const size_t at, const OpaqueDataTuple& tuple);
+
+    /// @brief Returns opaque data tuple at the specified position.
+    ///
+    ///  If the specified position is out of range an exception is thrown.
+    ///
+    /// @param at Index at which the tuple should be replaced.
+    /// @throw isc::OutOfRange if the tuple position is out of range.
+    OpaqueDataTuple getTuple(const size_t at) const;
+
+    /// @brief Returns the number of opaque data tuples added to the option.
+    size_t getTuplesNum() const {
+        return (tuples_.size());
+    }
+
+    /// @brief Returns collection of opaque data tuples carried in the option.
+    const TuplesCollection& getTuples() const {
+        return (tuples_);
+    }
+
+    /// @brief Checks if the Vendor Class holds the opaque data tuple with the
+    /// specified string.
+    ///
+    /// @param tuple_str String representation of the tuple being searched.
+    /// @return true if the specified tuple exists for this option.
+    bool hasTuple(const std::string& tuple_str) const;
+
+    /// @brief Returns the full length of the option, including option header.
+    virtual uint16_t len();
+
+    /// @brief Returns text representation of the option.
+    ///
+    /// @param indent Number of space characters before text.
+    /// @return Text representation of the option.
+    virtual std::string toText(int indent = 0);
+
+private:
+
+    /// @brief Returns option code appropriate for the specified universe.
+    ///
+    /// This function is called by the constructor to map the specified
+    /// universe to the option code.
+    ///
+    /// @param u universe (V4 or V6).
+    /// @return DHCPv4 V-I Vendor Class or DHCPv6 Vendor Class option code.
+    static uint16_t getOptionCode(Option::Universe u) {
+        return (u == V4 ? DHO_VIVCO_SUBOPTIONS : D6O_VENDOR_CLASS);
+    }
+
+    /// @brief Returns the tuple length field type for the given universe.
+    ///
+    /// This function returns the length field type which should be used
+    /// for the opaque data tuples being added to this option.
+    ///
+    /// @return Tuple length field type for the universe this option belongs to.
+    OpaqueDataTuple::LengthFieldType getLengthFieldType() const {
+        return (getUniverse() == V4 ? OpaqueDataTuple::LENGTH_1_BYTE :
+                OpaqueDataTuple::LENGTH_2_BYTES);
+    }
+
+    /// @brief Returns minimal length of the option for the given universe.
+    uint16_t getMinimalLength() const {
+        return (getUniverse() == Option::V4 ? 7 : 6);
+    }
+
+    /// @brief Enterprise ID.
+    uint32_t vendor_id_;
+
+    /// @brief Collection of opaque data tuples carried by the option.
+    TuplesCollection tuples_;
+
+};
+
+/// @brief Defines a pointer to the @c OptionVendorClass.
+typedef boost::shared_ptr<OptionVendorClass> OptionVendorClassPtr;
+
+}
+}
+
+#endif // OPTION_VENDOR_CLASS_H

+ 9 - 5
src/lib/dhcp/std_option_defs.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2014 Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -68,6 +68,11 @@ struct OptionDefParams {
 RECORD_DECL(FQDN_RECORDS, OPT_UINT8_TYPE, OPT_UINT8_TYPE, OPT_UINT8_TYPE,
             OPT_FQDN_TYPE);
 
+// V-I Vendor Class record fields.
+//
+// Opaque data is represented here by the binary data field.
+RECORD_DECL(VIVCO_RECORDS, OPT_UINT32_TYPE, OPT_BINARY_TYPE);
+
 /// @brief Definitions of standard DHCPv4 options.
 const OptionDefParams OPTION_DEF_PARAMS4[] = {
     { "subnet-mask", DHO_SUBNET_MASK, OPT_IPV4_ADDRESS_TYPE, false, NO_RECORD_DEF, "" },
@@ -192,8 +197,8 @@ const OptionDefParams OPTION_DEF_PARAMS4[] = {
     // dedicated classes to handle them. Until that happens
     // let's treat them as 'binary' options.
     { "domain-search", DHO_DOMAIN_SEARCH, OPT_BINARY_TYPE, false, NO_RECORD_DEF, "" },
-    { "vivco-suboptions", DHO_VIVCO_SUBOPTIONS,
-      OPT_BINARY_TYPE, false, NO_RECORD_DEF, "" },
+    { "vivco-suboptions", DHO_VIVCO_SUBOPTIONS, OPT_RECORD_TYPE,
+      false, RECORD_DEF(VIVCO_RECORDS), "" },
     { "vivso-suboptions", DHO_VIVSO_SUBOPTIONS, OPT_BINARY_TYPE,
       false, NO_RECORD_DEF, "" }
 
@@ -231,8 +236,7 @@ RECORD_DECL(REMOTE_ID_RECORDS, OPT_UINT32_TYPE, OPT_BINARY_TYPE);
 // status-code
 RECORD_DECL(STATUS_CODE_RECORDS, OPT_UINT16_TYPE, OPT_STRING_TYPE);
 // vendor-class
-RECORD_DECL(VENDOR_CLASS_RECORDS, OPT_UINT32_TYPE, OPT_UINT16_TYPE,
-            OPT_STRING_TYPE);
+RECORD_DECL(VENDOR_CLASS_RECORDS, OPT_UINT32_TYPE, OPT_BINARY_TYPE);
 
 /// Standard DHCPv6 option definitions.
 ///

+ 2 - 0
src/lib/dhcp/tests/Makefile.am

@@ -49,6 +49,7 @@ libdhcp___unittests_SOURCES += hwaddr_unittest.cc
 libdhcp___unittests_SOURCES += iface_mgr_unittest.cc
 libdhcp___unittests_SOURCES += iface_mgr_test_config.cc iface_mgr_test_config.h
 libdhcp___unittests_SOURCES += libdhcp++_unittest.cc
+libdhcp___unittests_SOURCES += opaque_data_tuple_unittest.cc
 libdhcp___unittests_SOURCES += option4_addrlst_unittest.cc
 libdhcp___unittests_SOURCES += option4_client_fqdn_unittest.cc
 libdhcp___unittests_SOURCES += option6_addrlst_unittest.cc
@@ -65,6 +66,7 @@ libdhcp___unittests_SOURCES += option_unittest.cc
 libdhcp___unittests_SOURCES += option_space_unittest.cc
 libdhcp___unittests_SOURCES += option_string_unittest.cc
 libdhcp___unittests_SOURCES += option_vendor_unittest.cc
+libdhcp___unittests_SOURCES += option_vendor_class_unittest.cc
 libdhcp___unittests_SOURCES += pkt4_unittest.cc
 libdhcp___unittests_SOURCES += pkt6_unittest.cc
 libdhcp___unittests_SOURCES += pkt_filter_unittest.cc

+ 28 - 16
src/lib/dhcp/tests/libdhcp++_unittest.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2014 Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -29,6 +29,7 @@
 #include <dhcp/option_int_array.h>
 #include <dhcp/option_string.h>
 #include <dhcp/option_vendor.h>
+#include <dhcp/option_vendor_class.h>
 #include <util/buffer.h>
 #include <util/encode/hex.h>
 
@@ -941,8 +942,14 @@ TEST_F(LibDhcpTest, stdOptionDefs4) {
     LibDhcpTest::testStdOptionDefs4(DHO_DOMAIN_SEARCH, begin, end,
                                     typeid(Option));
 
-    LibDhcpTest::testStdOptionDefs4(DHO_VIVCO_SUBOPTIONS, begin, end,
-                                    typeid(Option));
+    // V-I Vendor option requires specially crafted data.
+    const char vivco_data[] = {
+        1, 2, 3, 4, // enterprise id
+        3, 1, 2, 3  // first byte is opaque data length, the rest is opaque data
+    };
+    std::vector<uint8_t> vivco_buf(vivco_data, vivco_data + sizeof(vivco_data));
+    LibDhcpTest::testStdOptionDefs4(DHO_VIVCO_SUBOPTIONS, vivco_buf.begin(),
+                                    vivco_buf.end(), typeid(OptionVendorClass));
 
     LibDhcpTest::testStdOptionDefs4(DHO_VIVSO_SUBOPTIONS, begin, end,
                                     typeid(OptionVendor));
@@ -981,6 +988,14 @@ TEST_F(LibDhcpTest, stdOptionDefs6) {
     client_fqdn_buf.insert(client_fqdn_buf.end(), fqdn_buf.begin(),
                            fqdn_buf.end());
 
+    // Initialize test buffer for Vendor Class option.
+    const char vclass_data[] = {
+        0x00, 0x01, 0x02, 0x03,
+        0x00, 0x01, 0x02
+    };
+    std::vector<uint8_t> vclass_buf(vclass_data,
+                                    vclass_data + sizeof(vclass_data));;
+
     // The actual test starts here for all supported option codes.
     LibDhcpTest::testStdOptionDefs6(D6O_CLIENTID, begin, end,
                                     typeid(Option));
@@ -1018,8 +1033,9 @@ TEST_F(LibDhcpTest, stdOptionDefs6) {
     LibDhcpTest::testStdOptionDefs6(D6O_USER_CLASS, begin, end,
                                     typeid(Option));
 
-    LibDhcpTest::testStdOptionDefs6(D6O_VENDOR_CLASS, begin, end,
-                                    typeid(OptionCustom));
+    LibDhcpTest::testStdOptionDefs6(D6O_VENDOR_CLASS, vclass_buf.begin(),
+                                    vclass_buf.end(),
+                                    typeid(OptionVendorClass));
 
     LibDhcpTest::testStdOptionDefs6(D6O_VENDOR_OPTS, begin, end,
                                     typeid(OptionVendor),
@@ -1148,22 +1164,18 @@ TEST_F(LibDhcpTest, vendorClass6) {
     // Option vendor-class should be there
     ASSERT_FALSE(options.find(D6O_VENDOR_CLASS) == options.end());
 
-    // It should be of type OptionCustom
-    boost::shared_ptr<OptionCustom> vclass =
-        boost::dynamic_pointer_cast<OptionCustom> (options.begin()->second);
+    // It should be of type OptionVendorClass
+    boost::shared_ptr<OptionVendorClass> vclass =
+        boost::dynamic_pointer_cast<OptionVendorClass>(options.begin()->second);
     ASSERT_TRUE(vclass);
 
     // Let's investigate if the option content is correct
 
     // 3 fields expected: vendor-id, data-len and data
-    ASSERT_EQ(3, vclass->getDataFieldsNum());
-
-    EXPECT_EQ(4491, vclass->readInteger<uint32_t>
-              (VENDOR_CLASS_ENTERPRISE_ID_INDEX)); // vendor-id=4491
-    EXPECT_EQ(10, vclass->readInteger<uint16_t>
-              (VENDOR_CLASS_DATA_LEN_INDEX)); // data len = 10
-    EXPECT_EQ("eRouter1.0", vclass->readString
-              (VENDOR_CLASS_STRING_INDEX)); // data="eRouter1.0"
+    EXPECT_EQ(4491, vclass->getVendorId());
+    EXPECT_EQ(20, vclass->len());
+    ASSERT_EQ(1, vclass->getTuplesNum());
+    EXPECT_EQ("eRouter1.0", vclass->getTuple(0).getText());
 }
 
 } // end of anonymous space

+ 482 - 0
src/lib/dhcp/tests/opaque_data_tuple_unittest.cc

@@ -0,0 +1,482 @@
+// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+#include <dhcp/opaque_data_tuple.h>
+#include <util/buffer.h>
+#include <gtest/gtest.h>
+#include <algorithm>
+#include <sstream>
+#include <vector>
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::util;
+
+namespace {
+
+// This test checks that when the default constructor is called, the data buffer
+// is empty.
+TEST(OpaqueDataTuple, constructor) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // There should be no data in the tuple.
+    EXPECT_EQ(0, tuple.getLength());
+    EXPECT_TRUE(tuple.getData().empty());
+    EXPECT_TRUE(tuple.getText().empty());
+}
+
+// Test that the constructor which takes the buffer as argument parses the
+// wire data.
+TEST(OpaqueDataTuple, constructorParse1Byte) {
+    const char wire_data[] = {
+        0x0B,                               // Length is 11
+        0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, // Hello<space>
+        0x77, 0x6F, 0x72, 0x6C, 0x64        // world
+    };
+
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_1_BYTE, wire_data,
+                          wire_data + sizeof(wire_data));
+
+    EXPECT_EQ(11, tuple.getLength());
+    EXPECT_EQ("Hello world", tuple.getText());
+
+}
+
+// Test that the constructor which takes the buffer as argument parses the
+// wire data.
+TEST(OpaqueDataTuple, constructorParse2Bytes) {
+    const char wire_data[] = {
+        0x00, 0x0B,                         // Length is 11
+        0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, // Hello<space>
+        0x77, 0x6F, 0x72, 0x6C, 0x64        // world
+    };
+
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES, wire_data,
+                          wire_data + sizeof(wire_data));
+
+    EXPECT_EQ(11, tuple.getLength());
+    EXPECT_EQ("Hello world", tuple.getText());
+
+}
+
+
+// This test checks that it is possible to set the tuple data using raw buffer.
+TEST(OpaqueDataTuple, assignData) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // Initially the tuple buffer should be empty.
+    OpaqueDataTuple::Buffer buf = tuple.getData();
+    ASSERT_TRUE(buf.empty());
+    // Prepare some input data and assign to the tuple.
+    const uint8_t data1[] = {
+        0xCA, 0xFE, 0xBE, 0xEF
+    };
+    tuple.assign(data1, sizeof(data1));
+    // Tuple should now hold the data we assigned.
+    ASSERT_EQ(sizeof(data1), tuple.getLength());
+    buf = tuple.getData();
+    EXPECT_TRUE(std::equal(buf.begin(), buf.end(), data1));
+
+    // Prepare the other set of data and assign to the tuple.
+    const uint8_t data2[] = {
+        1, 2, 3, 4, 5, 6
+    };
+    tuple.assign(data2, sizeof(data2));
+    // The new data should have replaced the old data.
+    ASSERT_EQ(sizeof(data2), tuple.getLength());
+    buf = tuple.getData();
+    EXPECT_TRUE(std::equal(buf.begin(), buf.end(), data2));
+}
+
+// This test checks that it is possible to append the data to the tuple using
+// raw buffer.
+TEST(OpaqueDataTuple, appendData) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // Initially the tuple buffer should be empty.
+    OpaqueDataTuple::Buffer buf = tuple.getData();
+    ASSERT_TRUE(buf.empty());
+    // Prepare some input data and append to the empty tuple.
+    const uint8_t data1[] = {
+        0xCA, 0xFE, 0xBE, 0xEF
+    };
+    tuple.append(data1, sizeof(data1));
+    // The tuple should now hold only the data we appended.
+    ASSERT_EQ(sizeof(data1), tuple.getLength());
+    buf = tuple.getData();
+    EXPECT_TRUE(std::equal(buf.begin(), buf.end(), data1));
+    // Prepare the new set of data and append.
+    const uint8_t data2[] = {
+        1, 2, 3, 4, 5, 6
+    };
+    tuple.append(data2, sizeof(data2));
+    // We expect that the tuple now has both sets of data we appended. In order
+    // to verify that, we have to concatenate the input data1 and data2.
+    std::vector<uint8_t> data12(data1, data1 + sizeof(data1));
+    data12.insert(data12.end(), data2, data2 + sizeof(data2));
+    // The size of the tuple should be a sum of data1 and data2 lengths.
+    ASSERT_EQ(sizeof(data1) + sizeof(data2), tuple.getLength());
+    buf = tuple.getData();
+    EXPECT_TRUE(std::equal(buf.begin(), buf.end(), data12.begin()));
+}
+
+// This test checks that it is possible to assign the string to the tuple.
+TEST(OpaqueDataTuple, assignString) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // Initially, the tuple should be empty.
+    ASSERT_EQ(0, tuple.getLength());
+    // Assign some string data.
+    tuple.assign("Some string");
+    // Verify that the data has been assigned.
+    EXPECT_EQ(11, tuple.getLength());
+    EXPECT_EQ("Some string", tuple.getText());
+    // Assign some other string.
+    tuple.assign("Different string");
+    // The new string should have replaced the old string.
+    EXPECT_EQ(16, tuple.getLength());
+    EXPECT_EQ("Different string", tuple.getText());
+}
+
+// This test checks that it is possible to append the string to the tuple.
+TEST(OpaqueDataTuple, appendString) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // Initially the tuple should be empty.
+    ASSERT_EQ(0, tuple.getLength());
+    // Append the string to it.
+    tuple.append("First part");
+    ASSERT_EQ(10, tuple.getLength());
+    ASSERT_EQ("First part", tuple.getText());
+    // Now append the other string.
+    tuple.append(" and second part");
+    EXPECT_EQ(26, tuple.getLength());
+    // The resulting data in the tuple should be a concatenation of both
+    // strings.
+    EXPECT_EQ("First part and second part", tuple.getText());
+}
+
+// This test verifies that equals function correctly checks that the tuple
+// holds a given string but it doesn't hold the other. This test also
+// checks the assignment operator for the tuple.
+TEST(OpaqueDataTuple, equals) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // Tuple is supposed to be empty so it is not equal xyz.
+    EXPECT_FALSE(tuple.equals("xyz"));
+    // Assign xyz.
+    EXPECT_NO_THROW(tuple = "xyz");
+    // The tuple should be equal xyz, but not abc.
+    EXPECT_FALSE(tuple.equals("abc"));
+    EXPECT_TRUE(tuple.equals("xyz"));
+    // Assign abc to the tuple.
+    EXPECT_NO_THROW(tuple = "abc");
+    // It should be now opposite.
+    EXPECT_TRUE(tuple.equals("abc"));
+    EXPECT_FALSE(tuple.equals("xyz"));
+}
+
+// This test checks that the conversion of the data in the tuple to the string
+// is performed correctly.
+TEST(OpaqueDataTuple, getText) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // Initially the tuple should be empty.
+    ASSERT_TRUE(tuple.getText().empty());
+    // ASCII representation of 'Hello world'.
+    const char as_ascii[] = {
+        0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, // Hello<space>
+        0x77, 0x6F, 0x72, 0x6C, 0x64        // world
+    };
+    // Assign it to the tuple.
+    tuple.assign(as_ascii, sizeof(as_ascii));
+    // Conversion to string should give as the original text.
+    EXPECT_EQ("Hello world", tuple.getText());
+}
+
+// This test verifies the behavior of (in)equality and assignment operators.
+TEST(OpaqueDataTuple, operators) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // Tuple should be empty initially.
+    ASSERT_EQ(0, tuple.getLength());
+    // Check assignment.
+    EXPECT_NO_THROW(tuple = "Hello World");
+    EXPECT_EQ(11, tuple.getLength());
+    EXPECT_TRUE(tuple == "Hello World");
+    EXPECT_TRUE(tuple != "Something else");
+    // Assign something else to make sure it affects the tuple.
+    EXPECT_NO_THROW(tuple = "Something else");
+    EXPECT_EQ(14, tuple.getLength());
+    EXPECT_TRUE(tuple == "Something else");
+    EXPECT_TRUE(tuple != "Hello World");
+}
+
+// This test verifies that the tuple is inserted in the textual format to the
+// output stream.
+TEST(OpaqueDataTuple, operatorOutputStream) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // The tuple should be empty initially.
+    ASSERT_EQ(0, tuple.getLength());
+    // The tuple is empty, so assigning its content to the output stream should
+    // be no-op and result in the same text in the stream.
+    std::ostringstream s;
+    s << "Some text";
+    EXPECT_NO_THROW(s << tuple);
+    EXPECT_EQ("Some text", s.str());
+    // Now, let's assign some text to the tuple and call operator again.
+    // The new text should be added to the stream.
+    EXPECT_NO_THROW(tuple = " and some other text");
+    EXPECT_NO_THROW(s << tuple);
+    EXPECT_EQ(s.str(), "Some text and some other text");
+
+}
+
+// This test verifies that the value of the tuple can be initialized from the
+// input stream.
+TEST(OpaqueDataTuple, operatorInputStream) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // The tuple should be empty initially.
+    ASSERT_EQ(0, tuple.getLength());
+    // The input stream has some text. This text should be appended to the
+    // tuple.
+    std::istringstream s;
+    s.str("Some text");
+    EXPECT_NO_THROW(s >> tuple);
+    EXPECT_EQ("Some text", tuple.getText());
+    // Now, let's assign some other text to the stream. This new text should be
+    // assigned to the tuple.
+    s.str("And some other");
+    EXPECT_NO_THROW(s >> tuple);
+    EXPECT_EQ("And some other", tuple.getText());
+}
+
+// This test checks that the tuple is correctly encoded in the wire format when
+// the size of the length field is 1 byte.
+TEST(OpaqueDataTuple, pack1Byte) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_1_BYTE);
+    // Initially, the tuple should be empty.
+    ASSERT_EQ(0, tuple.getLength());
+    // The empty data doesn't make much sense, so the pack() should not
+    // allow it.
+    OutputBuffer out_buf(10);
+    EXPECT_THROW(tuple.pack(out_buf), OpaqueDataTupleError);
+    // Set the data for tuple.
+    std::vector<uint8_t> data;
+    for (int i = 0; i < 100; ++i) {
+        data.push_back(i);
+    }
+    tuple.assign(data.begin(), data.size());
+    // The pack should now succeed.
+    ASSERT_NO_THROW(tuple.pack(out_buf));
+    // The rendered buffer should be 101 bytes long - 1 byte for length,
+    // 100 bytes for the actual data.
+    ASSERT_EQ(101, out_buf.getLength());
+    // Get the rendered data into the vector for convenience.
+    std::vector<uint8_t>
+        render_data(static_cast<const uint8_t*>(out_buf.getData()),
+                    static_cast<const uint8_t*>(out_buf.getData()) + 101);
+    // The first byte is a length byte. It should hold the length of 100.
+    EXPECT_EQ(100, render_data[0]);
+    // Verify that the rendered data is correct.
+    EXPECT_TRUE(std::equal(render_data.begin() + 1, render_data.end(),
+                           data.begin()));
+    // Reset the output buffer for another test.
+    out_buf.clear();
+    // Fill in the tuple buffer so as it reaches maximum allowed length. The
+    // maximum length is 255 when the size of the length field is one byte.
+    for (int i = 100; i < 255; ++i) {
+        data.push_back(i);
+    }
+    ASSERT_EQ(255, data.size());
+    tuple.assign(data.begin(), data.size());
+    // The pack() should be successful again.
+    ASSERT_NO_THROW(tuple.pack(out_buf));
+    // The rendered buffer should be 256 bytes long. The first byte holds the
+    // opaque data length, the remaining bytes hold the actual data.
+    ASSERT_EQ(256, out_buf.getLength());
+    // Check that the data is correct.
+    render_data.assign(static_cast<const uint8_t*>(out_buf.getData()),
+                       static_cast<const uint8_t*>(out_buf.getData()) + 256);
+    EXPECT_EQ(255, render_data[0]);
+    EXPECT_TRUE(std::equal(render_data.begin() + 1, render_data.end(),
+                           data.begin()));
+    // Clear output buffer for another test.
+    out_buf.clear();
+    // Add one more value to the tuple. Now, the resulting buffer should exceed
+    // the maximum length. An attempt to pack() should fail.
+    data.push_back(255);
+    tuple.assign(data.begin(), data.size());
+    EXPECT_THROW(tuple.pack(out_buf), OpaqueDataTupleError);
+}
+
+// This test checks that the tuple is correctly encoded in the wire format when
+// the size of the length field is 2 bytes.
+TEST(OpaqueDataTuple, pack2Bytes) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // Initially, the tuple should be empty.
+    ASSERT_EQ(0, tuple.getLength());
+    // The empty data doesn't make much sense, so the pack() should not
+    // allow it.
+    OutputBuffer out_buf(10);
+    EXPECT_THROW(tuple.pack(out_buf), OpaqueDataTupleError);
+    // Set the data for tuple.
+    std::vector<uint8_t> data;
+    for (int i = 0; i < 512; ++i) {
+        data.push_back(i);
+    }
+    tuple.assign(data.begin(), data.size());
+    // The pack should now succeed.
+    ASSERT_NO_THROW(tuple.pack(out_buf));
+    // The rendered buffer should be 514 bytes long - 2 bytes for length,
+    // 512 bytes for the actual data.
+    ASSERT_EQ(514, out_buf.getLength());
+    // Get the rendered data into the vector for convenience.
+    std::vector<uint8_t>
+        render_data(static_cast<const uint8_t*>(out_buf.getData()),
+                    static_cast<const uint8_t*>(out_buf.getData()) + 514);
+    // The first two bytes hold the length of 512.
+    uint16_t len = (render_data[0] << 8) + render_data[1];
+    EXPECT_EQ(512, len);
+    // Verify that the rendered data is correct.
+    EXPECT_TRUE(std::equal(render_data.begin() + 2, render_data.end(),
+                           data.begin()));
+
+    // Without clearing the output buffer, try to do it again. The pack should
+    // append the data to the current buffer.
+    ASSERT_NO_THROW(tuple.pack(out_buf));
+    EXPECT_EQ(1028, out_buf.getLength());
+
+    // Check that we can render the buffer of the maximal allowed size.
+    data.assign(65535, 1);
+    ASSERT_NO_THROW(tuple.assign(data.begin(), data.size()));
+    ASSERT_NO_THROW(tuple.pack(out_buf));
+
+    out_buf.clear();
+
+    // Append one additional byte. The total length of the tuple now exceeds
+    // the maximal value. An attempt to render it should throw an exception.
+    data.assign(1, 1);
+    ASSERT_NO_THROW(tuple.append(data.begin(), data.size()));
+    EXPECT_THROW(tuple.pack(out_buf), OpaqueDataTupleError);
+}
+
+// This test verifies that the tuple is decoded from the wire format.
+TEST(OpaqueDataTuple, unpack1Byte) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_1_BYTE);
+    const char wire_data[] = {
+        0x0B,                               // Length is 11
+        0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, // Hello<space>
+        0x77, 0x6F, 0x72, 0x6C, 0x64        // world
+    };
+
+    ASSERT_NO_THROW(tuple.unpack(wire_data, wire_data + sizeof(wire_data)));
+    EXPECT_EQ(11, tuple.getLength());
+    EXPECT_EQ("Hello world", tuple.getText());
+}
+
+// This test verifies that the tuple having a length of 0, is decoded from
+// the wire format.
+TEST(OpaqueDataTuple, unpack1ByteZeroLength) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_1_BYTE);
+    EXPECT_NO_THROW(tuple = "Hello world");
+    ASSERT_NE(tuple.getLength(), 0);
+
+    const char wire_data[] = {
+        0
+    };
+    ASSERT_NO_THROW(tuple.unpack(wire_data, wire_data + sizeof(wire_data)));
+
+    EXPECT_EQ(0, tuple.getLength());
+}
+
+// This test verfifies that exception is thrown if the empty buffer is being
+// parsed.
+TEST(OpaqueDataTuple, unpack1ByteEmptyBuffer) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_1_BYTE);
+    const char wire_data[] = {
+        1, 2, 3
+    };
+    EXPECT_THROW(tuple.unpack(wire_data, wire_data), OpaqueDataTupleError);
+}
+
+// This test verifies that exception is thrown when parsing truncated buffer.
+TEST(OpaqueDataTuple, unpack1ByteTruncatedBuffer) {
+   OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_1_BYTE);
+    const char wire_data[] = {
+        10, 2, 3
+    };
+    EXPECT_THROW(tuple.unpack(wire_data, wire_data + sizeof(wire_data)),
+                 OpaqueDataTupleError);
+}
+
+// This test verifies that the tuple is decoded from the wire format.
+TEST(OpaqueDataTuple, unpack2Byte) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    std::vector<uint8_t> wire_data;
+    // Set tuple length to 400 (0x190).
+    wire_data.push_back(1);
+    wire_data.push_back(0x90);
+    // Fill in the buffer with some data.
+    for (int i = 0; i < 400; ++i) {
+        wire_data.push_back(i);
+    }
+    // The unpack shoud succeed.
+    ASSERT_NO_THROW(tuple.unpack(wire_data.begin(), wire_data.end()));
+    // The decoded length should be 400.
+    ASSERT_EQ(400, tuple.getLength());
+    // And the data should match.
+    EXPECT_TRUE(std::equal(wire_data.begin() + 2, wire_data.end(),
+                           tuple.getData().begin()));
+}
+
+// This test verifies that the tuple having a length of 0, is decoded from
+// the wire format.
+TEST(OpaqueDataTuple, unpack2ByteZeroLength) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    // Set some data for the tuple.
+    EXPECT_NO_THROW(tuple = "Hello world");
+    ASSERT_NE(tuple.getLength(), 0);
+    // The buffer holds just a length field with the value of 0.
+    const char wire_data[] = {
+        0, 0
+    };
+    // The empty tuple should be successfully decoded.
+    ASSERT_NO_THROW(tuple.unpack(wire_data, wire_data + sizeof(wire_data)));
+    // The data should be replaced with an empty buffer.
+    EXPECT_EQ(0, tuple.getLength());
+}
+
+// This test verifies that exception is thrown if the empty buffer is being
+// parsed.
+TEST(OpaqueDataTuple, unpack2ByteEmptyBuffer) {
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    //  Initialize the input buffer with some data, just to avoid initializing
+    // empty array.
+    const char wire_data[] = {
+        1, 2, 3
+    };
+    // Pass empty buffer (first iterator equal to second iterator).
+    // This should not be accepted.
+    EXPECT_THROW(tuple.unpack(wire_data, wire_data), OpaqueDataTupleError);
+}
+
+// This test verifies that exception if thrown when parsing truncated buffer.
+TEST(OpaqueDataTuple, unpack2ByteTruncatedBuffer) {
+   OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+   // Specify the data with the length of 10, but limit the buffer size to
+   // 2 bytes.
+   const char wire_data[] = {
+       0, 10, 2, 3
+   };
+   // This should fail because the buffer is truncated.
+   EXPECT_THROW(tuple.unpack(wire_data, wire_data + sizeof(wire_data)),
+                OpaqueDataTupleError);
+}
+
+
+} // anonymous namespace

+ 456 - 0
src/lib/dhcp/tests/option_vendor_class_unittest.cc

@@ -0,0 +1,456 @@
+// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+
+#include <exceptions/exceptions.h>
+#include <dhcp/option_vendor_class.h>
+#include <util/buffer.h>
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::util;
+
+namespace {
+
+// This test checks that the DHCPv4 option constructor sets the default
+// properties to the expected values. This constructor should add an
+// empty opaque data tuple (it is essentially the same as adding a 1-byte
+// long field which carries a value of 0).
+TEST(OptionVendorClass, constructor4) {
+    OptionVendorClass vendor_class(Option::V4, 1234);
+    EXPECT_EQ(1234, vendor_class.getVendorId());
+    // Option length is 1 byte for header + 1 byte for option size +
+    // 4 bytes of enterprise id + 1 byte for opaque data.
+    EXPECT_EQ(7, vendor_class.len());
+    // There should be one empty tuple.
+    ASSERT_EQ(1, vendor_class.getTuplesNum());
+    EXPECT_EQ(0, vendor_class.getTuple(0).getLength());
+}
+
+// This test checks that the DHCPv6 option constructor sets the default
+// properties to the expected values.
+TEST(OptionVendorClass, constructor6) {
+    OptionVendorClass vendor_class(Option::V6, 2345);
+    EXPECT_EQ(2345, vendor_class.getVendorId());
+    // Option length is 2 bytes for option code + 2 bytes for option size +
+    // 4 bytes of enterprise id.
+    EXPECT_EQ(8, vendor_class.len());
+    // There should be no tuples.
+    EXPECT_EQ(0, vendor_class.getTuplesNum());
+}
+
+// This test verifies that it is possible to append the opaque data tuple
+// to the option and then retrieve it.
+TEST(OptionVendorClass, addTuple) {
+    OptionVendorClass vendor_class(Option::V6, 2345);
+    // Initially there should be no tuples (for DHCPv6).
+    ASSERT_EQ(0, vendor_class.getTuplesNum());
+    // Create a new tuple and add it to the option.
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    tuple = "xyz";
+    vendor_class.addTuple(tuple);
+    // The option should now hold one tuple.
+    ASSERT_EQ(1, vendor_class.getTuplesNum());
+    EXPECT_EQ("xyz", vendor_class.getTuple(0).getText());
+    // Add another tuple.
+    tuple = "abc";
+    vendor_class.addTuple(tuple);
+    // The option should now hold exactly two tuples in the order in which
+    // they were added.
+    ASSERT_EQ(2, vendor_class.getTuplesNum());
+    EXPECT_EQ("xyz", vendor_class.getTuple(0).getText());
+    EXPECT_EQ("abc", vendor_class.getTuple(1).getText());
+
+    // Check that hasTuple correctly identifies existing tuples.
+    EXPECT_TRUE(vendor_class.hasTuple("xyz"));
+    EXPECT_TRUE(vendor_class.hasTuple("abc"));
+    EXPECT_FALSE(vendor_class.hasTuple("other"));
+
+    // Attempt to add the tuple with 1 byte long length field should fail
+    // for DHCPv6 option.
+    OpaqueDataTuple tuple2(OpaqueDataTuple::LENGTH_1_BYTE);
+    EXPECT_THROW(vendor_class.addTuple(tuple2), isc::BadValue);
+}
+
+// This test checks that it is possible to replace existing tuple.
+TEST(OptionVendorClass, setTuple) {
+    OptionVendorClass vendor_class(Option::V4, 1234);
+    // The DHCPv4 option should carry one empty tuple.
+    ASSERT_EQ(1, vendor_class.getTuplesNum());
+    ASSERT_TRUE(vendor_class.getTuple(0).getText().empty());
+    // Replace the empty tuple with non-empty one.
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_1_BYTE);
+    tuple = "xyz";
+    ASSERT_NO_THROW(vendor_class.setTuple(0, tuple));
+    // There should be one tuple with updated data.
+    ASSERT_EQ(1, vendor_class.getTuplesNum());
+    EXPECT_EQ("xyz", vendor_class.getTuple(0).getText());
+
+    // Add another one.
+    tuple = "abc";
+    vendor_class.addTuple(tuple);
+    ASSERT_EQ(2, vendor_class.getTuplesNum());
+    ASSERT_EQ("abc", vendor_class.getTuple(1).getText());
+
+    // Try to replace them with new tuples.
+    tuple = "new_xyz";
+    ASSERT_NO_THROW(vendor_class.setTuple(0, tuple));
+    ASSERT_EQ(2, vendor_class.getTuplesNum());
+    EXPECT_EQ("new_xyz", vendor_class.getTuple(0).getText());
+
+    tuple = "new_abc";
+    ASSERT_NO_THROW(vendor_class.setTuple(1, tuple));
+    ASSERT_EQ(2, vendor_class.getTuplesNum());
+    EXPECT_EQ("new_abc", vendor_class.getTuple(1).getText());
+
+    // For out of range position, exception should be thrown.
+    tuple = "foo";
+    EXPECT_THROW(vendor_class.setTuple(2, tuple), isc::OutOfRange);
+
+    // Attempt to add the tuple with 2 byte long length field should fail
+    // for DHCPv4 option.
+    OpaqueDataTuple tuple2(OpaqueDataTuple::LENGTH_2_BYTES);
+    EXPECT_THROW(vendor_class.addTuple(tuple2), isc::BadValue);
+}
+
+// Check that the returned length of the DHCPv4 option is correct.
+TEST(OptionVendorClass, len4) {
+    OptionVendorClass vendor_class(Option::V4, 1234);
+    ASSERT_EQ(7, vendor_class.len());
+    // Replace the default empty tuple.
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_1_BYTE);
+    tuple = "xyz";
+    ASSERT_NO_THROW(vendor_class.setTuple(0, tuple));
+    // The total length should get increased by the size of 'xyz'.
+    EXPECT_EQ(10, vendor_class.len());
+    // Add another tuple.
+    tuple = "abc";
+    vendor_class.addTuple(tuple);
+    // The total size now grows by the additional enterprise id and the
+    // 1 byte of the tuple length field and 3 bytes of 'abc'.
+    EXPECT_EQ(18, vendor_class.len());
+}
+
+// Check that the returned length of the DHCPv6 option is correct.
+TEST(OptionVendorClass, len6) {
+    OptionVendorClass vendor_class(Option::V6, 1234);
+    ASSERT_EQ(8, vendor_class.len());
+    // Add first tuple.
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    tuple = "xyz";
+    ASSERT_NO_THROW(vendor_class.addTuple(tuple));
+    // The total length grows by 2 bytes of the length field and 3 bytes
+    // consumed by 'xyz'.
+    EXPECT_EQ(13, vendor_class.len());
+    // Add another tuple and check that the total size gets increased.
+    tuple = "abc";
+    vendor_class.addTuple(tuple);
+    EXPECT_EQ(18, vendor_class.len());
+}
+
+// Check that the option is rendered to the buffer in wire format.
+TEST(OptionVendorClass, pack4) {
+    OptionVendorClass vendor_class(Option::V4, 1234);
+    ASSERT_EQ(1, vendor_class.getTuplesNum());
+    // By default, there is an empty tuple in the option. Let's replace
+    // it with the tuple with some data.
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_1_BYTE);
+    tuple = "Hello world";
+    vendor_class.setTuple(0, tuple);
+    // And add another tuple so as resulting option is a bit more complex.
+    tuple = "foo";
+    vendor_class.addTuple(tuple);
+
+    // Render the data to the buffer.
+    OutputBuffer buf(10);
+    ASSERT_NO_THROW(vendor_class.pack(buf));
+    ASSERT_EQ(26, buf.getLength());
+
+    // Prepare reference data.
+    const uint8_t ref[] = {
+        0x7C, 0x18,                         // option 124, length 24
+        0, 0, 0x4, 0xD2,                    // enterprise id 1234
+        0x0B,                               // tuple length is 11
+        0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, // Hello<space>
+        0x77, 0x6F, 0x72, 0x6C, 0x64,       // world
+        0, 0, 0x4, 0xD2,                    // enterprise id 1234
+        3,                                  // tuple length is 3
+        0x66, 0x6F, 0x6F                    // foo
+    };
+    // Compare the buffer with reference data.
+    EXPECT_EQ(0, memcmp(static_cast<const void*>(ref),
+                        static_cast<const void*>(buf.getData()), 26));
+}
+
+// Check that the DHCPv6 option is rendered to the buffer in wire format.
+TEST(OptionVendorClass, pack6) {
+    OptionVendorClass vendor_class(Option::V6, 1234);
+    ASSERT_EQ(0, vendor_class.getTuplesNum());
+    // Add tuple.
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    tuple = "Hello world";
+    vendor_class.addTuple(tuple);
+    // And add another tuple so as resulting option is a bit more complex.
+    tuple = "foo";
+    vendor_class.addTuple(tuple);
+
+    // Render the data to the buffer.
+    OutputBuffer buf(10);
+    ASSERT_NO_THROW(vendor_class.pack(buf));
+    ASSERT_EQ(26, buf.getLength());
+
+    // Prepare reference data.
+    const uint8_t ref[] = {
+        0x00, 0x10, 0x00, 0x16,             // option 16, length 22
+        0x00, 0x00, 0x04, 0xD2,             // enterprise id 1234
+        0x00, 0x0B,                         // tuple length is 11
+        0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, // Hello<space>
+        0x77, 0x6F, 0x72, 0x6C, 0x64,       // world
+        0x00, 0x03,                         // tuple length is 3
+        0x66, 0x6F, 0x6F                    // foo
+    };
+    // Compare the buffer with reference data.
+    EXPECT_EQ(0, memcmp(static_cast<const void*>(ref),
+                        static_cast<const void*>(buf.getData()),
+                        buf.getLength()));
+}
+
+// This function checks that the DHCPv4 option with two opaque data tuples
+// is parsed correctly.
+TEST(OptionVendorClass, unpack4) {
+    // Prepare data to decode.
+    const uint8_t buf_data[] = {
+        0, 0, 0x4, 0xD2,                    // enterprise id 1234
+        0x0B,                               // tuple length is 11
+        0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, // Hello<space>
+        0x77, 0x6F, 0x72, 0x6C, 0x64,       // world
+        0, 0, 0x4, 0xD2,                    // enterprise id 1234
+        3,                                  // tuple length is 3
+        0x66, 0x6F, 0x6F                    // foo
+    };
+    OptionBuffer buf(buf_data, buf_data + sizeof(buf_data));
+
+    OptionVendorClassPtr vendor_class;
+    ASSERT_NO_THROW(
+        vendor_class = OptionVendorClassPtr(new OptionVendorClass(Option::V4,
+                                                                  buf.begin(),
+                                                                  buf.end()));
+    );
+    EXPECT_EQ(DHO_VIVCO_SUBOPTIONS, vendor_class->getType());
+    EXPECT_EQ(1234, vendor_class->getVendorId());
+    ASSERT_EQ(2, vendor_class->getTuplesNum());
+    EXPECT_EQ("Hello world", vendor_class->getTuple(0).getText());
+    EXPECT_EQ("foo", vendor_class->getTuple(1).getText());
+}
+
+// This function checks that the DHCPv4 option with two opaque data tuples
+// is parsed correctly.
+TEST(OptionVendorClass, unpack6) {
+    // Prepare data to decode.
+    const uint8_t buf_data[] = {
+        0, 0, 0x4, 0xD2,                    // enterprise id 1234
+        0x00, 0x0B,                         // tuple length is 11
+        0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, // Hello<space>
+        0x77, 0x6F, 0x72, 0x6C, 0x64,       // world
+        0x00, 0x03,                         // tuple length is 3
+        0x66, 0x6F, 0x6F                    // foo
+    };
+    OptionBuffer buf(buf_data, buf_data + sizeof(buf_data));
+
+    OptionVendorClassPtr vendor_class;
+    ASSERT_NO_THROW(
+        vendor_class = OptionVendorClassPtr(new OptionVendorClass(Option::V6,
+                                                                  buf.begin(),
+                                                                  buf.end()));
+    );
+    EXPECT_EQ(D6O_VENDOR_CLASS, vendor_class->getType());
+    EXPECT_EQ(1234, vendor_class->getVendorId());
+    ASSERT_EQ(2, vendor_class->getTuplesNum());
+    EXPECT_EQ("Hello world", vendor_class->getTuple(0).getText());
+    EXPECT_EQ("foo", vendor_class->getTuple(1).getText());
+}
+
+
+// This test checks that the DHCPv4 option with opaque data of size 0
+// is correctly parsed.
+TEST(OptionVendorClass, unpack4EmptyTuple) {
+    // Prepare data to decode.
+    const uint8_t buf_data[] = {
+        0, 0, 0x4, 0xD2,                    // enterprise id 1234
+        0x00,                               // tuple length is 0
+    };
+    OptionBuffer buf(buf_data, buf_data + sizeof(buf_data));
+
+    OptionVendorClassPtr vendor_class;
+    ASSERT_NO_THROW(
+        vendor_class = OptionVendorClassPtr(new OptionVendorClass(Option::V4,
+                                                                  buf.begin(),
+                                                                  buf.end()));
+    );
+    EXPECT_EQ(DHO_VIVCO_SUBOPTIONS, vendor_class->getType());
+    EXPECT_EQ(1234, vendor_class->getVendorId());
+    ASSERT_EQ(1, vendor_class->getTuplesNum());
+    EXPECT_TRUE(vendor_class->getTuple(0).getText().empty());
+}
+
+// This test checks that the DHCPv6 option with opaque data of size 0
+// is correctly parsed.
+TEST(OptionVendorClass, unpack6EmptyTuple) {
+    // Prepare data to decode.
+    const uint8_t buf_data[] = {
+        0, 0, 0x4, 0xD2,  // enterprise id 1234
+        0x00, 0x00        // tuple length is 0
+    };
+    OptionBuffer buf(buf_data, buf_data + sizeof(buf_data));
+
+    OptionVendorClassPtr vendor_class;
+    ASSERT_NO_THROW(
+        vendor_class = OptionVendorClassPtr(new OptionVendorClass(Option::V6,
+                                                                  buf.begin(),
+                                                                  buf.end()));
+    );
+    EXPECT_EQ(D6O_VENDOR_CLASS, vendor_class->getType());
+    EXPECT_EQ(1234, vendor_class->getVendorId());
+    ASSERT_EQ(1, vendor_class->getTuplesNum());
+    EXPECT_TRUE(vendor_class->getTuple(0).getText().empty());
+}
+
+// This test checks that exception is thrown when parsing truncated DHCPv4
+// V-I Vendor Class option.
+TEST(OptionVendorClass, unpack4Truncated) {
+    // Prepare data to decode.
+    const uint8_t buf_data[] = {
+        0, 0, 0x4, 0xD2,                    // enterprise id 1234
+        0x0B,                               // tuple length is 11
+        0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, // Hello<space>
+        0x77, 0x6F, 0x72, 0x6C, 0x64,       // world
+        0, 0, 0x4, 0xD2,                    // enterprise id 1234
+    };
+    OptionBuffer buf(buf_data, buf_data + sizeof(buf_data));
+
+    EXPECT_THROW(OptionVendorClass (Option::V4, buf.begin(), buf.end()),
+                 isc::OutOfRange);
+}
+
+// This test checks that exception is thrown when parsing truncated DHCPv6
+// Vendor Class option.
+TEST(OptionVendorClass, unpack6Truncated) {
+    // Prepare data to decode.
+    const uint8_t buf_data[] = {
+        0, 0, 0x4, 0xD2,                    // enterprise id 1234
+        0x00, 0x0B,                         // tuple length is 11
+        0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, // Hello<space>
+        0x77, 0x6F, 0x72, 0x6C              // worl (truncated d!)
+    };
+    OptionBuffer buf(buf_data, buf_data + sizeof(buf_data));
+
+    EXPECT_THROW(OptionVendorClass (Option::V6, buf.begin(), buf.end()),
+                 isc::dhcp::OpaqueDataTupleError);
+}
+
+// This test checks that exception is thrown when parsing DHCPv4 V-I Vendor
+// Class option which doesn't have opaque data length. This test is different
+// from the corresponding test for v6 in that, the v4 test expects that
+// exception is thrown when parsing DHCPv4 option without data-len field
+// (has no tuples), whereas for DHCPv6 option it is perfectly ok that
+// option has no tuples (see class constructor).
+TEST(OptionVendorClass, unpack4NoTuple) {
+    // Prepare data to decode.
+    const uint8_t buf_data[] = {
+        0, 0, 0x4, 0xD2  // enterprise id 1234
+    };
+    OptionBuffer buf(buf_data, buf_data + sizeof(buf_data));
+
+    ASSERT_THROW(OptionVendorClass (Option::V4, buf.begin(), buf.end()),
+                 isc::OutOfRange);
+}
+
+// This test checks that the DHCPv6 Vendor Class option containing no opaque
+// data is parsed correctly. This test is different from the corresponding
+// test for v4 in that, the v6 test checks that the option parsing succeeds
+// when option has no opaque data tuples, whereas the v4 test expects that
+// parsing fails for DHCPv4 option which doesn't have opaque-data (see
+// class constructor).
+TEST(OptionVendorClass, unpack6NoTuple) {
+    // Prepare data to decode.
+    const uint8_t buf_data[] = {
+        0, 0, 0x4, 0xD2  // enterprise id 1234
+    };
+    OptionBuffer buf(buf_data, buf_data + sizeof(buf_data));
+
+    OptionVendorClassPtr vendor_class;
+    ASSERT_NO_THROW(
+        vendor_class = OptionVendorClassPtr(new OptionVendorClass(Option::V6,
+                                                                  buf.begin(),
+                                                                  buf.end()));
+    );
+    EXPECT_EQ(D6O_VENDOR_CLASS, vendor_class->getType());
+    EXPECT_EQ(1234, vendor_class->getVendorId());
+    EXPECT_EQ(0, vendor_class->getTuplesNum());
+}
+
+// Verifies correctness of the text representation of the DHCPv4 option.
+TEST(OptionVendorClass, toText4) {
+    OptionVendorClass vendor_class(Option::V4, 1234);
+    ASSERT_EQ(1, vendor_class.getTuplesNum());
+    // By default, there is an empty tuple in the option. Let's replace
+    // it with the tuple with some data.
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_1_BYTE);
+    tuple = "Hello world";
+    vendor_class.setTuple(0, tuple);
+    // And add another tuple so as resulting option is a bit more complex.
+    tuple = "foo";
+    vendor_class.addTuple(tuple);
+    // Check that the text representation of the option is as expected.
+    EXPECT_EQ("type=124, len=24,  enterprise id=0x4d2,"
+              " data-len0=11, vendor-class-data0='Hello world',"
+              " enterprise id=0x4d2, data-len1=3, vendor-class-data1='foo'",
+              vendor_class.toText());
+
+    // Check that indentation works.
+    EXPECT_EQ("   type=124, len=24,  enterprise id=0x4d2,"
+              " data-len0=11, vendor-class-data0='Hello world',"
+              " enterprise id=0x4d2, data-len1=3, vendor-class-data1='foo'",
+              vendor_class.toText(3));
+}
+
+// Verifies correctness of the text representation of the DHCPv4 option.
+TEST(OptionVendorClass, toText6) {
+    OptionVendorClass vendor_class(Option::V6, 1234);
+    ASSERT_EQ(0, vendor_class.getTuplesNum());
+    // By default, there is an empty tuple in the option. Let's replace
+    // it with the tuple with some data.
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    tuple = "Hello world";
+    vendor_class.addTuple(tuple);
+    // And add another tuple so as resulting option is a bit more complex.
+    tuple = "foo";
+    vendor_class.addTuple(tuple);
+    // Check that the text representation of the option is as expected.
+    EXPECT_EQ("type=16, len=22,  enterprise id=0x4d2,"
+              " data-len0=11, vendor-class-data0='Hello world',"
+              " data-len1=3, vendor-class-data1='foo'",
+              vendor_class.toText());
+
+    // Check that indentation works.
+    EXPECT_EQ("  type=16, len=22,  enterprise id=0x4d2,"
+              " data-len0=11, vendor-class-data0='Hello world',"
+              " data-len1=3, vendor-class-data1='foo'",
+              vendor_class.toText(2));
+}
+
+} // end of anonymous namespace
+