2
0
mirror of https://gitlab.isc.org/isc-projects/kea synced 2025-08-29 13:07:50 +00:00

[master] kea-dhcp6 now supports global host reservations

Merge branch '13-global-host-reservations-task-3-add-v6-support-for-new-hr_global-mode'
This commit is contained in:
Thomas Markwalder 2018-08-27 13:56:45 -04:00
commit a5484c4d88
9 changed files with 871 additions and 74 deletions

View File

@ -11,7 +11,7 @@ Primary developers:
- Marcin Siodelski (DHCPv4, DHCPv6 components, options handling, perfdhcp,
host reservation, lease file cleanup, lease expiration,
control agent, shared networks, high availability)
- Thomas Markwalder (DDNS, user_chk)
- Thomas Markwalder (DDNS, user_chk, global host reservations)
- Jeremy C. Reed (documentation, build system, testing, release engineering)
- Wlodek Wencel (testing, release engineering)
- Francis Dupont (crypto, perfdhcp, control agent)

View File

@ -3103,7 +3103,7 @@ should include options from the isc option space:
</simpara>
</listitem>
</itemizedlist>
<para>This feature is currently implemented for memfile backend.</para>
<para>
@ -3268,6 +3268,24 @@ should include options from the isc option space:
reservation checks when dealing with existing leases. Therefore, system
administrators are encouraged to use out-of-pool reservations if
possible.</para>
<para>Beginning with Kea 1.5.0, there is now support for global
host reservations. These are reservations that are specified at the
global level within the configuration and that do not belong to any
specific subnet. Kea will still match inbound client packets to a
subnet as before, but when the subnet's reservation mode is set to
<command>"global"</command>, Kea will look for host reservations only
among the global reservations defined. Typcially, such reservations would
be used to reserve hostnames for clients which may move from one subnet
to another.
</para>
<note>You can reserve any ip-address or prefix in a global reservation.
Just keep in mind that Kea will not do any sanity checking on the address
or prefix and that for Kea 1.5.0, support for global reservations should
be considered experimental.
</note>
</section>
<section xml:id="reservation6-conflict">
@ -3317,6 +3335,15 @@ should include options from the isc option space:
out-of-pool reservations. If the reserved address does not belong to a
pool, there is no way that other clients could get this address.
</para>
<note>
<para>The conflict resolution mechanism does not work for global
reservations. As of Kea 1.5.0, it is generally recommended to not use
global reservations for addresses or prefixes. If you want to use it
anyway, you have to manually ensure that the reserved values are not
in the dynamic pools.</para>
</note>
</section>
<section xml:id="reservation6-hostname">
@ -3538,10 +3565,10 @@ should include options from the isc option space:
Allowed values are:
<itemizedlist>
<listitem><simpara> <command>all</command> - enables all host reservation
types. This is the default value. This setting is the safest and the most
flexible. It allows in-pool and out-of-pool reservations. As all checks
are conducted, it is also the slowest.
<listitem><simpara> <command>all</command> - enables both in-pool
and out-of-pool host reservation types. This is the default value. This
setting is the safest and the most flexible. As all checks are conducted,
it is also the slowest. This does not check against global reservations.
</simpara></listitem>
<listitem><simpara> <command>out-of-pool</command> - allows only out of
@ -3551,7 +3578,18 @@ should include options from the isc option space:
with in-pool addresses, thus improving performance. Do not use this mode
if any of your reservations use in-pool address. Caution is advised when
using this setting. Kea does not sanity check the reservations against
<command>reservation-mode</command> and misconfiguration may cause problems.
<command>reservation-mode</command> and misconfiguration may cause
problems.
</simpara></listitem>
<listitem><simpara> <command>global</command> - allows only global
host reservations. With this setting in place, the server searches for
reservations for a client only among the defined global reservations.
If an address is specified, the server will skip the reservation checks
done when dealing in other modes, thus improving performance.
Caution is advised when using this setting: Kea does not sanity check
the reservations when <command>global</command> and
misconfiguration may cause problems.
</simpara></listitem>
<listitem><simpara>
@ -3576,9 +3614,44 @@ should include options from the isc option space:
}
]
}
</screen>
</screen>
</para>
<para>
An example configuration using global reservations is shown below:
<screen>
"Dhcp6": {
<userinput>
"reservations": [
{
"duid": "00:03:00:01:11:22:33:44:55:66",
"hostname": "host-one"
},
{
"duid": "00:03:00:01:99:88:77:66:55:44",
"hostname": "host-two"
}
],
</userinput>
"subnet6": [
{
"subnet": "2001:db8:1::/64",
<userinput>"reservation-mode": "global"</userinput>,
...
},
{
"subnet": "2001:db8:2::/64",
<userinput>"reservation-mode": "global"</userinput>,
...
}
]
}
</screen>
For more details regarding global reservations, see
<xref linkend="global-reservations6"/>.
</para>
<para>Another aspect of the host reservations are different types of
identifiers. Kea 1.1.0 supports two types of identifiers
in DHCPv6: hw-address and duid, but more identifier types
@ -3620,6 +3693,76 @@ If not specified, the default value is:
</screen>
</para>
</section>
<section id="global-reservations6">
<title>Global reservations in DHCPv6</title>
<para>In some deployments, such as mobile, clients can roam within the
network and there is a desire to specify certain parameters regardless of
the client's current location. To facilitate such a need, a global
reservation mechanism has been implemented. The idea behind it is that
regular host reservations are tied to specific subnets, by using specific
subnet-id. Kea 1.5.0 introduced a new capability to specify global
reservation that can be used in every subnet that has global reservations
enabled.</para>
<para>This feature can be used to assign certain parameters, such as
hostname or other dedicated, host-specific options. It can also be used to
assign addresses or prefixes. However, global reservations that assign
either of these bypass the whole topology determination provided by DHCP
logic implemented in Kea. It is very easy to misuse this feature and get
configuration that is inconsistent. To give a specific example, imagine a
global reservation for an address 2001:db8:1111::1 and two subnets
2001:db8:1111::/48 and 2001:db8:ffff::/48. If global reservations are used
in both subnets and a device matching global host reservations visits part
of the network that is covered by 2001:db8:ffff::/48, it will get an IP
address 2001:db8:ffff::1, which will be outside of the prefix announced
by its local router using Router Advertisements. Such a configuration
would be unsuable or at the very least ridden with issues, such as the
downlink traffic not reaching the device.</para>
<para>
To use global host reservations a configuration similar to the following
can be used:
<screen>
"Dhcp6:" {
// This specifies global reservations. They will apply to all subnets that
// have global reservations enabled.
<userinput>
"reservations": [
{
"hw-address": "aa:bb:cc:dd:ee:ff",
"hostname": "hw-host-dynamic"
},
{
"hw-address": "01:02:03:04:05:06",
"hostname": "hw-host-fixed",
// Use of IP address is global reservation is risky. If used outside of
// matching subnet, such as 3001::/64, it will result in a broken
// configuration being handled to the client.
"ip-address": "2001:db8:ff::77"
},
{
"duid": "01:02:03:04:05",
"hostname": "duid-host"
}
]</userinput>,
"valid-lifetime": 600,
"subnet4": [ {
"subnet": "2001:db8:1::/64",
<userinput>"reservation-mode": "global",</userinput>
"pools": [ { "pool": "2001:db8:1::-2001:db8:1::100" } ]
} ]
}
</screen>
</para>
<para>When using database backends, the global host reservations are
distinguished from regular reservations by using subnet-id value of
zero.</para>
<!-- see CfgHostOperations::createConfig6() in
src/lib/dhcpsrv/cfg_host_operations.cc -->

