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

[2898] Support for v6 relayed traffic added.

parent 66d1631e
...@@ -1481,11 +1481,27 @@ private: ...@@ -1481,11 +1481,27 @@ private:
std::string iface; std::string iface;
try { try {
iface = string_values_.getParam("interface"); iface = string_values_.getParam("interface");
} catch (DhcpConfigError) { } catch (const DhcpConfigError&) {
// iface not mandatory so swallow the exception // iface not mandatory so swallow the exception
} }
/// @todo: Convert this to logger once the parser is working reliably // Get interface-id option content. For now we support string
// represenation only
std::string ifaceid;
try {
ifaceid = string_values_.getParam("interface-id");
} catch (DhcpConfigError) {
// interface-id is not mandatory
}
if (!iface.empty() && !ifaceid.empty()) {
isc_throw(isc::dhcp::DhcpConfigError,
"parser error: interface (defined for locally reachable "
"subnets) and interface-id (defined for subnets reachable"
" via relays) cannot be defined at the same time for "
"subnet " << addr.toText() << "/" << (int)len);
}
stringstream tmp; stringstream tmp;
tmp << addr.toText() << "/" << (int)len tmp << addr.toText() << "/" << (int)len
<< " with params t1=" << t1 << ", t2=" << t2 << ", pref=" << " with params t1=" << t1 << ", t2=" << t2 << ", pref="
...@@ -1512,6 +1528,13 @@ private: ...@@ -1512,6 +1528,13 @@ private:
subnet_->setIface(iface); subnet_->setIface(iface);
} }
// Configure interface-id for remote interfaces, if defined
if (!ifaceid.empty()) {
OptionBuffer tmp(ifaceid.begin(), ifaceid.end());
OptionPtr opt(new Option(Option::V6, D6O_INTERFACE_ID, tmp));
subnet_->setInterfaceId(opt);
}
// We are going to move configured options to the Subnet object. // We are going to move configured options to the Subnet object.
// Configured options reside in the container where options // Configured options reside in the container where options
// are grouped by space names. Thus we need to get all space names // are grouped by space names. Thus we need to get all space names
...@@ -1591,6 +1614,7 @@ private: ...@@ -1591,6 +1614,7 @@ private:
factories["pool"] = PoolParser::factory; factories["pool"] = PoolParser::factory;
factories["option-data"] = OptionDataListParser::factory; factories["option-data"] = OptionDataListParser::factory;
factories["interface"] = StringParser::factory; factories["interface"] = StringParser::factory;
factories["interface-id"] = StringParser::factory;
FactoryMap::iterator f = factories.find(config_id); FactoryMap::iterator f = factories.find(config_id);
if (f == factories.end()) { if (f == factories.end()) {
......
...@@ -199,6 +199,12 @@ ...@@ -199,6 +199,12 @@
"item_default": "" "item_default": ""
}, },
{ "item_name": "interface-id",
"item_type": "string",
"item_optional": false,
"item_default": ""
},
{ "item_name": "renew-timer", { "item_name": "renew-timer",
"item_type": "integer", "item_type": "integer",
"item_optional": false, "item_optional": false,
......
...@@ -403,8 +403,13 @@ Dhcpv6Srv::copyDefaultOptions(const Pkt6Ptr& question, Pkt6Ptr& answer) { ...@@ -403,8 +403,13 @@ Dhcpv6Srv::copyDefaultOptions(const Pkt6Ptr& question, Pkt6Ptr& answer) {
if (clientid) { if (clientid) {
answer->addOption(clientid); answer->addOption(clientid);
} }
/// @todo: Should throw if there is no client-id (except anonymous INF-REQUEST)
// if this is a relayed message, we need to copy relay information
if (!question->relay_info_.empty()) {
answer->copyRelayInfo(question);
}
// TODO: Should throw if there is no client-id (except anonymous INF-REQUEST)
} }
void void
...@@ -523,18 +528,43 @@ Dhcpv6Srv::sanityCheck(const Pkt6Ptr& pkt, RequirementLevel clientid, ...@@ -523,18 +528,43 @@ Dhcpv6Srv::sanityCheck(const Pkt6Ptr& pkt, RequirementLevel clientid,
Subnet6Ptr Subnet6Ptr
Dhcpv6Srv::selectSubnet(const Pkt6Ptr& question) { Dhcpv6Srv::selectSubnet(const Pkt6Ptr& question) {
/// @todo: pass interface information only if received direct (non-relayed) message Subnet6Ptr subnet;
// Try to find a subnet if received packet from a directly connected client if (question->relay_info_.empty()) {
Subnet6Ptr subnet = CfgMgr::instance().getSubnet6(question->getIface()); // This is a direct (non-relayed) message
if (subnet) {
// Try to find a subnet if received packet from a directly connected client
Subnet6Ptr subnet = CfgMgr::instance().getSubnet6(question->getIface());
if (subnet) {
return (subnet);
}
// If no subnet was found, try to find it based on remote address
subnet = CfgMgr::instance().getSubnet6(question->getRemoteAddr());
return (subnet); return (subnet);
} } else {
// If no subnet was found, try to find it based on remote address // This is a relayed message
subnet = CfgMgr::instance().getSubnet6(question->getRemoteAddr()); OptionPtr interface_id = question->getAnyRelayOption(D6O_INTERFACE_ID,
Pkt6::RELAY_SEARCH_FIRST);
if (interface_id) {
subnet = CfgMgr::instance().getSubnet6(interface_id);
}
if (subnet) {
return (subnet);
}
// If no interface-id was specified (or not configured on server), let's
// try address matching
IOAddress link_addr = question->relay_info_.back().linkaddr_;
return (subnet); // if relay filled in link_addr field, then let's use it
if (link_addr != IOAddress("::")) {
subnet = CfgMgr::instance().getSubnet6(link_addr);
}
return (subnet);
}
} }
void void
......
...@@ -500,6 +500,110 @@ TEST_F(Dhcp6ParserTest, interfaceGlobal) { ...@@ -500,6 +500,110 @@ TEST_F(Dhcp6ParserTest, interfaceGlobal) {
EXPECT_EQ(1, rcode_); EXPECT_EQ(1, rcode_);
} }
// This test checks if it is possible to define a subnet with an
// interface-id option defined.
TEST_F(Dhcp6ParserTest, subnetInterfaceId) {
const string valid_interface_id = "foobar";
const string bogus_interface_id = "blah";
ConstElementPtr status;
// There should be at least one interface
string config = "{ "
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
"\"subnet6\": [ { "
" \"pool\": [ \"2001:db8:1::1 - 2001:db8:1::ffff\" ],"
" \"interface-id\": \"" + valid_interface_id + "\","
" \"subnet\": \"2001:db8:1::/64\" } ],"
"\"valid-lifetime\": 4000 }";
cout << config << endl;
ElementPtr json = Element::fromJSON(config);
EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
// returned value should be 0 (configuration success)
ASSERT_TRUE(status);
comment_ = parseAnswer(rcode_, status);
EXPECT_EQ(0, rcode_);
// try to get a subnet based on bogus interface-id option
OptionBuffer tmp(bogus_interface_id.begin(), bogus_interface_id.end());
OptionPtr ifaceid(new Option(Option::V6, D6O_INTERFACE_ID, tmp));
Subnet6Ptr subnet = CfgMgr::instance().getSubnet6(ifaceid);
EXPECT_FALSE(subnet);
// now try to get subnet for valid interface-id value
tmp = OptionBuffer(valid_interface_id.begin(), valid_interface_id.end());
ifaceid.reset(new Option(Option::V6, D6O_INTERFACE_ID, tmp));
subnet = CfgMgr::instance().getSubnet6(ifaceid);
ASSERT_TRUE(subnet);
EXPECT_TRUE(ifaceid->equal(subnet->getInterfaceId()));
}
// This test checks if it is not allowed to define global interface
// parameter.
TEST_F(Dhcp6ParserTest, interfaceIdGlobal) {
ConstElementPtr status;
string config = "{ \"interface\": [ \"all\" ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
"\"interface-id\": \"foobar\"," // Not valid. Can be defined in subnet only
"\"subnet6\": [ { "
" \"pool\": [ \"2001:db8:1::1 - 2001:db8:1::ffff\" ],"
" \"subnet\": \"2001:db8:1::/64\" } ],"
"\"valid-lifetime\": 4000 }";
cout << config << endl;
ElementPtr json = Element::fromJSON(config);
EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
// returned value should be 1 (parse error)
ASSERT_TRUE(status);
comment_ = parseAnswer(rcode_, status);
EXPECT_EQ(1, rcode_);
}
// This test checks if it is not possible to define a subnet with an
// interface (i.e. local subnet) and interface-id (remote subnet) defined.
TEST_F(Dhcp6ParserTest, subnetInterfaceAndInterfaceId) {
ConstElementPtr status;
string config = "{ \"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
"\"subnet6\": [ { "
" \"pool\": [ \"2001:db8:1::1 - 2001:db8:1::ffff\" ],"
" \"interface\": \"" + valid_iface_ + "\","
" \"interface-id\": \"foobar\","
" \"subnet\": \"2001:db8:1::/64\" } ],"
"\"valid-lifetime\": 4000 }";
cout << config << endl;
ElementPtr json = Element::fromJSON(config);
EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
// returned value should be 0 (configuration success)
ASSERT_TRUE(status);
comment_ = parseAnswer(rcode_, status);
EXPECT_EQ(1, rcode_);
}
// Test verifies that a subnet with pool values that do not belong to that // Test verifies that a subnet with pool values that do not belong to that
// pool are rejected. // pool are rejected.
TEST_F(Dhcp6ParserTest, poolOutOfSubnet) { TEST_F(Dhcp6ParserTest, poolOutOfSubnet) {
......
...@@ -98,6 +98,16 @@ public: ...@@ -98,6 +98,16 @@ public:
return (ia); return (ia);
} }
/// @brief generates interface-id option, based on text
///
/// @param iface_id textual representation of the interface-id content
///
/// @return pointer to the option object
OptionPtr generateInterfaceId(const string& iface_id) {
OptionBuffer tmp(iface_id.begin(), iface_id.end());
return OptionPtr(new Option(Option::V6, D6O_INTERFACE_ID, tmp));
}
// Generate client-id option // Generate client-id option
OptionPtr generateClientId(size_t duid_size = 32) { OptionPtr generateClientId(size_t duid_size = 32) {
...@@ -678,6 +688,7 @@ TEST_F(Dhcpv6SrvTest, SolicitBasic) { ...@@ -678,6 +688,7 @@ TEST_F(Dhcpv6SrvTest, SolicitBasic) {
// check that IA_NA was returned and that there's an address included // check that IA_NA was returned and that there's an address included
boost::shared_ptr<Option6IAAddr> addr = checkIA_NA(reply, 234, subnet_->getT1(), boost::shared_ptr<Option6IAAddr> addr = checkIA_NA(reply, 234, subnet_->getT1(),
subnet_->getT2()); subnet_->getT2());
ASSERT_TRUE(addr);
// Check that the assigned address is indeed from the configured pool // Check that the assigned address is indeed from the configured pool
checkIAAddr(addr, addr->getAddress(), subnet_->getPreferred(), subnet_->getValid()); checkIAAddr(addr, addr->getAddress(), subnet_->getPreferred(), subnet_->getValid());
...@@ -731,6 +742,7 @@ TEST_F(Dhcpv6SrvTest, SolicitHint) { ...@@ -731,6 +742,7 @@ TEST_F(Dhcpv6SrvTest, SolicitHint) {
// check that IA_NA was returned and that there's an address included // check that IA_NA was returned and that there's an address included
boost::shared_ptr<Option6IAAddr> addr = checkIA_NA(reply, 234, subnet_->getT1(), boost::shared_ptr<Option6IAAddr> addr = checkIA_NA(reply, 234, subnet_->getT1(),
subnet_->getT2()); subnet_->getT2());
ASSERT_TRUE(addr);
// check that we've got the address we requested // check that we've got the address we requested
checkIAAddr(addr, hint, subnet_->getPreferred(), subnet_->getValid()); checkIAAddr(addr, hint, subnet_->getPreferred(), subnet_->getValid());
...@@ -779,6 +791,7 @@ TEST_F(Dhcpv6SrvTest, SolicitInvalidHint) { ...@@ -779,6 +791,7 @@ TEST_F(Dhcpv6SrvTest, SolicitInvalidHint) {
// check that IA_NA was returned and that there's an address included // check that IA_NA was returned and that there's an address included
boost::shared_ptr<Option6IAAddr> addr = checkIA_NA(reply, 234, subnet_->getT1(), boost::shared_ptr<Option6IAAddr> addr = checkIA_NA(reply, 234, subnet_->getT1(),
subnet_->getT2()); subnet_->getT2());
ASSERT_TRUE(addr);
// Check that the assigned address is indeed from the configured pool // Check that the assigned address is indeed from the configured pool
checkIAAddr(addr, addr->getAddress(), subnet_->getPreferred(), subnet_->getValid()); checkIAAddr(addr, addr->getAddress(), subnet_->getPreferred(), subnet_->getValid());
...@@ -840,6 +853,9 @@ TEST_F(Dhcpv6SrvTest, ManySolicits) { ...@@ -840,6 +853,9 @@ TEST_F(Dhcpv6SrvTest, ManySolicits) {
subnet_->getT2()); subnet_->getT2());
boost::shared_ptr<Option6IAAddr> addr3 = checkIA_NA(reply3, 3, subnet_->getT1(), boost::shared_ptr<Option6IAAddr> addr3 = checkIA_NA(reply3, 3, subnet_->getT1(),
subnet_->getT2()); subnet_->getT2());
ASSERT_TRUE(addr1);
ASSERT_TRUE(addr2);
ASSERT_TRUE(addr3);
// Check that the assigned address is indeed from the configured pool // Check that the assigned address is indeed from the configured pool
checkIAAddr(addr1, addr1->getAddress(), subnet_->getPreferred(), subnet_->getValid()); checkIAAddr(addr1, addr1->getAddress(), subnet_->getPreferred(), subnet_->getValid());
...@@ -910,6 +926,7 @@ TEST_F(Dhcpv6SrvTest, RequestBasic) { ...@@ -910,6 +926,7 @@ TEST_F(Dhcpv6SrvTest, RequestBasic) {
// check that IA_NA was returned and that there's an address included // check that IA_NA was returned and that there's an address included
boost::shared_ptr<Option6IAAddr> addr = checkIA_NA(reply, 234, subnet_->getT1(), boost::shared_ptr<Option6IAAddr> addr = checkIA_NA(reply, 234, subnet_->getT1(),
subnet_->getT2()); subnet_->getT2());
ASSERT_TRUE(addr);
// check that we've got the address we requested // check that we've got the address we requested
checkIAAddr(addr, hint, subnet_->getPreferred(), subnet_->getValid()); checkIAAddr(addr, hint, subnet_->getPreferred(), subnet_->getValid());
...@@ -1592,6 +1609,113 @@ TEST_F(Dhcpv6SrvTest, selectSubnetIface) { ...@@ -1592,6 +1609,113 @@ TEST_F(Dhcpv6SrvTest, selectSubnetIface) {
EXPECT_EQ(subnet3, srv.selectSubnet(pkt)); EXPECT_EQ(subnet3, srv.selectSubnet(pkt));
} }
// This test verifies if selectSubnet() selects proper subnet for a given
// linkaddr in RELAY-FORW message
TEST_F(Dhcpv6SrvTest, selectSubnetRelayLinkaddr) {
NakedDhcpv6Srv srv(0);
Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 48, 1, 2, 3, 4));
Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"), 48, 1, 2, 3, 4));
Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"), 48, 1, 2, 3, 4));
Pkt6::RelayInfo relay;
relay.linkaddr_ = IOAddress("2001:db8:2::1234");
relay.peeraddr_ = IOAddress("fe80::1");
// CASE 1: We have only one subnet defined and we received relayed traffic.
// The only available subnet should NOT be selected.
CfgMgr::instance().deleteSubnets6();
CfgMgr::instance().addSubnet6(subnet1); // just a single subnet
Pkt6Ptr pkt = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
pkt->relay_info_.push_back(relay);
Subnet6Ptr selected = srv.selectSubnet(pkt);
EXPECT_FALSE(selected);
// CASE 2: We have three subnets defined and we received relayed traffic.
// Nothing should be selected.
CfgMgr::instance().deleteSubnets6();
CfgMgr::instance().addSubnet6(subnet1);
CfgMgr::instance().addSubnet6(subnet2);
CfgMgr::instance().addSubnet6(subnet3);
selected = srv.selectSubnet(pkt);
EXPECT_EQ(selected, subnet2);
// CASE 3: We have three subnets defined and we received relayed traffic
// that came out of subnet 2. We should select subnet2 then
CfgMgr::instance().deleteSubnets6();
CfgMgr::instance().addSubnet6(subnet1);
CfgMgr::instance().addSubnet6(subnet2);
CfgMgr::instance().addSubnet6(subnet3);
// source of the packet should have no meaning. Selection is based
// on linkaddr field in the relay
pkt->setRemoteAddr(IOAddress("2001:db8:1::baca"));
selected = srv.selectSubnet(pkt);
EXPECT_EQ(selected, subnet2);
// CASE 4: We have three subnets defined and we received relayed traffic
// that came out of undefined subnet. We should select nothing
CfgMgr::instance().deleteSubnets6();
CfgMgr::instance().addSubnet6(subnet1);
CfgMgr::instance().addSubnet6(subnet2);
CfgMgr::instance().addSubnet6(subnet3);
pkt->relay_info_.clear();
relay.linkaddr_ = IOAddress("2001:db8:4::1234");
pkt->relay_info_.push_back(relay);
selected = srv.selectSubnet(pkt);
EXPECT_FALSE(selected);
}
// This test verifies if selectSubnet() selects proper subnet for a given
// interface-id option
TEST_F(Dhcpv6SrvTest, selectSubnetRelayInterfaceId) {
NakedDhcpv6Srv srv(0);
Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 48, 1, 2, 3, 4));
Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"), 48, 1, 2, 3, 4));
Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"), 48, 1, 2, 3, 4));
subnet1->setInterfaceId(generateInterfaceId("relay1"));
subnet2->setInterfaceId(generateInterfaceId("relay2"));
// CASE 1: We have only one subnet defined and it is for interface-id "relay1"
// Packet came with interface-id "relay2". We should not select subnet1
CfgMgr::instance().deleteSubnets6();
CfgMgr::instance().addSubnet6(subnet1); // just a single subnet
Pkt6Ptr pkt = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
Pkt6::RelayInfo relay;
relay.linkaddr_ = IOAddress("2001:db8:2::1234");
relay.peeraddr_ = IOAddress("fe80::1");
OptionPtr opt = generateInterfaceId("relay2");
relay.options_.insert(pair<int, OptionPtr>(opt->getType(), opt));
pkt->relay_info_.push_back(relay);
// There is only one subnet configured and we are outside of that subnet
Subnet6Ptr selected = srv.selectSubnet(pkt);
EXPECT_FALSE(selected);
// CASE 2: We have only one subnet defined and it is for interface-id "relay2"
// Packet came with interface-id "relay2". We should select it
CfgMgr::instance().deleteSubnets6();
CfgMgr::instance().addSubnet6(subnet2); // just a single subnet
selected = srv.selectSubnet(pkt);
EXPECT_EQ(selected, subnet2);
// CASE 3: We have only 3 subnets defined: one remote for interface-id "relay1",
// one remote for interface-id "relay2" and third local
// packet comes with interface-id "relay2". We should select subnet2
CfgMgr::instance().deleteSubnets6();
CfgMgr::instance().addSubnet6(subnet1);
CfgMgr::instance().addSubnet6(subnet2);
CfgMgr::instance().addSubnet6(subnet3);
EXPECT_EQ(subnet2, srv.selectSubnet(pkt));
}
// This test verifies if the server-id disk operations (read, write) are // This test verifies if the server-id disk operations (read, write) are
// working properly. // working properly.
TEST_F(Dhcpv6SrvTest, ServerID) { TEST_F(Dhcpv6SrvTest, ServerID) {
......
...@@ -72,6 +72,60 @@ uint16_t Pkt6::len() { ...@@ -72,6 +72,60 @@ uint16_t Pkt6::len() {
} }
} }
OptionPtr Pkt6::getAnyRelayOption(uint16_t opt_type, RelaySearchOrder order) {
int start = 0;
int end = 0;
int direction = 0;
if (relay_info_.empty()) {
// there's no relay info, this is a direct message
return (OptionPtr());
}
switch (order) {
case RELAY_SEARCH_FROM_CLIENT:
// search backwards
start = relay_info_.size() - 1;
end = 0;
direction = -1;
break;
case RELAY_SEARCH_FROM_SERVER:
// search forward
start = 0;
end = relay_info_.size() - 1;
direction = 1;
break;
case RELAY_SEARCH_FIRST:
// look at the innermost relay only
start = relay_info_.size() - 1;
end = start;
direction = 0;
break;
case RELAY_SEARCH_LAST:
// look at the outermost relay only
start = 0;
end = 0;
direction = 0;
}
// this is a tricky loop. It must go from start to end, but it must work in
// both directions (start > end; or start < end). We can't use regular
// exit condition, because we don't know whether to use i <= end or i >= end
for (int i = start; ; i += direction) {
OptionPtr opt = getRelayOption(opt_type, i);
if (opt) {
return (opt);
}
if (i == end) {
break;
}
}
return getRelayOption(opt_type, end);
}
OptionPtr Pkt6::getRelayOption(uint16_t opt_type, uint8_t relay_level) { OptionPtr Pkt6::getRelayOption(uint16_t opt_type, uint8_t relay_level) {
if (relay_level >= relay_info_.size()) { if (relay_level >= relay_info_.size()) {
isc_throw(OutOfRange, "This message was relayed " << relay_info_.size() << " time(s)." isc_throw(OutOfRange, "This message was relayed " << relay_info_.size() << " time(s)."
...@@ -483,5 +537,32 @@ const char* Pkt6::getName() const { ...@@ -483,5 +537,32 @@ const char* Pkt6::getName() const {
return (getName(getType())); return (getName(getType()));
} }
void Pkt6::copyRelayInfo(const Pkt6Ptr& question) {
// we use index rather than iterator, because we need that as a parameter
// passed to getRelayOption()
for (int i = 0; i < question->relay_info_.size(); ++i) {
RelayInfo x;
x.msg_type_ = DHCPV6_RELAY_REPL;
x.hop_count_ = question->relay_info_[i].hop_count_;
x.linkaddr_ = question->relay_info_[i].linkaddr_;
x.peeraddr_ = question->relay_info_[i].peeraddr_;
// Is there interface-id option in this nesting level if there is,
// we need to echo it back
OptionPtr opt = question->getRelayOption(D6O_INTERFACE_ID, i);
// taken from question->RelayInfo_[i].options_
if (opt) {
x.options_.insert(pair<int, boost::shared_ptr<Option> >(opt->getType(), opt));
}
/// @todo: implement support for ERO (Echo Request Option, RFC4994)
// add this relay-repl info to our message
relay_info_.push_back(x);
}
}
} // end of isc::dhcp namespace } // end of isc::dhcp namespace
} // end of isc namespace } // end of isc namespace
...@@ -30,6 +30,9 @@ namespace isc { ...@@ -30,6 +30,9 @@ namespace isc {
namespace dhcp { namespace dhcp {
class Pkt6;
typedef boost::shared_ptr<Pkt6> Pkt6Ptr;
class Pkt6 { class Pkt6 {
public: public:
/// specifies non-relayed DHCPv6 packet header length (over UDP) /// specifies non-relayed DHCPv6 packet header length (over UDP)
...@@ -44,6 +47,28 @@ public: ...@@ -44,6 +47,28 @@ public:
TCP = 1 // there are TCP DHCPv6 packets (bulk leasequery, failover)