Commit 6364a03c authored by Marcin Siodelski's avatar Marcin Siodelski

[3688] DHCPv4 server assignes reserved hostname to the clients.

parent ac390543
......@@ -43,6 +43,7 @@
#include <asio.hpp>
#include <boost/bind.hpp>
#include <boost/foreach.hpp>
#include <boost/shared_ptr.hpp>
#include <iomanip>
......@@ -80,6 +81,152 @@ struct Dhcp4Hooks {
// module is called.
Dhcp4Hooks Hooks;
namespace {
/// @brief DHCPv4 message exchange.
///
/// This class represents the DHCPv4 message exchange. The message exchange
/// consists of the single client message, server response to this message
/// and the mechanisms to generate the server's response. The server creates
/// the instance of the @c DHCPv4Exchange for each inbound message that it
/// accepts for processing.
///
/// The use of the @c DHCPv4Exchange object as a central repository of
/// information about the message exchange simplifies the API of the
/// @c Dhcpv4Srv class.
///
/// Another benefit of using this class is that different methods of the
/// @c Dhcpv4Srv may share information. For example, the constructor of this
/// class selects the subnet and multiple methods of @c Dhcpv4Srv use this
/// subnet, without the need to select it again.
///
/// @todo This is the initial version of this class. In the future a lot of
/// code from the @c Dhcpv4Srv class will be migrated here.
class DHCPv4Exchange {
public:
/// @brief Constructor.
///
/// The constructor selects the subnet for the query and checks for the
/// static host reservations for the client which has sent the message.
/// The information about the reservations is stored in the
/// @c AllocEngine::ClientContext4 object, which can be obtained by
/// calling the @c getContext.
///
/// @param alloc_engine Pointer to the instance of the Allocation Engine
/// used by the server.
/// @param query Pointer to the client message.
DHCPv4Exchange(const AllocEnginePtr& alloc_engine, const Pkt4Ptr& query);
/// @brief Selects the subnet for the message processing.
///
/// The pointer to the selected subnet is stored in the @c ClientContext4
/// structure.
void selectSubnet();
/// @brief Selects the subnet for the message processing.
///
/// @todo This variant of the @c selectSubnet method is static and public so
/// as it may be invoked by the @c Dhcpv4Srv object. This is temporary solution
/// and the function will go away once the server code fully supports the use
/// of this class and it obtains the subnet from the context returned by the
/// @c getContext method.
///
/// @param query Pointer to the client's message.
/// @return Pointer to the selected subnet or NULL if no suitable subnet
/// has been found.
static Subnet4Ptr selectSubnet(const Pkt4Ptr& query);
/// @brief Returns the copy of the context for the Allocation engine.
AllocEngine::ClientContext4 getContext() const {
return (context_);
}
private:
/// @brief Pointer to the allocation engine used by the server.
AllocEnginePtr alloc_engine_;
/// @brief Pointer to the DHCPv4 message sent by the client.
Pkt4Ptr query_;
/// @brief Context for use with allocation engine.
AllocEngine::ClientContext4 context_;
};
/// @brief Type representing the pointer to the @c DHCPv4Exchange.
typedef boost::shared_ptr<DHCPv4Exchange> DHCPv4ExchangePtr;
DHCPv4Exchange::DHCPv4Exchange(const AllocEnginePtr& alloc_engine,
const Pkt4Ptr& query)
: alloc_engine_(alloc_engine), query_(query), context_() {
selectSubnet();
// Hardware address.
context_.hwaddr_ = query->getHWAddr();
// Client Identifier
OptionPtr opt_clientid = query->getOption(DHO_DHCP_CLIENT_IDENTIFIER);
if (opt_clientid) {
context_.clientid_.reset(new ClientId(opt_clientid->getData()));
}
// Check for static reservations.
alloc_engine->findReservation(context_);
};
void
DHCPv4Exchange::selectSubnet() {
context_.subnet_ = selectSubnet(query_);
}
Subnet4Ptr
DHCPv4Exchange::selectSubnet(const Pkt4Ptr& query) {
Subnet4Ptr subnet;
SubnetSelector selector;
selector.ciaddr_ = query->getCiaddr();
selector.giaddr_ = query->getGiaddr();
selector.local_address_ = query->getLocalAddr();
selector.remote_address_ = query->getRemoteAddr();
selector.client_classes_ = query->classes_;
selector.iface_name_ = query->getIface();
CfgMgr& cfgmgr = CfgMgr::instance();
subnet = cfgmgr.getCurrentCfg()->getCfgSubnets4()->selectSubnet(selector);
// Let's execute all callouts registered for subnet4_select
if (HooksManager::calloutsPresent(Hooks.hook_index_subnet4_select_)) {
CalloutHandlePtr callout_handle = getCalloutHandle(query);
// We're reusing callout_handle from previous calls
callout_handle->deleteAllArguments();
// Set new arguments
callout_handle->setArgument("query4", query);
callout_handle->setArgument("subnet4", subnet);
callout_handle->setArgument("subnet4collection",
cfgmgr.getCurrentCfg()->
getCfgSubnets4()->getAll());
// Call user (and server-side) callouts
HooksManager::callCallouts(Hooks.hook_index_subnet4_select_,
*callout_handle);
// Callouts decided to skip this step. This means that no subnet
// will be selected. Packet processing will continue, but it will
// be severely limited (i.e. only global options will be assigned)
if (callout_handle->getSkip()) {
LOG_DEBUG(dhcp4_logger, DBG_DHCP4_HOOKS,
DHCP4_HOOK_SUBNET4_SELECT_SKIP);
return (Subnet4Ptr());
}
// Use whatever subnet was specified by the callout
callout_handle->getArgument("subnet4", subnet);
}
return (subnet);
}
DHCPv4ExchangePtr exchange;
};
namespace isc {
namespace dhcp {
......@@ -137,6 +284,11 @@ Dhcpv4Srv::shutdown() {
shutdown_ = true;
}
isc::dhcp::Subnet4Ptr
Dhcpv4Srv::selectSubnet(const Pkt4Ptr& question) {
return (DHCPv4Exchange::selectSubnet(question));
}
Pkt4Ptr
Dhcpv4Srv::receivePacket(int timeout) {
return (IfaceMgr::instance().receive4(timeout));
......@@ -150,6 +302,9 @@ Dhcpv4Srv::sendPacket(const Pkt4Ptr& packet) {
bool
Dhcpv4Srv::run() {
while (!shutdown_) {
// Reset pointer to the current exchange.
exchange.reset();
// client's message and server's response
Pkt4Ptr query;
Pkt4Ptr rsp;
......@@ -592,7 +747,7 @@ Dhcpv4Srv::appendRequestedOptions(const Pkt4Ptr& question, Pkt4Ptr& msg) {
// Get the subnet relevant for the client. We will need it
// to get the options associated with it.
Subnet4Ptr subnet = selectSubnet(question);
Subnet4Ptr subnet = DHCPv4Exchange::selectSubnet(question);
// If we can't find the subnet for the client there is no way
// to get the options to be sent to a client. We don't log an
// error because it will be logged by the assignLease method
......@@ -629,7 +784,7 @@ Dhcpv4Srv::appendRequestedOptions(const Pkt4Ptr& question, Pkt4Ptr& msg) {
void
Dhcpv4Srv::appendRequestedVendorOptions(const Pkt4Ptr& question, Pkt4Ptr& answer) {
// Get the configured subnet suitable for the incoming packet.
Subnet4Ptr subnet = selectSubnet(question);
Subnet4Ptr subnet = DHCPv4Exchange::selectSubnet(question);
// Leave if there is no subnet matching the incoming packet.
// There is no need to log the error message here because
// it will be logged in the assignLease() when it fails to
......@@ -696,7 +851,7 @@ Dhcpv4Srv::appendBasicOptions(const Pkt4Ptr& question, Pkt4Ptr& msg) {
sizeof(required_options) / sizeof(required_options[0]);
// Get the subnet.
Subnet4Ptr subnet = selectSubnet(question);
Subnet4Ptr subnet = DHCPv4Exchange::selectSubnet(question);
if (!subnet) {
return;
}
......@@ -764,10 +919,16 @@ Dhcpv4Srv::processClientFqdnOption(const Option4ClientFqdnPtr& fqdn,
fqdn_resp->setFlag(Option4ClientFqdn::FLAG_E,
fqdn->getFlag(Option4ClientFqdn::FLAG_E));
if (exchange && exchange->getContext().host_ &&
!exchange->getContext().host_->getHostname().empty()) {
fqdn_resp->setDomainName(exchange->getContext().host_->getHostname(),
Option4ClientFqdn::FULL);
// Adjust the domain name based on domain name value and type sent by the
// client and current configuration.
d2_mgr.adjustDomainName<Option4ClientFqdn>(*fqdn, *fqdn_resp);
} else {
// Adjust the domain name based on domain name value and type sent by the
// client and current configuration.
d2_mgr.adjustDomainName<Option4ClientFqdn>(*fqdn, *fqdn_resp);
}
// Add FQDN option to the response message. Note that, there may be some
// cases when server may choose not to include the FQDN option in a
......@@ -818,17 +979,23 @@ Dhcpv4Srv::processHostnameOption(const OptionStringPtr& opt_hostname,
// By checking the number of labels present in the hostname we may infer
// whether client has sent the fully qualified or unqualified hostname.
/// @todo We may want to reconsider whether it is appropriate for the
/// client to send a root domain name as a Hostname. There are
/// also extensions to the auto generation of the client's name,
/// e.g. conversion to the puny code which may be considered at some point.
/// For now, we just remain liberal and expect that the DNS will handle
/// conversion if needed and possible.
if ((d2_mgr.getD2ClientConfig()->getReplaceClientName()) ||
(label_count < 2)) {
// If there is a hostname reservation for this client, use it.
if (exchange && exchange->getContext().host_ &&
!exchange->getContext().host_->getHostname().empty()) {
opt_hostname_resp->setValue(exchange->getContext().host_->getHostname());
} else if ((d2_mgr.getD2ClientConfig()->getReplaceClientName()) ||
(label_count < 2)) {
// Set to root domain to signal later on that we should replace it.
// DHO_HOST_NAME is a string option which cannot be empty.
/// @todo We may want to reconsider whether it is appropriate for the
/// client to send a root domain name as a Hostname. There are
/// also extensions to the auto generation of the client's name,
/// e.g. conversion to the puny code which may be considered at some point.
/// For now, we just remain liberal and expect that the DNS will handle
/// conversion if needed and possible.
opt_hostname_resp->setValue(".");
} else if (label_count == 2) {
// If there are two labels, it means that the client has specified
// the unqualified name. We have to concatenate the unqualified name
......@@ -933,7 +1100,7 @@ void
Dhcpv4Srv::assignLease(const Pkt4Ptr& question, Pkt4Ptr& answer) {
// We need to select a subnet the client is connected in.
Subnet4Ptr subnet = selectSubnet(question);
Subnet4Ptr subnet = DHCPv4Exchange::selectSubnet(question);
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
......@@ -1072,13 +1239,18 @@ Dhcpv4Srv::assignLease(const Pkt4Ptr& question, Pkt4Ptr& answer) {
}
}
// Use allocation engine to pick a lease for this client. Allocation engine
// will try to honour the hint, but it is just a hint - some other address
// may be used instead. If fake_allocation is set to false, the lease will
// be inserted into the LeaseMgr as well.
/// @todo pass the actual FQDN data.
AllocEngine::ClientContext4 ctx(subnet, client_id, hwaddr, hint, fqdn_fwd,
fqdn_rev, hostname, fake_allocation);
AllocEngine::ClientContext4 ctx;
if (exchange) {
ctx = exchange->getContext();
}
ctx.subnet_ = subnet;
ctx.clientid_ = client_id;
ctx.hwaddr_ = hwaddr;
ctx.requested_address_ = hint;
ctx.fwd_dns_update_ = fqdn_fwd;
ctx.rev_dns_update_ = fqdn_rev;
ctx.hostname_ = hostname;
ctx.fake_allocation_ = fake_allocation;
ctx.callout_handle_ = callout_handle;
Lease4Ptr lease = alloc_engine_->allocateLease4(ctx);
......@@ -1342,6 +1514,9 @@ Dhcpv4Srv::getNetmaskOption(const Subnet4Ptr& subnet) {
Pkt4Ptr
Dhcpv4Srv::processDiscover(Pkt4Ptr& discover) {
/// @todo Move this call to run() once we reorgnize some unit tests
/// which directly call this method.
exchange.reset(new DHCPv4Exchange(alloc_engine_, discover));
sanityCheck(discover, FORBIDDEN);
......@@ -1390,6 +1565,9 @@ Dhcpv4Srv::processDiscover(Pkt4Ptr& discover) {
Pkt4Ptr
Dhcpv4Srv::processRequest(Pkt4Ptr& request) {
/// @todo Move this call to run() once we reorgnize some unit tests
/// which directly call this method.
exchange.reset(new DHCPv4Exchange(alloc_engine_, request));
/// @todo Uncomment this (see ticket #3116)
/// sanityCheck(request, MANDATORY);
......@@ -1437,6 +1615,9 @@ Dhcpv4Srv::processRequest(Pkt4Ptr& request) {
void
Dhcpv4Srv::processRelease(Pkt4Ptr& release) {
/// @todo Move this call to run() once we reorgnize some unit tests
/// which directly call this method.
exchange.reset(new DHCPv4Exchange(alloc_engine_, release));
/// @todo Uncomment this (see ticket #3116)
/// sanityCheck(release, MANDATORY);
......@@ -1548,12 +1729,20 @@ Dhcpv4Srv::processRelease(Pkt4Ptr& release) {
}
void
Dhcpv4Srv::processDecline(Pkt4Ptr& /* decline */) {
Dhcpv4Srv::processDecline(Pkt4Ptr& decline) {
/// @todo Move this call to run() once we reorgnize some unit tests
/// which directly call this method.
exchange.reset(new DHCPv4Exchange(alloc_engine_, decline));
/// @todo Implement this (also see ticket #3116)
}
Pkt4Ptr
Dhcpv4Srv::processInform(Pkt4Ptr& inform) {
/// @todo Move this call to run() once we reorgnize some unit tests
/// which directly call this method.
exchange.reset(new DHCPv4Exchange(alloc_engine_, inform));
// DHCPINFORM MUST not include server identifier.
sanityCheck(inform, FORBIDDEN);
......@@ -1611,56 +1800,6 @@ Dhcpv4Srv::serverReceivedPacketName(uint8_t type) {
return (UNKNOWN);
}
Subnet4Ptr
Dhcpv4Srv::selectSubnet(const Pkt4Ptr& question) const {
Subnet4Ptr subnet;
SubnetSelector selector;
selector.ciaddr_ = question->getCiaddr();
selector.giaddr_ = question->getGiaddr();
selector.local_address_ = question->getLocalAddr();
selector.remote_address_ = question->getRemoteAddr();
selector.client_classes_ = question->classes_;
selector.iface_name_ = question->getIface();
CfgMgr& cfgmgr = CfgMgr::instance();
subnet = cfgmgr.getCurrentCfg()->getCfgSubnets4()->selectSubnet(selector);
// Let's execute all callouts registered for subnet4_select
if (HooksManager::calloutsPresent(hook_index_subnet4_select_)) {
CalloutHandlePtr callout_handle = getCalloutHandle(question);
// We're reusing callout_handle from previous calls
callout_handle->deleteAllArguments();
// Set new arguments
callout_handle->setArgument("query4", question);
callout_handle->setArgument("subnet4", subnet);
callout_handle->setArgument("subnet4collection",
cfgmgr.getCurrentCfg()->
getCfgSubnets4()->getAll());
// Call user (and server-side) callouts
HooksManager::callCallouts(hook_index_subnet4_select_,
*callout_handle);
// Callouts decided to skip this step. This means that no subnet
// will be selected. Packet processing will continue, but it will
// be severely limited (i.e. only global options will be assigned)
if (callout_handle->getSkip()) {
LOG_DEBUG(dhcp4_logger, DBG_DHCP4_HOOKS,
DHCP4_HOOK_SUBNET4_SELECT_SKIP);
return (Subnet4Ptr());
}
// Use whatever subnet was specified by the callout
callout_handle->getArgument("subnet4", subnet);
}
return (subnet);
}
bool
Dhcpv4Srv::accept(const Pkt4Ptr& query) const {
// Check that the message type is accepted by the server. We rely on the
......@@ -1725,7 +1864,8 @@ Dhcpv4Srv::acceptDirectRequest(const Pkt4Ptr& pkt) const {
// we validate the message type prior to calling this function.
return (false);
}
return ((pkt->getLocalAddr() != IOAddress::IPV4_BCAST_ADDRESS() || selectSubnet(pkt)));
return ((pkt->getLocalAddr() != IOAddress::IPV4_BCAST_ADDRESS()
|| DHCPv4Exchange::selectSubnet(pkt)));
}
bool
......@@ -2012,7 +2152,7 @@ void Dhcpv4Srv::classifyPacket(const Pkt4Ptr& pkt) {
bool Dhcpv4Srv::classSpecificProcessing(const Pkt4Ptr& query, const Pkt4Ptr& rsp) {
Subnet4Ptr subnet = selectSubnet(query);
Subnet4Ptr subnet = DHCPv4Exchange::selectSubnet(query);
if (!subnet) {
return (true);
}
......
......@@ -619,7 +619,7 @@ protected:
///
/// @param question client's message
/// @return selected subnet (or NULL if no suitable subnet was found)
isc::dhcp::Subnet4Ptr selectSubnet(const Pkt4Ptr& question) const;
static isc::dhcp::Subnet4Ptr selectSubnet(const Pkt4Ptr& question);
/// indicates if shutdown is in progress. Setting it to true will
/// initiate server shutdown procedure.
......
......@@ -64,6 +64,7 @@ Dhcp4Client::Dhcp4Client(boost::shared_ptr<NakedDhcpv4Srv> srv,
ciaddr_(IOAddress("0.0.0.0")),
curr_transid_(0),
dest_addr_("255.255.255.255"),
fqdn_(),
hwaddr_(generateHWAddr()),
iface_name_("eth0"),
relay_addr_("192.0.2.2"),
......@@ -150,6 +151,8 @@ Dhcp4Client::doDiscover(const boost::shared_ptr<IOAddress>& requested_addr) {
context_.query_ = createMsg(DHCPDISCOVER);
// Request options if any.
includePRL();
// Include FQDN or Hostname.
includeName();
if (requested_addr) {
addRequestedAddress(*requested_addr);
}
......@@ -239,6 +242,8 @@ Dhcp4Client::doRequest() {
// Request options if any.
includePRL();
// Include FQDN or Hostname.
includeName();
// Send the message to the server.
sendMsg(context_.query_);
// Expect response.
......@@ -249,6 +254,33 @@ Dhcp4Client::doRequest() {
}
}
void
Dhcp4Client::includeFQDN(const uint8_t flags, const std::string& fqdn_name,
Option4ClientFqdn::DomainNameType fqdn_type) {
fqdn_.reset(new Option4ClientFqdn(flags, Option4ClientFqdn::RCODE_CLIENT(),
fqdn_name, fqdn_type));
}
void
Dhcp4Client::includeHostname(const std::string& name) {
hostname_.reset(new OptionString(Option::V4, DHO_HOST_NAME, name));
}
void
Dhcp4Client::includeName() {
if (!context_.query_) {
isc_throw(Dhcp4ClientError, "pointer to the query must not be NULL"
" when adding FQDN or Hostname option");
}
if (fqdn_) {
context_.query_->addOption(fqdn_);
} else if (hostname_) {
context_.query_->addOption(hostname_);
}
}
void
Dhcp4Client::includePRL() {
if (!context_.query_) {
......
......@@ -217,6 +217,21 @@ public:
return (srv_);
}
/// @brief Creates an instance of the Client FQDN option to be included
/// in the client's message.
///
/// @param flags Flags.
/// @param fqdn_name Name in the textual format.
/// @param fqdn_type Type of the name (fully qualified or partial).
void includeFQDN(const uint8_t flags, const std::string& fqdn_name,
Option4ClientFqdn::DomainNameType fqdn_type);
/// @brief Creates an instance of the Hostname option to be included
/// in the client's message.
///
/// @param name Name to be stored in the option.
void includeHostname(const std::string& name);
/// @brief Modifies the client's HW address (adds one to it).
///
/// The HW address should be modified to test negative scenarios when the
......@@ -345,6 +360,13 @@ private:
/// @return An instance of the message created.
Pkt4Ptr createMsg(const uint8_t msg_type);
/// @brief Includes FQDN or Hostname option in the client's message.
///
/// This method checks if @c fqdn_ or @c hostname_ is specified and
/// includes it in the client's message. If both are specified, the
/// @c fqdn_ will be used.
void includeName();
/// @brief Include PRL Option in the query message.
///
/// This function creates the instance of the PRL (Parameter Request List)
......@@ -376,6 +398,12 @@ private:
/// @brief Currently used destination address.
asiolink::IOAddress dest_addr_;
/// @brief FQDN requested by the client.
Option4ClientFqdnPtr fqdn_;
/// @brief Hostname requested by the client.
OptionStringPtr hostname_;
/// @brief Current hardware address of the client.
HWAddrPtr hwaddr_;
......@@ -406,4 +434,4 @@ private:
} // end of namespace isc::dhcp
} // end of namespace isc
#endif // DHCP4_CLIENT
#endif // DHCP4_CLIENT_H
......@@ -17,9 +17,12 @@
#include <dhcp/option4_client_fqdn.h>
#include <dhcp/option_int_array.h>
#include <dhcp/tests/iface_mgr_test_config.h>
#include <dhcp4/tests/dhcp4_client.h>
#include <dhcp4/tests/dhcp4_test_utils.h>
#include <dhcp_ddns/ncr_msg.h>
#include <dhcpsrv/cfgmgr.h>
#include <dhcpsrv/lease_mgr.h>
#include <dhcpsrv/lease_mgr_factory.h>
#include <gtest/gtest.h>
#include <boost/scoped_ptr.hpp>
......@@ -32,20 +35,91 @@ using namespace isc::dhcp_ddns;
namespace {
/// @brief Set of JSON configurations used by the FQDN tests.
const char* CONFIGS[] = {
"{ \"interfaces-config\": {"
" \"interfaces\": [ \"*\" ]"
"},"
"\"valid-lifetime\": 3000,"
"\"subnet4\": [ { "
" \"subnet\": \"10.0.0.0/24\", "
" \"id\": 1,"
" \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
" \"option-data\": [ {"
" \"name\": \"routers\","
" \"code\": 3,"
" \"data\": \"10.0.0.200,10.0.0.201\","
" \"csv-format\": true,"
" \"space\": \"dhcp4\""
" } ],"
" \"reservations\": ["
" {"
" \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
" \"ip-address\": \"10.0.0.5\","
" \"hostname\": \"unique-host.example.org\""
" }"
" ]"
" }],"
"\"dhcp-ddns\": {"
"\"enable-updates\": true,"
"\"qualifying-suffix\": \"fake-suffix.isc.org.\""
"}"
"}",
"{ \"interfaces-config\": {"
" \"interfaces\": [ \"*\" ]"
"},"
"\"valid-lifetime\": 3000,"
"\"subnet4\": [ { "
" \"subnet\": \"10.0.0.0/24\", "
" \"id\": 1,"
" \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
" \"option-data\": [ {"
" \"name\": \"routers\","
" \"code\": 3,"
" \"data\": \"10.0.0.200,10.0.0.201\","
" \"csv-format\": true,"
" \"space\": \"dhcp4\""
" } ],"
" \"reservations\": ["
" {"
" \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
" \"ip-address\": \"10.0.0.5\","
" \"hostname\": \"foobar.org\""
" }"
" ]"
" }],"
"\"dhcp-ddns\": {"
"\"enable-updates\": true,"
"\"qualifying-suffix\": \"fake-suffix.isc.org.\""
"}"
"}"
};
class NameDhcpv4SrvTest : public Dhcpv4SrvTest {
public:
// Reference to D2ClientMgr singleton
D2ClientMgr& d2_mgr_;
/// @brief Pointer to the DHCP server instance.
NakedDhcpv4Srv* srv_;
/// @brief Interface Manager's fake configuration control.
IfaceMgrTestConfig iface_mgr_test_config_;
// Bit Constants for turning on and off DDNS configuration options.