Commit 92758ecf authored by Tomek Mrugalski's avatar Tomek Mrugalski 🛰
Browse files

[master] Merge branch 'master' into trac2325 (RENEW support in DHCPv6)

Conflicts:
	ChangeLog
parents a56f7ff8 7f6c9d05
524. [func] tomek
b10-dhcp6 is now able to handle RENEW messages. Leases are
renewed and REPLY responses are sent back to clients.
(Trac #2325, git 7f6c9d057cc0a7a10f41ce7da9c8565b9ee85246)
523. [bug] muks
Fixed a problem in inmem NSEC3 lookup (for, instance when using a
zone with no non-apex names) which caused exceptions when the zone
......
......@@ -22,6 +22,11 @@ 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_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
address assignment. The most likely cause is a problem with the client.
% DHCP6_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 IPv6 DHCP server.
......@@ -81,6 +86,13 @@ This message indicates that the server failed to grant (in response to
received REQUEST) a lease for a given client. There may be many reasons for
such failure. Each specific failure is logged in a separate log entry.
% DHCP6_REQUIRED_OPTIONS_CHECK_FAIL %1 message received from %2 failed the following check: %3
This message indicates that received DHCPv6 packet is invalid. This may be due
to a number of reasons, e.g. the mandatory client-id option is missing,
the server-id forbidden in that particular type of message is present,
there is more than one instance of client-id or server-id present,
etc. The exact reason for rejecting the packet is included in the message.
% DHCP6_NOT_RUNNING IPv6 DHCP server is not running
A warning message is issued when an attempt is made to shut down the
IPv6 DHCP server but it is not running.
......@@ -189,3 +201,18 @@ which the DHCPv6 server has not been configured. The cause is most likely due
to a misconfiguration of the server. The packet processing will continue, but
the response will only contain generic configuration parameters and no
addresses or prefixes.
% DHCP6_UNKNOWN_RENEW received RENEW from client (duid=%1, iaid=%2) in subnet %3
This warning message is printed when client attempts to renew a lease,
but no such lease is known by the server. It typically means that
client has attempted to use its lease past its lifetime: causes of this
include a adjustment of the clients date/time setting or poor support
for sleep/recovery. A properly implemented client will recover from such
a situation by restarting the lease allocation process after receiving
a negative reply from the server.
An alternative cause could be that the server has lost its database
recently and does not recognize its well-behaving clients. This is more
probable if you see many such messages. Clients will recover from this,
but they will most likely get a different IP addresses and experience
a brief service interruption.
......@@ -127,50 +127,57 @@ bool Dhcpv6Srv::run() {
continue;
}
LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL, DHCP6_PACKET_RECEIVED)
.arg(serverReceivedPacketName(query->getType()));
.arg(query->getName());
LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL_DATA, DHCP6_QUERY_DATA)
.arg(static_cast<int>(query->getType()))
.arg(query->getBuffer().getLength())
.arg(query->toText());
switch (query->getType()) {
case DHCPV6_SOLICIT:
rsp = processSolicit(query);
break;
try {
switch (query->getType()) {
case DHCPV6_SOLICIT:
rsp = processSolicit(query);
break;
case DHCPV6_REQUEST:
rsp = processRequest(query);
break;
case DHCPV6_REQUEST:
rsp = processRequest(query);
break;
case DHCPV6_RENEW:
rsp = processRenew(query);
break;
case DHCPV6_RENEW:
rsp = processRenew(query);
break;
case DHCPV6_REBIND:
rsp = processRebind(query);
break;
case DHCPV6_REBIND:
rsp = processRebind(query);
break;
case DHCPV6_CONFIRM:
rsp = processConfirm(query);
break;
case DHCPV6_CONFIRM:
rsp = processConfirm(query);
break;
case DHCPV6_RELEASE:
case DHCPV6_RELEASE:
rsp = processRelease(query);
break;
case DHCPV6_DECLINE:
rsp = processDecline(query);
break;
case DHCPV6_DECLINE:
rsp = processDecline(query);
break;
case DHCPV6_INFORMATION_REQUEST:
rsp = processInfRequest(query);
break;
case DHCPV6_INFORMATION_REQUEST:
rsp = processInfRequest(query);
break;
default:
// Only action is to output a message if debug is enabled,
// and that will be covered by the debug statement before
// the "switch" statement.
;
default:
// Only action is to output a message if debug is enabled,
// and that will be covered by the debug statement before
// the "switch" statement.
;
}
} catch (const RFCViolation& e) {
LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_REQUIRED_OPTIONS_CHECK_FAIL)
.arg(query->getName())
.arg(query->getRemoteAddr())
.arg(e.what());
}
if (rsp) {
......@@ -196,9 +203,6 @@ bool Dhcpv6Srv::run() {
}
}
}
// TODO add support for config session (see src/bin/auth/main.cc)
// so this daemon can be controlled from bob
}
return (true);
......@@ -374,6 +378,54 @@ OptionPtr Dhcpv6Srv::createStatusCode(uint16_t code, const std::string& text) {
return (option_status);
}
void Dhcpv6Srv::sanityCheck(const Pkt6Ptr& pkt, RequirementLevel clientid,
RequirementLevel serverid) {
Option::OptionCollection client_ids = pkt->getOptions(D6O_CLIENTID);
switch (clientid) {
case MANDATORY:
if (client_ids.size() != 1) {
isc_throw(RFCViolation, "Exactly 1 client-id option expected in "
<< pkt->getName() << ", but " << client_ids.size()
<< " received");
}
break;
case OPTIONAL:
if (client_ids.size() > 1) {
isc_throw(RFCViolation, "Too many (" << client_ids.size()
<< ") client-id options received in " << pkt->getName());
}
break;
case FORBIDDEN:
// doesn't make sense - client-id is always allowed
break;
}
Option::OptionCollection server_ids = pkt->getOptions(D6O_SERVERID);
switch (serverid) {
case FORBIDDEN:
if (server_ids.size() > 0) {
isc_throw(RFCViolation, "Exactly 1 server-id option expected, but "
<< server_ids.size() << " received in " << pkt->getName());
}
break;
case MANDATORY:
if (server_ids.size() != 1) {
isc_throw(RFCViolation, "Invalid number of server-id options received ("
<< server_ids.size() << "), exactly 1 expected in message "
<< pkt->getName());
}
break;
case OPTIONAL:
if (server_ids.size() > 1) {
isc_throw(RFCViolation, "Too many (" << server_ids.size()
<< ") server-id options received in " << pkt->getName());
}
}
}
Subnet6Ptr Dhcpv6Srv::selectSubnet(const Pkt6Ptr& question) {
Subnet6Ptr subnet = CfgMgr::instance().getSubnet6(question->getRemoteAddr());
......@@ -387,7 +439,7 @@ void Dhcpv6Srv::assignLeases(const Pkt6Ptr& question, Pkt6Ptr& answer) {
// We need to select a subnet the client is connected in.
Subnet6Ptr subnet = selectSubnet(question);
if (subnet) {
if (!subnet) {
// This particular client is out of luck today. We do not have
// information about the subnet he is connected to. This likely means
// misconfiguration of the server (or some relays). We will continue to
......@@ -395,12 +447,13 @@ void Dhcpv6Srv::assignLeases(const Pkt6Ptr& question, Pkt6Ptr& answer) {
// addresses or prefixes, no subnet specific configuration etc. The only
// thing this client can get is some global information (like DNS
// servers).
LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL_DATA, DHCP6_SUBNET_SELECTED)
.arg(subnet->toText());
} else {
// perhaps this should be logged on some higher level? This is most likely
// configuration bug.
LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_SUBNET_SELECTION_FAILED);
LOG_ERROR(dhcp6_logger, DHCP6_SUBNET_SELECTION_FAILED);
} else {
LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL_DATA, DHCP6_SUBNET_SELECTED)
.arg(subnet->toText());
}
// @todo: We should implement Option6Duid some day, but we can do without it
......@@ -414,6 +467,10 @@ void Dhcpv6Srv::assignLeases(const Pkt6Ptr& question, Pkt6Ptr& answer) {
OptionPtr opt_duid = question->getOption(D6O_CLIENTID);
if (opt_duid) {
duid = DuidPtr(new DUID(opt_duid->getData()));
} else {
LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_CLIENTID_MISSING);
// Let's drop the message. This client is not sane.
isc_throw(RFCViolation, "Mandatory client-id is missing in received message");
}
// Now that we have all information about the client, let's iterate over all
......@@ -426,7 +483,7 @@ void Dhcpv6Srv::assignLeases(const Pkt6Ptr& question, Pkt6Ptr& answer) {
opt != question->options_.end(); ++opt) {
switch (opt->second->getType()) {
case D6O_IA_NA: {
OptionPtr answer_opt = handleIA_NA(subnet, duid, question,
OptionPtr answer_opt = assignIA_NA(subnet, duid, question,
boost::dynamic_pointer_cast<Option6IA>(opt->second));
if (answer_opt) {
answer->addOption(answer_opt);
......@@ -439,8 +496,8 @@ void Dhcpv6Srv::assignLeases(const Pkt6Ptr& question, Pkt6Ptr& answer) {
}
}
OptionPtr Dhcpv6Srv::handleIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid, Pkt6Ptr question,
boost::shared_ptr<Option6IA> ia) {
OptionPtr Dhcpv6Srv::assignIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
Pkt6Ptr question, boost::shared_ptr<Option6IA> ia) {
// If there is no subnet selected for handling this IA_NA, the only thing to do left is
// to say that we are sorry, but the user won't get an address. As a convenience, we
// use a different status text to indicate that (compare to the same status code,
......@@ -535,8 +592,111 @@ OptionPtr Dhcpv6Srv::handleIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
return (ia_rsp);
}
OptionPtr Dhcpv6Srv::renewIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
Pkt6Ptr question, boost::shared_ptr<Option6IA> ia) {
Lease6Ptr lease = LeaseMgrFactory::instance().getLease6(*duid, ia->getIAID(),
subnet->getID());
if (!lease) {
// client renewing a lease that we don't know about.
// Create empty IA_NA option with IAID matching the request.
boost::shared_ptr<Option6IA> ia_rsp(new Option6IA(D6O_IA_NA, ia->getIAID()));
// Insert status code NoAddrsAvail.
ia_rsp->addOption(createStatusCode(STATUS_NoAddrsAvail,
"Sorry, no known leases for this duid/iaid."));
LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL, DHCP6_UNKNOWN_RENEW)
.arg(duid->toText())
.arg(ia->getIAID())
.arg(subnet->toText());
return (ia_rsp);
}
lease->preferred_lft_ = subnet->getPreferred();
lease->valid_lft_ = subnet->getValid();
lease->t1_ = subnet->getT1();
lease->t2_ = subnet->getT2();
lease->cltt_ = time(NULL);
LeaseMgrFactory::instance().updateLease6(lease);
// Create empty IA_NA option with IAID matching the request.
boost::shared_ptr<Option6IA> ia_rsp(new Option6IA(D6O_IA_NA, ia->getIAID()));
ia_rsp->setT1(subnet->getT1());
ia_rsp->setT2(subnet->getT2());
boost::shared_ptr<Option6IAAddr> addr(new Option6IAAddr(D6O_IAADDR,
lease->addr_, lease->preferred_lft_,
lease->valid_lft_));
ia_rsp->addOption(addr);
return (ia_rsp);
}
void Dhcpv6Srv::renewLeases(const Pkt6Ptr& renew, Pkt6Ptr& reply) {
// We need to renew addresses for all IA_NA options in the client's
// RENEW message.
// We need to select a subnet the client is connected in.
Subnet6Ptr subnet = selectSubnet(renew);
if (!subnet) {
// This particular client is out of luck today. We do not have
// information about the subnet he is connected to. This likely means
// misconfiguration of the server (or some relays). We will continue to
// process this message, but our response will be almost useless: no
// addresses or prefixes, no subnet specific configuration etc. The only
// thing this client can get is some global information (like DNS
// servers).
// perhaps this should be logged on some higher level? This is most likely
// configuration bug.
LOG_ERROR(dhcp6_logger, DHCP6_SUBNET_SELECTION_FAILED);
} else {
LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL_DATA, DHCP6_SUBNET_SELECTED)
.arg(subnet->toText());
}
// Let's find client's DUID. Client is supposed to include its client-id
// option almost all the time (the only exception is an anonymous inf-request,
// but that is mostly a theoretical case). Our allocation engine needs DUID
// and will refuse to allocate anything to anonymous clients.
OptionPtr opt_duid = renew->getOption(D6O_CLIENTID);
if (!opt_duid) {
// This should not happen. We have checked this before.
reply->addOption(createStatusCode(STATUS_UnspecFail,
"You did not include mandatory client-id"));
return;
}
DuidPtr duid(new DUID(opt_duid->getData()));
for (Option::OptionCollection::iterator opt = renew->options_.begin();
opt != renew->options_.end(); ++opt) {
switch (opt->second->getType()) {
case D6O_IA_NA: {
OptionPtr answer_opt = renewIA_NA(subnet, duid, renew,
boost::dynamic_pointer_cast<Option6IA>(opt->second));
if (answer_opt) {
reply->addOption(answer_opt);
}
break;
}
default:
break;
}
}
}
Pkt6Ptr Dhcpv6Srv::processSolicit(const Pkt6Ptr& solicit) {
sanityCheck(solicit, MANDATORY, FORBIDDEN);
Pkt6Ptr advertise(new Pkt6(DHCPV6_ADVERTISE, solicit->getTransid()));
copyDefaultOptions(solicit, advertise);
......@@ -549,6 +709,9 @@ Pkt6Ptr Dhcpv6Srv::processSolicit(const Pkt6Ptr& solicit) {
}
Pkt6Ptr Dhcpv6Srv::processRequest(const Pkt6Ptr& request) {
sanityCheck(request, MANDATORY, MANDATORY);
Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, request->getTransid()));
copyDefaultOptions(request, reply);
......@@ -561,8 +724,17 @@ Pkt6Ptr Dhcpv6Srv::processRequest(const Pkt6Ptr& request) {
}
Pkt6Ptr Dhcpv6Srv::processRenew(const Pkt6Ptr& renew) {
/// @todo: Implement this
sanityCheck(renew, MANDATORY, MANDATORY);
Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, renew->getTransid()));
copyDefaultOptions(renew, reply);
appendDefaultOptions(renew, reply);
appendRequestedOptions(renew, reply);
renewLeases(renew, reply);
return reply;
}
......@@ -596,48 +768,5 @@ Pkt6Ptr Dhcpv6Srv::processInfRequest(const Pkt6Ptr& infRequest) {
return reply;
}
const char*
Dhcpv6Srv::serverReceivedPacketName(uint8_t type) {
static const char* CONFIRM = "CONFIRM";
static const char* DECLINE = "DECLINE";
static const char* INFORMATION_REQUEST = "INFORMATION_REQUEST";
static const char* REBIND = "REBIND";
static const char* RELEASE = "RELEASE";
static const char* RENEW = "RENEW";
static const char* REQUEST = "REQUEST";
static const char* SOLICIT = "SOLICIT";
static const char* UNKNOWN = "UNKNOWN";
switch (type) {
case DHCPV6_CONFIRM:
return (CONFIRM);
case DHCPV6_DECLINE:
return (DECLINE);
case DHCPV6_INFORMATION_REQUEST:
return (INFORMATION_REQUEST);
case DHCPV6_REBIND:
return (REBIND);
case DHCPV6_RELEASE:
return (RELEASE);
case DHCPV6_RENEW:
return (RENEW);
case DHCPV6_REQUEST:
return (REQUEST);
case DHCPV6_SOLICIT:
return (SOLICIT);
default:
;
}
return (UNKNOWN);
}
};
};
......@@ -30,6 +30,23 @@
namespace isc {
namespace dhcp {
/// An exception that is thrown if a DHCPv6 protocol violation occurs while
/// processing a message (e.g. a mandatory option is missing)
class RFCViolation : public isc::Exception {
public:
/// @brief constructor
///
/// @param file name of the file, where exception occurred
/// @param line line of the file, where exception occurred
/// @param what text description of the issue that caused exception
RFCViolation(const char* file, size_t line, const char* what) :
isc::Exception(file, line, what) {}
};
/// @brief DHCPv6 server service.
///
/// This class represents DHCPv6 server. It contains all
......@@ -45,6 +62,12 @@ namespace dhcp {
class Dhcpv6Srv : public boost::noncopyable {
public:
/// @brief defines if certain option may, must or must not appear
typedef enum {
FORBIDDEN,
MANDATORY,
OPTIONAL
} RequirementLevel;
/// @brief Minimum length of a MAC address to be used in DUID generation.
static const size_t MIN_MAC_LEN = 6;
......@@ -83,24 +106,20 @@ public:
/// @brief Instructs the server to shut down.
void shutdown();
/// @brief Return textual type of packet received by server.
///
/// Returns the name of valid packet received by the server (e.g. SOLICIT).
/// If the packet is unknown - or if it is a valid DHCP packet but not one
/// expected to be received by the server (such as an ADVERTISE), the string
/// "UNKNOWN" is returned. This method is used in debug messages.
///
/// As the operation of the method does not depend on any server state, it
/// is declared static.
protected:
/// @brief verifies if specified packet meets RFC requirements
///
/// @param type DHCPv4 packet type
/// Checks if mandatory option is really there, that forbidden option
/// is not there, and that client-id or server-id appears only once.
///
/// @return Pointer to "const" string containing the packet name.
/// Note that this string is statically allocated and MUST NOT
/// be freed by the caller.
static const char* serverReceivedPacketName(uint8_t type);
/// @param pkt packet to be checked
/// @param clientid expectation regarding client-id option
/// @param serverid expectation regarding server-id option
/// @throw RFCViolation if any issues are detected
void sanityCheck(const Pkt6Ptr& pkt, RequirementLevel clientid,
RequirementLevel serverid);
protected:
/// @brief Processes incoming SOLICIT and returns response.
///
/// Processes received SOLICIT message and verifies that its sender
......@@ -186,11 +205,24 @@ protected:
/// @param question client's message (typically SOLICIT or REQUEST)
/// @param ia pointer to client's IA_NA option (client's request)
/// @return IA_NA option (server's response)
OptionPtr handleIA_NA(const isc::dhcp::Subnet6Ptr& subnet,
OptionPtr assignIA_NA(const isc::dhcp::Subnet6Ptr& subnet,
const isc::dhcp::DuidPtr& duid,
isc::dhcp::Pkt6Ptr question,
boost::shared_ptr<Option6IA> ia);
/// @brief Renews specific IA_NA option
///
/// Generates response to IA_NA. This typically includes finding a lease that
/// corresponds to the received address. If no such lease is found, an IA_NA
/// response is generated with an appropriate status code.
///
/// @param subnet subnet the sender belongs to
/// @param duid client's duid
/// @param question client's message
/// @param ia IA_NA option that is being renewed
OptionPtr renewIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
Pkt6Ptr question, boost::shared_ptr<Option6IA> ia);
/// @brief Copies required options from client message to server answer.
///
/// Copies options that must appear in any server response (ADVERTISE, REPLY)
......@@ -221,14 +253,24 @@ protected:
/// @brief Assigns leases.
///
/// TODO: This method is currently a stub. It just appends one
/// hardcoded lease. It supports addresses (IA_NA) only. It does NOT
/// support temporary addresses (IA_TA) nor prefixes (IA_PD).
/// It supports addresses (IA_NA) only. It does NOT support temporary
/// addresses (IA_TA) nor prefixes (IA_PD).
/// @todo: Extend this method once TA and PD becomes supported
///
/// @param question client's message (with requested IA_NA)
/// @param answer server's message (IA_NA options will be added here)
void assignLeases(const Pkt6Ptr& question, Pkt6Ptr& answer);
/// @brief Attempts to renew received addresses
///
/// It iterates through received IA_NA options and attempts to renew
/// received addresses. If no such leases are found, proper status
/// code is added to reply message. Renewed addresses are added
/// as IA_NA/IAADDR to reply packet.
/// @param renew client's message asking for renew
/// @param reply server's response
void renewLeases(const Pkt6Ptr& renew, Pkt6Ptr& reply);
/// @brief Sets server-identifier.
///
/// This method attempts to set server-identifier DUID. It loads it
......
......@@ -19,6 +19,7 @@
#include <dhcp/dhcp6.h>
#include <dhcp/duid.h>
#include <dhcp/option.h>
#include <dhcp/option_custom.h>
#include <dhcp/option6_addrlst.h>
#include <dhcp/option6_ia.h>
#include <dhcp/option6_iaaddr.h>
......@@ -57,8 +58,10 @@ public:
using Dhcpv6Srv::processSolicit;
using Dhcpv6Srv::processRequest;
using Dhcpv6Srv::processRenew;
using Dhcpv6Srv::createStatusCode;
using Dhcpv6Srv::selectSubnet;
using Dhcpv6Srv::sanityCheck;
};
class Dhcpv6SrvTest : public ::testing::Test {
......@@ -139,6 +142,33 @@ public:
return (addr);
}
// Checks that server rejected IA_NA, i.e. that it has no addresses and
// that expected status code really appears there.
// Status code indicates type of error encountered (in theory it can also
// indicate success, but servers typically don't send success status
// as this is the default result and it saves bandwidth)
void checkRejectedIA_NA(const boost::shared_ptr<Option6IA>& ia,
uint16_t expected_status_code) {
// Make sure there is no address assigned.
EXPECT_FALSE(ia->getOption(D6O_IAADDR));
// T1, T2 should be zeroed
EXPECT_EQ(0, ia->getT1());
EXPECT_EQ(0, ia->getT2());
boost::shared_ptr<OptionCustom> status =
boost::dynamic_pointer_cast<OptionCustom>(ia->getOption(D6O_STATUS_CODE));
EXPECT_TRUE(status);
if (status) {
// We don't have dedicated class for status code, so let's just interpret
// first 2 bytes as status. Remainder of the status code option content is
// just a text explanation what went wrong.
EXPECT_EQ(static_cast<uint16_t>(expected_status_code),
status->readInteger<uint16_t>(0));
}
}