Commit acc4ba20 authored by Tomek Mrugalski's avatar Tomek Mrugalski 🛰
Browse files

[master] Merge branch 'trac3203' (DHCP client classification, part 1)

Conflicts:
	src/bin/dhcp4/dhcp4_srv.cc
	src/bin/dhcp4/tests/dhcp4_test_utils.h
	src/bin/dhcp6/dhcp6_srv.cc
	src/lib/dhcp/pkt4.cc
	src/lib/dhcp/tests/pkt4_unittest.cc
parents c27219ff afea612c
732. [func] tomek
b10-dhcp4, b10-dhcp6: Support for simplified client classification
added. Incoming packets are now assigned to a client class based on
the content of the packet's user class option (DHCPv4) or vendor class
option (DHCPv6). Two classes (docsis3.0 and eRouter1.0) have class
specific behavior in b10-dhcp4. See DHCPv4 Client Classification and
DHCPv6 Client Classification in BIND10 Developer's Guide for details.
This is a first ticket in a series of planned at least three tickets.
(Trac #3203, git afea612c23143f81a4201e39ba793bc837c5c9f1)
731. [func] tmark
b10-dhcp4 now parses parameters which support DHCP-DDNS updates via
the DHCP-DDNS module, b10-dhcp-ddns. These parameters are part of new
......
......@@ -60,6 +60,7 @@
* - @subpage dhcpv4ConfigInherit
* - @subpage dhcpv4OptionsParse
* - @subpage dhcpv4DDNSIntegration
* - @subpage dhcpv4Classifier
* - @subpage dhcpv4Other
* - @subpage dhcp6
* - @subpage dhcpv6Session
......@@ -67,6 +68,7 @@
* - @subpage dhcpv6ConfigInherit
* - @subpage dhcpv6DDNSIntegration
* - @subpage dhcpv6OptionsParse
* - @subpage dhcpv6Classifier
* - @subpage dhcpv6Other
* - @subpage libdhcp
* - @subpage libdhcpIntro
......
......@@ -107,7 +107,7 @@ protected:
/// various configuration values. Installing the dummy handler
/// that guarantees to return success causes initial configuration
/// to be stored for the session being created and that it can
/// be later accessed with \ref isc::ConfigData::getFullConfig.
/// be later accessed with \ref isc::config::ConfigData::getFullConfig.
///
/// @param new_config new configuration.
///
......
......@@ -155,6 +155,36 @@ The default behaviour is constituted by the set of constants defined in the
(upper part of) dhcp4_srv.cc file. Once the configuration is implemented,
these constants will be removed.
@section dhcpv4Classifier DHCPv4 Client Classification
Kea DHCPv4 server currently supports simplified client classification. It is called
"simplified", because the incoming packets are classified based on the content
of the vendor class (60) option. More flexible classification is planned, but there
are no specific development dates agreed.
For each incoming packet, @ref isc::dhcp::Dhcpv4Srv::classifyPacket() method is called.
It attempts to extract content of the vendor class option and interpret as a name
of the class. For now, the code has been tested with two classes used in cable modem
networks: eRouter1.0 and docsis3.0, but any other content of the vendor class option will
be interpreted as a class name.
In principle any given packet can belong to zero or more classes. As the current
classifier is very modest, there's only one way to assign a class (based on vendor class
option), the ability to assign more than one class to a packet is not yet exercised.
Neverthless, there is such a possibility and it will be used in a near future. To
check whether a packet belongs to given class, isc::dhcp::Pkt4::inClass method should
be used.
Currently there is a short code section that alternates packet processing depending on
which class it belongs to. It is planned to move that capability to an external hook
library. See ticket #3275. The class specific behavior is:
- docsis3.0 packets have siaddr (next server) field set
- docsis3.0 packets have file field set to the content of the boot-file-name option
- eRouter1.0 packets have siaddr (next server) field cleared
Aforementioned modifications are conducted in @ref isc::dhcp::Dhcpv4Srv::classSpecificProcessing.
@section dhcpv4Other Other DHCPv4 topics
For hooks API support in DHCPv4, see @ref dhcpv4Hooks.
......
......@@ -32,6 +32,14 @@ This debug message is issued when the DHCP server was unable to process the
FQDN or Hostname option sent by a client. This is likely because the client's
name was malformed or due to internal server error.
% DHCP4_CLASS_PROCESSING_FAILED client class specific processing failed
This debug message means that the server processing that is unique for each
client class has reported a failure. The response packet will not be sent.
% DHCP4_CLASS_ASSIGNED client packet has been assigned to the following class(es): %1
This debug message informs that incoming packet has been assigned to specified
class or classes. This is a norma
% DHCP4_COMMAND_RECEIVED received command %1, arguments: %2
A debug message listing the command (and possible arguments) received
from the BIND 10 control system by the IPv4 DHCP server.
......
......@@ -21,6 +21,7 @@
#include <dhcp/option_int.h>
#include <dhcp/option_int_array.h>
#include <dhcp/option_vendor.h>
#include <dhcp/option_string.h>
#include <dhcp/pkt4.h>
#include <dhcp/docsis3_option_defs.h>
#include <dhcp4/dhcp4_log.h>
......@@ -312,6 +313,9 @@ Dhcpv4Srv::run() {
callout_handle->getArgument("query4", query);
}
// Assign this packet to one or more classes if needed
classifyPacket(query);
try {
switch (query->getType()) {
case DHCPDISCOVER:
......@@ -368,6 +372,18 @@ Dhcpv4Srv::run() {
continue;
}
// Let's do class specific processing. This is done before
// pkt4_send.
//
/// @todo: decide whether we want to add a new hook point for
/// doing class specific processing.
if (!classSpecificProcessing(query, rsp)) {
/// @todo add more verbosity here
LOG_DEBUG(dhcp4_logger, DBG_DHCP4_BASIC, DHCP4_CLASS_PROCESSING_FAILED);
continue;
}
// Specifies if server should do the packing
bool skip_pack = false;
......@@ -1766,5 +1782,86 @@ Dhcpv4Srv::ifaceMgrSocket4ErrorHandler(const std::string& errmsg) {
LOG_WARN(dhcp4_logger, DHCP4_OPEN_SOCKET_FAIL).arg(errmsg);
}
void Dhcpv4Srv::classifyPacket(const Pkt4Ptr& pkt) {
boost::shared_ptr<OptionString> vendor_class =
boost::dynamic_pointer_cast<OptionString>(pkt->getOption(DHO_VENDOR_CLASS_IDENTIFIER));
string classes = "";
if (!vendor_class) {
return;
}
// DOCSIS specific section
// Let's keep this as a series of checks. So far we're supporting only
// docsis3.0, but there are also docsis2.0, docsis1.1 and docsis1.0. We
// may come up with adding several classes, e.g. for docsis2.0 we would
// add classes docsis2.0, docsis1.1 and docsis1.0.
// Also we are using find, because we have at least one traffic capture
// where the user class was followed by a space ("docsis3.0 ").
// For now, the code is very simple, but it is expected to get much more
// complex soon. One specific case is that the vendor class is an option
// sent by the client, so we should not trust it. To confirm that the device
// is indeed a modem, John B. suggested to check whether chaddr field
// 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) + " ";
} else
if (vendor_class->getValue().find(DOCSIS3_CLASS_EROUTER) != std::string::npos) {
pkt->addClass(DOCSIS3_CLASS_EROUTER);
classes += string(DOCSIS3_CLASS_EROUTER) + " ";
} else {
classes += vendor_class->getValue();
pkt->addClass(vendor_class->getValue());
}
if (!classes.empty()) {
LOG_DEBUG(dhcp4_logger, DBG_DHCP4_BASIC, DHCP4_CLASS_ASSIGNED)
.arg(classes);
}
}
bool Dhcpv4Srv::classSpecificProcessing(const Pkt4Ptr& query, const Pkt4Ptr& rsp) {
Subnet4Ptr subnet = selectSubnet(query);
if (!subnet) {
return (true);
}
if (query->inClass(DOCSIS3_CLASS_MODEM)) {
// Set next-server. This is TFTP server address. Cable modems will
// download their configuration from that server.
rsp->setSiaddr(subnet->getSiaddr());
// Now try to set up file field in DHCPv4 packet. We will just copy
// content of the boot-file option, which contains the same information.
Subnet::OptionDescriptor desc =
subnet->getOptionDescriptor("dhcp4", DHO_BOOT_FILE_NAME);
if (desc.option) {
boost::shared_ptr<OptionString> boot =
boost::dynamic_pointer_cast<OptionString>(desc.option);
if (boot) {
std::string filename = boot->getValue();
rsp->setFile((const uint8_t*)filename.c_str(), filename.size());
}
}
}
if (query->inClass(DOCSIS3_CLASS_EROUTER)) {
// Do not set TFTP server address for eRouter devices.
rsp->setSiaddr(IOAddress("0.0.0.0"));
}
return (true);
}
} // namespace dhcp
} // namespace isc
......@@ -262,7 +262,7 @@ protected:
/// using separate options within their respective vendor-option spaces.
///
/// @param question DISCOVER or REQUEST message from a client.
/// @param msg outgoing message (options will be added here)
/// @param answer outgoing message (options will be added here)
void appendRequestedVendorOptions(const Pkt4Ptr& question, Pkt4Ptr& answer);
/// @brief Assigns a lease and appends corresponding options
......@@ -559,6 +559,25 @@ protected:
const std::string& option_space,
isc::dhcp::OptionCollection& options);
/// @brief Assigns incoming packet to zero or more classes.
///
/// @note For now, the client classification is very simple. It just uses
/// content of the vendor-class-identifier option as a class. The resulting
/// class will be stored in packet (see @ref isc::dhcp::Pkt4::classes_ and
/// @ref isc::dhcp::Pkt4::inClass).
///
/// @param pkt packet to be classified
void classifyPacket(const Pkt4Ptr& pkt);
/// @brief Performs packet processing specific to a class
///
/// This processing is a likely candidate to be pushed into hooks.
///
/// @param query incoming client's packet
/// @param rsp server's response
/// @return true if successful, false otherwise (will prevent sending response)
bool classSpecificProcessing(const Pkt4Ptr& query, const Pkt4Ptr& rsp);
private:
/// @brief Constructs netmask option based on subnet4
......
......@@ -3173,8 +3173,32 @@ TEST_F(Dhcpv4SrvFakeIfaceTest, vendorOptionsDocsisDefinitions) {
ASSERT_EQ(0, rcode_);
}
// Checks if client packets are classified properly
TEST_F(Dhcpv4SrvTest, clientClassification) {
NakedDhcpv4Srv srv(0);
// Let's create a relayed DISCOVER. This particular relayed DISCOVER has
// vendor-class set to docsis3.0
Pkt4Ptr dis1;
ASSERT_NO_THROW(dis1 = captureRelayedDiscover());
ASSERT_NO_THROW(dis1->unpack());
srv.classifyPacket(dis1);
EXPECT_TRUE(dis1->inClass("docsis3.0"));
EXPECT_FALSE(dis1->inClass("eRouter1.0"));
// Let's create a relayed DISCOVER. This particular relayed DISCOVER has
// vendor-class set to eRouter1.0
Pkt4Ptr dis2;
ASSERT_NO_THROW(dis2 = captureRelayedDiscover2());
ASSERT_NO_THROW(dis2->unpack());
srv.classifyPacket(dis2);
EXPECT_TRUE(dis2->inClass("eRouter1.0"));
EXPECT_FALSE(dis2->inClass("docsis3.0"));
}
/*I}; // end of isc::dhcp::test namespace
}; // end of isc::dhcp namespace
}; // end of isc namespace */
}; // end of anonymous namespace
......@@ -222,7 +222,8 @@ public:
/// @brief returns captured DISCOVER that went through a relay
///
/// See method code for a detailed explanation.
/// See method code for a detailed explanation. This is a discover from
/// docsis3.0 device (Cable Modem)
///
/// @return relayed DISCOVER
Pkt4Ptr captureRelayedDiscover();
......@@ -253,6 +254,13 @@ public:
createPacketFromBuffer(const Pkt4Ptr& src_pkt,
Pkt4Ptr& dst_pkt);
/// @brief returns captured DISCOVER that went through a relay
///
/// See method code for a detailed explanation. This is a discover from
/// eRouter1.0 device (CPE device integrated with cable modem)
///
/// @return relayed DISCOVER
Pkt4Ptr captureRelayedDiscover2();
/// @brief generates a DHCPv4 packet based on provided hex string
///
......@@ -443,6 +451,7 @@ public:
using Dhcpv4Srv::srvidToString;
using Dhcpv4Srv::unpackOptions;
using Dhcpv4Srv::name_change_reqs_;
using Dhcpv4Srv::classifyPacket;
};
}; // end of isc::dhcp::test namespace
......
......@@ -71,7 +71,10 @@ void Dhcpv4SrvTest::captureSetDefaultFields(const Pkt4Ptr& pkt) {
Pkt4Ptr Dhcpv4SrvTest::captureRelayedDiscover() {
/* string exported from Wireshark:
/* This is packet 1 from capture
dhcp-val/pcap/docsis-*-CG3000DCR-Registration-Filtered.cap
string exported from Wireshark:
User Datagram Protocol, Src Port: bootps (67), Dst Port: bootps (67)
Source port: bootps (67)
......@@ -98,7 +101,7 @@ Bootstrap Protocol
Magic cookie: DHCP
Option: (53) DHCP Message Type
Option: (55) Parameter Request List
Option: (60) Vendor class identifier
Option: (60) Vendor class identifier (docsis3.0)
Option: (125) V-I Vendor-specific Information
- suboption 1 (Option Request): requesting option 2
- suboption 5 (Modem Caps): 117 bytes
......@@ -129,6 +132,59 @@ Bootstrap Protocol
return (packetFromCapture(hex_string));
}
Pkt4Ptr Dhcpv4SrvTest::captureRelayedDiscover2() {
/* This is packet 5 from capture
dhcp-val/pcap/docsis-*-CG3000DCR-Registration-Filtered.cap
string exported from Wireshark:
User Datagram Protocol, Src Port: bootps (67), Dst Port: bootps (67)
Bootstrap Protocol
Message type: Boot Request (1)
Hardware type: Ethernet (0x01)
Hardware address length: 6
Hops: 1
Transaction ID: 0x5d05478f
Seconds elapsed: 5
Bootp flags: 0x0000 (Unicast)
Client IP address: 0.0.0.0 (0.0.0.0)
Your (client) IP address: 0.0.0.0 (0.0.0.0)
Next server IP address: 0.0.0.0 (0.0.0.0)
Relay agent IP address: 10.254.226.1 (10.254.226.1)
Client MAC address: Netgear_b8:15:15 (20:e5:2a:b8:15:15)
Client hardware address padding: 00000000000000000000
Server host name not given
Boot file name not given
Magic cookie: DHCP
Option: (53) DHCP Message Type
Option: (55) Parameter Request List
Option: (43) Vendor-Specific Information
Option: (60) Vendor class identifier (eRouter1.0)
Option: (15) Domain Name
Option: (61) Client identifier
Option: (57) Maximum DHCP Message Size
Option: (82) Agent Information Option
Option: (255) End */
string hex_string =
"010106015d05478f000500000000000000000000000000000afee20120e52ab8151500"
"0000000000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000000000000"
"000000000000000000000000000000000000000000000000000063825363350101370e"
"480102030406070c0f171a36337a2b63020745524f55544552030b45434d3a45524f55"
"544552040d324252323239553430303434430504312e3034060856312e33332e303307"
"07322e332e305232080630303039354209094347333030304443520a074e6574676561"
"720f0745524f555445523c0a65526f75746572312e300f14687364312e70612e636f6d"
"636173742e6e65742e3d0fff2ab815150003000120e52ab81515390205dc5219010420"
"000002020620e52ab8151409090000118b0401020300ff";
return (packetFromCapture(hex_string));
}
}; // end of isc::dhcp::test namespace
}; // end of isc::dhcp namespace
}; // end of isc namespace
......@@ -105,7 +105,7 @@ protected:
/// various configuration values. Installing the dummy handler
/// that guarantees to return success causes initial configuration
/// to be stored for the session being created and that it can
/// be later accessed with \ref isc::ConfigData::getFullConfig.
/// be later accessed with \ref isc::config::ConfigData::getFullConfig.
///
/// @param new_config new configuration.
///
......
......@@ -190,6 +190,33 @@ implemented within the context of the server and it has access to all objects
which define its configuration (including dynamically created option
definitions).
@section dhcpv6Classifier DHCPv6 Client Classification
Kea DHCPv6 server currently supports simplified client classification. It is called
"simplified", because the incoming packets are classified based on the content
of the vendor class (16) option. More flexible classification is planned, but there
are no specific development dates agreed.
For each incoming packet, @ref isc::dhcp::Dhcpv6Srv::classifyPacket() method is
called. It attempts to extract content of the vendor class option and interprets
as a name of the class. Although the RFC3315 says that the vendor class may
contain more than one chunk of data, the existing code handles only one data
block, because that is what actual devices use. For now, the code has been
tested with two classes used in cable modem networks: eRouter1.0 and docsis3.0,
but any other content of the vendor class option will be interpreted as a class
name.
In principle any given packet can belong to zero or more classes. As the current
classifier is very modest, there's only one way to assign a class (based on vendor class
option), the ability to assign more than one class to a packet is not yet exercised.
Neverthless, there is such a possibility and it will be used in a near future. To
check whether a packet belongs to given class, isc::dhcp::Pkt6::inClass method should
be used.
Currently there is no class behaviour coded in DHCPv6, hence no v6 equivalent of
@ref isc::dhcp::Dhcpv4Srv::classSpecificProcessing. Should any need for such a code arise,
it will be conducted in an external hooks library.
@section dhcpv6Other Other DHCPv6 topics
For hooks API support in DHCPv6, see @ref dhcpv6Hooks.
......
......@@ -27,6 +27,10 @@ successfully established a session with the BIND 10 control channel.
This debug message is issued just before the IPv6 DHCP server attempts
to establish a session with the BIND 10 control channel.
% DHCP6_CLASS_ASSIGNED client packet has been assigned to the following class(es): %1
This debug message informs that incoming packet has been assigned to specified
class or classes. This is a norma
% DHCP6_CLIENTID_MISSING mandatory client-id option is missing, message from %1 dropped
This error message indicates that the received message is being dropped
because it does not include the mandatory client-id option necessary for
......
......@@ -316,6 +316,9 @@ bool Dhcpv6Srv::run() {
callout_handle->getArgument("query6", query);
}
// Assign this packet to a class, if possible
classifyPacket(query);
try {
NameChangeRequestPtr ncr;
switch (query->getType()) {
......@@ -2423,5 +2426,39 @@ Dhcpv6Srv::ifaceMgrSocket6ErrorHandler(const std::string& errmsg) {
LOG_WARN(dhcp6_logger, DHCP6_OPEN_SOCKET_FAIL).arg(errmsg);
}
void Dhcpv6Srv::classifyPacket(const Pkt6Ptr& pkt) {
boost::shared_ptr<OptionCustom> vclass =
boost::dynamic_pointer_cast<OptionCustom>(pkt->getOption(D6O_VENDOR_CLASS));
if (!vclass) {
return;
}
string classes = "";
// 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()) {
LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_CLASS_ASSIGNED)
.arg(classes);
}
}
};
};
......@@ -533,6 +533,16 @@ protected:
size_t* relay_msg_offset,
size_t* relay_msg_len);
/// @brief Assigns incoming packet to zero or more classes.
///
/// @note For now, the client classification is very simple. It just uses
/// content of the vendor-class-identifier option as a class. The resulting
/// class will be stored in packet (see @ref isc::dhcp::Pkt6::classes_ and
/// @ref isc::dhcp::Pkt6::inClass).
///
/// @param pkt packet to be classified
void classifyPacket(const Pkt6Ptr& pkt);
private:
/// @brief Implements the error handler for socket open failure.
......
......@@ -1673,6 +1673,35 @@ TEST_F(Dhcpv6SrvTest, unpackOptions) {
EXPECT_EQ(0x0, option_bar->getValue());
}
// Checks if client packets are classified properly
TEST_F(Dhcpv6SrvTest, clientClassification) {
NakedDhcpv6Srv srv(0);
// Let's create a relayed SOLICIT. This particular relayed SOLICIT has
// vendor-class set to docsis3.0
Pkt6Ptr sol1;
ASSERT_NO_THROW(sol1 = captureDocsisRelayedSolicit());
ASSERT_NO_THROW(sol1->unpack());
srv.classifyPacket(sol1);
// It should belong to docsis3.0 class. It should not belong to eRouter1.0
EXPECT_TRUE(sol1->inClass("docsis3.0"));
EXPECT_FALSE(sol1->inClass("eRouter1.0"));
// Let's get a relayed SOLICIT. This particular relayed SOLICIT has
// vendor-class set to eRouter1.0
Pkt6Ptr sol2;
ASSERT_NO_THROW(sol2 = captureeRouterRelayedSolicit());
ASSERT_NO_THROW(sol2->unpack());
srv.classifyPacket(sol2);
EXPECT_TRUE(sol2->inClass("eRouter1.0"));
EXPECT_FALSE(sol2->inClass("docsis3.0"));
}
/// @todo: Add more negative tests for processX(), e.g. extend sanityCheck() test
/// to call processX() methods.
......
......@@ -112,6 +112,7 @@ public:
using Dhcpv6Srv::createStatusCode;
using Dhcpv6Srv::selectSubnet;
using Dhcpv6Srv::sanityCheck;
using Dhcpv6Srv::classifyPacket;
using Dhcpv6Srv::loadServerID;
using Dhcpv6Srv::writeServerID;
using Dhcpv6Srv::unpackOptions;
......@@ -477,6 +478,7 @@ public:
Pkt6Ptr captureSimpleSolicit();
Pkt6Ptr captureRelayedSolicit();
Pkt6Ptr captureDocsisRelayedSolicit();
Pkt6Ptr captureeRouterRelayedSolicit();
/// @brief Auxiliary method that sets Pkt6 fields
///
......
......@@ -17,9 +17,9 @@
#include <string>
/// @file wireshark.cc
///
///
/// @brief contains packet captures imported from Wireshark
///
///
/// These are actual packets captured over wire. They are used in various
/// tests.
///
......@@ -33,6 +33,10 @@
/// 6. Coding guidelines line restrictions apply, so wrap your code as necessary
/// 7. Make sure you decribe the capture appropriately
/// 8. Follow whatever rest of the methods are doing (set ports, ifaces etc.)
/// 9. To easily copy packet description, click File... -> Extract packet
/// dissections -> as plain text file...
/// (Make sure that the packet is expanded in the view. The text file will
/// contain whatever expansion level you have in the graphical tree.)
using namespace std;
......@@ -81,7 +85,7 @@ Pkt6Ptr Dhcpv6SrvTest::captureRelayedSolicit() {
// - ORO (7)
// string exported from Wireshark
string hex_string =
string hex_string =
"0c0500000000000000000000000000000000fc00000000000000000000000000000900"
"12000231350009002c010517100001000e0001000151b5e46208002758f1e80003000c"
"000000010000000000000000000600020007";
......@@ -102,7 +106,7 @@ Pkt6Ptr Dhcpv6SrvTest::captureRelayedSolicit() {
Pkt6Ptr isc::test::Dhcpv6SrvTest::captureDocsisRelayedSolicit() {
// This is an actual DOCSIS packet
// RELAY-FORW (12)
// RELAY-FORW (12)
// - Relay Message
// - SOLICIT (1)
// - client-id
......@@ -132,7 +136,7 @@ Pkt6Ptr isc::test::Dhcpv6SrvTest::captureDocsisRelayedSolicit() {
// - Suboption 1026: Cable Modem MAC addr = 10:0d:7f:00:07:88
// string exported from Wireshark
string hex_string =
string hex_string =