View File

@ -699,7 +699,13 @@ Dhcpv6SrvTest::configure(const std::string& config) {
void
Dhcpv6SrvTest::configure(const std::string& config, NakedDhcpv6Srv& srv) {
ConstElementPtr json;
ASSERT_NO_THROW(json = parseJSON(config));
try {
json = parseJSON(config);
} catch (const std::exception& ex) {
// Fatal failure on parsing error
FAIL() << "config parsing failed, test is broken: " << ex.what();
}
ConstElementPtr status;
// Disable the re-detect flag
@ -710,7 +716,8 @@ Dhcpv6SrvTest::configure(const std::string& config, NakedDhcpv6Srv& srv) {
ASSERT_TRUE(status);
int rcode;
ConstElementPtr comment = isc::config::parseAnswer(rcode, status);
ASSERT_EQ(0, rcode);
ASSERT_EQ(0, rcode) << "configuration failed, test is broken: "
<< comment->str();
CfgMgr::instance().commit();
}

View File

@ -321,7 +321,105 @@ const char* CONFIGS[] = {
" }"
" ]"
"} ]"
"}"
"}",
// Configuration 8: Global HRs TYPE_NAs
"{ "
"\"interfaces-config\": { \n"
" \"interfaces\": [ \"*\" ] \n"
"},\n "
"\"host-reservation-identifiers\": [ \"duid\", \"hw-address\" ], \n"
"\"reservations\": [ \n"
"{ \n"
" \"duid\": \"01:02:03:04\", \n"
" \"hostname\": \"duid-host-fixed\", \n"
" \"ip-addresses\": [ \"3001::1\" ] \n"
"}, \n"
"{ \n"
" \"duid\": \"01:02:03:05\", \n"
" \"hostname\": \"duid-host-dynamic\" \n"
"}, \n"
"{ \n"
" \"hw-address\": \"38:60:77:d5:ff:ee\", \n"
" \"hostname\": \"hw-host\" \n"
"} \n"
"], \n"
"\"valid-lifetime\": 4000, \n"
"\"preferred-lifetime\": 3000, \n"
"\"rebind-timer\": 2000, \n"
"\"renew-timer\": 1000, \n"
"\"mac-sources\": [ \"ipv6-link-local\" ], \n"
"\"subnet6\": [ \n"
" { \n"
" \"subnet\": \"2001:db8:1::/48\", \n"
" \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ], \n"
" \"interface\" : \"eth0\", \n"
" \"reservation-mode\": \"global\" \n"
" },"
" { \n"
" \"subnet\": \"2001:db8:2::/48\", \n"
" \"pools\": [ { \"pool\": \"2001:db8:2::/64\" } ], \n"
" \"interface\" : \"eth1\", \n"
" \"reservations\": [ \n"
" { \n"
" \"duid\": \"01:02:03:05\", \n"
" \"hostname\": \"subnet-duid-host\" \n"
" }] \n"
" }"
" ] \n"
"} \n"
,
// Configuration 9: Global HRs TYPE_PDs
"{ "
"\"interfaces-config\": { \n"
" \"interfaces\": [ \"*\" ] \n"
"},\n "
"\"host-reservation-identifiers\": [ \"duid\", \"hw-address\" ], \n"
"\"reservations\": [ \n"
"{ \n"
" \"duid\": \"01:02:03:04\", \n"
" \"hostname\": \"duid-host-fixed\", \n"
" \"prefixes\": [ \"4000::100/120\" ]"
"}, \n"
"{ \n"
" \"duid\": \"01:02:03:05\", \n"
" \"hostname\": \"duid-host-dynamic\" \n"
"} \n"
"], \n"
"\"valid-lifetime\": 4000, \n"
"\"preferred-lifetime\": 3000, \n"
"\"rebind-timer\": 2000, \n"
"\"renew-timer\": 1000, \n"
"\"mac-sources\": [ \"ipv6-link-local\" ], \n"
"\"subnet6\": [ \n"
" { \n"
" \"subnet\": \"2001:db8:1::/48\", \n"
" \"interface\" : \"eth0\", \n"
" \"reservation-mode\": \"global\", \n"
" \"pd-pools\": [ \n"
" { \n"
" \"prefix\": \"3000::\", \n"
" \"prefix-len\": 119, \n"
" \"delegated-len\": 120 \n"
" }] \n"
" },"
" { \n"
" \"subnet\": \"2001:db8:2::/48\", \n"
" \"interface\" : \"eth1\", \n"
" \"pd-pools\": [ \n"
" { \n"
" \"prefix\": \"3001::\", \n"
" \"prefix-len\": 119, \n"
" \"delegated-len\": 120 \n"
" }], \n"
" \"reservations\": [ \n"
" { \n"
" \"duid\": \"01:02:03:05\", \n"
" \"hostname\": \"subnet-duid-host\" \n"
" }] \n"
" }"
" ] \n"
"} \n"
};
/// @brief Base class representing leases and hints conveyed within IAs.
@ -701,6 +799,14 @@ public:
const Reservation& r5 = Reservation::UNSPEC(),
const Reservation& r6 = Reservation::UNSPEC()) const;
/// @brief Verifies that an SARR exchange results in the expected lease
///
/// @param client Client configured to request a single lease
/// @param exp_address expected address/prefix of the lease
/// @param exp_hostname expected hostname on the lease
void sarrTest(Dhcp6Client& client, const std::string& exp_address,
const std::string& exp_hostname);
/// @brief Configures client to include hint.
///
/// @param client Reference to a client.
@ -1063,6 +1169,25 @@ HostTest::requestEmptyIAs(Dhcp6Client& client) {
client.requestPrefix(6);
}
void
HostTest::sarrTest(Dhcp6Client& client, const std::string& exp_address,
const std::string& exp_hostname) {
// Perform 4-way exchange.
ASSERT_NO_THROW(client.doSARR());
// Verify that the client got a dynamic address
ASSERT_EQ(1, client.getLeaseNum());
Lease6 lease_client = client.getLease(0);
EXPECT_EQ(exp_address, lease_client.addr_.toText());
// Check that the server recorded the lease
// and that the server lease has expected hostname.
Lease6Ptr lease_server = checkLease(lease_client);
ASSERT_TRUE(lease_server);
EXPECT_EQ(exp_hostname, lease_server->hostname_);
}
// Test basic SARR scenarios against a server configured with one subnet
// containing two reservations. One reservation with a hostname, one
// without a hostname. Scenarios:
@ -1918,5 +2043,108 @@ TEST_F(HostTest, conflictResolutionReuseExpired) {
EXPECT_FALSE(client2.hasLeaseWithZeroLifetimeForPrefix(IOAddress("3000::"), 120));
}
// Verifies fundamental Global vs Subnet host reservations for NA leases
TEST_F(HostTest, globalReservationsNA) {
Dhcp6Client client;
ASSERT_NO_FATAL_FAILURE(configure(CONFIGS[8], *client.getServer()));
const Subnet6Collection* subnets = CfgMgr::instance().getCurrentCfg()->
getCfgSubnets6()->getAll();
ASSERT_EQ(2, subnets->size());
{
SCOPED_TRACE("Global HR by DUID with reserved address");
client.setDUID("01:02:03:04");
client.requestAddress(1234, IOAddress("::"));
// Should get global reserved address and reserved host name
ASSERT_NO_FATAL_FAILURE(sarrTest(client, "3001::1", "duid-host-fixed"));
}
{
SCOPED_TRACE("Global HR by DUID with dynamic address");
client.clearConfig();
client.setDUID("01:02:03:05");
client.requestAddress(1234, IOAddress("::"));
// Should get dynamic address and reserved host name
ASSERT_NO_FATAL_FAILURE(sarrTest(client, "2001:db8:1::", "duid-host-dynamic"));
}
{
SCOPED_TRACE("Global HR by HW Address with dynamic address");
client.clearConfig();
client.setDUID("33:44:55:66");
client.setLinkLocal(IOAddress("fe80::3a60:77ff:fed5:ffee"));
client.requestAddress(1234, IOAddress("::"));
// Should get dynamic address and hardware host name
ASSERT_NO_FATAL_FAILURE(sarrTest(client, "2001:db8:1::1", "hw-host"));
}
{
SCOPED_TRACE("Default subnet mode excludes Global HR");
client.clearConfig();
client.setInterface("eth1");
client.setDUID("01:02:03:04");
client.requestAddress(1234, IOAddress("::"));
// Should get dynamic address and no host name
ASSERT_NO_FATAL_FAILURE(sarrTest(client, "2001:db8:2::", ""));
}
{
SCOPED_TRACE("Subnet reservation over global");
client.clearConfig();
client.setInterface("eth1");
client.setDUID("01:02:03:05");
client.requestAddress(1234, IOAddress("::"));
// Should get dynamic address and host name
ASSERT_NO_FATAL_FAILURE(sarrTest(client, "2001:db8:2::1", "subnet-duid-host"));
}
}
// Verifies fundamental Global vs Subnet host reservations for PD leases
TEST_F(HostTest, globalReservationsPD) {
Dhcp6Client client;
ASSERT_NO_FATAL_FAILURE(configure(CONFIGS[9], *client.getServer()));
const Subnet6Collection* subnets = CfgMgr::instance().getCurrentCfg()->
getCfgSubnets6()->getAll();
ASSERT_EQ(2, subnets->size());
{
SCOPED_TRACE("Global HR by DUID with reserved prefix");
client.setDUID("01:02:03:04");
client.requestPrefix(1);
// Should get global reserved prefix and reserved host name
ASSERT_NO_FATAL_FAILURE(sarrTest(client, "4000::100", "duid-host-fixed"));
}
{
SCOPED_TRACE("Global HR by DUID with dynamic prefix");
client.clearConfig();
client.setDUID("01:02:03:05");
client.requestPrefix(1);
// Should get dynamic prefix and reserved host name
ASSERT_NO_FATAL_FAILURE(sarrTest(client, "3000::", "duid-host-dynamic"));
}
{
SCOPED_TRACE("Default subnet mode excludes Global HR");
client.clearConfig();
client.setInterface("eth1");
client.setDUID("01:02:03:04");
client.requestPrefix(1);
// Should get dynamic prefix and no host name
ASSERT_NO_FATAL_FAILURE(sarrTest(client, "3001::", ""));
}
{
SCOPED_TRACE("Subnet reservation over global");
client.clearConfig();
client.setInterface("eth1");
client.setDUID("01:02:03:05");
client.requestPrefix(1);
// Should get dynamic prefix and subnet reserved host name
ASSERT_NO_FATAL_FAILURE(sarrTest(client, "3001::100", "subnet-duid-host"));
}
}
} // end of anonymous namespace

