Commit 9b79fe00 authored by Marcin Siodelski's avatar Marcin Siodelski
Browse files

[master] Merge branch 'trac4552'

parents 48ecfec5 7612f815
......@@ -2854,6 +2854,35 @@ It is merely echoed by the server
</section>
<section id="reservation4-message-fields">
<title>Reserving Next Server, Server Hostname and Boot File Name</title>
<para>BOOTP/DHCPv4 messages include "siaddr", "sname" and "file" fields.
Even though, DHCPv4 includes corresponding options, such as option 66 and
option 67, some clients may not support these options. Thus, server
administrators often use "siaddr", "sname" and "file" fields instead.</para>
<para>With Kea, it is possible to make static reservations for these DHCPv4
message fields:</para>
<screen>
{
"subnet4": [ {
"reservations": [
{
"hw-address": "aa:bb:cc:dd:ee:ff",
<userinput>"next-server": "10.1.1.2",
"server-hostname": "server-hostname.example.org",
"boot-file-name": "/tmp/bootfile.efi"</userinput>
} ]
} ]
}</screen>
<para>Note that those parameters can be specified in combination with
other parameters for a reservation, e.g. reserved IPv4 address. These
parameters are optional, i.e. a subset of them can specified, or all of
them can be omitted.</para>
</section>
<section id="reservations4-mysql-pgsql">
<title>Storing host reservations in MySQL or PostgreSQL</title>
......
......@@ -513,6 +513,24 @@
"item_type": "string",
"item_optional": false,
"item_default": "0.0.0.0"
},
{
"item_name": "next-server",
"item_type": "string",
"item_optional": true,
"item_default": "0.0.0.0"
},
{
"item_name": "server-hostname",
"item_type": "string",
"item_optional": true,
"item_default": ""
},
{
"item_name": "boot-file-name",
"item_type": "string",
"item_optional": true,
"item_default": ""
} ]
}
},
......
......@@ -153,6 +153,9 @@ Dhcpv4Exchange::Dhcpv4Exchange(const AllocEnginePtr& alloc_engine,
// Check for static reservations.
alloc_engine->findReservation(*context_);
// Set siaddr, sname and file.
setReservedMessageFields();
}
}
};
......@@ -333,6 +336,27 @@ Dhcpv4Exchange::setHostIdentifiers() {
}
}
void
Dhcpv4Exchange::setReservedMessageFields() {
ConstHostPtr host = context_->host_;
// Nothing to do if host reservations not specified for this client.
if (host) {
if (!host->getNextServer().isV4Zero()) {
resp_->setSiaddr(host->getNextServer());
}
if (!host->getServerHostname().empty()) {
resp_->setSname(reinterpret_cast<
const uint8_t*>(host->getServerHostname().c_str()));
}
if (!host->getBootFileName().empty()) {
resp_->setFile(reinterpret_cast<
const uint8_t*>(host->getBootFileName().c_str()));
}
}
}
const std::string Dhcpv4Srv::VENDOR_CLASS_PREFIX("VENDOR_CLASS_");
Dhcpv4Srv::Dhcpv4Srv(uint16_t port, const bool use_bcast,
......@@ -1515,12 +1539,6 @@ Dhcpv4Srv::assignLease(Dhcpv4Exchange& ex) {
return;
}
// Set up siaddr. Perhaps assignLease is not the best place to call this
// as siaddr has nothing to do with a lease, but otherwise we would have
// to select subnet twice (performance hit) or update too many functions
// at once.
/// @todo: move subnet selection to a common code
resp->setSiaddr(subnet->getSiaddr());
// Get the server identifier. It will be used to determine the state
// of the client.
......@@ -2635,9 +2653,12 @@ Dhcpv4Srv::vendorClassSpecificProcessing(const Dhcpv4Exchange& ex) {
if (query->inClass(VENDOR_CLASS_PREFIX + DOCSIS3_CLASS_MODEM)) {
// Set next-server. This is TFTP server address. Cable modems will
// download their configuration from that server.
rsp->setSiaddr(subnet->getSiaddr());
// Do not override
if (rsp->getSiaddr().isV4Zero()) {
// Set next-server. This is TFTP server address. Cable modems will
// download their configuration from that server.
rsp->setSiaddr(subnet->getSiaddr());
}
// Now try to set up file field in DHCPv4 packet. We will just copy
// content of the boot-file option, which contains the same information.
......@@ -2664,6 +2685,12 @@ Dhcpv4Srv::vendorClassSpecificProcessing(const Dhcpv4Exchange& ex) {
rsp->setSiaddr(IOAddress::IPV4_ZERO_ADDRESS());
}
// Set up siaddr. Do not override siaddr if host specific value or
// vendor class specific value present.
if (rsp->getSiaddr().isV4Zero()) {
rsp->setSiaddr(subnet->getSiaddr());
}
return (true);
}
......
......@@ -150,6 +150,10 @@ private:
/// host-reservation-identifiers
void setHostIdentifiers();
/// @brief Sets reserved values of siaddr, sname and file in the
/// server's response.
void setReservedMessageFields();
/// @brief Pointer to the allocation engine used by the server.
AllocEnginePtr alloc_engine_;
/// @brief Pointer to the DHCPv4 message sent by the client.
......
......@@ -23,7 +23,7 @@ namespace test {
Dhcp4Client::Configuration::Configuration()
: routers_(), dns_servers_(), log_servers_(), quotes_servers_(),
serverid_("0.0.0.0") {
serverid_("0.0.0.0"), siaddr_(asiolink::IOAddress::IPV4_ZERO_ADDRESS()) {
reset();
}
......@@ -34,6 +34,9 @@ Dhcp4Client::Configuration::reset() {
log_servers_.clear();
quotes_servers_.clear();
serverid_ = asiolink::IOAddress("0.0.0.0");
siaddr_ = asiolink::IOAddress::IPV4_ZERO_ADDRESS();
sname_.clear();
boot_file_name_.clear();
lease_ = Lease4();
}
......@@ -178,6 +181,17 @@ Dhcp4Client::applyConfiguration() {
if (opt_vendor) {
config_.vendor_suboptions_ = opt_vendor->getOptions();
}
// siaddr
config_.siaddr_ = resp->getSiaddr();
// sname
OptionBuffer buf = resp->getSname();
// sname is a fixed length field holding null terminated string. Use
// of c_str() guarantess that only a useful portion (ending with null
// character) is assigned.
config_.sname_.assign(std::string(buf.begin(), buf.end()).c_str());
// (boot)file
buf = resp->getFile();
config_.boot_file_name_.assign(std::string(buf.begin(), buf.end()).c_str());
// Server Identifier
OptionCustomPtr opt_serverid = boost::dynamic_pointer_cast<
OptionCustom>(resp->getOption(DHO_DHCP_SERVER_IDENTIFIER));
......
......@@ -81,6 +81,12 @@ public:
/// @brief Holds server id of the server which responded to the client's
/// request.
asiolink::IOAddress serverid_;
/// @brief Holds returned siaddr.
asiolink::IOAddress siaddr_;
/// @brief Holds returned sname.
std::string sname_;
/// @brief Holds returned (boot)file.
std::string boot_file_name_;
/// @brief Constructor.
Configuration();
......
......@@ -71,6 +71,15 @@ namespace {
/// - The same as configuration 4, but using the following order of
/// host-reservation-identifiers: duid, circuit-id, hw-address.
///
/// - Configuration 6:
/// - This configuration provides reservations for next-server,
/// server-hostname and boot-file-name value.
/// - 1 subnet: 10.0.0.0/24
/// - 1 reservation for this subnet:
/// - Client's HW address: aa:bb:cc:dd:ee:ff
/// - next-server = 10.0.0.7
/// - server name = "some-name.example.org"
/// - boot-file-name = "bootfile.efi"
const char* DORA_CONFIGS[] = {
// Configuration 0
"{ \"interfaces-config\": {"
......@@ -233,6 +242,26 @@ const char* DORA_CONFIGS[] = {
" }"
" ]"
"} ]"
"}",
// Configuration 6
"{ \"interfaces-config\": {"
" \"interfaces\": [ \"*\" ]"
"},"
"\"valid-lifetime\": 600,"
"\"next-server\": \"10.0.0.1\","
"\"subnet4\": [ { "
" \"subnet\": \"10.0.0.0/24\", "
" \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
" \"reservations\": [ "
" {"
" \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
" \"next-server\": \"10.0.0.7\","
" \"server-hostname\": \"some-name.example.org\","
" \"boot-file-name\": \"bootfile.efi\""
" }"
" ]"
"} ]"
"}"
};
......@@ -989,6 +1018,32 @@ TEST_F(DORATest, changingHWAddress) {
EXPECT_FALSE(client.config_.lease_.client_id_);
}
// This test verifies that the server assigns reserved values for the
// siaddr, sname and file fields carried within DHCPv4 message.
TEST_F(DORATest, messageFieldsReservations) {
// Client has a reservation.
Dhcp4Client client(Dhcp4Client::SELECTING);
// Set explicit HW address so as it matches the reservation in the
// configuration used below.
client.setHWAddress("aa:bb:cc:dd:ee:ff");
// Configure DHCP server.
configure(DORA_CONFIGS[6], *client.getServer());
// Client performs 4-way exchange and should obtain a reserved
// address and fixed fields.
ASSERT_NO_THROW(client.doDORA(boost::shared_ptr<
IOAddress>(new IOAddress("0.0.0.0"))));
// Make sure that the server responded.
ASSERT_TRUE(client.getContext().response_);
Pkt4Ptr resp = client.getContext().response_;
// Make sure that the server has responded with DHCPACK.
ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
// Check that the reserved values have been assigned.
EXPECT_EQ("10.0.0.7", client.config_.siaddr_.toText());
EXPECT_EQ("some-name.example.org", client.config_.sname_);
EXPECT_EQ("bootfile.efi", client.config_.boot_file_name_);
}
// This test checks the following scenario:
// 1. Client A performs 4-way exchange and obtains a lease from the dynamic pool.
// 2. Reservation is created for the client A using an address out of the dynamic
......
......@@ -39,6 +39,16 @@ namespace {
/// - Domain Name Server option present: 192.0.2.202, 192.0.2.203.
/// - Log Servers option present: 192.0.2.200 and 192.0.2.201
/// - Quotes Servers option present: 192.0.2.202, 192.0.2.203.
///
/// - Configuration 2:
/// - This configuration provides reservations for next-server,
/// server-hostname and boot-file-name value.
/// - 1 subnet: 192.0.2.0/24
/// - 1 reservation for this subnet:
/// - Client's HW address: aa:bb:cc:dd:ee:ff
/// - next-server = 10.0.0.7
/// - server name = "some-name.example.org"
/// - boot-file-name = "bootfile.efi"
const char* INFORM_CONFIGS[] = {
// Configuration 0
"{ \"interfaces-config\": {"
......@@ -91,7 +101,26 @@ const char* INFORM_CONFIGS[] = {
" \"data\": \"10.0.0.202,10.0.0.203\""
" } ]"
" } ]"
"}"
"}",
// Configuration 2
"{ \"interfaces-config\": {"
" \"interfaces\": [ \"*\" ]"
"},"
"\"valid-lifetime\": 600,"
"\"next-server\": \"10.0.0.1\","
"\"subnet4\": [ { "
" \"subnet\": \"192.0.2.0/24\", "
" \"reservations\": [ "
" {"
" \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
" \"next-server\": \"10.0.0.7\","
" \"server-hostname\": \"some-name.example.org\","
" \"boot-file-name\": \"bootfile.efi\""
" }"
" ]"
"} ]"
"}",
};
/// @brief Test fixture class for testing DHCPINFORM.
......@@ -364,6 +393,32 @@ TEST_F(InformTest, relayedClientNoCiaddr) {
EXPECT_EQ("192.0.2.203", client.config_.dns_servers_[1].toText());
}
// This test verifies that the server assigns reserved values for the
// siaddr, sname and file fields carried within DHCPv4 message.
TEST_F(InformTest, messageFieldsReservations) {
// Client has a reservation.
Dhcp4Client client(Dhcp4Client::SELECTING);
// Message is relayed.
client.useRelay();
// Set explicit HW address so as it matches the reservation in the
// configuration used below.
client.setHWAddress("aa:bb:cc:dd:ee:ff");
// Configure DHCP server.
configure(INFORM_CONFIGS[2], *client.getServer());
// Client sends DHCPINFORM and should receive reserved fields.
ASSERT_NO_THROW(client.doInform());
// Make sure that the server responded.
ASSERT_TRUE(client.getContext().response_);
Pkt4Ptr resp = client.getContext().response_;
// Make sure that the server has responded with DHCPACK.
ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
// Check that the reserved values have been assigned.
EXPECT_EQ("10.0.0.7", client.config_.siaddr_.toText());
EXPECT_EQ("some-name.example.org", client.config_.sname_);
EXPECT_EQ("bootfile.efi", client.config_.boot_file_name_);
}
/// This test verifies that after a client completes its INFORM exchange,
/// appropriate statistics are updated.
TEST_F(InformTest, statisticsInform) {
......
......@@ -572,10 +572,13 @@ CfgHosts::add4(const HostPtr& host) {
DuidPtr duid = host->getDuid();
// There should be at least one resource reserved: hostname, IPv4
// address, IPv6 address or prefix.
// address, siaddr, sname, file or IPv6 address or prefix.
if (host->getHostname().empty() &&
(host->getIPv4Reservation().isV4Zero()) &&
(!host->hasIPv6Reservation()) &&
!host->hasIPv6Reservation() &&
host->getNextServer().isV4Zero() &&
host->getServerHostname().empty() &&
host->getBootFileName().empty() &&
host->getCfgOption4()->empty() &&
host->getCfgOption6()->empty()) {
std::ostringstream s;
......
......@@ -5,6 +5,7 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#include <config.h>
#include <dhcp/pkt4.h>
#include <dhcpsrv/host.h>
#include <util/encode/hex.h>
#include <util/strutil.h>
......@@ -74,14 +75,20 @@ Host::Host(const uint8_t* identifier, const size_t identifier_len,
const asiolink::IOAddress& ipv4_reservation,
const std::string& hostname,
const std::string& dhcp4_client_classes,
const std::string& dhcp6_client_classes)
const std::string& dhcp6_client_classes,
const asiolink::IOAddress& next_server,
const std::string& server_host_name,
const std::string& boot_file_name)
: identifier_type_(identifier_type),
identifier_value_(), ipv4_subnet_id_(ipv4_subnet_id),
ipv6_subnet_id_(ipv6_subnet_id),
ipv4_reservation_(asiolink::IOAddress::IPV4_ZERO_ADDRESS()),
hostname_(hostname), dhcp4_client_classes_(dhcp4_client_classes),
dhcp6_client_classes_(dhcp6_client_classes), host_id_(0),
cfg_option4_(new CfgOption()), cfg_option6_(new CfgOption()) {
dhcp6_client_classes_(dhcp6_client_classes),
next_server_(asiolink::IOAddress::IPV4_ZERO_ADDRESS()),
server_host_name_(server_host_name), boot_file_name_(boot_file_name),
host_id_(0), cfg_option4_(new CfgOption()), cfg_option6_(new CfgOption()) {
// Initialize host identifier.
setIdentifier(identifier, identifier_len, identifier_type);
......@@ -90,6 +97,11 @@ Host::Host(const uint8_t* identifier, const size_t identifier_len,
// Validate and set IPv4 address reservation.
setIPv4Reservation(ipv4_reservation);
}
if (!next_server.isV4Zero()) {
// Validate and set next server address.
setNextServer(next_server);
}
}
Host::Host(const std::string& identifier, const std::string& identifier_name,
......@@ -97,14 +109,19 @@ Host::Host(const std::string& identifier, const std::string& identifier_name,
const asiolink::IOAddress& ipv4_reservation,
const std::string& hostname,
const std::string& dhcp4_client_classes,
const std::string& dhcp6_client_classes)
const std::string& dhcp6_client_classes,
const asiolink::IOAddress& next_server,
const std::string& server_host_name,
const std::string& boot_file_name)
: identifier_type_(IDENT_HWADDR),
identifier_value_(), ipv4_subnet_id_(ipv4_subnet_id),
ipv6_subnet_id_(ipv6_subnet_id),
ipv4_reservation_(asiolink::IOAddress::IPV4_ZERO_ADDRESS()),
hostname_(hostname), dhcp4_client_classes_(dhcp4_client_classes),
dhcp6_client_classes_(dhcp6_client_classes), host_id_(0),
cfg_option4_(new CfgOption()), cfg_option6_(new CfgOption()) {
dhcp6_client_classes_(dhcp6_client_classes),
next_server_(asiolink::IOAddress::IPV4_ZERO_ADDRESS()),
server_host_name_(server_host_name), boot_file_name_(boot_file_name),
host_id_(0), cfg_option4_(new CfgOption()), cfg_option6_(new CfgOption()) {
// Initialize host identifier.
setIdentifier(identifier, identifier_name);
......@@ -113,6 +130,11 @@ Host::Host(const std::string& identifier, const std::string& identifier_name,
// Validate and set IPv4 address reservation.
setIPv4Reservation(ipv4_reservation);
}
if (!next_server.isV4Zero()) {
// Validate and set next server address.
setNextServer(next_server);
}
}
const std::vector<uint8_t>&
......@@ -339,6 +361,37 @@ Host::addClientClassInternal(ClientClasses& classes,
}
}
void
Host::setNextServer(const asiolink::IOAddress& next_server) {
if (!next_server.isV4()) {
isc_throw(isc::BadValue, "next server address '" << next_server
<< "' is not a valid IPv4 address");
} else if (next_server.isV4Bcast()) {
isc_throw(isc::BadValue, "invalid next server address '"
<< next_server << "'");
}
next_server_ = next_server;
}
void
Host::setServerHostname(const std::string& server_host_name) {
if (server_host_name.size() > Pkt4::MAX_SNAME_LEN - 1) {
isc_throw(isc::BadValue, "server hostname length must not exceed "
<< (Pkt4::MAX_SNAME_LEN - 1));
}
server_host_name_ = server_host_name;
}
void
Host::setBootFileName(const std::string& boot_file_name) {
if (boot_file_name.size() > Pkt4::MAX_FILE_LEN - 1) {
isc_throw(isc::BadValue, "boot file length must not exceed "
<< (Pkt4::MAX_FILE_LEN - 1));
}
boot_file_name_ = boot_file_name;
}
std::string
Host::toText() const {
std::ostringstream s;
......@@ -363,6 +416,16 @@ Host::toText() const {
s << " ipv4_reservation=" << (ipv4_reservation_.isV4Zero() ? "(no)" :
ipv4_reservation_.toText());
// Add next server.
s << " siaddr=" << (next_server_.isV4Zero() ? "(no)" :
next_server_.toText());
// Add server host name.
s << " sname=" << (server_host_name_.empty() ? "(empty)" : server_host_name_);
// Add boot file name.
s << " file=" << (boot_file_name_.empty() ? "(empty)" : boot_file_name_);
if (ipv6_reservations_.empty()) {
s << " ipv6_reservations=(none)";
......
......@@ -214,6 +214,9 @@ public:
/// separated by commas. The names get trimmed by this constructor.
/// @param dhcp6_client_classes A string holding DHCPv6 client class names
/// separated by commas. The names get trimmed by this constructor.
/// @param next_server IPv4 address of next server (siaddr).
/// @param server_host_name Server host name (a.k.a. sname).
/// @param boot_file_name Boot file name (a.k.a. file).
///
/// @throw BadValue if the provided values are invalid. In particular,
/// if the identifier is invalid.
......@@ -223,7 +226,10 @@ public:
const asiolink::IOAddress& ipv4_reservation,
const std::string& hostname = "",
const std::string& dhcp4_client_classes = "",
const std::string& dhcp6_client_classes = "");
const std::string& dhcp6_client_classes = "",
const asiolink::IOAddress& next_server = asiolink::IOAddress::IPV4_ZERO_ADDRESS(),
const std::string& server_host_name = "",
const std::string& boot_file_name = "");
/// @brief Constructor.
///
......@@ -258,6 +264,9 @@ public:
/// separated by commas. The names get trimmed by this constructor.
/// @param dhcp6_client_classes A string holding DHCPv6 client class names
/// separated by commas. The names get trimmed by this constructor.
/// @param next_server IPv4 address of next server (siaddr).
/// @param server_host_name Server host name (a.k.a. sname).
/// @param boot_file_name Boot file name (a.k.a. file).
///
/// @throw BadValue if the provided values are invalid. In particular,
/// if the identifier is invalid.
......@@ -266,7 +275,10 @@ public:
const asiolink::IOAddress& ipv4_reservation,
const std::string& hostname = "",
const std::string& dhcp4_client_classes = "",
const std::string& dhcp6_client_classes = "");
const std::string& dhcp6_client_classes = "",
const asiolink::IOAddress& next_server = asiolink::IOAddress::IPV4_ZERO_ADDRESS(),
const std::string& server_host_name = "",
const std::string& boot_file_name = "");
/// @brief Replaces currently used identifier with a new identifier.
///
......@@ -449,6 +461,43 @@ public:
return (dhcp6_client_classes_);
}
/// @brief Sets new value for next server field (siaddr).
///
/// @param next_server New address of a next server.
///
/// @throw isc::BadValue if the provided address is not an IPv4 address,
/// is broadcast address.
void setNextServer(const asiolink::IOAddress& next_server);
/// @brief Returns value of next server field (siaddr).
const asiolink::IOAddress& getNextServer() const {
return (next_server_);
}
/// @brief Sets new value for server hostname (sname).
///
/// @param server_host_name New value for server hostname.
///
/// @throw BadValue if hostname is longer than 63 bytes.
void setServerHostname(const std::string& server_host_name);
/// @brief Returns value of server hostname (sname).
const std::string& getServerHostname() const {
return (server_host_name_);
}
/// @brief Sets new value for boot file name (file).
///
/// @param boot_file_name New value of boot file name.
///
/// @throw BadValue if boot file name is longer than 128 bytes.
void setBootFileName(const std::string& boot_file_name);
/// @brief Returns value of boot file name (file).
const std::string& getBootFileName() const {
return (boot_file_name_);
}
/// @brief Returns pointer to the DHCPv4 option data configuration for
/// this host.
///
......@@ -527,6 +576,12 @@ private:
ClientClasses dhcp4_client_classes_;
/// @brief Collection of classes associated with a DHCPv6 client.
ClientClasses dhcp6_client_classes_;
/// @brief Next server (a.k.a. siaddr, carried in DHCPv4 message).
asiolink::IOAddress next_server_;
/// @brief Server host name (a.k.a. sname, carried in DHCPv4 message).
std::string server_host_name_;
/// @brief Boot file name (a.k.a. file, carried in DHCPv4 message)
std::string boot_file_name_;
/// @brief HostID (a unique identifier assigned when the host is stored in
/// MySQL or Pgsql)
......
......@@ -63,6 +63,12 @@ const size_t OPTION_FORMATTED_VALUE_MAX_LEN = 8192;
/// @brief Maximum length of option space name.
const size_t OPTION_SPACE_MAX_LEN = 128;
/// @brief Maximum length of the server hostname.
const size_t SERVER_HOSTNAME_MAX_LEN = 64;
/// @brief Maximum length of the boot file name.
const size_t BOOT_FILE_NAME_MAX_LEN = 128;
/// @brief Numeric value representing last supported identifier.
///
/// This value is used to validate whether the identifier type stored in
......@@ -81,7 +87,7 @@ class MySqlHostExchange {
private: