Commit 4772ee58 authored by Marcin Siodelski's avatar Marcin Siodelski
Browse files

[master] Merge branch 'trac3705'

parents aa2a2767 58a1b310
......@@ -39,6 +39,16 @@
# If mac-sources are not specified, a default value of 'any' is used.
"mac-sources": [ "client-link-addr-option", "duid", "ipv6-link-local" ],
# RFC6422 defines a mechanism called relay-supplied options option. The relay
# agent may insert certain options that the server will echo back to the
# client, if certain criteria are met. One condition is that the option must
# be RSOO-enabled (i.e. allowed to be echoed back). IANA maintains a list
# of those options here:
# http://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml#options-relay-supplied
# However, it is possible to allow the server to echo back additional options.
# This entry marks options 110, 120 and 130 as RSOO-enabled.
"relay-supplied-options": [ "110", "120", "130" ],"
# Addresses will be assigned with preferred and valid lifetimes
# being 3000 and 4000, respectively. Client is told to start
# renewing after 1000 seconds. If the server does not repond
......
......@@ -885,6 +885,9 @@ temporarily override a list of interface names and listen on all interfaces.
<row><entry>clt-time</entry><entry>46</entry><entry>uint32</entry><entry>false</entry></row>
<row><entry>lq-relay-data</entry><entry>47</entry><entry>record</entry><entry>false</entry></row>
<row><entry>lq-client-link</entry><entry>48</entry><entry>ipv6-address</entry><entry>true</entry></row>
<row><entry>erp-local-domain-name</entry><entry>65</entry><entry>fqdn</entry><entry>false</entry></row>
<row><entry>rsoo</entry><entry>66</entry><entry>empty</entry><entry>false</entry></row>
<row><entry>client-linklayer-addr</entry><entry>79</entry><entry>binary</entry><entry>false</entry></row>
</tbody>
</tgroup>
</table>
......@@ -1368,6 +1371,43 @@ should include options from the isc option space:
</para>
</section>
<section id="dhcp6-rsoo">
<title>Relay-Supplied Options</title>
<para><ulink url="http://tools.ietf.org/html/rfc6422">RFC 6422</ulink>
defines a mechanism called Relay-Supplied DHCP Options. In certain cases relay
agents are the only entities that may have specific information. They can
insert options when relaying messages from the client to the server. The
server will then do certain checks and copy those options to the response
that will be sent to the client.</para>
<para>There are certain conditions that must be met for the option to be
included. First, the server must not provide the option by itself. In
other words, if both relay and server provide an option, the server always
takes precedence. Second, the option must be RSOO-enabled. IANA mantains a
list of RSOO-enabled options <ulink url="http://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml#options-relay-supplied">here</ulink>.
However, there may be cases when system administrators want to echo other
options. Kea can be instructed to treat other options as RSOO-enabled.
For example, to mark options 110, 120 and 130 as RSOO-enabled, the following
syntax should be used:
<screen>
"Dhcp6": {
<userinput>"relay-supplied-options": [ "110", "120", "130" ],</userinput>
...
}
</screen>
</para>
<para>As of March 2015, only option 65 is RSOO-enabled by IANA. This
option will always be treated as such and there's no need to explicitly
mark it. Also, when enabling standard options, it is possible to use their
names, rather than option code, e.g. (e.g. use
<command>dns-servers</command> instead of <command>23</command>). See
<xref linkend="dhcp6-std-options-list" /> for the names. In certain cases
it could also work for custom options, but due to the nature of the parser
code this may be unreliable and should be avoided.
</para>
</section>
<section id="dhcp6-client-classifier">
<title>Client Classification in DHCPv6</title>
<note>
......@@ -2425,7 +2465,8 @@ should include options from the isc option space:
<simpara><emphasis>Dynamic Host Configuration Protocol for IPv6</emphasis>,
<ulink url="http://tools.ietf.org/html/rfc3315">RFC 3315</ulink>:
Supported messages are SOLICIT,
ADVERTISE, REQUEST, RELEASE, RENEW, REBIND, CONFIRM and REPLY.</simpara>
ADVERTISE, REQUEST, RELEASE, RENEW, REBIND, INFORMATION-REQUEST,
CONFIRM and REPLY.</simpara>
</listitem>
<listitem>
<simpara><emphasis>IPv6 Prefix Options for
......@@ -2452,6 +2493,13 @@ should include options from the isc option space:
<ulink url="http://tools.ietf.org/html/rfc4704">RFC 4704</ulink>:
Supported option is CLIENT_FQDN.</simpara>
</listitem>
<listitem>
<simpara><emphasis>Relay-Supplied DHCP Options</emphasis>,
<ulink url="http://tools.ietf.org/html/rfc6422">RFC 6422</ulink>:
Full functionality is supported: OPTION_RSOO, ability of the server
to echo back the options, checks whether an option is RSOO-enabled,
ability to mark additional options as RSOO-enabled.</simpara>
</listitem>
<listitem>
<simpara><emphasis>Client Link-Layer Address Option in
DHCPv6</emphasis>,
......
......@@ -256,7 +256,7 @@ bool Dhcpv6Srv::run() {
// Handle next signal received by the process. It must be called after
// an attempt to receive a packet to properly handle server shut down.
// The SIGTERM or SIGINT will be received prior to, or during execution
// of select() (select is invoked by recivePacket()). When that happens,
// of select() (select is invoked by receivePacket()). When that happens,
// select will be interrupted. The signal handler will be invoked
// immediately after select(). The handler will set the shutdown flag
// and cause the process to terminate before the next select() function
......@@ -444,6 +444,19 @@ bool Dhcpv6Srv::run() {
}
if (rsp) {
// Process relay-supplied options. It is important to call this very
// late in the process, because we now have all the options the
// server wanted to send already set. This is important, because
// RFC6422, section 6 states:
//
// The server SHOULD discard any options that appear in the RSOO
// for which it already has one or more candidates.
//
// So we ignore any RSOO options if there's an option with the same
// code already present.
processRSOO(query, rsp);
rsp->setRemoteAddr(query->getRemoteAddr());
rsp->setLocalAddr(query->getLocalAddr());
......@@ -2711,5 +2724,39 @@ Daemon::getVersion(bool extended) {
return (tmp.str());
}
void Dhcpv6Srv::processRSOO(const Pkt6Ptr& query, const Pkt6Ptr& rsp) {
if (query->relay_info_.empty()) {
// RSOO is inserted by relay agents, nothing to do here if it's
// a direct message.
return;
}
// Get RSOO configuration.
ConstCfgRSOOPtr cfg_rsoo = CfgMgr::instance().getCurrentCfg()->getCfgRSOO();
// Let's get over all relays (encapsulation levels). We need to do
// it in the same order as the client packet traversed the relays.
for (int i = query->relay_info_.size(); i > 0 ; --i) {
OptionPtr rsoo_container = query->getRelayOption(D6O_RSOO, i - 1);
if (rsoo_container) {
// There are RSOO options. Let's get through them one by one
// and if it's RSOO-enabled and there's no such option provided yet,
// copy it to the server's response
const OptionCollection& rsoo = rsoo_container->getOptions();
for (OptionCollection::const_iterator opt = rsoo.begin();
opt != rsoo.end(); ++opt) {
// Echo option if it is RSOO enabled option and there is no such
// option added yet.
if (cfg_rsoo->enabled(opt->second->getType()) &&
!rsp->getOption(opt->second->getType())) {
rsp->addOption(opt->second);
}
}
}
}
}
};
};
......@@ -606,6 +606,16 @@ protected:
/// @return HWaddr pointer (or NULL if configured methods fail)
static HWAddrPtr getMAC(const Pkt6Ptr& pkt);
/// @brief Processes Relay-supplied options, if present
///
/// This method implements RFC6422. It checks if there are any RSOO options
/// inserted by the relay agents in the query message. If there are, they
/// are copied over to the response if they meet the following criteria:
/// - the option is marked as RSOO-enabled (see relay-supplied-options
/// configuration parameter)
/// - there is no such option provided by the server)
void processRSOO(const Pkt6Ptr& query, const Pkt6Ptr& rsp);
/// @brief this is a prefix added to the contend of vendor-class option
///
/// If incoming packet has a vendor class option, its content is
......
......@@ -41,6 +41,7 @@
#include <boost/shared_ptr.hpp>
#include <iostream>
#include <limits>
#include <map>
#include <vector>
......@@ -554,6 +555,87 @@ public:
ParserCollection subnets_;
};
/// @brief Parser for list of RSOO options
///
/// This parser handles a Dhcp6/relay-supplied-options entry. It contains a
/// list of RSOO-enabled options which should be sent back to the client.
///
/// The options on this list can be specified using an option code or option
/// name. Therefore, the values on the list should always be enclosed in
/// "quotes".
class RSOOListConfigParser : public DhcpConfigParser {
public:
/// @brief constructor
///
/// As this is a dedicated parser, it must be used to parse
/// "relay-supplied-options" parameter only. All other types will throw exception.
///
/// @param param_name name of the configuration parameter being parsed
/// @throw BadValue if supplied parameter name is not "relay-supplied-options"
RSOOListConfigParser(const std::string& param_name) {
if (param_name != "relay-supplied-options") {
isc_throw(BadValue, "Internal error. RSOO configuration "
"parser called for the wrong parameter: " << param_name);
}
}
/// @brief parses parameters value
///
/// Parses configuration entry (list of sources) and adds each element
/// to the RSOO list.
///
/// @param value pointer to the content of parsed values
virtual void build(isc::data::ConstElementPtr value) {
try {
BOOST_FOREACH(ConstElementPtr source_elem, value->listValue()) {
std::string option_str = source_elem->stringValue();
// This option can be either code (integer) or name. Let's try code first
int64_t code = 0;
try {
code = boost::lexical_cast<int64_t>(option_str);
// Protect against the negative value and too high value.
if (code < 0) {
isc_throw(BadValue, "invalid option code value specified '"
<< option_str << "', the option code must be a"
" non-negative value");
} else if (code > std::numeric_limits<uint16_t>::max()) {
isc_throw(BadValue, "invalid option code value specified '"
<< option_str << "', the option code must not be"
" greater than '" << std::numeric_limits<uint16_t>::max()
<< "'");
}
} catch (const boost::bad_lexical_cast &) {
// Oh well, it's not a number
}
if (!code) {
OptionDefinitionPtr def = LibDHCP::getOptionDef(Option::V6, option_str);
if (def) {
code = def->getCode();
} else {
isc_throw(BadValue, "unable to find option code for the "
" specified option name '" << option_str << "'"
" while parsing the list of enabled"
" relay-supplied-options");
}
}
CfgMgr::instance().getStagingCfg()->getCfgRSOO()->enable(code);
}
} catch (const std::exception& ex) {
// Rethrow exception with the appended position of the parsed
// element.
isc_throw(DhcpConfigError, ex.what() << " (" << value->getPosition() << ")");
}
}
/// @brief Does nothing.
virtual void commit() {}
};
} // anonymous namespace
namespace isc {
......@@ -597,6 +679,8 @@ namespace dhcp {
} else if (config_id.compare("mac-sources") == 0) {
parser = new MACSourcesListConfigParser(config_id,
globalContext());
} else if (config_id.compare("relay-supplied-options") == 0) {
parser = new RSOOListConfigParser(config_id);
} else {
isc_throw(DhcpConfigError,
"unsupported global configuration parameter: "
......
......@@ -3780,4 +3780,120 @@ TEST_F(Dhcp6ParserTest, hostReservationPerSubnet) {
EXPECT_EQ(Subnet::HR_ALL, subnet->getHostReservationMode());
}
/// The goal of this test is to verify that configuration can include
/// Relay Supplied options (specified as numbers).
TEST_F(Dhcp6ParserTest, rsooNumbers) {
ConstElementPtr status;
EXPECT_NO_THROW(status = configureDhcp6Server(srv_,
Element::fromJSON("{ " + genIfaceConfig() + ","
"\"relay-supplied-options\": [ \"10\", \"20\", \"30\" ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
"\"subnet6\": [ ], "
"\"valid-lifetime\": 4000 }")));
// returned value should be 0 (success)
checkResult(status, 0);
// The following codes should be enabled now
EXPECT_TRUE(CfgMgr::instance().getStagingCfg()->getCfgRSOO()->enabled(10));
EXPECT_TRUE(CfgMgr::instance().getStagingCfg()->getCfgRSOO()->enabled(20));
EXPECT_TRUE(CfgMgr::instance().getStagingCfg()->getCfgRSOO()->enabled(30));
// This option is on the IANA list, so it should be allowed all the time
// (http://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml)
EXPECT_TRUE(CfgMgr::instance().getStagingCfg()->getCfgRSOO()
->enabled(D6O_ERP_LOCAL_DOMAIN_NAME));
// Those options are not enabled
EXPECT_FALSE(CfgMgr::instance().getStagingCfg()->getCfgRSOO()->enabled(25));
EXPECT_FALSE(CfgMgr::instance().getStagingCfg()->getCfgRSOO()->enabled(1));
}
/// The goal of this test is to verify that configuration can include
/// Relay Supplied options (specified as names).
TEST_F(Dhcp6ParserTest, rsooNames) {
ConstElementPtr status;
EXPECT_NO_THROW(status = configureDhcp6Server(srv_,
Element::fromJSON("{ " + genIfaceConfig() + ","
"\"relay-supplied-options\": [ \"dns-servers\", \"remote-id\" ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
"\"subnet6\": [ ], "
"\"valid-lifetime\": 4000 }")));
// returned value should be 0 (success)
checkResult(status, 0);
for (uint16_t code = 0; code < D6O_NAME_SERVERS; ++code) {
EXPECT_FALSE(CfgMgr::instance().getStagingCfg()->getCfgRSOO()
->enabled(code)) << " for option code " << code;
}
// The following code should be enabled now
EXPECT_TRUE(CfgMgr::instance().getStagingCfg()->getCfgRSOO()
->enabled(D6O_NAME_SERVERS));
for (uint16_t code = D6O_NAME_SERVERS + 1; code < D6O_REMOTE_ID; ++code) {
EXPECT_FALSE(CfgMgr::instance().getStagingCfg()->getCfgRSOO()
->enabled(code)) << " for option code " << code;
}
// Check remote-id. It should be enabled.
EXPECT_TRUE(CfgMgr::instance().getStagingCfg()->getCfgRSOO()
->enabled(D6O_REMOTE_ID));
for (uint16_t code = D6O_REMOTE_ID + 1; code < D6O_ERP_LOCAL_DOMAIN_NAME; ++code) {
EXPECT_FALSE(CfgMgr::instance().getStagingCfg()->getCfgRSOO()
->enabled(code)) << " for option code " << code;
}
// This option is on the IANA list, so it should be allowed all the time
// (http://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml)
EXPECT_TRUE(CfgMgr::instance().getStagingCfg()->getCfgRSOO()
->enabled(D6O_ERP_LOCAL_DOMAIN_NAME));
for (uint16_t code = D6O_ERP_LOCAL_DOMAIN_NAME + 1; code < 300; ++code) {
EXPECT_FALSE(CfgMgr::instance().getStagingCfg()->getCfgRSOO()
->enabled(code)) << " for option code " << code;
}
}
TEST_F(Dhcp6ParserTest, rsooNegativeNumber) {
ConstElementPtr status;
EXPECT_NO_THROW(status = configureDhcp6Server(srv_,
Element::fromJSON("{ " + genIfaceConfig() + ","
"\"relay-supplied-options\": [ \"80\", \"-2\" ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
"\"subnet6\": [ ], "
"\"valid-lifetime\": 4000 }")));
// returned value should be 0 (success)
checkResult(status, 1);
EXPECT_TRUE(errorContainsPosition(status, "<string>"));
}
TEST_F(Dhcp6ParserTest, rsooBogusName) {
ConstElementPtr status;
EXPECT_NO_THROW(status = configureDhcp6Server(srv_,
Element::fromJSON("{ " + genIfaceConfig() + ","
"\"relay-supplied-options\": [ \"bogus\", \"dns-servers\" ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
"\"subnet6\": [ ], "
"\"valid-lifetime\": 4000 }")));
// returned value should be 0 (success)
checkResult(status, 1);
EXPECT_TRUE(errorContainsPosition(status, "<string>"));
}
};
......@@ -76,6 +76,8 @@ Dhcp6Client::applyRcvdConfiguration(const Pkt6Ptr& reply) {
for (Opts::const_iterator opt = opts.begin(); opt != opts.end(); ++opt) {
Option6IAPtr ia = boost::dynamic_pointer_cast<Option6IA>(opt->second);
if (!ia) {
// This is not IA, so let's just store it.
config_.options_.insert(*opt);
continue;
}
......@@ -453,13 +455,19 @@ Dhcp6Client::sendMsg(const Pkt6Ptr& msg) {
srv_->shutdown_ = false;
// The client is configured to send through the relay. We achieve that
// adding the relay structure.
if (use_relay_) {
Pkt6::RelayInfo relay;
relay.linkaddr_ = relay_link_addr_;
relay.peeraddr_ = asiolink::IOAddress("fe80::1");
relay.msg_type_ = DHCPV6_RELAY_FORW;
relay.hop_count_ = 1;
msg->relay_info_.push_back(relay);
if (use_relay_ || !relay_info_.empty()) {
if (relay_info_.empty()) {
// Let's craft the relay info by hand
Pkt6::RelayInfo relay;
relay.linkaddr_ = relay_link_addr_;
relay.peeraddr_ = asiolink::IOAddress("fe80::1");
relay.msg_type_ = DHCPV6_RELAY_FORW;
relay.hop_count_ = 1;
msg->relay_info_.push_back(relay);
} else {
// The test provided relay_info_, let's use that.
msg->relay_info_ = relay_info_;
}
}
// Repack the message to simulate wire-data parsing.
msg->pack();
......
......@@ -74,10 +74,17 @@ public:
/// @brief Holds the current client configuration obtained from the
/// server over DHCP.
///
/// Currently it simply contains the collection of leases acquired.
/// Currently it simply contains the collection of leases acquired
/// and a list of options. Note: this is a simple copy of all
/// non-IA options and often includes "protocol" options, like
/// server-id and client-id.
struct Configuration {
/// @brief List of received leases
std::vector<LeaseInfo> leases_;
/// @brief List of received options
OptionCollection options_;
/// @brief Status code received in the global option scope.
uint16_t status_code_;
......@@ -103,6 +110,21 @@ public:
status_code_ = 0;
received_status_code_ = false;
}
/// @brief Finds an option with the specific code in the received
/// configuration.
///
/// @param code Option code.
///
/// @return Pointer to the option if the option exists, or NULL if
/// the option doesn't exist.
OptionPtr findOption(const uint16_t code) const {
std::multimap<unsigned int, OptionPtr>::const_iterator it = options_.find(code);
if (it != options_.end()) {
return (it->second);
}
return (OptionPtr());
}
};
/// @brief Holds the DHCPv6 messages taking part in transaction between
......@@ -388,6 +410,12 @@ public:
/// @brief Link address of the relay to be used for relayed messages.
asiolink::IOAddress relay_link_addr_;
/// @brief RelayInfo (information about relays)
///
/// Dhcp6Client will typically contruct this info itself, but if
/// it is provided here by the test, this data will be used as is.
std::vector<Pkt6::RelayInfo> relay_info_;
/// @brief Controls whether the client will send ORO
///
/// The actual content of the ORO is specified in oro_.
......
......@@ -40,6 +40,7 @@
#include <hooks/server_hooks.h>
#include <dhcp6/tests/dhcp6_test_utils.h>
#include <dhcp6/tests/dhcp6_client.h>
#include <dhcp/tests/pkt_captures.h>
#include <config/ccsession.h>
#include <boost/pointer_cast.hpp>
......@@ -2094,6 +2095,262 @@ TEST_F(Dhcpv6SrvTest, relayOverrideAndClientClass) {
EXPECT_TRUE(subnet1 == srv_.selectSubnet(sol));
}
/// @brief Creates RSOO option with suboptions
///
/// Creates Relay-Supplied Options option that includes nested options. The
/// codes of those nested options are specified in codes parameter. Content of
/// the options is controlled with payload parameter. When it is zero, option
/// code will be used (e.g. option 100 will contain repeating bytes of value 100).
/// When non-zero is used, payload will be used. Each suboption length is always
/// set to the arbitrarily chosen value of 10.
///
/// @param codes a vector of option codes to be created
/// @param payload specified payload (0 = fill payload with repeating option code)
/// @return RSOO with nested options
OptionPtr createRSOO(const std::vector<uint16_t> codes, uint8_t payload = 0) {
OptionDefinitionPtr def = LibDHCP::getOptionDef(Option::V6, D6O_RSOO);
if (!def) {
isc_throw(BadValue, "Can't find RSOO definition");
}
OptionPtr rsoo_container(new OptionCustom(*def, Option::V6));
for (int i = 0; i < codes.size(); ++i) {
OptionBuffer buf(10, payload ? payload : codes[i]); // let's make the option 10 bytes long
rsoo_container->addOption(OptionPtr(new Option(Option::V6, codes[i], buf)));
}
return (rsoo_container);
}
// Test that the server processes RSOO (Relay Supplied Options option) correctly,
// i.e. it includes in its response the options that are inserted by the relay.
// The server must do this only for options that are RSOO-enabled.
TEST_F(Dhcpv6SrvTest, rsoo) {
Dhcp6Client client;
string config =
"{"
" \"relay-supplied-options\": [ \"110\", \"120\", \"130\" ],"
" \"preferred-lifetime\": 3000,"
" \"rebind-timer\": 2000, "
" \"renew-timer\": 1000, "
" \"subnet6\": [ { "
" \"pools\": [ { \"pool\": \"2001:db8::/64\" } ],"
" \"subnet\": \"2001:db8::/48\" "
" } ],"
" \"valid-lifetime\": 4000"
"}";
EXPECT_NO_THROW(configure(config, *client.getServer()));
// Now pretend the packet came via one relay.
Pkt6::RelayInfo relay;
relay.msg_type_ = DHCPV6_RELAY_FORW;
relay.hop_count_ = 1;
relay.linkaddr_ = IOAddress("2001:db8::1");
relay.peeraddr_ = IOAddress("fe80::1");
vector<uint16_t> rsoo1;
rsoo1.push_back(109);
rsoo1.push_back(110);
rsoo1.push_back(111);
// The relay will request echoing back 3 options: 109, 110, 111.
// The configuration allows echoing back only 110.
OptionPtr opt = createRSOO(rsoo1);
relay.options_.insert(make_pair(opt->getType(), opt));
client.relay_info_.push_back(relay);
client.doSARR();
// Option 110 should be copied to the client
EXPECT_NE(client.config_.options_.find(110), client.config_.options_.end());
// Options 109 and 111 should not be copied (they are not RSOO-enabled)
EXPECT_EQ(client.config_.options_.find(109), client.config_.options_.end());
EXPECT_EQ(client.config_.options_.find(111), client.config_.options_.end());
}
// Test that the server processes RSOO (Relay Supplied Options option) correctly
// when there are more relays. In particular, the following case is tested:
// if relay1 inserts option A and B, relay2 inserts option B and C, the response
// should include options A, B and C. The server must use instance of option B
// that comes from the first relay, not the second one.
TEST_F(Dhcpv6SrvTest, rsoo2relays) {
Dhcp6Client client;
string config =
"{"
" \"relay-supplied-options\": [ \"110\", \"120\", \"130\" ],"
" \"preferred-lifetime\": 3000,"
" \"rebind-timer\": 2000, "
" \"