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:
std::string iface;
try {
iface = string_values_.getParam("interface");
} catch (DhcpConfigError) {
} catch (const DhcpConfigError&) {
// 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;
tmp << addr.toText() << "/" << (int)len
<< " with params t1=" << t1 << ", t2=" << t2 << ", pref="
......@@ -1512,6 +1528,13 @@ private:
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.
// Configured options reside in the container where options
// are grouped by space names. Thus we need to get all space names
......@@ -1591,6 +1614,7 @@ private:
factories["pool"] = PoolParser::factory;
factories["option-data"] = OptionDataListParser::factory;
factories["interface"] = StringParser::factory;
factories["interface-id"] = StringParser::factory;
FactoryMap::iterator f = factories.find(config_id);
if (f == factories.end()) {
......
......@@ -199,6 +199,12 @@
"item_default": ""
},
{ "item_name": "interface-id",
"item_type": "string",
"item_optional": false,
"item_default": ""
},
{ "item_name": "renew-timer",
"item_type": "integer",
"item_optional": false,
......
......@@ -403,8 +403,13 @@ Dhcpv6Srv::copyDefaultOptions(const Pkt6Ptr& question, Pkt6Ptr& answer) {
if (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
......@@ -523,7 +528,10 @@ Dhcpv6Srv::sanityCheck(const Pkt6Ptr& pkt, RequirementLevel clientid,
Subnet6Ptr
Dhcpv6Srv::selectSubnet(const Pkt6Ptr& question) {
/// @todo: pass interface information only if received direct (non-relayed) message
Subnet6Ptr subnet;
if (question->relay_info_.empty()) {
// This is a direct (non-relayed) message
// Try to find a subnet if received packet from a directly connected client
Subnet6Ptr subnet = CfgMgr::instance().getSubnet6(question->getIface());
......@@ -533,8 +541,30 @@ Dhcpv6Srv::selectSubnet(const Pkt6Ptr& question) {
// If no subnet was found, try to find it based on remote address
subnet = CfgMgr::instance().getSubnet6(question->getRemoteAddr());
return (subnet);
} else {
// This is a relayed message
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_;
// 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
......
......@@ -500,6 +500,110 @@ TEST_F(Dhcp6ParserTest, interfaceGlobal) {
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
// pool are rejected.
TEST_F(Dhcp6ParserTest, poolOutOfSubnet) {
......
......@@ -98,6 +98,16 @@ public:
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
OptionPtr generateClientId(size_t duid_size = 32) {
......@@ -678,6 +688,7 @@ TEST_F(Dhcpv6SrvTest, SolicitBasic) {
// check that IA_NA was returned and that there's an address included
boost::shared_ptr<Option6IAAddr> addr = checkIA_NA(reply, 234, subnet_->getT1(),
subnet_->getT2());
ASSERT_TRUE(addr);
// Check that the assigned address is indeed from the configured pool
checkIAAddr(addr, addr->getAddress(), subnet_->getPreferred(), subnet_->getValid());
......@@ -731,6 +742,7 @@ TEST_F(Dhcpv6SrvTest, SolicitHint) {
// check that IA_NA was returned and that there's an address included
boost::shared_ptr<Option6IAAddr> addr = checkIA_NA(reply, 234, subnet_->getT1(),
subnet_->getT2());
ASSERT_TRUE(addr);
// check that we've got the address we requested
checkIAAddr(addr, hint, subnet_->getPreferred(), subnet_->getValid());
......@@ -779,6 +791,7 @@ TEST_F(Dhcpv6SrvTest, SolicitInvalidHint) {
// check that IA_NA was returned and that there's an address included
boost::shared_ptr<Option6IAAddr> addr = checkIA_NA(reply, 234, subnet_->getT1(),
subnet_->getT2());
ASSERT_TRUE(addr);
// Check that the assigned address is indeed from the configured pool
checkIAAddr(addr, addr->getAddress(), subnet_->getPreferred(), subnet_->getValid());
......@@ -840,6 +853,9 @@ TEST_F(Dhcpv6SrvTest, ManySolicits) {
subnet_->getT2());
boost::shared_ptr<Option6IAAddr> addr3 = checkIA_NA(reply3, 3, subnet_->getT1(),
subnet_->getT2());
ASSERT_TRUE(addr1);
ASSERT_TRUE(addr2);
ASSERT_TRUE(addr3);
// Check that the assigned address is indeed from the configured pool
checkIAAddr(addr1, addr1->getAddress(), subnet_->getPreferred(), subnet_->getValid());
......@@ -910,6 +926,7 @@ TEST_F(Dhcpv6SrvTest, RequestBasic) {
// check that IA_NA was returned and that there's an address included
boost::shared_ptr<Option6IAAddr> addr = checkIA_NA(reply, 234, subnet_->getT1(),
subnet_->getT2());
ASSERT_TRUE(addr);
// check that we've got the address we requested
checkIAAddr(addr, hint, subnet_->getPreferred(), subnet_->getValid());
......@@ -1592,6 +1609,113 @@ TEST_F(Dhcpv6SrvTest, selectSubnetIface) {
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
// working properly.
TEST_F(Dhcpv6SrvTest, ServerID) {
......
......@@ -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) {
if (relay_level >= relay_info_.size()) {
isc_throw(OutOfRange, "This message was relayed " << relay_info_.size() << " time(s)."
......@@ -483,5 +537,32 @@ const char* Pkt6::getName() const {
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 namespace
......@@ -30,6 +30,9 @@ namespace isc {
namespace dhcp {
class Pkt6;
typedef boost::shared_ptr<Pkt6> Pkt6Ptr;
class Pkt6 {
public:
/// specifies non-relayed DHCPv6 packet header length (over UDP)
......@@ -44,6 +47,28 @@ public:
TCP = 1 // there are TCP DHCPv6 packets (bulk leasequery, failover)
};
/// @brief defines relay search pattern
///
/// Defines order in which options are searched in a message that
/// passed through mulitple relays. RELAY_SEACH_FROM_CLIENT will
/// start search from the relay that was the closest to the client
/// (i.e. innermost in the encapsulated message, which also means
/// this was the first relay that forwarded packet received by the
/// server and this will be the last relay that will handle the
/// response that server sent towards the client.).
/// RELAY_SEARCH_FROM_SERVER is the opposite. This will be the
/// relay closest to the server (i.e. outermost in the encapsulated
/// message, which also means it was the last relay that relayed
/// the received message and will be the first one to process
/// server's response). RELAY_SEARCH_FIRST is option from the first
/// relay (closest to the client), RELAY_SEARCH_LAST is the
/// last relay (closest to the server).
enum RelaySearchOrder {
RELAY_SEARCH_FROM_CLIENT = 1,
RELAY_SEARCH_FROM_SERVER = 2,
RELAY_SEARCH_FIRST = 3,
RELAY_SEARCH_LAST = 4
};
/// @brief structure that describes a single relay information
///
......@@ -201,6 +226,18 @@ public:
/// @return pointer to the option (or NULL if there is no such option)
OptionPtr getRelayOption(uint16_t option_code, uint8_t nesting_level);
/// @brief returns first instance of a specified option
///
/// When client's packet traverses multiple relays, each passing relay
/// may insert extra options. This method allows getting specific instance
/// of a given option (closest to the client, closest to the server, etc.)
/// See @ref RelaySearchOrder for detailed description.
///
/// @param option_code searched option
/// @param order option search order (see @ref RelaySearchOrder)
/// @return option pointer (or NULL if no option matches specified criteria)
OptionPtr getAnyRelayOption(uint16_t option_code, RelaySearchOrder order);
/// @brief Returns all instances of specified type.
///
/// Returns all instances of options of the specified type. DHCPv6 protocol
......@@ -356,6 +393,14 @@ public:
/// be freed by the caller.
const char* getName() const;
/// @brief copies relay information from client's packet to server's response
///
/// This information is not simply copied over. Some parameter are
/// removed, msg_type_is updated (RELAY-FORW => RELAY-REPL), etc.
///
/// @param question client's packet
void copyRelayInfo(const Pkt6Ptr& question);
/// relay information
///
/// this is a public field. Otherwise we hit one of the two problems:
......@@ -494,8 +539,6 @@ protected:
boost::posix_time::ptime timestamp_;
}; // Pkt6 class
typedef boost::shared_ptr<Pkt6> Pkt6Ptr;
} // isc::dhcp namespace
} // isc namespace
......
......@@ -22,6 +22,7 @@
#include <dhcp/option_int.h>
#include <dhcp/option_int_array.h>
#include <dhcp/pkt6.h>
#include <util/range_utilities.h>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/scoped_ptr.hpp>
......@@ -44,6 +45,18 @@ class Pkt6Test : public ::testing::Test {
public:
Pkt6Test() {
}
/// @brief generates an option with given code (and length) and random content
///
/// @param code option code
/// @param len data length (data will be randomized)
///
/// @return pointer to the new option
OptionPtr generateRandomOption(uint16_t code, size_t len = 10) {
OptionBuffer data(len);
util::fillRandom(data.begin(), data.end());
return OptionPtr(new Option(Option::V6, code, data));
}
};
TEST_F(Pkt6Test, constructor) {
......@@ -552,4 +565,94 @@ TEST_F(Pkt6Test, relayPack) {
EXPECT_EQ(0, memcmp(relay_opt_data, relay_opt_data, sizeof(relay_opt_data)));
}