View File

@ -483,14 +483,37 @@ ConstHostPtr
AllocEngine::ClientContext6::currentHost() const {
Subnet6Ptr subnet = host_subnet_ ? host_subnet_ : subnet_;
if (subnet) {
auto host = hosts_.find(subnet->getID());
SubnetID id = (subnet_->getHostReservationMode() == Network::HR_GLOBAL ?
SUBNET_ID_GLOBAL : subnet->getID());
auto host = hosts_.find(id);
if (host != hosts_.cend()) {
return (host->second);
}
}
return (ConstHostPtr());
}
ConstHostPtr
AllocEngine::ClientContext6::globalHost() const {
Subnet6Ptr subnet = host_subnet_ ? host_subnet_ : subnet_;
if (subnet && subnet_->getHostReservationMode() == Network::HR_GLOBAL) {
auto host = hosts_.find(SUBNET_ID_GLOBAL);
if (host != hosts_.cend()) {
return (host->second);
}
}
return (ConstHostPtr());
}
bool
AllocEngine::ClientContext6::hasGlobalReservation(const IPv6Resrv& resv) const {
ConstHostPtr ghost = globalHost();
return (ghost && ghost->hasReservation(resv));
}
void AllocEngine::findReservation(ClientContext6& ctx) {
ctx.hosts_.clear();
@ -505,6 +528,17 @@ void AllocEngine::findReservation(ClientContext6& ctx) {
SharedNetwork6Ptr network;
subnet->getSharedNetwork(network);
if (subnet->getHostReservationMode() == Network::HR_GLOBAL) {
ConstHostPtr ghost = findGlobalReservation(ctx);
if (ghost) {
ctx.hosts_[SUBNET_ID_GLOBAL] = ghost;
// @todo In theory, to support global as part of HR_ALL,
// we would just keep going, instead of returning.
return;
}
}
// If the subnet belongs to a shared network it is usually going to be
// more efficient to make a query for all reservations for a particular
// client rather than a query for each subnet within this shared network.
@ -567,6 +601,24 @@ void AllocEngine::findReservation(ClientContext6& ctx) {
}
}
ConstHostPtr
AllocEngine::findGlobalReservation(ClientContext6& ctx) {
ConstHostPtr host;
BOOST_FOREACH(const IdentifierPair& id_pair, ctx.host_identifiers_) {
// Attempt to find a host using a specified identifier.
host = HostMgr::instance().get6(SUBNET_ID_GLOBAL, id_pair.first,
&id_pair.second[0], id_pair.second.size());
// If we found matching global host we're done.
if (host) {
break;
}
}
return (host);
}
Lease6Collection
AllocEngine::allocateLeases6(ClientContext6& ctx) {
@ -1020,7 +1072,6 @@ void
AllocEngine::allocateReservedLeases6(ClientContext6& ctx,
Lease6Collection& existing_leases) {
// If there are no reservations or the reservation is v4, there's nothing to do.
if (ctx.hosts_.empty()) {
LOG_DEBUG(alloc_engine_logger, ALLOC_ENGINE_DBG_TRACE,
@ -1029,6 +1080,11 @@ AllocEngine::allocateReservedLeases6(ClientContext6& ctx,
return;
}
if (allocateGlobalReservedLeases6(ctx, existing_leases)) {
// global reservation provided the lease, we're done
return;
}
// Let's convert this from Lease::Type to IPv6Reserv::Type
IPv6Resrv::Type type = ctx.currentIA().type_ == Lease::TYPE_NA ?
IPv6Resrv::TYPE_NA : IPv6Resrv::TYPE_PD;
@ -1039,8 +1095,7 @@ AllocEngine::allocateReservedLeases6(ClientContext6& ctx,
BOOST_FOREACH(const Lease6Ptr& lease, existing_leases) {
if ((lease->valid_lft_ != 0)) {
if ((ctx.hosts_.count(lease->subnet_id_) > 0) &&
ctx.hosts_[lease->subnet_id_]->hasReservation(IPv6Resrv(type, lease->addr_,
lease->prefixlen_))) {
ctx.hosts_[lease->subnet_id_]->hasReservation(makeIPv6Resrv(*lease))) {
// We found existing lease for a reserved address or prefix.
// We'll simply extend the lifetime of the lease.
LOG_DEBUG(alloc_engine_logger, ALLOC_ENGINE_DBG_TRACE,
@ -1188,6 +1243,127 @@ AllocEngine::allocateReservedLeases6(ClientContext6& ctx,
}
}
bool
AllocEngine::allocateGlobalReservedLeases6(ClientContext6& ctx,
Lease6Collection& existing_leases) {
// Get the global host
ConstHostPtr ghost = ctx.globalHost();
if (!ghost) {
return (false);
}
// We want to avoid allocating a new lease for an IA if there is already
// a valid lease for which client has reservation. So, we first check if
// we already have a lease for a reserved address or prefix.
BOOST_FOREACH(const Lease6Ptr& lease, existing_leases) {
if ((lease->valid_lft_ != 0) &&
(ghost->hasReservation(makeIPv6Resrv(*lease)))) {
// We found existing lease for a reserved address or prefix.
// We'll simply extend the lifetime of the lease.
LOG_DEBUG(alloc_engine_logger, ALLOC_ENGINE_DBG_TRACE,
ALLOC_ENGINE_V6_ALLOC_HR_LEASE_EXISTS)
.arg(ctx.query_->getLabel())
.arg(lease->typeToText(lease->type_))
.arg(lease->addr_.toText());
// Besides IP reservations we're also going to return other reserved
// parameters, such as hostname. We want to hand out the hostname value
// from the same reservation entry as IP addresses. Thus, let's see if
// there is any hostname reservation.
if (!ghost->getHostname().empty()) {
// We have to determine whether the hostname is generated
// in response to client's FQDN or not. If yes, we will
// need to qualify the hostname. Otherwise, we just use
// the hostname as it is specified for the reservation.
OptionPtr fqdn = ctx.query_->getOption(D6O_CLIENT_FQDN);
ctx.hostname_ = CfgMgr::instance().getD2ClientMgr().
qualifyName(ghost->getHostname(), static_cast<bool>(fqdn));
}
// If this is a real allocation, we may need to extend the lease
// lifetime.
if (!ctx.fake_allocation_ && conditionalExtendLifetime(*lease)) {
LeaseMgrFactory::instance().updateLease6(lease);
}
return(true);
}
}
// There is no lease for a reservation in this IA. So, let's now iterate
// over reservations specified and try to allocate one of them for the IA.
// Let's convert this from Lease::Type to IPv6Reserv::Type
IPv6Resrv::Type type = ctx.currentIA().type_ == Lease::TYPE_NA ?
IPv6Resrv::TYPE_NA : IPv6Resrv::TYPE_PD;
const IPv6ResrvRange& reservs = ghost->getIPv6Reservations(type);
BOOST_FOREACH(IPv6ResrvTuple type_lease_tuple, reservs) {
// We do have a reservation for address or prefix.
const IOAddress& addr = type_lease_tuple.second.getPrefix();
uint8_t prefix_len = type_lease_tuple.second.getPrefixLen();
// We have allocated this address/prefix while processing one of the
// previous IAs, so let's try another reservation.
if (ctx.isAllocated(addr, prefix_len)) {
continue;
}
// If there's a lease for this address, let's not create it.
// It doesn't matter whether it is for this client or for someone else.
if (!LeaseMgrFactory::instance().getLease6(ctx.currentIA().type_, addr)) {
if (!ghost->getHostname().empty()) {
// If there is a hostname reservation here we should stick
// to this reservation. By updating the hostname in the
// context we make sure that the database is updated with
// this new value and the server doesn't need to do it and
// its processing performance is not impacted by the hostname
// updates.
// We have to determine whether the hostname is generated
// in response to client's FQDN or not. If yes, we will
// need to qualify the hostname. Otherwise, we just use
// the hostname as it is specified for the reservation.
OptionPtr fqdn = ctx.query_->getOption(D6O_CLIENT_FQDN);
ctx.hostname_ = CfgMgr::instance().getD2ClientMgr().
qualifyName(ghost->getHostname(), static_cast<bool>(fqdn));
}
// Ok, let's create a new lease...
CalloutHandle::CalloutNextStep callout_status = CalloutHandle::NEXT_STEP_CONTINUE;
Lease6Ptr lease = createLease6(ctx, addr, prefix_len, callout_status);
// ... and add it to the existing leases list.
existing_leases.push_back(lease);
if (ctx.currentIA().type_ == Lease::TYPE_NA) {
LOG_INFO(alloc_engine_logger, ALLOC_ENGINE_V6_HR_ADDR_GRANTED)
.arg(addr.toText())
.arg(ctx.query_->getLabel());
} else {
LOG_INFO(alloc_engine_logger, ALLOC_ENGINE_V6_HR_PREFIX_GRANTED)
.arg(addr.toText())
.arg(static_cast<int>(prefix_len))
.arg(ctx.query_->getLabel());
}
// We found a lease for this client and this IA. Let's return.
// Returning after the first lease was assigned is useful if we
// have multiple reservations for the same client. If the client
// sends 2 IAs, the first time we call allocateReservedLeases6 will
// use the first reservation and return. The second time, we'll
// go over the first reservation, but will discover that there's
// a lease corresponding to it and will skip it and then pick
// the second reservation and turn it into the lease. This approach
// would work for any number of reservations.
return (true);
}
}
return(false);
}
void
AllocEngine::removeNonmatchingReservedLeases6(ClientContext6& ctx,
Lease6Collection& existing_leases) {
@ -1210,19 +1386,12 @@ AllocEngine::removeNonmatchingReservedLeases6(ClientContext6& ctx,
BOOST_FOREACH(const Lease6Ptr& candidate, copy) {
// If we have reservation we should check if the reservation is for
// the candidate lease. If so, we simply accept the lease.
if (ctx.hosts_.count(candidate->subnet_id_) > 0) {
if (candidate->type_ == Lease6::TYPE_NA) {
if (ctx.hosts_[candidate->subnet_id_]->hasReservation(
IPv6Resrv(IPv6Resrv::TYPE_NA, candidate->addr_))) {
continue;
}
} else {
if (ctx.hosts_[candidate->subnet_id_]->hasReservation(
IPv6Resrv(IPv6Resrv::TYPE_PD,candidate->addr_,
candidate->prefixlen_))) {
continue;
}
}
IPv6Resrv resv = makeIPv6Resrv(*candidate);
if ((ctx.hasGlobalReservation(resv)) ||
((ctx.hosts_.count(candidate->subnet_id_) > 0) &&
(ctx.hosts_[candidate->subnet_id_]->hasReservation(resv)))) {
// We have a subnet reservation
continue;
}
// The candidate address doesn't appear to be reserved for us.
@ -1357,46 +1526,41 @@ AllocEngine::removeNonreservedLeases6(ClientContext6& ctx,
for (Lease6Collection::iterator lease = existing_leases.begin();
lease != existing_leases.end(); ++lease) {
IPv6Resrv resv(ctx.currentIA().type_ == Lease::TYPE_NA ?
IPv6Resrv::TYPE_NA : IPv6Resrv::TYPE_PD,
(*lease)->addr_, (*lease)->prefixlen_);
// If there is no reservation for this subnet.
if ((ctx.hosts_.count((*lease)->subnet_id_) > 0) &&
(ctx.hosts_[(*lease)->subnet_id_]->hasReservation(resv))) {
// If there is reservation for this keep it.
IPv6Resrv resv = makeIPv6Resrv(*(*lease));
if (ctx.hasGlobalReservation(resv) ||
((ctx.hosts_.count((*lease)->subnet_id_) > 0) &&
(ctx.hosts_[(*lease)->subnet_id_]->hasReservation(resv)))) {
continue;
}
} else {
// We have reservations, but not for this lease. Release it.
// Remove this lease from LeaseMgr
LeaseMgrFactory::instance().deleteLease((*lease)->addr_);
// We have reservations, but not for this lease. Release it.
// Update DNS if required.
queueNCR(CHG_REMOVE, *lease);
// Remove this lease from LeaseMgr
LeaseMgrFactory::instance().deleteLease((*lease)->addr_);
// Need to decrease statistic for assigned addresses.
StatsMgr::instance().addValue(
StatsMgr::generateName("subnet", (*lease)->subnet_id_,
ctx.currentIA().type_ == Lease::TYPE_NA ?
"assigned-nas" : "assigned-pds"),
static_cast<int64_t>(-1));
// Update DNS if required.
queueNCR(CHG_REMOVE, *lease);
/// @todo: Probably trigger a hook here
// Need to decrease statistic for assigned addresses.
StatsMgr::instance().addValue(
StatsMgr::generateName("subnet", (*lease)->subnet_id_,
ctx.currentIA().type_ == Lease::TYPE_NA ?
"assigned-nas" : "assigned-pds"),
static_cast<int64_t>(-1));
// Add this to the list of removed leases.
ctx.currentIA().old_leases_.push_back(*lease);
/// @todo: Probably trigger a hook here
// Set this pointer to NULL. The pointer is still valid. We're just
// setting the Lease6Ptr to NULL value. We'll remove all NULL
// pointers once the loop is finished.
lease->reset();
// Add this to the list of removed leases.
ctx.currentIA().old_leases_.push_back(*lease);
// Set this pointer to NULL. The pointer is still valid. We're just
// setting the Lease6Ptr to NULL value. We'll remove all NULL
// pointers once the loop is finished.
lease->reset();
if (--total == 1) {
// If there's only one lease left, break the loop.
break;
}
if (--total == 1) {
// If there's only one lease left, break the loop.
break;
}
}
@ -1740,10 +1904,11 @@ AllocEngine::extendLease6(ClientContext6& ctx, Lease6Ptr lease) {
}
}
// Check if the lease still belongs to the subnet and that the use of this subnet
// is allowed per client classification. If not, remove this lease.
if (((lease->type_ != Lease::TYPE_PD) && !ctx.subnet_->inRange(lease->addr_)) ||
!ctx.subnet_->clientSupported(ctx.query_->getClasses())) {
// If the lease is not global and it is either out of range (NAs only) or it
// is not permitted by subnet client classification, delete it.
if (!(ctx.hasGlobalReservation(makeIPv6Resrv(*lease))) &&
(((lease->type_ != Lease::TYPE_PD) && !ctx.subnet_->inRange(lease->addr_)) ||
!ctx.subnet_->clientSupported(ctx.query_->getClasses()))) {
// Oh dear, the lease is no longer valid. We need to get rid of it.
// Remove this lease from LeaseMgr

View File

@ -484,6 +484,23 @@ public:
/// @return Pointer to the host object.
ConstHostPtr currentHost() const;
/// @brief Returns global host reservation if there is one
///
/// If the current subnet's reservation mode is global and
/// there is a global host (i.e. reservation belonging to
/// the global subnet), return it. Otherwise return an
/// empty pointer.
///
/// @return Pointer to the host object.
ConstHostPtr globalHost() const;
/// @brief Determines if a global reservation exists
///
/// @return true if there current subnet's reservation mode is
/// global and there is global host containing the given
/// lease reservation, false otherwise
bool hasGlobalReservation(const IPv6Resrv& resv) const;
/// @brief Default constructor.
ClientContext6();
@ -747,6 +764,30 @@ public:
/// @param ctx Client context that contains all necessary information.
static void findReservation(ClientContext6& ctx);
/// @brief Attempts to find the host reservation for the client.
///
/// This method attempts to find a "global" host reservation matching the
/// client identifier. It will return the first global reservation that
/// matches per the configured list of host identifiers, or an empty
/// pointer if no matches are found.
///
/// @param ctx Client context holding various information about the client.
/// @return Pointer to the reservation found, or an empty pointer.
static ConstHostPtr findGlobalReservation(ClientContext6& ctx);
/// @brief Creates an IPv6Resrv instance from a Lease6
///
/// @param lease Reference to the Lease6
/// @return The newly formed IPv6Resrv instance
static IPv6Resrv makeIPv6Resrv(const Lease6& lease) {
if (lease.type_ == Lease::TYPE_NA) {
return (IPv6Resrv(IPv6Resrv::TYPE_NA, lease.addr_,
(lease.prefixlen_ ? lease.prefixlen_ : 128)));
}
return (IPv6Resrv(IPv6Resrv::TYPE_PD, lease.addr_, lease.prefixlen_));
}
private:
/// @brief creates a lease and inserts it in LeaseMgr if necessary
@ -801,16 +842,36 @@ private:
/// @brief Creates new leases based on reservations.
///
/// This method allocates new leases, based on host reservation. Existing
/// leases are specified in existing_leases parameter. A new lease is not created,
/// if there is a lease for specified address on existing_leases list or there is
/// a lease used by someone else.
/// This method allcoates new leases, based on host reservations.
/// Existing leases are specified in the existing_leases parameter.
/// It first calls @c allocateGlobalReservedLeases6 to accomodate
/// subnets using global reservations. If that method allocates
/// addresses, we return, otherwise we continue and check for non-global
/// reservations. A new lease is not created, if there is a lease for
/// specified address on existing_leases list or there is a lease used by
/// someone else.
///
/// @param ctx client context that contains all details (subnet, client-id, etc.)
/// @param existing_leases leases that are already associated with the client
void
allocateReservedLeases6(ClientContext6& ctx, Lease6Collection& existing_leases);
/// @brief Creates new leases based on global reservations.
///
/// This method is used by @allocateReservedLeases6, to allocate new leases based
/// on global reservation if one exists and global reservations are enabled for
/// the selected subnet. It differs from it's caller by looking only at the global
/// reservation and therefore has no need to iterate over the selected subnet or it's
/// siblings looking for host reservations. Like it's caller, existing leases are
/// specified in existing_leases parameter. A new lease is not created, if there is
/// a lease for specified address on existing_leases list or there is a lease used by
/// someone else.
///
/// @param ctx client context that contains all details (subnet, client-id, etc.)
/// @param existing_leases leases that are already associated with the client
bool
allocateGlobalReservedLeases6(ClientContext6& ctx, Lease6Collection& existing_leases);
/// @brief Removes leases that are reserved for someone else.
///
/// Goes through the list specified in existing_leases and removes those that

View File

@ -753,7 +753,7 @@ TEST_F(AllocEngine6Test, smallPool6) {
// Create a subnet with a pool that has one address.
initSubnet(IOAddress("2001:db8:1::"), addr, addr);
// Initialize FQDN for a lease.
initFqdn("myhost.example.com", true, true);
@ -2717,6 +2717,197 @@ TEST_F(SharedNetworkAlloc6Test, requestRunningOut) {
EXPECT_TRUE(leases.empty());
}
// Verifies that client with a global hostname reservation can get and
// renew a dynamic lease from their selected subnet.
TEST_F(AllocEngine6Test, globalHostDynamicAddress) {
boost::scoped_ptr<AllocEngine> engine;
ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
ASSERT_TRUE(engine);
HostPtr host(new Host(&duid_->getDuid()[0], duid_->getDuid().size(),
Host::IDENT_DUID, SUBNET_ID_UNUSED, SUBNET_ID_GLOBAL,
asiolink::IOAddress("0.0.0.0")));
host->setHostname("ghost1");
CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
CfgMgr::instance().commit();
subnet_->setHostReservationMode(Network::HR_GLOBAL);
// Create context which will be used to try to allocate leases
Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false, query);
ctx.currentIA().iaid_ = iaid_;
// Look up the reservation.
findReservation(*engine, ctx);
// Make sure we found our host.
ConstHostPtr current = ctx.currentHost();
ASSERT_TRUE(current);
ASSERT_EQ("ghost1", current->getHostname());
// Check that we have been allocated a dynamic address.
Lease6Ptr lease;
ASSERT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
ASSERT_TRUE(lease);
EXPECT_EQ("2001:db8:1::10", lease->addr_.toText());
// We're going to rollback the clock a little so we can verify a renewal.
// We verify the "time" change in case the lease returned to us
// by expectOneLease ever becomes a copy and not what is in the lease mgr.
--lease->cltt_;
Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
lease->addr_);
ASSERT_TRUE(from_mgr);
EXPECT_EQ(from_mgr->cltt_, lease->cltt_);
// This is what the client will send in his renew message.
AllocEngine::HintContainer hints;
hints.push_back(make_pair(IOAddress("2001:db8:1::10"), 128));
// Set test fixture hostname_ to the expected value. This gets checked in
// renewTest.
hostname_ = "ghost1";
// Client should receive a lease.
Lease6Collection renewed = renewTest(*engine, pool_, hints, true);
ASSERT_EQ(1, renewed.size());
// And the lease lifetime should be extended.
EXPECT_GT(renewed[0]->cltt_, lease->cltt_)
<< "Lease lifetime was not extended, but it should";
}
// Verifies that client with a global address reservation can get and
// renew a lease for an arbitrary address.
TEST_F(AllocEngine6Test, globalHostReservedAddress) {
boost::scoped_ptr<AllocEngine> engine;
ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
ASSERT_TRUE(engine);
HostPtr host(new Host(&duid_->getDuid()[0], duid_->getDuid().size(),
Host::IDENT_DUID, SUBNET_ID_UNUSED, SUBNET_ID_GLOBAL,
asiolink::IOAddress("0.0.0.0")));
host->setHostname("ghost1");
IPv6Resrv resv(IPv6Resrv::TYPE_NA, asiolink::IOAddress("3001::1"), 128);
host->addReservation(resv);
CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
CfgMgr::instance().commit();
subnet_->setHostReservationMode(Network::HR_GLOBAL);
// Create context which will be used to try to allocate leases
Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false, query);
ctx.currentIA().iaid_ = iaid_;
// Look up the reservation.
findReservation(*engine, ctx);
// Make sure we found our host.
ConstHostPtr current = ctx.currentHost();
ASSERT_TRUE(current);
ASSERT_EQ("ghost1", current->getHostname());
// Check that we have been allocated the fixed address.
Lease6Ptr lease;
ASSERT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
ASSERT_TRUE(lease);
EXPECT_EQ("3001::1", lease->addr_.toText());
// We're going to rollback the clock a little so we can verify a renewal.
// We verify the "time" change in case the lease returned to us
// by expectOneLease ever becomes a copy and not what is in the lease mgr.
--lease->cltt_;
Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
lease->addr_);
ASSERT_TRUE(from_mgr);
EXPECT_EQ(from_mgr->cltt_, lease->cltt_);
// This is what the client will send in his renew message.
AllocEngine::HintContainer hints;
hints.push_back(make_pair(IOAddress("3001::1"), 128));
// Set test fixture hostname_ to the expected value. This gets checked in
// renewTest.
hostname_ = "ghost1";
// Client should receive a lease.
Lease6Collection renewed = renewTest(*engine, pool_, hints, false);
ASSERT_EQ(1, renewed.size());
// And the lease lifetime should be extended.
EXPECT_GT(renewed[0]->cltt_, lease->cltt_)
<< "Lease lifetime was not extended, but it should";
}
// Verifies that client with a global prefix reservation can get and
// renew a lease for an arbitrary prefix.
TEST_F(AllocEngine6Test, globalHostReservedPrefix) {
boost::scoped_ptr<AllocEngine> engine;
ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
ASSERT_TRUE(engine);
HostPtr host(new Host(&duid_->getDuid()[0], duid_->getDuid().size(),
Host::IDENT_DUID, SUBNET_ID_UNUSED, SUBNET_ID_GLOBAL,
asiolink::IOAddress("0.0.0.0")));
host->setHostname("ghost1");
IPv6Resrv resv(IPv6Resrv::TYPE_PD, asiolink::IOAddress("3001::"), 64);
host->addReservation(resv);
CfgMgr::instance().getStagingCfg()->getCfgHosts()->add(host);
CfgMgr::instance().commit();
subnet_->setHostReservationMode(Network::HR_GLOBAL);
// Create context which will be used to try to allocate leases
Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false, query);
ctx.currentIA().type_ = Lease::TYPE_PD;
ctx.currentIA().iaid_ = iaid_;
// Look up the reservation.
findReservation(*engine, ctx);
// Make sure we found our host.
ConstHostPtr current = ctx.currentHost();
ASSERT_TRUE(current);
ASSERT_EQ("ghost1", current->getHostname());
// Check that we have been allocated the fixed address.
Lease6Ptr lease;
ASSERT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
ASSERT_TRUE(lease);
EXPECT_EQ("3001::", lease->addr_.toText());
// We're going to rollback the clock a little so we can verify a renewal.
// We verify the "time" change in case the lease returned to us
// by expectOneLease ever becomes a copy and not what is in the lease mgr.
--lease->cltt_;
Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
lease->addr_);
ASSERT_TRUE(from_mgr);
EXPECT_EQ(from_mgr->cltt_, lease->cltt_);
// This is what the client will send in his renew message.
AllocEngine::HintContainer hints;
hints.push_back(make_pair(IOAddress("3001::"), 64));
// Set test fixture hostname_ to the expected value. This gets checked via
// renewTest.
hostname_ = "ghost1";
// We need a PD pool to fake renew_test
Pool6Ptr dummy_pool(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8::"), 64, 64));
// Client should receive a lease.
Lease6Collection renewed = renewTest(*engine, dummy_pool, hints, false);
ASSERT_EQ(1, renewed.size());
// And the lease lifetime should be extended.
EXPECT_GT(renewed[0]->cltt_, lease->cltt_)
<< "Lease lifetime was not extended, but it should";
}
}; // namespace test
}; // namespace dhcp
}; // namespace isc

View File

@ -44,7 +44,7 @@ namespace test {
bool testStatistics(const std::string& stat_name, const int64_t exp_value,
const SubnetID subnet_id) {
try {
std::string name = (!subnet_id ? stat_name :
std::string name = (subnet_id == SUBNET_ID_UNUSED ? stat_name :
StatsMgr::generateName("subnet", subnet_id, stat_name));
ObservationPtr observation = StatsMgr::instance().getObservation(name);
if (observation) {

View File

@ -44,7 +44,7 @@ namespace test {
/// @return true if the statistic manager holds a particular value,
/// false otherwise.
bool testStatistics(const std::string& stat_name, const int64_t exp_value,
const SubnetID subnet_id = 0);
const SubnetID subnet_id = SUBNET_ID_UNUSED);
/// @brief Allocation engine with some internal methods exposed
class NakedAllocEngine : public AllocEngine {
@ -178,9 +178,11 @@ public:
EXPECT_EQ(lease->subnet_id_, subnet_->getID());
if (expected_in_subnet) {
EXPECT_TRUE(subnet_->inRange(lease->addr_));
EXPECT_TRUE(subnet_->inRange(lease->addr_))
<< " address: " << lease->addr_.toText();
} else {
EXPECT_FALSE(subnet_->inRange(lease->addr_));
EXPECT_FALSE(subnet_->inRange(lease->addr_))
<< " address: " << lease->addr_.toText();
}
if (expected_in_pool) {
@ -374,7 +376,7 @@ public:
createHost6(bool add_to_host_mgr, IPv6Resrv::Type type,
const asiolink::IOAddress& addr, uint8_t prefix_len) {
HostPtr host(new Host(&duid_->getDuid()[0], duid_->getDuid().size(),
Host::IDENT_DUID, SubnetID(0), subnet_->getID(),
Host::IDENT_DUID, SUBNET_ID_UNUSED, subnet_->getID(),
asiolink::IOAddress("0.0.0.0")));
IPv6Resrv resv(type, addr, prefix_len);
host->addReservation(resv);